UnityでJSONを扱う方法を解説【JsonUtility】

プログラマであればJSONというデータをフォーマットは知っているor聞いたことはある多いでしょう。しかし、多数の場面で使われる故、言語や環境が違った場合度のライブラリを使えばいいだっけということになりがちです。さらに、ライブラリによっては機能の非対応など落とし穴もあります。なので今回はUnityが公式でサポートしているJsonUtilityの扱い方と注意点や対処法について解説していきます。



JSONとは

構造

JSON: Javascript Object Notation の略で文字通り javascriptのデータ構造が元となっています。
JSONはキーと値ををワンセットで保持するのですが、その値に配列や、連動配列を入れ子にすることができるので、データを構造的に持つことができます。
それでは、Jsonのそれぞれの要素を見ていきましょう。

連想配列

{}は連想配列でキー:値という形でデータを記述します。そのデータが複数ある場合はカンマ(,)でつなぎます。

{
    HP:10,
    ATK:29,
    name: "enemy1"
},
{
    HP:13,
    ATK:43,
    name: "enemy2"
}

配列

[]は配列で同じ型のデータを順番を保持した形で持ちます。これもデータが複数ある場合は(,)でつなぎます。

[1,2,3]

組み合わせ

JSONは上記の構造しかもちません。それゆえに覚えることは少ないですが、
上記を組み合わせることで、複雑な情報も階層的に記述していくことができます。
例として複数の敵の情報をJSONで表してみましょう。

{
    enemies:[
        {
            name: "enemy1"
            HP:10,
            ATK:29,
        },
        {
            name: "enemy2"
            HP:13,
            ATK:43,
        }
    ]
}

上記は 敵の情報をJSONで表したものです。

JSONのメリット

さてJSONの概要はわかりましたが、これであればCSV(わからない方はExcellを考えてください)とかでもいいのではと思う人もいるでしょう。
確かにその通りです。ですが、JSONの一番のメリットは構造が不揃いのデータでも簡単に表せるのです。
例えば、CSVの場合だと、ただ単にデータを順番に並べているだけで関係性を記述していないので、
特定の要素が欠損したり、おかしなものになったとしてもそれを検知することはできません。
例えばこんな感じです

name,flag
enemy1,on
enemy2,off
enemy3

上はCSVで敵とそのフラグについて記述しています。
flagが立っている場合onにして立ってない場合をoffとしてます。あえてenemy3のflagには何も書いていません。
このままだと、enemy3のflagが何なのかがわかりませんが、csvの記述的にはこれでで問題ないのです。
しかし、これをゲームデータとして渡すとなると話が別です。このデータ欠損は実行されてデータロードした段階までわからないのです。その結果、エラーがはいて止まればまだいいほうですが、よからぬ動作した暁には原因の発見までに多くの時間を使うことになります。このように、CSVでは、\”flag\”がon offに対応するという情報をもてないので意図しない形で動いてしまうことがあります。

上記に対して JSONのメリットはデータの構造を表せることなのです。
それは キーと値をセットで持つことで、関係性を記述できることにあるのです。
なので、値がない場合は構造に破綻があるということで即座にエラーを吐くので安心です。

UntiyでのJsonUtilityの使い方

JSONの概要がわかったところで、UntiyでのJSONの使い方を見ていきましょう。
JSONを扱うオブジェクトはUnityの公式が提供しているJsonUtilityを使用します。
これは、実行速度が早い代わりに、いろいろな制約や機能として不十分なところがあります。これらを実際の使い方を交えて見ていきましょう。

クラスからJSONへの変換

まずはクラスをJSONに形式に変換する方法を見ていきましょう。

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

public class jsonClass : MonoBehaviour {

    // Use this for initialization
    public int HP = 1;
    public int ATK = 3;
    public string name = "hello";

	void Start () {
        Debug.Log(JsonUtility.ToJson(this));
		
	}
	
	// Update is called once per frame
	void Update () {
		
	}
}

これがコードの全体となります。

    public int HP = 1;
    public int ATK = 3;
    public string name = \"hello\";

この部分でクラスのフィールドを定義しています。JSONはキーと値をセットで持つと言いましたね。
JsonUtilityの場合は、変数名:変数に格納されている値
という方式で変換します。

    Debug.Log(JsonUtility.ToJson(this));

上記の JsonUtility.ToJsonがクラスをJsonに変換する関数で、クラスのインスタンスを引数に取ります。
今回の場合は、実際に動かしているjsonClassそのものをクラス内部で変換するので thisで渡しています。

そしてそれを確認するために Debug.logでコンソール画面に出力しています。

試しに適当なオブジェクトにこのスクリプトを貼り付けて、実行してみてください。

Jsonからクラスへ

Jsonからクラスへ変換する場合は二つのアプローチがあります。そしてこれは少々厄介な問題もはらんでいます。
実際に例を出しながら解説していきましょう。

クラスを生成してインスタンスを返す

jsonUtility使用クラス

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using JsonData;



public class JsonClass : MonoBehaviour {

