シンプルなアクションゲームを作ってみようの第28回です。
今回はゲームデータのセーブとロード機能を作成していきます。
前回はゲームシーンに霧を発生させる機能を作成しました。
シンプルなアクションゲームを作ってみようの他の記事は
シンプルなアクションゲームを作るのを通してUnityの使い方を学ぶカテゴリです。
から参照出来ます。
ゲームデータのセーブとロードについて
通常のゲームであればゲームのオプションで設定した音量や操作方法やRPGゲームであれば現在のレベルやHPやMPといったデータもゲームデータとなります。
今回作成しているシンプルなアクションゲームのセーブすべきデータとしてはゲームステージのクリアに要した時間です。
ゲームデータの保存方法としてはスタンドアロン形式(単体で実行出来るもの)の場合はファイルを作成しそこにデータを保存する事が出来ます。
オンラインゲームであればサーバーにデータを保存し、ユーザーがログインすることでサーバーからデータを取得し続きから再開が出来ます。
UnityではPlayerPrefsを使ってデータの保存が出来ます。
PlayerPrefsを使ったデータの保存先はビルドするプラットフォームによって保存先が変わります。
保存先はUnityのスクリプトリファレンスを参照してください。
注意事項
PlayerPrefsはおそらくPlayerPreferencesの略なので環境設定の事だと思われます。
Windowsのスタンドアロン形式の場合はWindowsのレジストリにPlayerPrefsのデータが保存されます。
レジストリはソフトウェアの環境設定の保存として利用されることがあります。
つまり設定情報の保存に使用する領域であって、ゲームのプレイデータをレジストリに保存されたり、データが残ってしまうのを良しとしない人もいます。
レジストリのデータが増えるとパソコンの速度も落ちるかもしれません。
なのでスタンドアロン形式の場合はファイルにデータを保存するやり方のほうがいいかもしれません。
データのセーブとロード機能を作成する
データのセーブとロード機能を作成していきます。
データのセーブとロードの処理をするスクリプトを作成する
データのセーブとロードの実際の処理を担当するスクリプトを作成していきます。
Assets/Scriptsフォルダに新しくDataProcessingScriptスクリプトを作成します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 | using System.Collections; using System.Collections.Generic; using UnityEngine; public static class DataProcessingScript { // 最高タイムの保存 public static void SaveData(string stageName, int seconds) { if (PlayerPrefs.HasKey(stageName)) { // 既に記録がある場合は以前の記録より早い場合のみ保存 if (seconds < PlayerPrefs.GetInt(stageName)) { PlayerPrefs.SetInt(stageName, seconds); } } else { // 記録がない場合はそのままデータを保存 PlayerPrefs.SetInt(stageName, seconds); } } // 最高タイムの読み込み public static int LoadData(string stageName) { return PlayerPrefs.GetInt(stageName, int.MaxValue); } // データの削除 public static void DeleteData(string stageName) { // データがある場合はそのデータを削除 if (PlayerPrefs.HasKey(stageName)) { // PlayerPrefs.DeleteAllは絶対に使わない!!(Unityの設定ファイルも消える可能性あり) PlayerPrefs.DeleteKey(stageName); } } } |
DataProcessingScriptはMonoBehaviourを継承してクラスを作りません。
このスクリプトはゲームオブジェクトに取り付けず処理だけを呼び出されて実行します。
何も継承はしていませんがObjectクラスを自動で継承する形になります。
またDataProcessingScriptは静的なメソッドのみを持つのでクラスにもstaticを付け静的なクラスとします(インスタンス化して使う必要がない)。
SaveDataメソッドは第1引数でデータ名を受け取り、第2引数でデータである秒数を受け取ります。
またstaticを付けているのでインスタンス化して呼び出す必要がなく
1 2 3 | DataProcessingScript.SaveData("データ名", データ); |
のように呼び出せます。
PlayerPrefs.HasKeyメソッドで引数で指定したキーのデータがあるかどうかをチェックしています。
データが存在していれば引数で受け取ったデータとPlayerPrefsに保存してあるデータを比較し、今保存しているデータの方が小さい時(速くゴールした)だけPlayerPrefs.SetIntメソッドを使って引数で受け取ったデータ名にデータを保存しています。
それ以外の時(データ名が存在しない時)は受け取ったデータ名に受け取ったデータを保存します。
LoadDataメソッドは引数で受け取ったデータ名のデータをPlayerPrefsから取得しますが、第2引数で値を指定するとその値がデフォルト値として返されます。
なのでデータがなければint.MaxValueの値(int型の最大値)が返されます。
DeleteDataメソッドは引数で受け取ったデータを削除します。
if文で引数で受け取ったデータがあるかどうかをチェックし、存在すればそのデータを削除します。
コメントに書いてありますが、PlayerPrefs.DeleteAllメソッドは絶対に使わないでください。
Unityエディターの設定データも消えてUnityエディターが正常に動かなくなる可能性があります。
以前、わたくしが実際にやってしまって大変なことになりました。(^_^;)
DataProcessingScriptの処理を呼び出す処理の追加
データの処理はDataProcessingScriptが行うので、後はその処理をするメソッドを呼び出すだけです。
データのセーブ機能の作成
まずはデータのセーブ機能を作成していきます。
ゲームのクリアタイムのデータはゲームをクリアした時に保存するのでGameManagerスクリプトに処理を追加します。
フィールドにtimerScriptを追加します。
1 2 3 4 | // タイマースクリプト private TimerScript timerScript; |
次にTitleシーン以外の時の処理でTimerScriptの取得処理を入れます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 | void Start() { // ゲーム関連初期化 GameOver = false; IsCountDown = true; if (SceneManager.GetActiveScene().name != "Title") { gameOverCanvas = GameObject.Find("GameOver").GetComponent<Canvas>(); goToNextButton = gameOverCanvas.transform.Find("GoToNextButton").gameObject; goToTitleButton = gameOverCanvas.transform.Find("GoToTitleButton").gameObject; gameOverCanvas.enabled = false; countDownText = GameObject.Find("CountDownText").GetComponent<TextMeshProUGUI>(); timerScript = FindObjectOfType<TimerScript>(); // PostProcessVolumeの取得 postProcessVolume = FindObjectOfType<PostProcessVolume>(); if (SceneManager.GetActiveScene().name != "Stage2") { postProcessVolume.profile.GetSetting<DepthOfField>().enabled.Override(true); } else { // Stage2の時は霧を有効にする RenderSettings.fog = true; RenderSettings.fogDensity = 0.15f; // 試しにPostProcessingStackのDepthOfFieldを無効にしてみる postProcessVolume.profile.GetSetting<DepthOfField>().enabled.Override(false); } StartCoroutine(CountDown()); } else { // タイトルシーンの時はステージ番号を1にする gameManagementData.StageNum = 1; } } |
次にゲームクリア時に呼び出されるClearGameメソッドの最初にDataProcessingScriptのSaveDataメソッドの呼び出しを入れます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | // ゲームクリア時に実行する public void ClearGame() { // データのセーブ DataProcessingScript.SaveData("Stage" + gameManagementData.StageNum, timerScript.GetSeconds()); GameOver = true; gameManagementData.StageNum++; if (gameManagementData.StageNum < SceneManager.sceneCountInBuildSettings) { goToTitleButton.SetActive(false); } else { goToNextButton.SetActive(false); } gameOverCanvas.enabled = true; } |
データ名はStageという文字列に現在のステージ番号であるgameManagementData.StageNumを足したもの(Stage1やStage2)で保存するデータとしてtimerScript.GetSecondsメソッドで取得したint型の数値を渡します。
これでデータのセーブ機能が出来ました。
データのロード機能の作成
次はデータのロード機能を作成していきます。
データのロード機能はTitleシーンに取り付けていきます。
Titleシーンを開きます。
データロード用のUIの作成
ヒエラルキー上で右クリックからUI→Canvasを選択し、名前をDataCanvasとします。
DataCanvasを選択した状態で右クリックからUI→Panelを選択し、名前をDataPanelとします。
DataPanelゲームオブジェクトを選択し、インスペクタのAdd ComponentからLayout→Vertical Layout Groupを選択し取り付けます。
Vertical Layout GroupのChild AlignmentをMiddle Centerにし、子要素のUIが画面の中央に並ぶようにします。
Control Child SizeのWidthとHeightにチェックを入れ子要素のUIの幅と高さのサイズをコントロールします。
Child Force ExpandのWidthとHeightにチェックを入れ子要素の幅と高さの拡張をします。
次にデータを表示するテキストを作成していきます。
DataPanelを選択した状態で右クリックからUI→Text – TextMeshProを選択し、名前をTitleTextとします。
TextMeshProのText InputにBest Timeと入力します。
Vertex Colorは黒色にします。
AlignmentはCenterとMiddleを選択します。
TitleTextゲームオブジェクトを選択した状態でCtrl+Dキーを3かい押し複製したら、名前をStage1Text、Stage2Text、Stage3Textと変更します。
次にDataPanelを選択した状態で右クリックからUI→Panelを選択し、名前をButtonPanelとします。
ButtonPanelはDataPanelの子になります。
ButtonPanelのインスペクタのAdd ComponentからLayout→Horizontal Layout Groupを選択し取り付けます。
ButtonPanelのImageのColorでRGBを0、Aを100とします。
Horizontal Layout GroupのAlignmentをMiddle Centerとします。
Child Force ExpandのWidthとHeightにチェックを入れます。
ButtonPanelを選択した状態で右クリックからUI→Button – TextMeshProを選択し、名前をDeleteDataButtonとします。
子要素のText(TMP)ゲームオブジェクトを選択しインスペクタのTextMeshProのText InputにDelete All Dataと入力します。
DeleteDataButtonを選択した状態でCtrl+Dキーを押して複製し、名前をGoToTitleButtonとします。
子要素のText(TMP)のインスペクタのTextMeshProのText InputにGo To Titleと入力します。
これでDataCanvasのUIが出来ました。
階層は以下のようになります。
MainCanvasを一旦非アクティブにし、ゲームビューで表示を確認すると以下のようになります。
データ表示UIを開くボタンの作成
データ表示用のUIの画面を開くボタンをMainButtonPanelの子要素に作成します。
ヒエラルキーのMainCanvasの子のMainButtonPanelの子のStartButtonを選択し、Ctrl+Dキーを押して複製し、名前をDataButtonとします。
DataButtonの子のText(TMP)のインスペクタのTextMeshProのText InputにDataと入力します。
このままだとスタートボタンとデータボタンが重なっているので、MainButtonPanelを選択し、インスペクタのAdd ComponentからLayout→Horizontal Layout Groupを選択し取り付けます。
データ表示用のUI操作スクリプト
データを表示するUIの表示・非表示をするスクリプトを作成します。
Assets/Scriptsフォルダに新しくSetARecordScriptスクリプトを作成し、DataCanvasゲームオブジェクトに取り付けます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 | using System; using System.Collections; using System.Collections.Generic; using TMPro; using UnityEngine; public class SetARecordScript : MonoBehaviour { // ステージ記録の表示用テキスト private TextMeshProUGUI stage1Text; private TextMeshProUGUI stage2Text; private TextMeshProUGUI stage3Text; // メインタイトル [SerializeField] private GameObject mainTitle; // メインキャンバスのボタンパネル [SerializeField] private GameObject mainButtonPanel; // データウインドウ private Canvas dataWindowCanvas; // Start is called before the first frame update void Start() { dataWindowCanvas = GetComponent<Canvas>(); dataWindowCanvas.enabled = false; stage1Text = dataWindowCanvas.transform.Find("DataPanel/Stage1Text").GetComponent<TextMeshProUGUI>(); stage2Text = dataWindowCanvas.transform.Find("DataPanel/Stage2Text").GetComponent<TextMeshProUGUI>(); stage3Text = dataWindowCanvas.transform.Find("DataPanel/Stage3Text").GetComponent<TextMeshProUGUI>(); } public void SwitchingDataWindow(bool open) { dataWindowCanvas.enabled = open; // データウインドウを開く場合 if (open) { // メインタイトルに非表示 mainTitle.SetActive(false); // メインのキャンバスのボタンパネルを非表示 mainButtonPanel.SetActive(false); var timeSpan = new TimeSpan(0, 0, DataProcessingScript.LoadData("Stage1")); stage1Text.SetText("Stage1 {0:00}:{1:00}:{2:00}", timeSpan.Hours, timeSpan.Minutes, timeSpan.Seconds); timeSpan = new TimeSpan(0, 0, DataProcessingScript.LoadData("Stage2")); stage2Text.SetText("Stage2 {0:00}:{1:00}:{2:00}", timeSpan.Hours, timeSpan.Minutes, timeSpan.Seconds); timeSpan = new TimeSpan(0, 0, DataProcessingScript.LoadData("Stage3")); stage3Text.SetText("Stage3 {0:00}:{1:00}:{2:00}", timeSpan.Hours, timeSpan.Minutes, timeSpan.Seconds); } else { // メインタイトルの表示 mainTitle.SetActive(true); // データウインドウを閉じる場合はメインキャンバスボタンを表示 mainButtonPanel.SetActive(true); } } public void DeleteData() { // 全データ DataProcessingScript.DeleteData("Stage1"); DataProcessingScript.DeleteData("Stage2"); DataProcessingScript.DeleteData("Stage3"); // 画面の非表示 SwitchingDataWindow(false); } } |
Stage1~Stage3までのデータを表示する各テキストをフィールドとして用意し入れるようにします。
mainTitleはインスペクタでMainTitleゲームオブジェクトを設定します。
mainButtonPanelはインスペクタでMainButtonPanelゲームオブジェクトをドラッグ&ドロップして設定します。
Startメソッドでは自身のCanvasを取得しdataWindowCanvasに入れます。
最初はデータ表示のUIは表示したくないのでdataWindowCanvas.enabledにfalseを入れています。
dataWindowCanvasからStage1~Stage3のデータを表示するテキストを取得しています。
SwitchingDataWindowメソッドはMainCanvasのパネルのアクティブ・非アクティブとDataCanvasのCanvasの有効・無効化を操作してどちらのUIを表示するかの切り替えをしているメソッドです。
引数でデータ表示UIを開くかどうかのopenを受け取りデータ表示UIを表示する場合はDataProcessingScriptのLoadDataメソッドを使ってデータをロードし、それをTimeSpan型のインスタンス生成時に渡してそれを加工して、各データ表示テキストに表示しています。
DeleteDataメソッドはDeleteDataButtonを押した時に呼び出すメソッドで、Stage1からStage3までのPlayerPrefsのデータを削除してSwitchingDataWindowメソッドを呼んでデータ表示UIを非表示にしています。
今回の場合はStage1からStage3までしかないので個別にデータの削除をしています。
ゲームステージが増えたら繰り返し分を使って全データを削除する方が便利です。
そもそも全データ削除機能しか搭載していないので本当ならば個別データの削除機能を付けた方が良さそうですね。
ボタンを押した時に実行するメソッドの設定
UIが出来たのでボタンを押した時に実行するメソッドを設定していきます。
MainCanvasの子のDataButtonのインスペクタのOn ClickでDataCanvasゲームオブジェクトをドラッグ&ドロップし、SwitchingDataWindowメソッドを実行するようにし、チェックボックスにチェックを入れます。
次はDataCanvasの子要素のDeleteDataButtonのインスペクタのButtonのOn Clickの+を押し、DataCanvasをドラッグ&ドロップしてDeleteDataメソッドを設定します。
次はGoToTitleButtonのインスペクタのButtonのOn Clickの+を押してDataCanvasをドラッグ&ドロップして、SwitchingDataWindowメソッドを設定し、チェックボックスのチェックを外します。
これで機能が出来ました。
DataCanvasのSetARecordScriptの設定を忘れない様にしてください。
実行して確認してみる
機能が出来たので実行して確認してみましょう。
上のようになりました。
終わりに
今回はゲームデータのセーブ・ロード機能を作成しました。
次回はゲームのビルド等をしてみます。