Unity

UnityでJsonファイルを使ってセーブする方法+Listを使った入れ子構造

TETRY 3Dで使われている技術 3

こんにちはGE planetです。

今回はUnityのセーブ方法であるPlayersPrefsではなくJsonを使って大量のデータをセーブする方法を紹介します。同様の記事がすでにいくつもありますが、忘れないようにするためと汎用的に使えるように整理する意味も込めて、今回紹介致します。

コンテンツ

構造

TETRY 3Dで使われているセーブは2種類あります。1つはPlayersPrefsに暗号化を施した、小さいデータ向きのセーブ。これは音量や音楽名、その他セッティングなど頻繁に変更されるものに使用しています。

もう1つはJsonファイルに書き出すもので、ゲームの状況をセーブする時に使います。

早速Jsonセーブのファイルを見ていきます。

構成ファイルは以下のとおりです。

  • SaveManager.cs
  • SaveData.cs
  • SingletonMonoBehaviour.cs

この内の「SingletonMonoBehaviour」は

で解説されています。そのまま使わせて頂きました。

シングルトンとは1つしかないオブジェクトのことで、Unity上ではFindやGetCompornentをしないでも利用できるようになります。

「SaveManager」はこれを継承して使います。

SaveManager

まずは中身を見ます。

using System.Collections.Generic;
using System.IO;
using UnityEngine;

public class SaveManager : SingletonMonoBehaviour<SaveManager>
{
    SaveData _data;
    public string[] field1 = new string[2], field2 = new string[2], field3 = new string[2], field4 = new string[2];
    public List<string[]> FIELD1;

    // Use this for initialization
    protected override void Awake()
    {
        if (this != Instance)
        {
            Destroy(this);
            return;
        }

        DontDestroyOnLoad(this.gameObject);

    }

    // Use this for initialization
    void Start()
    {
        FIELD1 = new List<string[]>() { field1, field2, field3, field4 };

    }

    // Update is called once per frame
    void Update()
    {

    }

    public void GameDataSending()
    {
        //データの初期化
        _data = new SaveData();
        _data.main();

        //ここからセーブしたいデータを格納する.
        int p = 0;
        foreach (string[] g in FIELD1)
        {
            int e = 0;
            foreach (string h in g)
            {
                _data.FIELD1[p][e] = h;
                e += 1;
            }
            p += 1;
        }
        p = 0;

        SaveGame();

    }

    public void GameDateReceiving()
    {
        _data = new SaveData();
        _data = LoadFromJson(SaveData.FilePath);
        _data.main();

        FIELD1 = _data.FIELD1;
    }

    /// <summary>
    /// ファイル書き込み
    /// </summary>
    /// <param name="filePath">ファイルのある場所</param>
    public void SaveToJson(string filePath, SaveData data)
    {
        using (FileStream fs = new FileStream(filePath, FileMode.Create, FileAccess.Write))
        {
            using (StreamWriter sw = new StreamWriter(fs))
            {
                sw.WriteLine(JsonUtility.ToJson(data));
                sw.Flush();
                sw.Close();
            }
            fs.Close();
        }
    }

    /// <summary>
    /// ファイル読み込みする
    /// </summary>
    /// <param name="filePath">ファイルのある場所</param>
    /// <returns></returns>
    public SaveData LoadFromJson(string filePath)
    {
        if (!File.Exists(filePath))
        {//ファイルがない場合FALSE.
            Debug.Log("FileEmpty!");
            return new SaveData();//ファイルが無いときはnewする.
        }

        using (FileStream fs = new FileStream(filePath, FileMode.Open, FileAccess.Read))
        {
            using (StreamReader sr = new StreamReader(fs))
            {
                SaveData sd = JsonUtility.FromJson<SaveData>(sr.ReadToEnd());
                if (sd == null) return new SaveData();
                return sd;
            }
        }
    }

    public void SaveGame()
    {
        SaveToJson(SaveData.FilePath, _data);
    }

    public void DeleteSave()
    {
        File.Delete(SaveData.FilePath);
    }

}

