今回はUnityでゲームのデータを保存してみたいと思います。
ゲームのプレイデータを保存出来るようにし、同じ状態で再開出来るというのは今や必須の機能ですね。
昔のゲームみたいにパスワードを入力していく機能を実装するのもある意味面白いとは思いますが・・・・(^_^;)
Unityでゲームデータを扱うのは難しくありません。
Unityでゲームデータを扱う方法
データを保存
1 2 3 4 5 | PlayerPrefs.SetString("Data", "SaveData"); PlayerPrefs.SetInt("Data", 10); PlayerPrefs.SetFloat("Data", 3.14f); |
↑のようにPlayerPrefsを使うと第1引数で指定した名前のキーを作成し、そこにデータを保存する事が出来ます。
第1引数には保存するデータの名前を指定します。
関数名は保存するデータの形式を指定します。
データの読み出し
値を取り出す時は
1 2 3 4 5 | PlayerPrefs.GetString("Data"); PlayerPrefs.GetInt("Data"); PlayerPrefs.GetFloat("Data"); |
とするだけです。
取得したデータをキャラクターのステータススクリプトのHPやMP等のデータに代入し使用します。
データの削除
データを削除する時は
1 2 3 4 | PlayerPrefs.DeleteKey("Data"); // キーの値を削除 //PlayerPrefs.DeleteAll; // すべてのデータを削除 |
↑のように特定のデータを削除したり、全部のデータを削除する事も出来ます。
ただPlayerPrefs.DeleteAllは使わないでください。Unity自身の設定ファイルが消えてしまいます。
指定したデータが存在するかどうか
1 2 3 4 5 | if(PlayerPrefs.HasKey("Data")) { Debug.Log("Dataデータは存在します"); } |
データが存在するかどうかは↑のようにHasKeyの引数にデータの名前を指定します。
データが保存されるタイミング
今まで見て来たデータの更新はOnApplicationQuitイベントの中で行われるようです。
OnApplicationQuit関数はアプリケーションが終了する直前に実行されるので途中でゲームが止まってしまった場合はデータの保存がされません。
こういったことがないように
1 2 3 | PlayerPrefs.Save(); |
↑のように呼び出すと強制でデータ保存がされるようになります。
ですが、この処理は時間がかかるようでゲームプレイ中にこの処理をさせるのはよくないようです。
詳しくは
https://docs.unity3d.com/ja/current/ScriptReference/PlayerPrefs.html
Unityのスクリプトリファレンスに記載があるので参照してください。
ゲームデータを作り実際に操作してみる
それではこれらの機能を使って実際にデータを保存したり読み出したりしてみたいと思います。
JSONデータについて
単純なデータの保存はさきほど見て来た通りにシンプルに記述すれば出来ますので試してみてください。
今回は保存したいデータをJSON形式で保存したりそのデータを読み出して
再度ゲームデータに戻すという事をしてみたいと思います。
JSON形式はデータの保存方法の1つで
1 2 3 | {"hp":10,"mp":5} |
という感じでデータを扱います。
なぜJSON形式でデータを保存するかと言うとオブジェクトのプロパティ値を1つのキーで保存する事が出来る為です。
例えばhp、mpを保存したい場合
1 2 3 4 | PlayerPrefs.SetInt("HP", hp); PlayerPrefs.SetInt("MP", mp); |
と複数の記述が必要ですが、JSON形式のデータはstring型なので一括で保存出来ます。
例えばdata変数に
1 2 3 | {"hp":10,"mp":5} |
のようなStringのデータが入っているとすれば
1 2 3 | PlayerPrefs.SetString("PlayerData", data); |
とすればいいので1つ1つのデータを個別に処理する必要がなくなります。
JSON形式でデータの保存と読み出しをする為には保存するデータがSerializable属性で宣言されているオブジェクトに限ります。
Serializable属性はC#だと明示的にしないといけませんがJavaScriptの場合はデフォルトでなっているようです。
また、保存出来るデータはpublic変数のみで、static、privateで宣言されたフィールドはデータ保存されません。
[SerializeField]アトリビュートを付ければprivateで宣言したフィールドも保存出来ます。
それを含めて確認してみます。
JSON形式のデータを作成は自分でJSON形式の文字列を作成してPlayerPrefsに保存し、PlayerPrefsから取得したデータを自分で解析して個々のデータに変換する事も出来ますが、それらを簡単に行う事が出来るクラスがあるのでそれらを使います。
1 2 3 4 5 | JsonUtility.ToJson(クラス); // クラスのプロパティをJSON形式のデータとして作成 JsonUtility.FromJsonOverwrite(JSONデータ, クラス); // 取得したJSONデータをクラスのプロパティに上書き JsonUtility.FromJson<クラス>(JSONデータ); // JSONデータをクラスのプロパティに設定 |
↑のように記述すればJSONデータを扱えます。
ゲームデータを保持し、値を返すSavaDataスクリプトの作成
まずはゲームデータを扱うスクリプトSaveDataを作成しましょう。
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 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 | using UnityEngine; using System.Collections; using System; [Serializable] public class SaveData : object { // publicデータ public int hp; // staticデータ static int mp; // privateデータ private float power; // フラグデータ public bool flag; // シリアライズなデータ [SerializeField] private Vector3 vector; [SerializeField] private GameObject obj; public void SetHp(int hp) { this.hp = hp; } public void SetMp(int tempMp) { mp = tempMp; } public void SetPower(int power) { this.power = power; } public void SetFlag(bool flag) { this.flag = flag; } public void SetVector(Vector3 vector) { this.vector = vector; } public void SetObj(GameObject obj) { this.obj = obj; } public int GetHp() { return hp; } public int GetMp() { return mp; } public float GetPower() { return power; } public bool IsFlag() { return flag; } public Vector3 GetVector() { return vector; } public GameObject GetObj() { return obj; } public string GetNormalData() { return "hp: " + hp + " mp: " + mp + " power: " + power + " flag: " + flag + " vector: " + vector + " obj: " + obj; } public string GetJsonData() { return JsonUtility.ToJson(this); } } |
objectを継承したクラスとしてSaveDataを作成する為、ゲームオブジェクトに取り付ける事は出来ませんので、後で別のスクリプトからSaveDataクラスのインスタンスを作成します。
明示的にobjectクラスを継承せずとも
1 2 3 4 5 | [Serializable] public class SaveData { } |
上のように書いても自動でobjectクラスが継承される形でクラスが作られます。
static、privateのフィールドが保存されないのを確認する為に宣言しておきます。
また[SerializeField]アトリビュートを付けたprivateフィールドも保存されるかどうか確認します。
スクリプトは長いですが個々の値をセットしたり取得したりするだけがほとんどで、GetNormalDataメソッドは全てのフィールドデータを返し、GetJsonDataはフィールド値をJSON形式にした文字列を返すだけです。
このSaveDataスクリプトで宣言されているhp、mp、power、flag、vector、objをJSON形式のデータに変換しセーブするデータとします。
実際はpublicか[SerializeField]アトリビュートを取り付けたpublicかprivateなフィールドデータしか保存されません。
アクセス修飾子の前にJavaScriptなら@SerializeField、C#なら[SerializeField]でシリアライズ出来るフィールドに出来ます。
SaveDataクラスのインスタンスを生成するCreateNewDataスクリプトの作成
SaveDataスクリプトはobjectクラスを継承して作成したので、ゲームオブジェクトに設定する事が出来ません。
そこで別のCreateNewDataスクリプトを作成し、このスクリプトからSaveDataのインスタンスを作成するようにします。
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 | using UnityEngine; using System.Collections; using System; public class CreateNewData : MonoBehaviour { private SaveData saveData; [SerializeField] private GameObject obj; // スタート時にデータを生成する void Start() { saveData = new SaveData (); // 初期データをセット saveData.SetHp(10); saveData.SetMp(20); saveData.SetPower(30); saveData.SetFlag(true); saveData.SetVector (new Vector3(1f, 3f, 6f)); saveData.SetObj (obj); } public SaveData GetSaveData() { return saveData; } } |
Startメソッドで適当なSaveDataクラスのインスタンスを作成し、適当なデータを作成して保持します。
SaveDataのフィールドを個別に設定していますが、SaveDataにコンストラクタを作成し設定してもかまいません。
外部スクリプトからSaveDataのデータを取得したい時はGetSaveDataメソッドを使ってアクセスするようにします。
ボタンを押した時に呼び出すスクリプトProcessingUIの作成
次はボタンを押した時、入力フィールドを変更した時に呼び出すスクリプトProcessingUIを作成していきます。
このスクリプトはボタンが押された時や入力フィールドを変更した時にSaveDataスクリプトのデータを整形し、UIテキストに表示する為のものです。
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 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 | using UnityEngine; using System.Collections; using UnityEngine.UI; public class ProcessingUI : MonoBehaviour { // データ表示テキスト [SerializeField] private Text dataText; // データを生成し保持しているスクリプト private CreateNewData createNewData; [SerializeField] private InputField hpField; [SerializeField] private InputField mpField; [SerializeField] private InputField powerField; void Start () { createNewData = GetComponent <CreateNewData>(); } // データ表示のテキストを空にする public void ResetText() { dataText.text = ""; } // 現在のオブジェクトの変数のデータを表示する public void ShowParameter() { ResetText(); dataText.text = createNewData.GetSaveData ().GetNormalData(); } // 現在のオブジェクトのJSONデータを表示 public void ShowJsonData() { ResetText(); dataText.text = createNewData.GetSaveData ().GetJsonData(); } // 現在のオブジェクトのJSONデータを保存する public void SaveData() { ResetText(); PlayerPrefs.SetString("PlayerData", createNewData.GetSaveData ().GetJsonData()); } public void SetHp() { createNewData.GetSaveData ().SetHp(int.Parse(hpField.text)); } public void SetMp() { createNewData.GetSaveData ().SetMp(int.Parse(mpField.text)); } public void SetPower() { createNewData.GetSaveData ().SetPower(int.Parse(powerField.text)); } // データをロードしてオブジェクトのフィールドにデータを入れる public void LoadFromJsonOverwrite() { ResetText(); if(PlayerPrefs.HasKey("PlayerData")) { var data = PlayerPrefs.GetString("PlayerData"); JsonUtility.FromJsonOverwrite(data, createNewData.GetSaveData ()); dataText.text = createNewData.GetSaveData ().GetJsonData (); } } // データをロードしインスタンスを生成する public void CreateData() { ResetText (); if(PlayerPrefs.HasKey("PlayerData")) { var data = PlayerPrefs.GetString("PlayerData"); SaveData otherSaveData = JsonUtility.FromJson<SaveData> (data); dataText.text = otherSaveData.GetJsonData (); } } // データを削除する public void DeleteData() { ResetText (); //PlayerPrefs.DeleteKey("PlayerData"); } } |
CreateNewDataスクリプトは先ほど作成したスクリプトでここからSaveDataスクリプトを参照するようにしています。
メソッドのそれぞれの役割は、
ResetTextはテキストを空にする。
ShowParameterはSaveDataの全データをそのまま表示する。
ShowJsonDataはSaveDataのJSON形式に変換したデータを表示する。
SaveDataはSaveDataのデータをPlayerPrefsに保存する。
SetHp、SetMp、SetPowerはテキストの値でSaveDataの値を書き換える。
LoadFromJsonOverwriteはPlayerPrefsから取得したデータをSaveDataのデータに上書きする。
CreateDataはPlayerPrefsから取得したデータをSaveDataクラスとして返す。
DeleteDataはPlayerPrefsのデータを削除します。
となります。
これらのメソッドはButtonを押した時やInputFieldを操作した時に呼び出し実行します。
ButtonやInputFieldのUIはこの後作成します。
ゲームデータの確認サンプルUIの作成
ではこれらのスクリプトを実行する為のサンプルを作成していきます。
実際にデータを書き込めるかというサンプルなので詳細な作り方の説明は割愛させて頂きます。
↑のような構成で『データ表示部』『データ入力部』『データ処理ボタン部』とわけて作成します。
データ表示部はデータ処理ボタンが押された時にそのデータを表示する部分です。
構成は↑のようになります。
CanvasにProcessingUIスクリプトとCreateNewDataスクリプトを設定します。
それぞれ対応するTextやInputFieldを設定し、CreateNewDataのインスペクタではMain Cameraを設定するようにしました。
イベントの設定
ButtonPanelの子要素のボタン群のOn ClickにはProcessingUIスクリプトの対応するメソッドを呼び出すように設定します。
ボタンを押した時のイベントの実行に関しては
を参照してください。
ShowParameterボタンはShowParameterメソッドを設定。
ShowJSONDataボタンはShowJsonDataメソッドを設定します。
LoadFromJsonOverwriteボタンはLoadFromJsonOverwriteメソッドを設定します。
CreateNewDataボタンはCreateNewDataメソッドを設定します。
SaveボタンはSaveDataメソッドを設定します。
DeleteボタンはDeleteDataメソッドを設定します。
次にInputFieldの入力を終えた時のイベントOnEndEditイベントの所でProcessingUIスクリプトのメソッドを呼び出します。
HPFieldはSetHpメソッドを設定します。
MPFieldはSetMpメソッドを設定します。
PowerFieldはSetPowerメソッドを設定します。
これで機能が完成したので、Unityの実行ボタンを押してデータを確認してみましょう。
↑のようになりました。
ちょっとわかり辛いですが、JSON形式でデータの保存が出来ているのがわかると思います。
長ったらしく書いていますが、JSON形式でまとめて保存しなくても
1 2 3 | PlayerPrefs.SetInt("HP", 10); |
と個別で保存するのでもいいと思います。
privateやstaticで宣言されたデータは保存されないので間違ってprivateで宣言していてデータを読み出したけど前のデータのままだった
なんて事も起こりえますのでprivateでもデータを保存する場合は[SerializeField]アトリビュートを取り付けておきます。
ゲームデータをゲームフォルダにファイルとして保存する
PlayerPrefsを使うとゲームデータの保存は出来ますが、ゲームフォルダには保存されません。
そこでゲームデータをゲームフォルダ内に指定したファイル名で保存してみたいと思います。
UnityWebplayerやWebGL形式のゲームではファイル操作が出来ないと思うので対象外です(PlayerPrefsを使ってください)。
この処理に関しては
を参考にさせて頂きました。
ファイル操作のサンプルUIを作成する
まずはファイル操作をする時に使うUIを作成していきます。
↑のようにCanvasを作り、子要素にSaveボタン(UI→Button)、Loadボタン(UI→Button)、Dataフィールド(UI→InputField)を作成します。
↑がUIの位置を調整した画像です。
Data(InputField)を選択して、インスペクタのInputFieldのLine TypeをMulti Line Newlineに変更します。
これは入力フィールド内でEnterキーを押して改行した場合に次の行に移りそのまま入力が出来るようにする為です。
データをファイルに保存したり読み出したりするSaveLoadFileスクリプトの作成
それではデータをファイルに保存したり読みだしたりするスクリプトSaveLoadFileを作成します。
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 66 67 68 | using UnityEngine; using System.Collections; using UnityEngine.UI; using System.IO; using System.Runtime.Serialization.Formatters.Binary; using System; public class SaveLoadFile : MonoBehaviour { // 入力フィールド public InputField inputField; // ファイルストリーム private FileStream fileStream; // バイナリフォーマッター private BinaryFormatter bf; public void Save() { bf = new BinaryFormatter(); fileStream = null; try { // ゲームフォルダにfiledata.datファイルを作成 fileStream = File.Create(Application.dataPath + "/filedata.dat"); // クラスの作成 Data data = new Data(); // 入力フィールドのテキストをクラスのデータに保存 data.dataText = inputField.text; // ファイルにクラスを保存 bf.Serialize(fileStream, data); } catch(IOException e1) { Debug.Log("ファイルオープンエラー"); } finally { if(fileStream != null) { fileStream.Close(); } } } public void Load() { bf = new BinaryFormatter(); fileStream = null; try { // ファイルを読み込む fileStream = File.Open(Application.dataPath + "/filedata.dat", FileMode.Open); // 読み込んだデータをデシリアライズ Data data = bf.Deserialize(fileStream) as Data; inputField.text = data.dataText; } catch(FileNotFoundException e1) { Debug.Log("ファイルがありません"); } catch(IOException e2) { Debug.Log("ファイルオープンエラー"); } finally { if(fileStream != null) { fileStream.Close(); } } } // 保存するデータクラス [Serializable] class Data { public string dataText; } } |
ファイルの読み書きをするにはC#のファイル読み書きオブジェクトを使います(Unityのスクリプトファイルには載っていない)。
C#を使ったことがある方は問題はなさそうですね。
行っていることはSaveメソッドが呼ばれた時にApplication.dataPathでゲームフォルダを取得しファイル名を指定してファイルの作成、
保存するデータをBinaryFormatterでシリアライズしてファイルに保存しています。
IOSやAndroidの場合はApplication.persistentDataPathでデバイスのパブリックなディレクトリにアクセス出来るようです。
保存するデータはデータ用クラスを作成し、public変数で宣言したデータを格納するようになっています。
try-catch-finallyは例外処理でファイルを作成する時に例外が発生した時の処理を記述しています(最後のファイル閉じの例外処理してないかも・・・)。
ここら辺はJavaやC#の本を見た方がいいかも・・・(^_^;)
辺りも参考にしてみてください。
今回作成したSaveメソッドでは常に新しいファイルを作成する仕様になっていますが、ファイルが存在する時は上書きにするといったように書き換えるといいと思います。
Loadメソッドは保存したデータを読み出してデシリアライズし入力フィールドDataに表示しているだけです。
これでスクリプトが出来たので、Canvasにこのスクリプトを設定してください。
SaveLoadFileスクリプトのインスペクタのinputFieldにはData(InputField)を設定してください。
SaveボタンのOnClickイベントにはCanvasのSaveLoadFileスクリプトのSave関数を設定し、
LoadボタンのOnClickイベントにはCanvasのSaveLoadFileスクリプトのLoad関数を設定します。
イベントに関しては
を参照してください。
ファイルが出力されるか確認する
それではUnityの実行をして確認してみましょう。
↑のように入力フィールドに文字を書いた後Saveボタンを押しファイルに書き込んで入力フィールド内を変更、
そしてLoadボタンを押すと保存したファイルを読み込んで表示する為さきほどの文字列が再び表示されます。
UnityEditorを実行中にApplication.dataPath以下にファイルを作成するとAssets以下にファイルが作成されるので、確認してみてください。
スタンドアロン形式でビルドし、名前をaとするとa.exeとa_Dataというフォルダが同じ階層に出来ます。
ゲームを実行してファイルを作成すると
↑のようにゲームフォルダ(a_Data)以下にゲームデータファイルが作成されていることが確認出来ました。
これでゲームデータをファイルに出力出来ることがわかりました。
終わりに
外部ファイルにデータを保存するのはゲームデータの容量が大きくなった時だと思います。
小規模のゲームならPlayerPrefsを使うだけでいいような気がします。
(PlayerPrefsでデータを保存出来るのは10MBぐらい?)
ファイルを操作するのってなんか怖いですもんね(^_^;)
この記事はずいぶん前に書いた(と言っても2カ月ぐらい前)内容で本日あらためて確認の為に記事を読み直しましたが、なんだかサンプルがわかり辛いですね・・・。
もっと簡単にセーブ、ロードが確認出来るサンプルにすればよかったと思いました。
思ったなら簡単なサンプルを作成せい!(-_-)/~~~ピシー!ピシー!
サンプルの部分を考えなければデータのセーブとロードの部分が楽に出来るというのが解っていただけるかも?