概要
UnityにはJsonUtilityというとても便利な機能がありまして、自分もゲームのデータファイルにJsonを採用しているのでとても助かっています。
そこで、まぁてっとり早く使い方をおさらい兼共有しておこうかなと。
手順
① 必要なファイルの準備
まず、JsonUtilityでシリアライズ/デシリアライズする対象のクラスを宣言しよう。
Data.cs
using UnityEngine;
using System;
[Serializable]
class Data
{
[SerializeField]
private int _num = 0;
[SerializeField]
private int[] _nums = null;
[SerializeField]
private string _str = string.Empty;
public int num
{
get { return _num; }
set { _num = value; }
}
public int[] nums
{
get { return _nums; }
set { _nums = value; }
}
public string str
{
get { return _str; }
set { _str = value; }
}
private Data(){ }
}
クラスの内容は、
①整数値
②整数の配列
③文字列
の3つ。
次に必要なのは実際にデータを保存するためのJsonファイル。
Data.json
{
"_num": 100,
"_nums":[ 100, 200, 300 ],
"_str":"hogehoge"
}
② JsonUtilityを使って読み込む
Loader.cs
using UnityEngine;
using System.IO;
class Loader : MonoBehaviour
{
private void Start()
{
string filePath = "Data.json"
// Jsonを元にデータクラスのインスタンスを作成(デシリアライズ)
Data data = JsonUtility.FromJson<Data>(filePath);
Debug.Log(data.num);
Debug.Log(data.nums)
Debug.Log(data.str);
// データクラスの情報をJsonに書き込む(シリアライズ)
string jsonText = JsonUtility.ToJson(data);
using (StreamWriter writer = new StreamWriter(filePath))
{
writer.WriteLine(jsonText );
writer.Close();
}
}
}
これで読み書きできる。
-
読み込みについて
見ての通りなのであまり補足することはない。
注意なのが、Jsonファイル内の要素を受け入れるための変数がクラスに用意されていない場合、読み込みに失敗してクラスのインスタンス化自体が失敗する。多分。何らかインスタンスに悪影響を与えることは間違いない。
しかし、Json内の要素ではない変数がクラスに存在する場合は何の支障もなくデシリアライズされる。
この挙動はとても重要で、本日の本題の元ネタである。詳しくは後述 -
書き込みについて
JsonUtilityで行えるのは、データクラスのインスタンスを文字列化するところまで。
よって、その文字列を外部ファイルに保存するのはまた別の機能を利用しなくてはならない。
幸い、C#には標準で書き込める機能があるのでそれを使おう。
ちなみに、今回は最低限かつクソースなのが自明にならない程度の記述にしているので、本来はもっと丁寧にファイルチェックとかした方がいいです。
② データをより安全に運用する
ここまでの内容で、最低限Jsonファイルの読み書きが実現できる。
しかし、せっかくC#を使っているのに配列などというものでデータを管理したくない。
List使おう。
ということで以下コード。
Data.cs
using UnityEngine;
using System;
using System.Collections.Generic;
[Serializable]
class Data : ISerializationCallbackReceiver
{
[SerializeField]
private int _num = 0;
[SerializeField]
private int[] _nums = null;
[SerializeField]
private string _str = string.Empty;
private List<int> _numList = null;
public int num
{
get { return _num; }
set { _num = value; }
}
public List<int> numList
{
get { return _numList; }
set { _numList = value; }
}
public string str
{
get { return _str; }
set { _str = value; }
}
private Data(){ }
// シリアライズ前の処理
public void OnBeforeSerialize()
{
if(this._numList == null) return;
// リストを配列に変換
this._nums = this._numsList.ToArray();
}
// デシリアライズ後の処理
public void OnAfterSerialize()
{
if(this._nums == null) return;
// 配列を元にリストを作成
this._numsList = new List<int>(this._nums);
}
}
DataにISerializationCallbackReceiverを継承させる。
ISerializationCallbackReceiverはインターフェースなので、特定のメソッドを実装しなければならない。
その特定のメソッドというのが、__OnBeforeSerialize__と__OnAfterSerialize__である。
メソッド名を見ればわかる通り、
OnBeforeSerializeはデータのシリアライズ前に呼ばれる処理
OnAfterSerializeはデータのデシリアライズ後に呼ばれる処理
である。
こうしたインターフェースを継承することで、Jsonの一連のパース処理の中にしれっと処理を挟むことが可能。
また、Jsonに定数データをまとめて、ゲームの初期化時に読み込んで使うだけなら、リストに変換する前の元の配列はnullを代入して破棄しても構わない。
懇切丁寧な解説に、更に心優しい補足
①構造体は対応してません
今回はデータクラスをclassで定義したけど、メモリとかインスタンスの所在とか気にする人は構造体でデータを持ちたいと思う人もいると思う。
しかし、残念ながら構造体にISerializationCallbackReceiverを継承させても正しく動作してくれません。
②本記事のコードについて
Jsonに全く関係ないけど、変数名の頭文字にアンダーバーつけたのは、あくまでも解説用の変数なので意味が特になく、命名に迷ったけどぶっちゃけこの記事書くのにそんなに頭を使いたくなかったので安易な感じに着地しました。
本番では、絶対にコードでの実装の問題をデータファイルに波及させてはいけませんよ。
③Dictionaryについて
JsonUtilityは、標準ではDictionary型のサポートはしてないです。
そこで、今回のISerializationCallbackReceiverを使えば、Jsonではstringと配列でディクショナリを表現しておき、デシリアライズ時にメンバ変数に受取り、OnAfterDeserializeでうまくDictionary型の変数に詰め込み直す、というようなやり方も出来ます。
ただ、記事の本筋に関係ないのと、自分としてはその実装が求められた時点でデータの構造に問題がある気がします。
他の環境ではJsonでディクショナリ型を標準で読み書きするパーサーは普通に存在するので、出来ないJsonUtilityのほうが異端かもしれませんが、まぁうまく使ってください。
終わり