ISerializationCallbackReceiver
というものが、随分前に追加されたことは聞いていたのですが、機会がなかったため今まで使っていませんでした。
今回使い機会がありましたので、今更ですがISerializationCallbackReceiver
のメモ書きです。
この記事にはUnity2017.1.0f3を使用しています。
ISerializationCallbackReceiverって何それ
ISerializationCallbackReceiver
は、継承することでデシリアライズ後とシリアライズ前に指定された関数が呼ばれるようになるという便利なものなのです。
スクリプトリファレンス
docs.unity3d.com
では実際に使ってみましょう。
JsonUtilityときっと仲良し
ISerializationCallbackReceiver
はJsonUtility
と相性がいいのではないでしょうか。
今回僕もJsonデータを変換する時にISerializationCallbackReceiver
を使用しました。
Jsonデータの中に日付の情報が入っているとしましょう。
以下のように文字列で年月日が記述されています。
{ "m_dateText":"2017-08-06" }
この情報をJsonUtility
を使って変換するクラスを作成してみます。
このような形になると思います。
using UnityEngine; [System.Serializable] public class TestJson { [SerializeField] private string m_dateText = null; public string DateText { get { return m_dateText; } } } // class TestJson
Jsonに記述された情報は、数値や文字列になるため、対応する変数も同じ型の変数になります。
今回でしたらstring
型ですね。
ただ実際スクリプト上でこの日付情報を扱う際に、string
型ではとても扱いにくいです。
日付情報ならばSystem.DateTime
型として使いたいものです。
となるとこのstring
型の日付情報を誰かしらがSystem.DateTime
型に変換する処理を呼び出さなくてはいけません。
出来ることならば、JsonUtility.FromJson()
でJsonデータがクラスに変換されるときに自動的にSystem.DateTime
型に変換してくれるのが望ましいですよね。
そんなときに、ISerializationCallbackReceiver
ですよ。
ISerializationCallbackReceiver
を継承することにより、OnAfterDeserialize()
とOnBeforeSerialize()
を記述する必要が出てきます。
そのうちOnAfterDeserialize()
はデシリアライズ後に呼ばれる関数になります。
この関数に上記の日付情報をSystem.DateTime
型に変換する処理を記述します。
using UnityEngine; [System.Serializable] public class TestJson : ISerializationCallbackReceiver { [SerializeField] private string m_dateText = null; private System.DateTime m_dateData = new System.DateTime(); public System.DateTime DateData { get { return m_dateData; } } public void OnAfterDeserialize() { if( !string.IsNullOrEmpty( m_dateText ) ) { System.DateTime date = new System.DateTime(); if( System.DateTime.TryParse( m_dateText, out date ) ) { m_dateData = date; } } } public void OnBeforeSerialize() { } } // class TestJson
JsonUtility
を使ってJsonデータをこのクラスに変換してみましょう。
using UnityEngine; public class TestComponent : MonoBehaviour { [SerializeField] private string jsonData = null; private void Start() { var jsonClass = new TestJson(); jsonClass = JsonUtility.FromJson<TestJson>( jsonData ); Debug.Log( jsonClass.DateData ); } } // class TestComponent
8/6/2017 12:00:00 AM
JsonデータをJsonUtility.FromJson()
で受け取った後、OnAfterDeserialize()
の処理が呼ばれるため、JsonUtility
を使ったTestComponent
コンポーネント側からすれば、自動でSystem.DateTime
型に変換してくれたように見えます。
これは便利です。
逆にクラスの情報からJsonデータを作成する場合は、OnBeforeSerialize()
内に処理を記述することで、System.DateTime
型の情報をstring
型の変数に渡すこともできます。
using UnityEngine; [System.Serializable] public class TestJson : ISerializationCallbackReceiver { [SerializeField] private string m_dateText = null; private System.DateTime m_dateData = new System.DateTime(); public System.DateTime DateData { get { return m_dateData; } set { m_dateData = value; } } public void OnAfterDeserialize() { if( !string.IsNullOrEmpty( m_dateText ) ) { System.DateTime date = new System.DateTime(); if( System.DateTime.TryParse( m_dateText, out date ) ) { m_dateData = date; } } } public void OnBeforeSerialize() { m_dateText = DateData.ToString( "yyyy-MM-dd" ); } } // class TestJson
using UnityEngine; public class TestComponent : MonoBehaviour { [SerializeField] private string jsonData = null; private void Start() { var jsonClass = new TestJson(); jsonClass.DateData = new System.DateTime( 2018, 5, 30 ); jsonData = JsonUtility.ToJson( jsonClass ); Debug.Log( jsonData ); } } // class TestComponent
{"m_dateText":"2018-05-30"}
このようにデータを扱う側からすれば、自動的に必要な形にデータを変換してくれるように見えるため、実際のデータの型とスクリプト上で扱う型の違いを気にしなくていいようになります。
他にもfloat
型の情報を、指定した小数点以下を丸めてからJsonデータに変換するときなど使えると思います。
MonoBehaviourにも使ってみたんだけど……
そしてこの便利なISerializationCallbackReceiver
はMonoBehaviour
を継承したコンポーネントにも使うことができますよ。
using UnityEngine; public class TestComponent : MonoBehaviour, ISerializationCallbackReceiver { public void OnAfterDeserialize() { } public void OnBeforeSerialize() { } } // class TestComponent
この場合一つ疑問がありますね。
Awake()
やStart()
と比べて処理の順番はどうなっているのだろうか。
まあ、Awake()
のスクリプトリファレンスに、コンストラクタよりAwake()
を使ったほうがいい理由として、Awake()
ならエディタ上で設定した値が反映されているからというのがあったような気がするので、Awake()
よりかは前に処理が終わっていると思うのですが、一応確認してみましょう。
docs.unity3d.com
using UnityEngine; public class TestComponent : MonoBehaviour, ISerializationCallbackReceiver { private void Awake() { Debug.LogFormat( "Awake()" ); } private void OnEnable() { Debug.LogFormat( "OnEnable()" ); } private void Start() { Debug.LogFormat( "Start()" ); } public void OnAfterDeserialize() { Debug.LogFormat( "OnAfterDeserialize()" ); } public void OnBeforeSerialize() { Debug.LogFormat( "OnBeforeSerialize()" ); } } // class TestComponent
上記のスクリプトを記述したら、ログがえらいことになってしまった。
ちなみにこの状態、エディタをまだ再生していません!
な、なにが起きてしまったのやら。
あっ、もしかして、エディタのシリアライズが毎フレーム呼ばれているのだろうか。
とりあえず再生してみましょう。
ログがよくわからない。
多分これエディタ再生前のログも入っちゃてるだろう!
ちょっとスクリプトを修正しましょう。
OnBeforeSerialize()
のログは出さないようにして、かつエディタ時のログかわからないので、少し細工をいれて再検証します。
using UnityEngine; public class TestComponent : MonoBehaviour, ISerializationCallbackReceiver { private int m_deserializeCount = 0; private void Awake() { Debug.LogFormat( "Awake() Count={0}", m_deserializeCount ); } private void OnEnable() { Debug.LogFormat( "OnEnable() Count={0}", m_deserializeCount ); } private void Start() { Debug.LogFormat( "Start() Count={0}", m_deserializeCount ); } public void OnAfterDeserialize() { Debug.LogFormat( "OnAfterDeserialize() Count={0}", m_deserializeCount ); m_deserializeCount++; } public void OnBeforeSerialize() { // Debug.LogFormat( "OnBeforeSerialize()" ); } } // class TestComponent
OnAfterDeserialize() Count=1
OnAfterDeserialize() Count=0
Awake() Count=1
OnEnable() Count=1
Start() Count=1
えーと、恐らく一番上のログはエディタ再生前のだと思うので、それ以下のログを見ましょう。
ログを見る限りOnAfterDeserialize()
はAwake()
の前に呼ばれていそうだ。
うん、検証前に考えていた動きと一緒だね、多分!
ちなみにOnAfterDeserialize()
はエディタ上の値を変更しても呼ばれるみたいです。
これはエディタの再生状態はどちらでも関係ないみたい。
さいごに
さて、ISerializationCallbackReceiver
のスクリプトリファレンスにはこのような例のスクリプトが載せられています。
using UnityEngine; using System; using System.Collections.Generic; public class SerializationCallbackScript : MonoBehaviour, ISerializationCallbackReceiver { public List<int> _keys = new List<int> { 3, 4, 5 }; public List<string> _values = new List<string> { "I", "Love", "Unity" }; //Unity doesn't know how to serialize a Dictionary public Dictionary<int, string> _myDictionary = new Dictionary<int, string>(); public void OnBeforeSerialize() { _keys.Clear(); _values.Clear(); foreach (var kvp in _myDictionary) { _keys.Add(kvp.Key); _values.Add(kvp.Value); } } public void OnAfterDeserialize() { _myDictionary = new Dictionary<int, string>(); for (int i = 0; i != Math.Min(_keys.Count, _values.Count); i++) _myDictionary.Add(_keys[i], _values[i]); } void OnGUI() { foreach (var kvp in _myDictionary) GUILayout.Label("Key: " + kvp.Key + " value: " + kvp.Value); } }
Dictionary
型の情報をList
型に変換したり、その逆をしたりしています。
これはDictionary
型はエディタ上で通常は表示できないため、エディタ上で表示するList
型と、スクリプト上で使うDictionary
を、ISerializationCallbackReceiver
の関数を使って行き来させているというものです。
ただし、先ほど見た通りOnBeforeSerialize()
はエディタ中では山ほど呼ばれているんですよ。
すなわちこのOnBeforeSerialize()
に記述されているDictionary
型情報をList
型に変換する処理は、エディタの裏では常に呼ばれ続けているということになります。
そう考えると、ちょっと怖いですね。
という夏の怖い話でした。