【Unity】 jsonファイルを使ったセーブとロード

# ■ はじめに
ゲームに不可欠な**セーブデータ**を作りましょう！！
今回は、スコアのランキングを例に**データのセーブとロード**の処理を実装していきます。

(個人的な忘備録のついでとして記入してます。もっと効率的な方法があるかもですが、ご了承ください)


# ■ 実装結果
こんな感じのができます。**一度ゲームを終了しても、保存されたデータをもとにランキングが表示されます。**
![savedata.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2419795/4a2ac5ec-defb-c239-4e1d-f6d9b9e41bfa.gif)

GitHubにもアップロードしたので、どうぞ↓

https://github.com/kiku09020/Unity_SaveAndLoad.git


# ■ 説明
## ○ 必要なもの
1. **保存するデータ**のクラス
2. **セーブやロード**の処理をするクラス
3. **ランキングの表示、ランキングの値の保存**などの処理をするクラス

・3つのクラスのみで作成できます。コードの量もそこまで多くないですが、少し複雑かもです。
・今回はランキングを例としていますが、**ハイスコアやゲームのセーブデータの保存などに流用できます。**


## ○ すること
### ● 保存データクラス
1. 保存したい変数を宣言
1. クラスのシリアライズ


### ● セーブロードクラス
1. **ファイルのパスを取得**
1. **開始時、ファイルがあるかどうか確認**
1. **ファイルがなかったら作成**
1. ファイルがあれば、**読み込んで「保存データクラス」型の変数に格納**
1. **終了時、データを保存**


### ● ランキングクラス
1. **「セーブロードクラス」から「保存データクラス」型の変数を参照**
1. ランキングに表示するテキストのオブジェクトとコンポーネントの取得
1. 常時、ランキングを表示
1. **入力欄に数値を入力後、Enterボタンを押したらランキング内の数値入れ替え、数値の保存**
1. **Deleteボタンを押したらランキング内の数値を削除**


# ■コード
## ① 保存データクラス(SaveData)
```SaveData.cs
[System.Serializable]
public class SaveData {
    public const int rankCnt = 3;
    public int[] rank = new int[rankCnt];
}
```
:::note
- **`[System.Serializable]`でシリアル化する必要があります。**
- **宣言するメンバ変数には、`public`を付ける必要があります。**
:::


## ② セーブロードクラス(DataManager)
``` DataManager.cs
using System.IO;
using UnityEngine;

public class DataManager : MonoBehaviour 
{
    [HideInInspector] public SaveData data;     // json変換するデータのクラス
    string filepath;                            // jsonファイルのパス
    string fileName = "Data.json";              // jsonファイル名

    //-------------------------------------------------------------------
    // 開始時にファイルチェック、読み込み
    void Awake()
    {
        // パス名取得
        filepath = Application.dataPath + "/" + fileName;       

        // ファイルがないとき、ファイル作成
        if (!File.Exists(filepath)) {
            Save(data);
        }

        // ファイルを読み込んでdataに格納
        data = Load(filepath);          
    }

    //-------------------------------------------------------------------
    // jsonとしてデータを保存
    void Save(SaveData data)
    {
        string json = JsonUtility.ToJson(data);                 // jsonとして変換
        StreamWriter wr = new StreamWriter(filepath, false);    // ファイル書き込み指定
        wr.WriteLine(json);                                     // json変換した情報を書き込み
        wr.Close();                                             // ファイル閉じる
    }

    // jsonファイル読み込み
    SaveData Load(string path)
    {
        StreamReader rd = new StreamReader(path);               // ファイル読み込み指定
        string json = rd.ReadToEnd();                           // ファイル内容全て読み込む
        rd.Close();                                             // ファイル閉じる
                                                                
        return JsonUtility.FromJson<SaveData>(json);            // jsonファイルを型に戻して返す
    }

    //-------------------------------------------------------------------
    // ゲーム終了時に保存
    void OnDestroy()
    {
        Save(data);
    }
}
```
:::note
- **ファイルへの書き込み、読み込みには`using System.IO;`が必要**になります。
:::

### Awake()
<details><summary>該当箇所のコード(クリックで展開)</summary>

```cs
    // 開始時にファイルチェック、読み込み
    void Awake()
    {
        // パス名取得
        filepath = Application.dataPath + "/" + fileName;       

        // ファイルがないとき、ファイル作成
        if (!File.Exists(filepath)) {
            Save(data);
        }

        // ファイルを読み込んでdataに格納
        data = Load(filepath);          
    }
```
</details>