まずは「public class SaveManager : SingletonMonoBehaviour<SaveManager>」で継承します。

これはUnityのゲーム内に置くオブジェクトです。Jsonへの書き込みはMonoBehaviourを継承しないネイティブなスクリプトのデータを使う必要があります。

そこで「SaveData」オブジェクトをインスタンス化して書き込むためのクラスが「SaveManager」です。

シングルトンなオブジェクトなのでAwake()でDontDestroyOnLoadします。こうすることでシーン間をまたいでも破壊されず、1個体で有り続けます。

今回は配列の配列(array[][])をListに格納してUnityで扱いやすくしたセーブデータ構造を取り扱います。Unityでは2次までの配列しか使えなかったため、Listに格納したのですが今はどうなんでしょうか?

ともあれ

public string[] field1 = new string[2], field2 = new string[2], field3 = new string[2], field4 = new string[2];
public List<string[]> FIELD1;

    void Start()
    {
        FIELD1 = new List<string[]>() { field1, field2, field3, field4 };

    }

配列たちの初期化を行います。TETRY 3Dでは2000個のオブジェクトの情報を格納するために「field」を2000個用意しました。自動で宣言する方法はあるのでしょうか…

    public void GameDataSending()
    {
        //データを初期化
        _data = new SaveData();
        _data.main();

        //ここからセーブしたいデータを格納する.
        int p = 0;
        foreach (string[] g in FIELD1)
        {
            int e = 0;
            foreach (string h in g)
            {
                _data.FIELD1[p][e] = h;
                e += 1;
            }
            p += 1;
        }
        p = 0;

        SaveGame();

    }

    public void SaveGame()
    {
        SaveToJson(SaveData.FilePath, _data);
    }

ゲームをセーブするにはまず別のスクリプトでデータを「SaveManager」に移します。例えば、

    public void SaveTest()
    {
        SaveManager.Instance.FIELD1[0][0] = "あたま";
        SaveManager.Instance.FIELD1[0][1] = "いぬ";
        SaveManager.Instance.FIELD1[1][0] = "うしろ";
        SaveManager.Instance.FIELD1[1][1] = "えにっき";
        SaveManager.Instance.FIELD1[2][0] = "おや";
        SaveManager.Instance.FIELD1[2][1] = "かげ";
        SaveManager.Instance.FIELD1[3][0] = "きこり";
        SaveManager.Instance.FIELD1[3][1] = "くない";
        SaveManager.Instance.GameDataSending();
    }

このようにします。シングルトンな「SaveManager」はどのスクリプトからも「SaveManager.Instance.」でアクセス出来ます。

データの入力が終わったら「GameDataSending()」を実行します。

「GameDataSending()」内ではさらに受け取ったデータを「SaveData」へ格納する処理が行われています。

最後に「SaveGame()」によって呼ばれるのが、

    /// <summary>
    /// ファイル書き込み
    /// </summary>
    /// <param name="filePath">ファイルのある場所</param>
    public void SaveToJson(string filePath, SaveData data)
    {
        using (FileStream fs = new FileStream(filePath, FileMode.Create, FileAccess.Write))
        {
            using (StreamWriter sw = new StreamWriter(fs))
            {
                sw.WriteLine(JsonUtility.ToJson(data));
                sw.Flush();
                sw.Close();
            }
            fs.Close();
        }
    }

「SaveToJson(string filePath, SaveData data)」です。

FileStreamを利用して作成したJsonファイルに書き込み保存します。

読み込みの手順は逆にすれば良いので、

    public void GameDateReceiving()
    {
        _data = new SaveData();
        _data = LoadFromJson(SaveData.FilePath);
        _data.main();

        FIELD1 = _data.FIELD1;
    }

    /// <summary>
    /// ファイル読み込みする
    /// </summary>
    /// <param name="filePath">ファイルのある場所</param>
    /// <returns></returns>
    public SaveData LoadFromJson(string filePath)
    {
        if (!File.Exists(filePath))
        {//ファイルがない場合FALSE.
            Debug.Log("FileEmpty!");
            return new SaveData();//ファイルが無いときはnewする.
        }

        using (FileStream fs = new FileStream(filePath, FileMode.Open, FileAccess.Read))
        {
            using (StreamReader sr = new StreamReader(fs))
            {
                SaveData sd = JsonUtility.FromJson<SaveData>(sr.ReadToEnd());
                if (sd == null) return new SaveData();
                return sd;
            }
        }
    }

