徳島ゲーム開発ごっこ 技術ブログ

ゲームを作るために役に立ったり立たなかったりする技術を学んでいきます!

【Unity】ISerializationCallbackReceiverを使ってみたんだ

 ISerializationCallbackReceiverというものが、随分前に追加されたことは聞いていたのですが、機会がなかったため今まで使っていませんでした。
 今回使い機会がありましたので、今更ですがISerializationCallbackReceiverのメモ書きです。


この記事にはUnity2017.1.0f3を使用しています。

ISerializationCallbackReceiverって何それ

 ISerializationCallbackReceiverは、継承することでデシリアライズ後シリアライズ前に指定された関数が呼ばれるようになるという便利なものなのです。

スクリプトリファレンス
docs.unity3d.com

 では実際に使ってみましょう。
 

JsonUtilityときっと仲良し

 ISerializationCallbackReceiverJsonUtilityと相性がいいのではないでしょうか。
 今回僕も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

f:id:urahimono:20170806104016p:plain

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

f:id:urahimono:20170806104039p:plain

{"m_dateText":"2018-05-30"}

 このようにデータを扱う側からすれば、自動的に必要な形にデータを変換してくれるように見えるため、実際のデータの型とスクリプト上で扱う型の違いを気にしなくていいようになります。
 他にもfloat型の情報を、指定した小数点以下を丸めてからJsonデータに変換するときなど使えると思います。

MonoBehaviourにも使ってみたんだけど……

 そしてこの便利なISerializationCallbackReceiverMonoBehaviourを継承したコンポーネントにも使うことができますよ。

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

f:id:urahimono:20170806104100g:plain
f:id:urahimono:20170806104117p:plain

 上記のスクリプトを記述したら、ログがえらいことになってしまった。
 ちなみにこの状態、エディタをまだ再生していません!
 な、なにが起きてしまったのやら。
 あっ、もしかして、エディタのシリアライズが毎フレーム呼ばれているのだろうか。
 とりあえず再生してみましょう。

f:id:urahimono:20170806104134p:plain

 ログがよくわからない。
 多分これエディタ再生前のログも入っちゃてるだろう!
 ちょっとスクリプトを修正しましょう。
 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

f:id:urahimono:20170806104155p:plain

OnAfterDeserialize() Count=1
OnAfterDeserialize() Count=0
Awake() Count=1
OnEnable() Count=1
Start() Count=1

 えーと、恐らく一番上のログはエディタ再生前のだと思うので、それ以下のログを見ましょう。
 ログを見る限りOnAfterDeserialize()Awake()の前に呼ばれていそうだ。
 うん、検証前に考えていた動きと一緒だね、多分!

 ちなみにOnAfterDeserialize()はエディタ上の値を変更しても呼ばれるみたいです。
 これはエディタの再生状態はどちらでも関係ないみたい。

f:id:urahimono:20170806104213g:plain

さいごに

 さて、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型に変換する処理は、エディタの裏では常に呼ばれ続けているということになります。

 そう考えると、ちょっと怖いですね。

 という夏の怖い話でした。