:::note
- `filepath = Application.dataPath + "/" + fileName;`で**ファイルのパス名を取得**します。
    - `Application.dataPath`は、プラットフォームによってパスが変わります。([詳細](https://qiita.com/w_yang/items/8458cd790607d14b1b36))

    - **※追記:** Android で使用する場合、 **`Application.datapath` を `Application.persistentDataPath`に変更する**
    ```Example.cs
    #if UNITY_EDITOR
        filepath = Application.dataPath + "/" + fileName;

    #elif UNITY_ANDROID
        filepath = Application.persistentDataPath + "/" + fileName;    

    #endif
    ```

- `if (!File.Exists(filepath))`で**データファイルの有無を確認**します。
- **ファイルが無い場合、`Save(data);`でファイルを作成**します。
- **`data = Load(filepath);`でファイルを読み込み**ます。
:::

#### 参照
- [Application.dataPath(Unityリファレンス)](https://docs.unity3d.com/ja/2019.4/ScriptReference/Application-dataPath.html)
- [File.Exists(MSリファレンス)](https://learn.microsoft.com/ja-jp/dotnet/api/system.io.file.exists?view=net-6.0)


---

### Save(SaveData data)
<details><summary>該当箇所のコード(クリックで展開)</summary>

```cs
    // jsonとしてデータを保存
    void Save(SaveData data)
    {
        string json = JsonUtility.ToJson(data);                 // jsonとして変換
        StreamWriter wr = new StreamWriter(filepath, false);    // ファイル書き込み指定
        wr.WriteLine(json);                                     // json変換した情報を書き込み
        wr.Close();                                             // ファイル閉じる
    }
```
</details>

:::note
- 引数dataは、**保存するSaveData型のデータ**を受けとります。
- `JsonUtility.ToJson(data); `で**SaveData型からjson(string型)に変換します。**
- `StreamWriter wr = new StreamWriter(filepath, false);`で**上書き保存するよう指定します。**
- `wr.WriteLine(json);`で**ファイルにSaveData内の変数を書き込みます。**
:::

#### 参照
- [JsonUtility(Unityリファレンス)](https://docs.unity3d.com/ja/2018.4/ScriptReference/JsonUtility.html)
- [SreamWriter(MSリファレンス)](https://learn.microsoft.com/ja-jp/dotnet/api/system.io.streamwriter?view=net-6.0)


### Load(string path)

<details><summary>該当箇所のコード(クリックで展開)</summary>

```cs
    // jsonファイル読み込み
    SaveData Load(string path)
    {
        StreamReader rd = new StreamReader(path);               // ファイル読み込み指定
        string json = rd.ReadToEnd();                           // ファイル内容全て読み込む
        rd.Close();                                             // ファイル閉じる
                                                                
        return JsonUtility.FromJson<SaveData>(json);            // jsonファイルを型に戻して返す
    }
```

</details>

:::note
- 引数pathは、**読み込むjsonファイルのパス**を受けとります。
- `return JsonUtility.FromJson<SaveData>(json);`で、**jsonファイルからSaveData型に変換して返します。**
:::
#### 参照
- [StreamReader(MSリファレンス)](https://learn.microsoft.com/ja-jp/dotnet/api/system.io.streamreader?view=net-6.0)

---
### OnDestroy()
<details><summary>該当箇所のコード(クリックで展開)</summary>

```cs
    // ゲーム終了時に保存
    void OnDestroy()
    {
        Save(data);
    }
```

</details>

:::note
- ゲーム終了時にファイルをセーブするようにしてます。
:::


## ③ ランキングクラス(Ranking)
```Ranking.cs
using UnityEngine;
using UnityEngine.UI;

public class Ranking : MonoBehaviour
{
    /* 値 */
    string[]    rankNames = { "1st", "2nd", "3rd" };        // ランキング名
    const int   rankCnt = SaveData.rankCnt;                 // ランキング数

    /* コンポーネント取得用 */
    Text[]      rankTexts = new Text[rankCnt];              // ランキングのテキスト
    SaveData    data;                                       // 参照するセーブデータ

    //-------------------------------------------------------------------
    void Start()
    {
        data = GetComponent<DataManager>().data;            // セーブデータをDataManagerから参照

        for (int i = 0; i < rankCnt; i++) {
            Transform rankChilds = GameObject.Find("RankTexts").transform.GetChild(i);      // 子オブジェクト取得
            rankTexts[i] = rankChilds.GetComponent<Text>();                                 // 子オブジェクトのコンポーネント取得
        }
    }

    //-------------------------------------------------------------------
    void FixedUpdate()
    {
        DispRank();
    }

    // ランキング表示
    void DispRank()
    {
        for (int i = 0; i < rankCnt; i++) {
            rankTexts[i].text = (rankNames[i] + " : " + data.rank[i]);
        }
    }

    // ランキング保存
    public void SetRank()
    {
        InputField inpFld = GameObject.Find("InputField").GetComponent<InputField>();
        int score = int.Parse(inpFld.text);     // string -> int

        // スコアがランキング内の値よりも大きいときは入れ替え
        for (int i = 0; i < rankCnt; i++) {
            if (score > data.rank[i]) {
                var rep = data.rank[i];
                data.rank[i] = score;
                score = rep;
            }
        }
    }

    // ランクデータの削除
    public void DelRank()
    {
        for (int i = 0; i < rankCnt; i++) {
            data.rank[i] = 0;
        }
    }
}

```
:::note
- TextやInputFieldなどを使用するので、**`using UnityEngine.UI;`が必要**になります。
- ランキングの要素数`rankCnt`は、**SaveDataクラスから参照**してます。
- **DataManagerクラスでAwakeをしてからRankingクラスのStartを実行しないといけません。**
:::

### Start()
<details><summary>該当箇所のコード(クリックで展開)</summary>

```cs
    void Start()
    {
        data = GetComponent<DataManager>().data;            // セーブデータをDataManagerから参照

        for (int i = 0; i < rankCnt; i++) {
            Transform rankChilds = GameObject.Find("RankTexts").transform.GetChild(i);      // 子オブジェクト取得
            rankTexts[i] = rankChilds.GetComponent<Text>();                                 // 子オブジェクトのコンポーネント取得
        }
    }
```

</details>

:::note
- **dataをDataManagerから参照**しています。
- for文で**ランキングのテキストのオブジェクトとコンポーネントを取得**しています。
:::

### DispRank()

<details><summary>該当箇所のコード(クリックで展開)</summary>

```cs
    // ランキング表示
    void DispRank()
    {
        for (int i = 0; i < rankCnt; i++) {
            rankTexts[i].text = (rankNames[i] + " : " + data.rank[i]);
        }
    }
```

</details>

:::note
- for文でランキングのテキストを変更します。
- FixedUpdate内で常時実行しています。
:::

### SetRank()

<details><summary>該当箇所のコード(クリックで展開)</summary>

```cs
    // ランキング保存
    public void SetRank()
    {
        InputField inpFld = GameObject.Find("InputField").GetComponent<InputField>();
        int score = int.Parse(inpFld.text);     // string -> int

        // スコアがランキング内の値よりも大きいときは入れ替え
        for (int i = 0; i < rankCnt; i++) {
            if (score > data.rank[i]) {
                var rep = data.rank[i];
                data.rank[i] = score;
                score = rep;
            }
        }
    }
```

</details>

:::note
- **`int.Parse(inpFld.text)`InputFieldで入力された値を取得**します。
- for文で、**入力された値とランキング内の値の判定と入れ替えをしています。**
:::

### DelRank()

<details><summary>該当箇所のコード(クリックで展開)</summary>

```cs
    // ランクデータの削除
    public void DelRank()
    {
        for (int i = 0; i < rankCnt; i++) {
            data.rank[i] = 0;
        }
    }
```

</details>

:::note
- データの値を全て0にしています。
:::

# ■Unity操作
## ○必要なもの
- 空のオブジェクト(Manager)
- Canvas
    - InputField
    - Enterボタン
    - Deleteボタン
    - ランキング表示用テキスト×3

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2419795/689e0426-00c0-52ec-5d08-9ce359c8306d.png)

## ○すること
- **空のオブジェクト(Manager)に"DataManager","Ranking"をコンポーネントに追加**する
    ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2419795/63277015-52ce-abfc-0564-a860ccec71b7.png)

- **InputFieldの"InputField"コンポーネント内の"Content Type"を"Integer Number"に変更して、入力を整数のみに制限する**
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2419795/d1e94d1a-2c64-dbc0-5651-0a8215c6290f.png)

- **Enter,Deleteボタンの"OnClick()"にManagerの"Ranking"スクリプトのpublic関数を入れる**
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2419795/de73d44d-0b30-287e-08c3-fba0217b0d4e.png)



# ■さいごに
- 重要なデータは暗号化しないと、書き換えとか簡単にできちゃうので、その辺もそのうち勉強します。
- まだ実際のゲームにこんな感じのシステムを使用できてないので、今後とも頑張ります。


# ■参考
- [UnityでJSONファイルを扱う方法(Alterbo.jp)](https://alterbo.jp/blog/hisshi3-2011/)
- [JSON 形式にシリアライズ(Unityリファレンス)](https://docs.unity3d.com/ja/2018.4/Manual/JSONSerialization.html)