「SaveData」をnewしたあとに「LoadFromJson(string filePath)」でJsonファイルを「SaveData」に格納します。それからListをインスタンス化して、「SaveManager」のListにそのまま貼り付ければUnityのオブジェクトに格納できます。

それでは「SaveData」を見てみましょう。

SaveData

using System.Collections.Generic;
using UnityEngine;

[SerializeField]
public class SaveData
{

    private static string filePath = Application.persistentDataPath + "/savedata.json";//セーブデータのファイルパス
    public static string FilePath
    {//ファイルパスのプロパティ
        get { return filePath; }
    }

    //ここにSaveManagerと同じ構造の変数を用意する.
    public string[] field1 = new string[2], field2 = new string[2], field3 = new string[2], field4 = new string[2];
    public List<string[]> FIELD1;


    public void main()
    {
        //Listなど初期化
        FIELD1 = new List<string[]>() { field1, field2, field3, field4 };
    }

}

まずはファイルの保存場所をFilePathに格納します。今回は「persistentDataPath」を使用しています。ファイル名はお好みに。

データの構造はSaveManagerと全く同じにする必要があります。

また、Main()内に必要な処理を入れておいて、データ移動前に実行すると良いと思います。

SingletonMonoBehaviour

using UnityEngine;

public class SingletonMonoBehaviour<T> : MonoBehaviourWithInit where T : MonoBehaviourWithInit
{
    private static T instance;
    public static T Instance
    {
        get
        {
            if (instance == null)
            {
                instance = (T)FindObjectOfType(typeof(T));

                if (instance == null)
                {
                    Debug.LogError(typeof(T) + "is nothing");
                }
            }
            else
            {
                instance.InitIfNeeded();
            }
            return instance;
        }
    }

}

public class MonoBehaviourWithInit : MonoBehaviour
{

    //初期化したかどうかのフラグ(一度しか初期化が走らないようにするため)
    private bool _isInitialized = false;

    /// <summary>
    /// 必要なら初期化する
    /// </summary>
    public void InitIfNeeded()
    {
        if (_isInitialized)
        {
            return;
        }
        Init();
        _isInitialized = true;
    }

    /// <summary>
    /// 初期化(Awake時かその前の初アクセス、どちらかの一度しか行われない)
    /// </summary>
    protected virtual void Init() { }

    //sealed overrideするためにvirtualで作成
    protected virtual void Awake() { }

}

Awake()のチェックを省略しています。詳しくはリンク先を御覧ください。

使い方

使い方はまず「SaveManager」を空のGame Objectに貼り付けてください。

それからゲーム側のスクリプトでセーブしたいデータを「SaveManager」で用意した変数に格納していきます。

すべて格納したら「SaveManager.Instance.GameDataSending()」を実行します。

ロードのときは「SaveManager.Instance.GameDateReceiving()」を実行したあとに、「SaveManager」内のデータをゲーム側のスクリプトに格納します。

データはList化されているのでfor文などで1つづつ取り出すと良いでしょう。

まとめ

今回大量かつ複雑な構造のデータをセーブアンドロードする方法をご紹介しました。

今回は高速化を理由に暗号化やハッシュ化は省いていますが、「GameDataSending()」の中で「GameSave()」する前に色々と処理すればできるかもしれません。試したことはありません。

JsonなのでListやジャグ配列に限らず様々な構造をセーブできますが、stringの形での書き込みになるのでそこだけ要確認ですね。

以上で第3回TETRY 3Dに用いられる技術を終わります。

それではご精読ありがとうございました。

GE Planetでした。



0件のコメントを表示

コメントする