##はじめに
ゲーム開発に没頭するあまり、全く記事を書いてなかった事を思い出しました。
(下書きだけ書いて、ずっと放置してた…)
いや、ゲーム開発って本当大変ですね。
プライベート時間を殆ど献上しているにも関わらず、やってもやっても終わりませーん
リリースを達成しているゲームクリエイターの先輩方は本当に偉大だと実感します。
と、前置きはこのくらいにして、
前回記事にしたセーブシステムが最終的にどういう実装になったのか、記事にしたいと思います(端的に言えば、頑張ったアピールです笑)
##1. データの実装
はい、セーブと言えばデータですね。
一般的かはさておき、足りない頭で必死に考えた結果、
データは最終的に、以下の3パターンに分類するという結論になりました。
No | データ | 概要 | 例 | セーブ対象 | ロードタイミング |
---|---|---|---|---|---|
1 | システム | ゲーム全体の可変設定 | 音量/速度 | ○ | 起動時 |
2 | プレイ | ゲームプレイ中の断面 | キャラステータス | ○ | コンティニュー時 |
3 | 内部 | ゲーム全体の固定設定 | フラグ/リスト | × | ×(起動時に毎回初期化) |
No1と2に関しては前回の設計時から特に変わってません。
もう少し詳しく書くと、以下のイメージです。
<システムデータ>
ゲーム全体の『可変』設定情報 = ゲーム全体に関わるフラグ/設定値
→例えばセーブ画面が1~10まであったら、最後にセーブに使った画面は何番か、とかですね。
コンティニュー時は最後にセーブに使った画面が真っ先に開く方が嬉しいですし。
<プレイデータ>
セーブを実行したタイミングの『プレイ中パラメータ情報』
→Live2Dキャラの状態(表情とか)、キャラステータス、プレイ中のフラグ
<内部データ>
そして、No3はゲーム内のパラメータを一元管理するために、新たに追加しました。
これに書くのは、ゲーム全体の『固定』設定情報や、ゲーム起動時に『初期化されて問題ないフラグ』です。
・固定設定情報(エンディング一覧、キャラクタ一覧、アイテム一覧とか)
・各種パラメータの設定(パラメータの最大値とか、パラメータの変化率とか)
・処理実行中フラグ(起動中、ロード中、シナリオ/アニメーション再生中とか)
元々は色々なクラスのプロパティに分散され、記載されていたんですが、
この手のフラグは他のクラスから参照するパターンも多いし、
殆ど同じ内容のプロパティが各クラスに散見されるので、
**一箇所に纏めて管理しないとメンテナンス大変だなぁ。。**という事で作成しました。
##2. セーブ機能の実装
前回記事にした設計をそのまま実装しました。
処理のイメージは以下の感じです。
###システムデータ###
システムデータを記録するタイミングは以下のパターン(うろ覚え)
- config画面で設定を更新する度に保存
- プレイデータセーブ時に一緒に保存
- ゲームクリア時/ゲームオーバー時/画面遷移時などの節目で保存
処理自体はSystemDataクラスのインスタンスをJsonに変換してファイルとして書き出す。
というシンプルなものです。
※コード自体は他の記事でもっと丁寧に書いてるので、そっちを見るのがおススメ!笑 ちなみに、下記コードはシステムデータとプレイデータの共通処理になってます。
public string Path => Application.persistentDataPath + "/<フォルダ>";
public void Save(string type, string file)
{
string filePath = $"{Path}/{file}";
string jsonData;
if (type == "play"){jsonData = JsonUtility.ToJson(playData);}
else if (type == "system"){jsonData = JsonUtility.ToJson(systemData);}
else {Debug.LogError("FileType not match");return;}
if (!Directory.Exists(Path)){Directory.CreateDirectory(Path);}
StreamWriter writer = new StreamWriter(filePath, false); //true追記、false上書き
writer.WriteLine(jsonData);
writer.Flush();
writer.Close();
}
###プレイデータ
設計で書きましたが、今回のセーブ画面はサムネイル方式です。
なのでセーブ時にプレイ画面のスクリーンショットを取得しています。
- ゲーム画面の「SAVE」ボタンクリックをトリガーに以下を処理
①スクリーンショット取得
②セーブ画面への遷移
- セーブ画面の「SaveBox(imageオブジェクト)」のクリックをトリガーに以下を処理
③システムデータ更新
④SaveBoxに各種情報を表示
⑤システムデータ/プレイデータ保存
①スクリーンショット取得
スクリーンショットを撮るだけながらScreenCaptureで一発です。
ただ自分は取得した画像の解像度を落として、テキストに変換してます。
(その方が一元管理しやすいかな…って事で)
まぁ思った以上に画質が劣化するんですけどね笑
public void ScreenShot(int width, int height)
{
Texture2D tex = ScreenCapture.CaptureScreenshotAsTexture();
var rt = RenderTexture.GetTemporary(width, height);
Graphics.Blit(tex, rt);
var preRT = RenderTexture.active;
RenderTexture.active = rt;
var ret = new Texture2D(width, height);
ret.ReadPixels(new Rect(0, 0, width, height), 0, 0);
ret.Apply();
RenderTexture.active = preRT;
tmpSaveImage = System.Convert.ToBase64String(ret.EncodeToPNG());
}
②セーブ画面への遷移
これは単純にゲーム内の時間を止めて、セーブ画面オブジェクトをアクティブにしてるだけ
③システムデータ更新
更新するデータ=SaveBoxに表示する概要情報です。
例えば、保存日時や進行状況、サムネイル画像とか
何で保存しているかと言えば、ゲーム起動時にセーブ画面を最新状態に戻すためです。
ちなみに設計で詳しく書きましたが、プレイデータはID(数値)で管理していて、
IDをキーに「システムデータ内のSave一覧(概要)情報」「セーブ画面のSaveBoxオブジェクト」を紐づけてます。
なので、SaveBoxの『ID=1』がクリックされたら、システムデータ内に『ID=1』としてセーブの概要情報保存 + プレイデータを『ID=1』として保存という処理になります。(ロードも同じで『ID=1』がクリックされたら『ID=1』のプレイデータをロードという流れです)
④SaveBoxに各種情報を表示
SaveBoxに上記で保存した概要情報=保存日時や進行状況、サムネイル画像を反映します。
まぁこれはオブジェクト内のTxtやImageを更新するだけです。
なお前述の通り、画像はテキストに変換してるので、ここで画像に戻す必要があります。
var img = new Texture2D(1, 1);
byte[] bytes = System.Convert.FromBase64String(<画像テキスト>);
img.LoadImage(bytes);
⑤システムデータ/プレイデータ保存
処理自体は↑のシステムデータ保存、に記載したメソッドを使ってます。
かなりざっくりとした記載になりましたが、上記でサムネイル方式のセーブ機能が作れます。
方法は他にも色々あると思いますが、大事なのは実装方法というより、データの考え方な気がします。
##3. ロード機能の実装
ロード機能はSaveの逆です。
保存したJsonをデシリアライズする、ですね。
※ちなみにシステムデータは、起動時に存在しなければ新規作成するように記述してます。
public string Path => Application.persistentDataPath + "/<フォルダ>";
public void Load(string type, string file)
{
string filePath = $"{Path}/{file}";
if (!File.Exists(filePath)){Debug.LogError("Not Found");return;}
StreamReader reader = new StreamReader(filePath);
string jsonData = reader.ReadToEnd();
if (type == "play"){playData = JsonUtility.FromJson<PlayData>(jsonData);}
else if (type == "system"){systemData = JsonUtility.FromJson<SystemData>(jsonData);}
else {Debug.LogError("FileType not match");return;}
reader.Close();
}
動作タイミングは「システムデータは起動時」「プレイデータはロード画面でSaveBoxがクリックされた時」にしています。
今回の実装で肝なのは、ゲーム起動時に「セーブ画面に表示する概要情報」を「システムデータに保存されている情報に更新」するという部分でしょうか。処理自体はSave処理の時と同様で、SaveBoxオブジェクトの内容を更新するだけですけどね。
ロード処理はこんなモノなのですが、個人的に書いておきたい事があります。
それはロード時にプレイ画面の断面に戻す処理の苦労です。(苦労したから書いておきたい)
基本的には、必要な情報を保存しておいて、それ通りに画面を更新するだけなんですが、問題なのはLive2dなんですよね。
※今回のゲームはLive2dを使ってるのです。
自分が知る限り、Live2dを使ったゲームで「プレイ断面を保存する」ようなモノってあまり無いんですよね。
それはゲームのジャンルだったり、モデルの作り、使い方もあると思いますが、少なくとも自分のゲームは「保存時のLive2dパラメータに戻す」必要があるんです。
例えば表情とか。セーブ時は泣いてたキャラクターが、ロードしたら笑ってた、とか…違和感半端ないですからね…
これが頭を悩ませるポイントでした。。
まぁ基本的には以下のパターンになると思われます。
①expression機能を使う
②モーションブレンドを使用し、ブレンド率を記録しておき、ロード時に戻す
③モデルのパラメータを直接書き換える
個人的には③がベストだったんですが、落とし穴がありまして…
モデルのパラメータを直接書き換えるには「LateUpdateメソッド」を使う必要があるみたいなんですが、これってLateUpdateを止めた瞬間、パラメータが巻き戻るんですよね…(ずっとメソッドを動かして固定し続けないといけないのだろうか…)
ただ、メソッドを常時動かしてパラメータを固定すると、アニメーション時に対象のパラメータが動かなくなります。
パラメータがLateUpdateでひたすら上書きされ続けるので、アニメーションを再生しても即座に上書きされ、対象のパラメータが動きません笑
まず教訓なのは、**Unityから直接操作するパラメータとアニメーションで使うパラメータは別にしておくべき。**なんでしょうね…。一旦は②と③の併用でなんとかなってますが、もうちょっと考えないとダメそう…(Live2d周りは悩みが尽きません…)
##おわりに
かなりザックリとした記事になりましたが、サムネイル方式のセーブシステムをどう実装したかを書いてみました。
Unityで、この方式のセーブ実装は殆ど情報が無いので、何かしら参考になる部分があれば良いな…くらいの気落ちです。
まぁこれが最適な方法なのかは正直わかりません!笑
『必死に方法を模索して、自分の思う機能を実装出来た』という達成感だけは凄く感じています()
さて、ゲーム作りは本当に大変ですが、リリースに向けて最後まで全力で走りぬくぞー!
(同志の皆さんも同じような苦労をしてるんだと思うと、本当に頭が上がりません…)