【Unity】Jsonファイルとしてデータを保存する

##はじめに
このアカウントは、プログラムについて勉強し始めて1年立たずの専門学校生によるものです。
新たに学んだ技術や知識をなるべくわかりやすくまとめておこうと思います。
アドバイス・ご指摘などありましたら、どうぞご遠慮無く。
##データの保存
ゲーム制作をしていると必ずぶつかる壁にデータの保存があると思います。
今回は、私がゲーム制作で使用した保存方法とスクリプトをご紹介します。

##Jsonファイルとしてデータを保存・読み書きする
データの保存方法にも何種類かありますが、今回はJsonファイルで保存する方法を使ってみました。
今回のスクリプトはこちら[title](https://kan-kikuchi.hatenablog.com/entry/Json_SaveData)の記事を参考にさせていただきました。
（kanのメモ帳-個人ゲーム開発者kan.kikuchiのメモ的技術ブログ-/クラスを丸ごと保存するデータ管理方法【Unity】）

##スクリプト
魚を取るゲームを制作していたので、所々魚関係の変数や名前が出てきます。

```c#:PlayerDataInstance.cs
using System.Collections.Generic;
using UnityEngine;
using System;
using System.IO;
using System.Runtime.Serialization.Formatters.Binary;
using UnityEditor;

/// <summary>
/// .jsonファイルでプレイヤーデータを保存する
/// </summary>
[Serializable]
public class PlayerDataInstance : ISerializationCallbackReceiver
{
    //プレイヤーデータの実態、初アクセス時にデータをロード
    private static PlayerDataInstance _instance = null;
    public static PlayerDataInstance Instance
    {
        get
        {
            if (_instance == null)
            {
                Load();
            }
            return _instance;
        }
    }

    [SerializeField]
    private static string _jsonText = "";

    //================================================
    //保存されるデータ
    //================================================

    [SerializeField]
    int                             money;         // 所持金
    [SerializeField]
    string                          lanceName;  // 装備しているLanceStatusDataの名前
    [SerializeField]
    private string                  _fishDictJson = "";
    public Dictionary<string, Fish> FishDict = new Dictionary<string,Fish>();// 銛で突いた魚の名前と数を保存

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

    /// <summary>
    /// PlayerData->Jsonに変換される前に実行される。
    /// </summary>
    public void OnBeforeSerialize()
    {
        //Dictionaryはそのまま保存されないので、個別にシリアライズしてテキストで保存
        _fishDictJson = Serialize(FishDict);
    }


    /// <summary>
    /// Json->PlayerDataに変換された後に実行される。
    /// </summary>
    public void OnAfterDeserialize()
    {
        //保存されているテキストがあればDictionaryにデシリアライズ
        if (!string.IsNullOrEmpty(_fishDictJson))
        {
            FishDict = Deserialize<Dictionary<string, Fish>>(_fishDictJson);
        }
    }

    /// <summary>
    /// 引数のオブジェクトをシリアライズして返す
    /// </summary>
    /// <typeparam name="T"></typeparam>
    /// <param name="obj"></param>
    /// <returns></returns>
    private static string Serialize<T>(T obj)
    {
        BinaryFormatter binaryFormatter = new BinaryFormatter();
        MemoryStream memoryStream       = new MemoryStream();
        binaryFormatter.Serialize(memoryStream, obj);
        return Convert.ToBase64String(memoryStream.GetBuffer());
    }
    /// <summary>
    /// 引数のテキストを指定されたクラスにデシリアライズして返す。
    /// </summary>
    /// <typeparam name="T"></typeparam>
    /// <param name="str"></param>
    /// <returns></returns>
    private static T Deserialize<T>(string str)
    {
        BinaryFormatter binaryFormatter = new BinaryFormatter();
        MemoryStream memoryStream       = new MemoryStream(Convert.FromBase64String(str));
        return (T)binaryFormatter.Deserialize(memoryStream);
    }

    //================================================
    //取得
    //================================================

    /// <summary>
    /// データを再読み込みする。
    /// </summary>
    public void Reload()
    {
        JsonUtility.FromJsonOverwrite(GetJson(), this);
    }

    /// <summary>
    /// データを読み込む。
    /// </summary>
    private static void Load()
    {
        _instance = JsonUtility.FromJson<PlayerDataInstance>(GetJson());
    }


    private static string GetJson()
    {
        //既にJsonを取得している場合はそれを返す。
        if (!string.IsNullOrEmpty(_jsonText))
        {
            return _jsonText;
        }

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

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

        return _jsonText;
    }

    //================================================
    //保存
    //================================================

    /// <summary>
    /// データをJsonにして保存する。
    /// </summary>
    public void Save()
    {
        _jsonText = JsonUtility.ToJson(this);
        File.WriteAllText(GetSaveFilePath(), _jsonText);
    }

    //================================================
    //削除
    //================================================

    /// <summary>
    /// データをすべて削除し、初期化する。
    /// </summary>
    public void Delete()
    {
        _jsonText = JsonUtility.ToJson(new PlayerDataInstance());
        Reload();
    }

    //================================================
    //保存先のパス
    //================================================

    private static string GetSaveFilePath()
    {
        string filePath = "PlayerDataInstance";
        //確認しやすいようにエディタではAssetsと同じ階層に保存
        //それ以外ではApplication.persistentDataPath以下に保存するように。
#if UNITY_EDITOR
        filePath += ".json";
#else
        filePath = Application.persistentDataPath + "/" + filePath;
#endif
        Debug.Log(filePath);
        return filePath;
    }

    //================================================
    //PlayerDataUtility
    //================================================

    /// <summary>
    /// 所持金を取得する。
    /// </summary>
    /// <returns></returns>
    public int GetMoney()
    {
        return money;
    }

    /// <summary>
    /// 装備品の名前を取得する
    /// </summary>
    /// <returns></returns>
    public string GetLanceName()
    {
        return lanceName;
    }

    /// <summary>
    /// 銛で獲った魚の名前と数を取得する
    /// </summary>
    /// <returns></returns>
    public Dictionary<string,Fish> GetFish()
    {
        return FishDict;
    }

    /// <summary>
    /// 保存するデータを設定する。
    /// </summary>
    /// <param name="data"></param>
    public void SetPlayerData(PlayerData data)
    {
        LanceStatusData _lsd = data.GetLance();
        lanceName            = _lsd.GetEquipmentName();
        money                = data.GetMoney();
        FishDict             = data.GetFish();
    }
}
```

##使用例
上のスクリプトを実際に使用したものを以下に記載します。

```C#:PlayerData.cs
using System.Collections.Generic;
using UnityEngine;
using System;
using System.IO;
using System.Runtime.Serialization.Formatters.Binary;
using UnityEditor;

[Serializable]
public class PlayerData : MonoBehaviour
{
//（中略）
    private void Start()
    {
        //保存しておいたデータを取得する
        //static変数を取得
        PlayerDataInstance _pInstance = PlayerDataInstance.Instance;
        dataList                      = gameObject.GetComponent<DataList>();

        //各データを保存していたファイルから取得
        string lanceName = _pInstance.GetLanceName();

        //初期状態だとlanceNameが""なので、初期装備を設定
        //lanceNameが存在するなら該当する装備を取得
        if (lanceName == "")
        {
            lance          = dataList.GetLance("ボロのモリ");
            lance.IsBought = true;
        }
        else
        {
            lance = dataList.GetLance(lanceName);
        }
        money  = _pInstance.GetMoney();
        fishes = _pInstance.GetFish();
    }
//以下省略
}
```

このように、PlayerDataInstanceで設けたstatic変数を呼び出せば、保存していたデータを取得することができます。
データの上書き保存やデータ削除も同様に、static変数から“Save()”、“Delete()”を呼び出すことで可能になります。

##使用上の注意
ここでは実際に私が失敗したり悩んだ部分、ほかの使い方などを書いておきます。

######InstanceDataのスクリプト（1つ目のスクリプト）はオブジェクトにアタッチできない
スクリプトの属性がMonoBehaviorではないため、オブジェクトにアタッチすることができません。
その代わり、オブジェクトにアタッチしなくても、スクリプトがプロジェクト内に存在すれば機能してくれます。

######InstanceDataのスクリプトのみで十分使用可能
作っていたゲームの構造上、プレイヤーデータ本体（2つ目のスクリプト）と保存部分（1つ目のスクリプト）を分けましたが、保存部分のみでも使用可能です。
例えば、ゲームを通して常にプレイヤーが存在する場合（RPGなど）はstatic変数を呼び出すことで問題なく動作するはずです。

######.jsonファイルは開発中はプロジェクトのフォルダ内に作成される
.jsonファイルはプロジェクトのフォルダ（Assetsなどと同じ）内に生成されます。
DataPathを見ていただければわかるかと思いますが、iOSのみ保存場所が異なります。

##まとめ
データの保存には、ほかにもいろいろな方法があります。
例えば、Unityに実装されているPlayerPrefsを利用する方法なんかが有名です。
また、改善点などありましたらコメントにてご教授いただけると、私のスキルアップにつながります。
ぜひよろしくお願いいたします。