    // Use this for initialization
    public int HP = 1;
    public int ATK = 3;
    public string name = "hello";

	void Start () {
        JsonDataClass jsonDataClass;

        string jsonString = "{\"HP\":12,\"ATK\":6,\"name\":\"asdf\"}";
        jsonDataClass = JsonUtility.FromJson<JsonDataClass>(jsonString);
        
        Debug.Log(jsonDataClass.ATK);
        Debug.Log(jsonDataClass.name);
        Debug.Log(jsonDataClass.HP);
	}
	
	// Update is called once per frame
	void Update () {
		
	}
}

データ格納用クラス

namespace JsonData
{
    public class JsonDataClass
    {
        public int HP;
        public int ATK;
        public string name;
    }
}

これは、json形式の文字列を受け取ってそれのクラスのインスタンスを新たに生成します。
まず前提として今回はjsonからクラスを生成するので、まずはJsonデータ格納専用クラスを定義しましょう。
それがこのJsonDataClassです。

では実際にコードを見ていきましょう。jsonUtility使用クラスのを見てください。

        string jsonString = "{\"HP\":12,\"ATK\":6,\"name\":\"asdf\"}";

まず Jsonの文字列を作ります。そして値を格納するのですが、

        jsonDataClass = JsonUtility.FromJson<JsonDataClass>(jsonString);

C#は先に型を定義しないと行けないので、
生成対象のクラスの型情報を与えなければなりません。
なのでジェネリクスで生成するクラスの型情報を与えます。そして、生成したクラスに値を入れるためにjsonの文字列を引数として渡します。
ためしに、debug.logで確認してみてください。値が格納されたのが確認できます。

ただし、この関数には重大な落とし穴があります。
結論からいうと、クラスのインスタンスを生成する対象がMonoBehaviorを継承している場合、クラスの生成に失敗します。
詳しく説明すると長くなるので省略しますが、Unityの機能を使用する際にはMonoBehaviorを継承しなければならず、この継承によっていろいろな制約が発生するのです。
例えばインスタンスを生成しようとするときやデータの読み取りなど色々ありますが、それが原因となって失敗します。

ちょっと混乱するかもしれませんが、問題なのは「クラスを新たにインスタンスする」ということなのです。なので、すでに生成されているものや対象外なので、一つ前の例やこのあとの例でMonoBehaivorを対象にしていても問題なく動きます。

既存の変数を上書きする

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

public class JsonClass : MonoBehaviour {

    // Use this for initialization
    public int HP = 1;
    public int ATK = 3;
    public string name = "hello";

	void Start () {
        //JsonClass jsonClass = Instantiate(JsonClass);
        string jsonString = "{\"HP\":12,\"ATK\":6,\"name\":\"asdf\"}";
        JsonUtility.FromJsonOverwrite(jsonString, this);
        Debug.Log(this.ATK);
        Debug.Log(this.name);
        Debug.Log(this.HP);
	}
	
	// Update is called once per frame
	void Update () {
		
	}
}

これは Overwriteの名前通り 二つ目の引数に対してjsonから抽出したデータを格納します。
つまり、既存の変数の値を上書きする形で実装することになります。

        string jsonString = "{\"HP\":12,\"ATK\":6,\"name\":\"asdf\"}";
        JsonUtility.FromJsonOverwrite(jsonString, this);

先程と同様にjsonの文字列を作って入れてますが、今回は新たに生成せずに既存ものに上書きします。なので、ジェネリクスで型は指定せず
代わりに、Jsonの文字列に上書きするクラスのインスタンスを入れています。
この関数が実行された際には戻り値を返さず、代わりに、第二引数のインスタンスが参照渡しによって書き換えられるのです。
試しに、Debug.logの中身を確認してみてください。 変更されています。

Jsonに変換されない場合

ここまでで大体の使い方を解説しましたが、jsonに変換できない場合などの注意点にも触れておきます。最大の注意点、それはクラスのフィールドがJsonに変換できないものであっても、
JsonUtilityはエラーを吐かず無視します。その結果、意図しない挙動をしてバグの原因になってしまう上に実行しないと判別がつかないため非常に厄介です。
なので変換されない場合というものを解説します。

結論からいうと、変数がシリアライズできないものだと変換できなくなります。
シリアライズについて解説すると長くなるので省略しますが、簡単にいうと、クラスのデータ(バイナリ)をデータを保存できる形(テキスト)に変換するものだと考えてください。
詳しい仕様は
https://docs.unity3d.com/ja/current/Manual/script-Serialization.html
https://docs.unity3d.com/ja/current/Manual/JSONSerialization.html

上記の二つにお読みください。ただ、結構面倒なので、最低限動くものを記載していきます。
大まかには以下の条件を満たしていると変換されません。

  • private(例外あり)
  • const
  • static
  • NonSerializedなフィールド
  • readonly
  • Dictionary
  • 抽象クラス
  • などなど

