【Unity】インタフェイスや抽象クラスがシリアライズできない問題をISerializationCallbackReceiverを使って解決する

インタフェイスや抽象クラスはシリアライズできないのでインスタンスの状態を保存したいときなどに困ります。
ここではそんなときのスタンダードな解決策であるISerializationCallbackReceiverを使ってみます。

シリアライズできないケース

困るケースの例としてインタフェイスを保持するようなScriptableObjectを考えます。

using UnityEngine;

public class SomeScriptableObject : ScriptableObject 
{
    // インタフェイスや抽象クラスはシリアライズされない
    public ICharacter character;
}

public class Character: ICharacter
{
    public int Id { get; set; }
}

public interface ICharacter
{
    int Id { get; set; }
}

characterの情報をシリアライズしたいけど、
インタフェイスなのでシリアライズできません。

シリアライズする

ScriptableObjectやMonoBehaviourにISerializationCallbackReceiverを実装すると
シリアライズ前後のコールバックが取れるようになります。

using UnityEngine;

public class SomeScriptableObject : ScriptableObject, ISerializationCallbackReceiver 
{
    public ICharacter character;

   #region serialization

    // シリアライズのためだけに使う変数
    // SerializeFieldにしてインスペクタ非表示
    // 今回はシリアライズ用だとわかるように変な名前にした
    [SerializeField, HideInInspector]
    private int __characterId = -1;

    public void OnBeforeSerialize()
    {
        // characterを保存するのに必要な情報をシリアライズする
        __characterId = character == null ? -1 : character.Id;
    }

    public void OnAfterDeserialize()
    {
        // IDからCharacterを再現するような処理を書く
        character = __characterId == -1 ? null : new Character(){ Id = __characterId };
    }

   #endregion
}

public class Character: ICharacter
{
    public int Id { get; set; }
}

public interface ICharacter
{
    int Id { get; set; }
}

これでcharacterオブジェクトの代わりにIdがシリアライズされるようになります。
参照するべきでない変数が存在してしまうのは気持ち悪いですがしょうがない。

確認

実際にjsonにしてみて問題ないか確認してみます。

using UnityEngine;

public class SomeBehaviour : MonoBehaviour {

    [SerializeField]
    private SomeScriptableObject _scriptableObject;

    public void Awake()
    {
        // キャラクターを生成してシリアライズ
        _scriptableObject.character = new Character(){ Id = 123 };
        var json = JsonUtility.ToJson(_scriptableObject);

        Debug.Log(json); // {"__characterId":123}

        // Characterにnullを入れてデシリアライズ
        _scriptableObject.character = null;
        JsonUtility.FromJsonOverwrite(json, _scriptableObject);

        Debug.Log(_scriptableObject.character.Id); // 123
    }
}

結果はコメントを参照してください。
問題なくシリアライズされていることが確認できました。

参考

www.urablog.xyz