LoginSignup
2
1

More than 1 year has passed since last update.

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

Last updated at Posted at 2021-07-12

はじめに

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

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

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

1. データの実装

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

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

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

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

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

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

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

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

2. セーブ機能の実装

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

システムデータ

システムデータを記録するタイミングは以下のパターン(うろ覚え)

1. config画面で設定を更新する度に保存
2. プレイデータセーブ時に一緒に保存
3. ゲームクリア時/ゲームオーバー時/画面遷移時などの節目で保存

処理自体は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();
    }

プレイデータ

設計で書きましたが、今回のセーブ画面はサムネイル方式です。
なのでセーブ時にプレイ画面のスクリーンショットを取得しています。

1. ゲーム画面の「SAVE」ボタンクリックをトリガーに以下を処理
 ①スクリーンショット取得
 ②セーブ画面への遷移

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

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

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

2
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
1