ただ、privateに関しては [SerializeField] と追加することでそのフィールドをシリアライズすることが可能になります。
チェックすべき点は以下の点です。

    1.アクセスレベル
    2.型がシリアライズ可能かどうか?
    3.ToJson関数を使用する場合はMonoBehaviorを継承しているかどうか

の点です。もしJsonに変換されないという場合は上記をチェックしましょう。

他のライブラリ

いくら早いとはいえ、いくつかの制約があるのは面倒です。特にDictionaryなどのデータ型を使えないのは状況によっては厳しいものがあるので、
別のJsonライブラリも紹介しておきます。

Json.NET
C#における有名なJsonを扱うためのライブラリです。
こちらはDictionaryも使えるので、困ったら選択しに入れるのもありです。
処理速度自体はJsonUtilityと比べて遅いのですが、パフォーマンスを求められる環境でない限りは普通に使ってもよいと思います。

実践例

それでは、最後に簡単なゲームの実装を考えてみましょう。今回の場合はノベルゲームを考えてみます。
今回はこう言った要素で考えてみます。

  • 立ち絵
  • セリフ
  • 背景

それぞれの情報をワンアクション分(背景、立ち絵、セリフを毎回ロードする)保持するJSONを考えてみましょう。
まず考えるべきはどの情報をどのような形で持つか?ということです。
例えば、
背景はローカルで持っておくので、ローカルのファイル名を指そうか?
立ち絵の情報も同じようにしよう。
セリフはそのままべた書きをセリフをロードしよう
といった形で、なんの情報を持つか?という部分とそれを受け取ってどのように処理するか?という部分突き詰めて考えましょう。

なので、今回のクラスとJSONの構造は

データ格納クラス

    // ローカルの背景ファイル名
    public string background;
    // ローカルの立ち絵ファイル名
    public string standChara;
    // セリフの文字列
    public string[] serif;

としましょう。

そして実際にそのデータを活用するクラスを考えてみましょう。
そのクラスはUnityに実際にデータを反映させるのでMonoBehaviorは継承していないといけません。
その上で、Uiや画像などのオブジェクトにアクセスできるので、

使用クラス

    // セリフ表示用のゲームオブジェクト例
    public GameObject UIText
    // 背景表示用のゲームオブジェクト例
    public GameObject UIImage
    // 立ち絵キャラのゲームオブジェクト例
    public GameObject UIChara

    void Start () {
        string jsonString = "{\"background\":\"normal\",\"standChara\":[\"chara_plate\"],\"serif\":[\"hello\",\"test\"]}";
        NovelData novelData = JsonUtility.FromJson<NovelData>(jsonString);
        // 画像を変更したり・・・・ 
	}

という感じになります。

ここで考えないといけないのが、

string jsonString = "{\"background\":\"normal\",\"standChara\":[\"chara_plate\"],\"serif\":[\"hello\",\"test\"]}";

の部分です。このクラスにjsonをいちいちプログラムにべた書きするのはアホの極みなのでJsonをロードするか生成することを考えなければなりません。逆に言うとそれだけでなんちゃってノベルゲームエンジンができます。

JSONのデータロードの方法

それでは、最後にjsonをどうやってロードしていくかについて解説していきます。
これには複数ありますが、今回はデータをサーバーに置く場合と、ローカルに置く場合の代表例を紹介します。

API

JSONといえばRESTAPIです。RESTAPIはそのURLを叩くと結果がJSONとして帰ってきます。なので上のノベルゲームエンジンの例だと、データを別のサーバーに置いておいて、それをAPIでjson形式取得という形になります。
これは見てわかる通り、サーバーにデータをおいて通信することを前提としています。
また、この構造はメジャーなため、たくさんのサンプルがあります。
サーバーとの通信を考えている場合は練習がてら作ってみてもいいかもしれません。

ローカルファイル

二つ目の方法としてはJSONをローカルにファイルとして置いておいて読み込みという方法があります。しかし、JSONは構造的にデータを扱えるとはいえ、JSONを延々と手作業で編集していくのは非常に手間です。なので、人間が扱いやすいデータ形式でデータを編集しておいて、最後にJSONに変換してやるという方法があります。一番有名な例ですと、CSVでエクセルで編集したファイルを用意したものをjson形式へコンバートしてやればよいのです。これのメリットはローカルで完結するので、簡単なテストや編集するのに手間がかからない点です。デザインや機能を決めるためにとりあえず作る場合はローカルで実験していきましょう。

まとめ

いかがだったでしょうか。JSONはデータを扱ううえで非常に使い勝手のいいツールです。しかし、だからこそ、JSONの構造の設計だったりどういった場面でそれを使うべきといった部分が非常に重要になってきます。ちなみに、実装例のノベルゲームの例は筆者が大学時代に作ったものです。あのままだとロードする回数とかが多いとか、試行錯誤した結果考えられる部分もあるので、自分のゲームを基に実践を繰り返すことでベストプラクティスを身に着けていきましょう。









この記事をかいた人

assa

京都でエンジニアをやっています。assaまたはえつーと覚えてください。 本業はwebで趣味でいろいろいじっています。よしなに