Unityゲーム制作奮闘記 - 実装 - セーブシステム

##はじめに

ゲーム開発に没頭するあまり、全く記事を書いてなかった事を思い出しました。
（下書きだけ書いて、ずっと放置してた…）

いや、ゲーム開発って本当大変ですね。
プライベート時間を殆ど献上しているにも関わらず、やってもやっても終わりませーん
リリースを達成しているゲームクリエイターの先輩方は本当に偉大だと実感します。

と、前置きはこのくらいにして、
前回記事にしたセーブシステムが最終的にどういう実装になったのか、記事にしたいと思います（端的に言えば、頑張ったアピールです笑）


##1. データの実装

はい、セーブと言えばデータですね。
一般的かはさておき、足りない頭で必死に考えた結果、
データは最終的に、以下の3パターンに分類するという結論になりました。

|No|  データ  |  概要  | 例|セーブ対象|ロードタイミング|
| ---- | ---- | ---- | ---- | ---- | ---- |
|1|  システム  |  ゲーム全体の**可変**設定  |音量/速度|○|起動時|
|2|  プレイ |  ゲームプレイ中の断面  |キャラステータス|○|コンティニュー時|
|3|  内部  |  ゲーム全体の**固定**設定  |フラグ/リスト|×|×(起動時に毎回初期化)|

No1と2に関しては前回の設計時から特に変わってません。
もう少し詳しく書くと、以下のイメージです。

**<システムデータ>**
`ゲーム全体の『可変』設定情報 = ゲーム全体に関わるフラグ/設定値`
→例えばセーブ画面が1~10まであったら、最後にセーブに使った画面は何番か、とかですね。
コンティニュー時は最後にセーブに使った画面が真っ先に開く方が嬉しいですし。

**<プレイデータ>**
`セーブを実行したタイミングの『プレイ中パラメータ情報』`
→Live2Dキャラの状態（表情とか）、キャラステータス、プレイ中のフラグ

**<内部データ>**
そして、No3はゲーム内のパラメータを一元管理するために、新たに追加しました。
これに書くのは、ゲーム全体の『固定』設定情報や、ゲーム起動時に『初期化されて問題ないフラグ』です。

・固定設定情報（エンディング一覧、キャラクタ一覧、アイテム一覧とか）
・各種パラメータの設定（パラメータの最大値とか、パラメータの変化率とか）
・処理実行中フラグ（起動中、ロード中、シナリオ/アニメーション再生中とか）

元々は色々なクラスのプロパティに分散され、記載されていたんですが、
この手のフラグは他のクラスから参照するパターンも多いし、
殆ど同じ内容のプロパティが各クラスに散見されるので、
**一箇所に纏めて管理しないとメンテナンス大変だなぁ。。**という事で作成しました。


##2. セーブ機能の実装
前回記事にした設計をそのまま実装しました。
処理のイメージは以下の感じです。

###システムデータ###
システムデータを記録するタイミングは以下のパターン（うろ覚え）
<font color="Blue">
1. config画面で設定を更新する度に保存
2. プレイデータセーブ時に一緒に保存
3. ゲームクリア時/ゲームオーバー時/画面遷移時などの節目で保存
</font>

処理自体は**SystemDataクラスのインスタンス**をJsonに変換してファイルとして書き出す。
というシンプルなものです。
`※コード自体は他の記事でもっと丁寧に書いてるので、そっちを見るのがおススメ！笑
　ちなみに、下記コードはシステムデータとプレイデータの共通処理になってます。`

```C#
    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();
    }
```

###プレイデータ
設計で書きましたが、今回のセーブ画面はサムネイル方式です。
なのでセーブ時にプレイ画面のスクリーンショットを取得しています。
<font color="Blue">
1. ゲーム画面の「SAVE」ボタンクリックをトリガーに以下を処理
　①スクリーンショット取得
　②セーブ画面への遷移
</font><font color="Blue">
2. セーブ画面の「SaveBox（imageオブジェクト）」のクリックをトリガーに以下を処理
　③システムデータ更新
　④SaveBoxに各種情報を表示
　⑤システムデータ/プレイデータ保存
</font>

**①スクリーンショット取得**
スクリーンショットを撮るだけながらScreenCaptureで一発です。
ただ自分は取得した画像の解像度を落として、テキストに変換してます。
（その方が一元管理しやすいかな…って事で）
まぁ思った以上に画質が劣化するんですけどね笑

```C#
    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を更新するだけです。

なお前述の通り、画像はテキストに変換してるので、ここで画像に戻す必要があります。

```C#
        var img = new Texture2D(1, 1);
        byte[] bytes = System.Convert.FromBase64String(<画像テキスト>);
        img.LoadImage(bytes);
```

**⑤システムデータ/プレイデータ保存**
処理自体は↑のシステムデータ保存、に記載したメソッドを使ってます。

かなりざっくりとした記載になりましたが、上記でサムネイル方式のセーブ機能が作れます。
方法は他にも色々あると思いますが、大事なのは実装方法というより、データの考え方な気がします。

##3. ロード機能の実装
ロード機能はSaveの逆です。
保存したJsonをデシリアライズする、ですね。
※ちなみにシステムデータは、起動時に存在しなければ新規作成するように記述してます。

```C#
    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で、この方式のセーブ実装は殆ど情報が無いので、何かしら参考になる部分があれば良いな…くらいの気落ちです。

まぁこれが最適な方法なのかは正直わかりません！笑
『必死に方法を模索して、自分の思う機能を実装出来た』という達成感だけは凄く感じています（）

さて、ゲーム作りは本当に大変ですが、リリースに向けて最後まで全力で走りぬくぞー！
（同志の皆さんも同じような苦労をしてるんだと思うと、本当に頭が上がりません…）