【Unity】もうJSONと睨めっこはしない。【Editor拡張】

# はじめに
こんにちは！愛知工業大学システム工学研究会の、@wdtkrです。普段はUnityでゲーム作ったり、コーディングしたりしてます。
また、本記事は[STECH / 愛知工業大学 システム工学研究会 共同企画 Advent Calendar 2022](https://qiita.com/advent-calendar/2022/stech-ait-advent)の18日目の記事です。

# 📝概要
UnityでJSONデータを扱い、そのJSONデータを**インスペクタ**から書き換えれるようにする方法です。
セーブデータ等としての活用も出来ます。

下の方に完成したコードを載せているので、先にそちらを見て、全体像から掴んだ方が分かりやすいかもしれません。
完成したものはこんな風になっています。

![ezgif-5-7cc12f0bfc.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2708333/075ca2c3-eba5-2b99-4e0a-ca287ad136cc.gif)

# 目次
* [経緯](#経緯)
* [開発環境](#開発環境)
* [実装](#実装)
    * [書き換えるまでの準備編](#書き換えるまでの準備編)
        * [JSONデータを作る](#jsonデータを作る)
        * [クラスを用意する](#クラスを用意する)
        * [JSONを読み込む](#jsonを読み込む)
        * [JSONをクラスへと変換する](#jsonをクラスへと変換する)
    * [インスペクタから書き換え編](#インスペクタから書き換え編)
        * [インスペクタから入力をする](#インスペクタから入力をする)
        * [エディター拡張用のスクリプトを作る](#エディター拡張用のスクリプトを作る)
        * [インスペクタから書き換えれるようにする](#インスペクタから書き換えれるようにする)
        * [本当に書き換えられているかをチェック](#本当に書き換えられているかをチェック)
* [完成したコード](#完成したコード)
* [まとめ](#まとめ)

# 📖経緯
自語り多めなので、[この章は飛ばしてもらって大丈夫です](#開発環境)👍
インターンにて、Unityを使って運用中のサービス（ゲーム）にアウトプットをしています（2022年12月現在）。
その際に、既に所持しているアイテム一覧や、ユーザーの装備によって変更されるプレイヤーのステータス、などの、実際にサーバーを介して送られてくるデータ**ではなく**、
Unityのエディターから実行した際に、サーバーを介さずやり取りする**ダミーデータ**がJSONデータで扱われていました。

それまでは、ダミーデータに登録されているステータス（例えば攻撃力やHP）を変更する場合、JSONを直に書き換えていました。
数百行あるようなJSONがいくつかあり、その中から変更したい箇所を探し書き換えるという作業は、慣れれば簡単かもしれないですが、初見では非常に時間がかかるものでした。
そのため、直にJSONファイルを開いて編集するのではなく、Unityのインスペクタ、もしくはウィンドウから数値等を入力するだけで自動でJSONが書き変えられるようにしたい、と思ったのが、事の発端です。

# ⚙開発環境
Unity 2020.3.17f1（JetBrains Rider 2022.2.4）

# 💻実装
全体の流れとしては、
1. JSONを読み込む
2. それを独自のクラスに変換する
3. クラスの変数に任意の値を代入する 
4. JSONに変換する　
5. インスペクタからJSONを書き換える

といった流れになっています。
## 書き換えるまでの準備編
この章では、実際にJSONを書き換えれるようにするまでの、クラス実装等の事前準備を行います。
### JSONデータを作る
今回、以下のようなJSONを入力で受け付けるとします。
``data.json``は、大きく分けると**status**と**items**の二つの要素を持ち、itemsは配列です。
``` data.json
{
  "status": {
    "hp": 88,
    "power": 200
  },
  "items": [
    {
      "id": 1,
      "name": "hoge",
      "count": 2
    },
    {
      "id": 2,
      "name": "fuga",
      "count": 10
    }
  ]
}
```
ところで、JSONファイルってそもそも見辛くないですか？
そんな時は、[JSON Editor Online](https://jsoneditoronline.org/#left=local.jiyuya)というこのサイト。
このサイトにJSONファイルをペーストすれば、ただのテキストの状態から**ツリー構造**に見た目を変えれたり、二つのJSONファイルを比較してどの部分が違うのかなどを教えてくれたりするため、非常に使い勝手が良くお勧めです（まあ、それでも面倒だから、エディタから編集出来るようこの実装を行なったのですが）。
![スクリーンショット 2022-12-07 17.02.00.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2708333/12c692a8-7c82-762e-6ba1-edaee8183077.png)

### クラスを用意する
SampleScript.csというスクリプトの中に書いていきます。

それぞれの値を変数として保持するために、そこにクラスを3つ作成していきます。
クラスを作成する上で注意が必要なのが、``data.json``**で指定したKey（"hp"や"power"など）と、それぞれのクラスのメンバーの名前は一致させる必要があります**。
これらが一致していると、JSONからクラスへと変換する際に、自動で各変数への割り当てがされます。
また、``[System.Serializable]``属性の付与を忘れないようにしましょう。事前に``using System;``でインポートしている場合には、``[Serializable]``を付与しましょう。
```SampleScript.cs
[System.Serializable]
public class Status
{
    public int hp;
    public int power;
}

[System.Serializable]
public class Item
{
    public int id;
    public string name;
    public int count;
}

[System.Serializable]
public class Data
{
    public Status status;
    public Item[] items;
}
Data data = new Data();
```
HPや攻撃力といったプレイヤーのステータス情報を持つのが**Status**クラス、アイテムの情報を持つのが**Item**です。
そして、Statusクラスの変数statusと、Itemを複数個持たせるためのitemsを持たせたクラスが**Data**です。この中にData.jsonの要素を入れます。

### JSONを読み込む
JSONファイルを任意の場所に配置し、そのパスを指定して読み込みます。
今回は仮に、Asset下に``data.json``を配置したとします。
```SampleScript.cs
using System.IO;

string path = "/data.json";
string json;

public void LoadJson()
{
    json = File.ReadAllText(Application.dataPath + path);
    data = JsonUtility.FromJson<Data>(json);
}
```
System.IO.Fileというクラスを使って、JSONファイルを読み込みます。
``Application.dataPath``で、``/Users/~/GitHub/sample-for-Qiita/Assets``のように、Assetフォルダまでのパスが取得できます。そのパスに開きたいファイルの名前（``/data.json``）を足すことで、任意のJSONまでのパスを通しています。
``File.ReadAllText``は、テキストファイルを開き、そのファイル内のすべてのテキストを読み取った後、ファイルを閉じる、という一連の流れを行ってくれるメソッドです。

Fileクラスの詳細は[こちら](https://learn.microsoft.com/ja-jp/dotnet/api/system.io.file?view=net-7.0)から。もっと知りたい方は是非。



### JSONをクラスへと変換する
先ほどの``File.ReadAllText``で読み込んだ文字列を、独自のクラスへと変換します。
```SampleScript.cs
data = JsonUtility.FromJson<Data>(json);
```
``JsonUtility.FromJson<型>(文字列)``で、文字列を特定の型へと変換することが出来ます。たったの一行で。
さらに、Statusの要素やitemsの各要素には、``data.status.hp``であったり、``data.items[0].name``などでアクセスが可能です。

また、JsonUtilityには他にも
* ToJson（逆に、クラスからJSONにする）
* FromJsonOverwrite（読み込んだJSONで、クラスの情報を上書き）

の２つがあります。詳しいJsonUtiletyについてのリファレンスは[こちら](https://docs.unity3d.com/ja/2018.4/ScriptReference/JsonUtility.html)から。

## インスペクタから書き換え編
さて、ここまででJSONの読み込みとクラスへの変換を行ってきました。
本題の書き換えまではもうあと少しです。

### インスペクタから入力をする
まずは、適当なGameObjectを``CreateEmpty``から作成し、それにSampleScript.csをAddComponentしましょう。


次に、インスペクタから入力が出来るようにしましょう。
```SampleScript.cs
[SerializeField] Status inputStatus;
[SerializeField] Item[] inputItems;
```
![スクリーンショット 2022-12-07 18.23.06.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2708333/1b0f8102-b9dd-fa82-8a08-2637c8f9db00.png)
こんな感じになっていればOKです。

### エディター拡張用のスクリプトを作る
インスペクタに入力した情報を、インスペクタ上からJSONに反映出来るようにするには、エディター拡張用スクリプトが必要なので作っていきます。

エディター拡張用のスクリプトを配置する場所は``Asset/Editor``である必要があります。
Assetフォルダ内にEditorフォルダを作成し、そこにEditor用のスクリプトを作成してください。
![スクリーンショット 2022-12-07 17.12.05.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2708333/e101cf7c-2299-ed5a-4614-6506b629e8d1.png)

さっくり中身を書いていきます。
```SampleEditor.cs
using UnityEditor;
using UnityEngine;

[CustomEditor(typeof(SampleScript))]
public class SampleEditor : Editor
{
    public override void OnInspectorGUI()
    {
        base.OnInspectorGUI();
        var sample = target as SampleScript;
        
        if (GUILayout.Button("JSONを読み込む"))
        {
            sample.LoadJson();
        }
        
        if (GUILayout.Button("JSONを更新する"))
        {
            sample.UpdateJson();
        }
        
    }
}
```
``[CustomEditor(typeof(SampleScript))]``のtypeofaの後ろには、最初に作成したスクリプトのクラス名を入れます。
``public override void OnInspectorGUI()``は、インスペクタが表示されている時に実行される関数で、
``if (GUILayout.Button("")``をその中に書くと、インスペクタにボタンを追加出来ます。そのボタンを押すと、中に書かれている処理が実行されるようになっています。

targetはEditorのメンバ変数で、object型であるため、SampleScriptでキャストをしています。
そのインスタンスから、先ほどのLoadJsonなどの関数を呼び出します。UpdateJsonに関しては、次に作成します。

Editorについてのリファレンスは[こちら](https://docs.unity3d.com/ja/2019.4/ScriptReference/Editor.html)から
### インスペクタから書き換えれるようにする
本題ですね。
先ほどのUpdateJsonを作りつつ、LoadJsonにも少し手を加えます。
```SampleScript.cs
public void LoadJson()
{
    json = File.ReadAllText(Application.dataPath + path);
    data = JsonUtility.FromJson<Data>(json);
        
    inputStatus = data.status;
    inputItems = data.items;
}

public void UpdateJson()
{
    data.status = inputStatus;
    data.items = inputItems;
    string output = JsonUtility.ToJson(data,true);

    File.WriteAllText(Application.dataPath + path, output);
}
```

LoadJsonは、読み込んだJSONのデータを、インスペクタに初期値として表示するため、読み込む関数です。
UpdateJsonは、インスペクタに入力した値を、JSONファイルに上書きする関数です。
``File.WriteAllText``が上書きの処理で、第一引数にパス、第二引数に文字列にしたJSONデータを指定します。

![スクリーンショット 2022-12-07 18.38.57.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2708333/05d1281e-dd93-4a4a-d75e-21c7660df0be.png)
こんな感じ表示されていれば、エディタ拡張も出来ています。

### 本当に書き換えられているかをチェック
実際、本当に書き換えられているのかを、インスペクタから簡単にチェックしてみたいと思います。
``data.json``を開いて確認してもらっても全く問題ありません。なんならそっちの方が見やすいです。
```
[SerializeField][Multiline] string output;
```
output変数に、SerializeField属性と、Multiline属性を付け、グローバル変数として宣言します。
Multilineは、改行が正しく表示される特性があるため、JSONを表示させるのと相性が良いです。
（スクロール出来ない点、3行までしか表示されない点はかなり不便ですが・・・）
![ezgif-5-7cc12f0bfc.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2708333/ef045b9d-c24b-d778-354c-c919de3e38f4.gif)
こんな感じで、書き換えが行えていることが確認できたと思います。

# 完成したコード
```SampleScript.cs
using System;
using System.IO;
using UnityEngine;

public class SampleScript : MonoBehaviour
{
    [Serializable]
    public class Status
    {
        public int hp;
        public int power;
    }
    
    [Serializable]
    public class Item
    {
        public int id;
        public string name;
        public int count;
    }
    
    [Serializable]
    public class Data
    {
        public Status status;
        public Item[] items;
    }

    Data data = new Data();
    
    string path = "/data.json";
    string json;

    [SerializeField] Status inputStatus;
    [SerializeField] Item[] inputItems;
    
    [SerializeField][Multiline] string output;

    public void LoadJson()
    {
        json = File.ReadAllText(Application.dataPath + path);
        data = JsonUtility.FromJson<Data>(json);
        
        inputStatus = data.status;
        inputItems = data.items;
    }

    public void UpdateJson()
    {
        data.status = inputStatus;
        data.items = inputItems;

        output = JsonUtility.ToJson(data,true);
        
        File.WriteAllText(Application.dataPath + path, output);
    }
}

```

```SampleEditor.cs
using UnityEditor;
using UnityEngine;

[CustomEditor(typeof(SampleScript))]
public class SampleEditor : Editor
{
    public override void OnInspectorGUI()
    {
        base.OnInspectorGUI();
        var sample = target as SampleScript;
        
        if (GUILayout.Button("JSONを読み込む"))
        {
            sample.LoadJson();
        }
        
        if (GUILayout.Button("JSONを更新する"))
        {
            sample.UpdateJson();
        }
    }
}
```

# まとめ
今回は、UnityでJSONデータを扱い、そのJSONデータをインスペクタから編集する方法を作り、解説しました。
いちいちJSONと睨めっこしなくても、簡単にパラメータを調整出来るようになったと思います。
ぜひこれを活かして、セーブデータ等に落とし込んで見てください。

また実際にインターンにて作ったものは、インスペクタではなく、エディタ拡張で作成できる”ウィンドウ”に表示をしていたり、もっと多くの機能があるものでした。
それをインスペクタで簡易的に再現したのが、この記事で作成したツールです。
そのため近いうちに、これをインスペクタではなくウィンドウで表示し、さらに機能を盛り込んだものを、続きとして記事に上げるかもしれません。