【Unity(C#)】JsonUtilityを使ったセーブ、ロードの実装

##JsonUtility
Unityの用意してくれた機能です。

JsonUtilityを利用してセーブとロードの機能を実装していきます。

今回のセーブ、ロードはアプリを終了しても
データが保存されている、読み込める状態を指します。

下記リンクのお絵描きした絵のデータをセーブ、ロードしていきます。

【参考リンク】：[【Unity(C#)】ハンドトラッキングで簡易版VRお絵かきアプリ](https://qiita.com/OKsaiyowa/items/b5045fc240d1d9a57880)

##JSONとは
>JSON (JavaScript Object Notation)は、軽量のデータ交換フォーマットです。人間にとって読み書きが容易で、マシンにとっても簡単にパースや生成を行なえる形式です。

【参考リンク】：[JSON の紹介](https://www.json.org/json-ja.html)

描いた絵のデータ(TrailRendererのパラメータ)をJSON形式に変換して保存、
保存したJSON形式のデータを読み込んで、TrailRendererのパラメータに再設定する　
ということを行えばセーブ、ロードが可能になります。

##シリアライズ、デシリアライズ

>シリアライズとは、複数の並列データを直列化して送信することである。

>具体的には、メモリ上に存在する情報を、ファイルとして保存したり、ネットワークで送受信したりできるように変換することである。他方、既にファイルとして存在しているデータや、一旦シリアライズされたデータがネットワークから送られてきた際に、プログラムで扱えるようにする作業をデシリアライズと呼ぶ。

【参考リンク】：[辞典・百科事典の検索サービス - Weblio辞書](https://www.weblio.jp/content/%E3%82%B7%E3%83%AA%E3%82%A2%E3%83%A9%E3%82%A4%E3%82%BA)

今回の例で言うと

描いた絵のデータ(TrailRendererのパラメータ)を
`JSON形式に変換してファイルへの保存に利用可能に`→**シリアライズ**
`保存したJSON形式のデータを読み込んでUnity(C#)で利用可能に`→**デシリアライズ**
となります

##コード

前提として、今回作成したお絵描きアプリは
一筆ごとにTrailRendererを生成しています。

実装の順序としては下記です。
**①TrailRendererのパラメータを保存する構造体を作成
②複数個存在するTrailRendererそれぞれのパラメータをまとめてリスト化
③作成したリストをJson形式にシリアライズし保存
④読み込み時にデシリアライズし、リスト分のTrailRendererを再生成して設定**

---

では順を追って見ていきます
まずデータを入れる構造体を定義します。
こちらの構造体にはシリアライズを可能にするために
`[Serializable]`というアトリビュートが必要です。

```C#
using System;
using UnityEngine;

/// <summary>
/// ペイントデータを入れる構造体
/// </summary>
[Serializable]
public struct PaintData
{
    /// <summary>
    /// TrailRendererの頂点用配列
    /// </summary>
    public Vector3[] PaintVertices;

    /// <summary>
    /// Paintオブジェクト生成された位置
    /// </summary>
    public Vector3 PaintObjectPosition;
    
    /// <summary>
    /// Paintの色情報用
    /// </summary>
    public Color PaintColor;
}
```

---

次に、リストを作成します。
わざわざクラスを用意してリストを定義しているのは、
**リストを直接Json形式にシリアライズすることができない**からです。
やってみると空のリストが返ってきました。

なので、クラスをJson形式にシリアライズする形をとっています。

こちらにも`[Serializable]`をつけます。

```C#
using System;
using System.Collections.Generic;

/// <summary>
/// Jsonとして扱うデータ　リストを利用するためにクラスでラップ
/// </summary>
[Serializable]
public class PaintDataWrapper
{
    /// <summary>
    /// ペイントデータを入れるリスト
    /// </summary>
    public List<PaintData> DataList = new List<PaintData>();
}
```

---

つづいて、シリアライズ、デシリアライズのロジックです。
usingステートメントは外部リソースにアクセスするときに便利らしいです。

外部リソースを利用したあと、勝手に利用停止してくれる、みたいな感じです。
メモリリークと呼ばれる現象を防ぐことができます。

【参考リンク】：[外部リソースの解放には using ステートメントを使う](https://qiita.com/Nossa/items/947f8985e4d5f13d9bc2)

```C#
using System;
using System.IO;
using UnityEngine;

/// <summary>
/// Save、Load機能の実装
/// </summary>
public static class JsonDataManager
{
    /// <summary>
    /// パスを取得 & セーブファイル名記録
    /// </summary>
    private static string getFilePath () { return Application.persistentDataPath + "/PaintData" + ".json"; } 
    
    /// <summary>
    /// 書き込み機能
    /// </summary>
    /// <param name="paintDataWrapper">シリアライズするデータ</param>
    public static void Save(PaintDataWrapper paintDataWrapper)
    {
        //シリアライズ実行
        string jsonSerializedData = JsonUtility.ToJson(paintDataWrapper);
        Debug.Log(jsonSerializedData);
        
        //実際にファイル作って書き込む
        using (var sw = new StreamWriter (getFilePath(), false)) 
        {
            try
            {
                //ファイルに書き込む
                sw.Write (jsonSerializedData);
            }
            catch (Exception e) //失敗した時の処理
            {
                Debug.Log (e);
            }
        }
    }

    /// <summary>
    /// 読み込み機能
    /// </summary>
    /// <returns>デシリアライズした構造体</returns>
    public static PaintDataWrapper Load()
    {
        PaintDataWrapper jsonDeserializedData = new paintDataWrapper();
        
        try 
        {
            //ファイルを読み込む
            using (FileStream fs = new FileStream (getFilePath(), FileMode.Open))
            using (StreamReader sr = new StreamReader (fs)) 
            {
                string result = sr.ReadToEnd ();
                Debug.Log(result);
                
                //読み込んだJsonを構造体にぶちこむ
                jsonDeserializedData = JsonUtility.FromJson<PaintDataWrapper>(result);
            }
        }
        catch (Exception e) //失敗した時の処理
        {
            Debug.Log (e);
        }

        //デシリアライズした構造体を返す
        return jsonDeserializedData;
    }
}
```

---

最後に先ほどのロジックの利用側です。

```C#
    [SerializeField] private Transform _paintTrailRendererParent;

    [SerializeField] private GameObject _paintTrailRendererPrefab;
    
    private MaterialPropertyBlock _materialPropertyBlock;

    private int _propertyID;
    
    private void Start()
    {
        _materialPropertyBlock = new MaterialPropertyBlock();

        //色のプロパティIDをintで保持
        _propertyID = Shader.PropertyToID("_Color");
    }
   
    /// <summary>
    /// セーブ機能
    /// </summary>
    private void save()
    {
        //ここでTrailRendererの情報を構造体、及びリストに格納する
        PaintDataWrapper paintDataWrapper = new PaintDataWrapper();
        PaintData paintData = new PaintData();
        
        //Paintオブジェクト(TrailRenderer)のリストを作成

        foreach (Transform child in _paintTrailRendererParent.transform)
        {
            //TrailRendererの情報を取得
            TrailRenderer tr = child.GetComponent<TrailRenderer>();
            int posCount = tr.positionCount;
            Vector3[] posArray = new Vector3[posCount];
            int vertCount = tr.GetPositions(posArray);
            
            //構造体にTrailRendererの座標を格納
            paintData.PaintObjectPosition = child.position;
            
            //構造体にTrailRendererの頂点座標の配列を格納
            paintData.PaintVertices = posArray;

            //描画した頂点座標を確認
            for (int i = 0; i < vertCount; i++)
            {
                Debug.Log(posArray[i]);
            }
            
            //構造体に色情報を格納
            _materialPropertyBlock.SetColor(_propertyID,tr.material.color);
            paintData.PaintColor = _materialPropertyBlock.GetColor(_propertyID);
            
            //構造体をリストに追加
            paintDataWrapper.DataList.Add(paintData);
        }
        
        //シリアライズ
        JsonDataManager.Save(paintDataWrapper);
    }

    /// <summary>
    /// ロード機能
    /// </summary>
    private void load()
    {
        //デシリアライズ
        PaintDataWrapper paintDataWrapper =  JsonDataManager.Load();

        foreach (PaintData paintData in paintDataWrapper.DataList)
        {
            //リストのデータ分Instantiate
            GameObject paintObj = Instantiate(_paintTrailRendererPrefab, paintData.PaintObjectPosition ,Quaternion.identity,_paintTrailRendererParent);
           
            //==============================================================================================================
            //　TrailRenderer再設定
            //==============================================================================================================
            
            TrailRenderer paintObjTrailRenderer = paintObj.GetComponent<TrailRenderer>();
            
            //全ての頂点を復元
            paintObjTrailRenderer.AddPositions(paintData.PaintVertices);
            
            //色情報を復元
            _materialPropertyBlock.SetColor(_propertyID, paintData.PaintColor);
            paintObjTrailRenderer.SetPropertyBlock(_materialPropertyBlock);
        }
    }
```

##デモ

見たところで伝わりませんが、ちゃんとできてました。
![PaintSaveLoad.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/244071/723bdbe3-36d8-55f7-bca1-f270cea16dc2.gif)

##参考リンク
[UnityのJsonUtilityでデータをJson化する](https://nekosuko.jp/blog/485)

[【Unity(C#)】TrailRendererの頂点座標を取得する方法](https://qiita.com/OKsaiyowa/items/bfcc4a661b8cc2102e86)
[【Unity】Utf8JsonでJsonファイルを読み込み・書き込みする](https://yumineko.com/unity-json-io/)