(:3[kanのメモ帳]

個人ゲーム開発者kan.kikuchiのメモ的技術ブログ。月木更新でUnity関連がメイン。

(:3[kanのメモ帳]


本ブログの運営者kan.kikuchiが個人で開発したゲームです!

    

クラスを丸ごと保存するデータ管理方法【Unity】


このエントリーをはてなブックマークに追加


この記事はUnity Advent Calendar 2016の15日目の記事です。


この記事でのバージョン
Unity 5.4.0f3


はじめに

今回はタイトル通り、クラスを丸ごと保存するデータの管理方法のご紹介です!


ざっくり言うと、永続化したいデータを一つ一つ保存したりするのは面倒だから、

一つのクラスにまとめちゃってそれをJSONで保存しちゃおうという試み。


例えば以下のような感じで、

SaveData.Instanceを介して簡単にデータの取得、変更、保存などができる感じです。

//データの取得
int    value = SaveData.Instance.SampleInt;
string text  = SaveData.Instance.SampleString;

//データの変更
SaveData.Instance.SampleInt    = 5;
SaveData.Instance.SampleString = "テスト";

//全データの保存
SaveData.Instance.Save ();

//全データの再読み込み
SaveData.Instance.Reload ();

//全データの削除
SaveData.Instance.Delete ();

一括でデータの管理が行えるので、

異なるデータを保存出来るような機能(セーブスロット?)みたいなものも、比較的簡単に実装できます。


また、後述するJsonUtilityのおかげで、

開発途中にデータの項目が増えたり減ったりしても、前の形式のデータが正常に読み込めます。


さらにJson形式で出力するので、

データを簡単に書き換えてテストできたりと、色々な面で便利な管理方法かと思います!


ただし、jsonのファイルを書き出す(SaveData.Instance.Save)のに時間がかかり、処理落ちが

発生する可能性があるので、画面の暗転時に保存するなど、タイミングに気を付ける必要があります。


SaveData

では早速コードです。



上記のファイルを作成するだけで、すぐに使えます。

自分でインスタンスを作成したり、GameObjectにaddしたりする必要もありません。


例えば上記のコードのまま、SaveData.Instance.Save();を実行すると、

以下のようにJsonが保存されます。

なお、確認しやすいようにエディタではAssetsと同じ階層に保存しています。


f:id:kan_kikuchi:20161124133054j:plain

{
    "SampleBool": false, 
    "SampleInt": 5, 
    "SampleIntList": [
        2, 
        3, 
        5, 
        7, 
        11, 
        13, 
        17, 
        19
    ], 
    "SampleString": "テスト", 
    "_sampleDictJson": "長いので省略"
}



保存するデータの設定

保存するデータを変更したい場合は、SaveDataの変数を変更します。


f:id:kan_kikuchi:20161124133212p:plain


なお、保存されるデータはpublicまたは[SerializeField]の付いた変数だけです。


また、publicであったり[SerializeField]を付けても、Dictionaryやobjectは保存されません。


なので例にあるSampleDictのように、OnBeforeSerializeでstringに変換、

OnAfterDeserializeでstringからDictionaryに変換することで、保存を可能にしています。


なお、OnBeforeSerializeとOnAfterDeserializeは

ISerializationCallbackReceiverを実装することより、変換前後に勝手に実行されます。

シリアライズやデシリアライズ時にコールバックを受信するインターフェース


ただし、この方法だと、Dictionaryの内容が出力したテキストをパッと見ただけでは分からないため、

Dictionaryの値を確認したり変更したりする事はできません。


f:id:kan_kikuchi:20161124134222j:plain


一応、Dictのkeyとvalueを保存するためのクラスを作成し、

そのリストを保存するような形にすれば、

テキストを使ってDictionaryの値を確認したり変更したりする事は可能です。

//=================================================================================
//保存されるデータ(public or SerializeFieldを付ける)
//=================================================================================

[Serializable]
private class SampleDictPair{
  public string Key;
  public int    Value;

  public SampleDictPair(KeyValuePair<string, int> pair){
    Key   = pair.Key;
    Value = pair.Value;
  }
}

[SerializeField]
private List<SampleDictPair> _sampleDictPairList = new List<SampleDictPair>();

//=================================================================================
//シリアライズ,デシリアライズ時のコールバック
//=================================================================================

/// <summary>
/// SaveData→Jsonに変換される前に実行される。
/// </summary>
public void OnBeforeSerialize(){
  _sampleDictPairList = new List<SampleDictPair>();

   foreach (KeyValuePair<string, int> pair in SampleDict) {
    _sampleDictPairList.Add (new SampleDictPair (pair));
  }
}

/// <summary>
/// Json→SaveDataに変換された後に実行される。
/// </summary>
public void OnAfterDeserialize(){
  if(_sampleDictPairList.Count != 0){
    SampleDict = new Dictionary<string, int> ();

    foreach (SampleDictPair sampleDictPair in _sampleDictPairList) {
      SampleDict [sampleDictPair.Key] = sampleDictPair.Value;
    }
  }
}


上記のコードを使って出力したjsonが以下の通りです。

{
    "_sampleDictPairList": [
        {
            "Key": "Key1",
            "Value": 50
        },
        {
            "Key": "Key2",
            "Value": 150
        },
        {
            "Key": "Key3",
            "Value": 550
        }
    ]
}


ただこのやり方でも、保存したいDictionaryのkeyとvalueの組み合わせだけ

保存用クラスを作成しなければいけないため、結構面倒です。


なお、保存用のクラス(SampleDictPair)を汎用的に使えるようにするために、

keyやvalueをジェネリックやobjectにするとシリアライズできなくなり、保存できなくなる

ので注意が必要です。


クラスや構造体の保存

先ほどのSampleDictPairのように、

Serializableが付いているクラスや構造体は、他の変数と同様に保存する事が可能です。


例えば以下のような感じ。

//=================================================================================
//保存されるデータ(public or SerializeFieldを付ける)
//=================================================================================

public PlayerData  PlayerData;
public SettingData SettingData;
[Serializable]
public class PlayerData{
  public string Name;
  public int HP, ATK;
}

[Serializable]
public class SettingData{
  public SystemLanguage Language;
  public bool IsMute;
}


上記のコードを使って出力したjsonが以下の通りです。

{
    "PlayerData": {
        "Name": "",
        "HP": 0,
        "ATK": 0
    },
    "SettingData": {
        "Language": 0,
        "IsMute": false
    }
}


ただし、先ほどと同様に保存される条件があるので、

例えばTransformを保存してもpositionやrotationは保存されず、instanceIDだけが保存されます。

//=================================================================================
//保存されるデータ(public or SerializeFieldを付ける)
//=================================================================================

public Transform Transform;
{
    "Transform": {
        "instanceID": -748432
    }
}



暗号化

開発中は保存データを任意に書き換えられると楽ですが、

リリース時にその状態では簡単にチートがが出来てしまいます。

なので、念のためにjsonは暗号化がしておいた方が良いでしょう。


という事で、以下のページを参考に暗号化用のクラス、StringEncryptorを作成してみました

パスワードで文字列を暗号化する: .NET Tips: C#, VB.NET



なお、暗号化&復号化時のパスワードはStringEncryptorのpassで指定しています。


このStringEncryptorを使って、SaveとGetJsonにそれぞれ暗号化と復号化を施すと以下のような感じになります。

/// <summary>
/// データをJsonにして保存する。
/// </summary>
public void Save(){
  _jsonText = JsonUtility.ToJson(this);
  File.WriteAllText (GetSaveFilePath(), StringEncryptor.Encrypt(_jsonText));
}
//保存しているJsonを取得する
private static string GetJson(){
  //既にJsonを取得している場合はそれを返す。
  if(!string.IsNullOrEmpty(_jsonText)){
    return _jsonText;
  }

  //Jsonを保存している場所のパスを取得。
  string filePath = GetSaveFilePath();

  //Jsonが存在するか調べてから取得し変換&復号化する。存在しなければ新たなクラスを作成し、それをJsonに変換する。
  if(File.Exists(filePath)){
    _jsonText = StringEncryptor.Decrypt(File.ReadAllText (filePath));
  }
  else{
    _jsonText = JsonUtility.ToJson(new SaveData ());
  }

  return _jsonText;
}


上記のコードを使って出力したjsonが以下の通りです。


f:id:kan_kikuchi:20161125134430p:plain


JsonUtility

今回の実装方法の核とも言えるのがJsonUtilityです。

JSON データを操作するためのユーティリティ関数


今の所、JsonUtilityが出来る事は3つだけ。

  • JSONからオブジェクトを作成する。 (FromJson)
  • JSONを読み取り、オブジェクトのデータを上書きする。(FromJsonOverwrite)
  • オブジェクトをJSONに変換する(ToJson)


なお、staticなオブジェクトは処理できないので、

SaveDataはStaicでなくシングルトンで実装しています。


また、最初の方にも書いた通り、

Jsonの形式と変換先のクラスの形式が完全に一致していなくても問題ありません。


たとえば以下のような例で、Jsonからクラスに変換すると、

{
    "SampleInt1": 111,
    "SampleInt2": 222
}
public int SampleInt2 = 2;
public int SampleInt3 = 3; 


  1. SampleInt1はクラス側に存在しないので無視される。
  2. SampleInt2は両方に存在するのでjsonの値(222)が設定される。
  3. SampleInt3はjsonに存在しないのでクラス側の初期値(3)が設定される。



といった具合です。

なので、データを後から追加、削除してもエラーが出ないというわけです。


おわりに

Jsonで保存する事とは直接関係ありませんが、今回の紹介した実装方法は簡易的なものなので、

そのままだとどこからでも取得&変更が可能な点には注意してください。