【Unity】Unity2019.3から抽象クラスやインタフェースがシリアライズ可能に!使い方と注意点

Unity2019.3から抽象クラスやインタフェースがシリアライズ対象として指定できるようになりました。
この記事ではその使い方と注意点についてまとめます。

Unity2019.3.0

従来のシリアライズ - SerializeField

まず従来のシリアライズとその制約についてまとめます。
フィールドをpublicにするか、SerializeFieldアトリビュートをつけるとその値はシリアライズされます。

using UnityEngine;

public class Example : MonoBehaviour
{
    public int _someInt1;

    [SerializeField]
    private int _someInt2;
}

シリアライズされた値はInspectorから編集することができます。

f:id:halya_11:20200130121239p:plain

そしてこれらの値はPrefabやScene、ScriptableObjectに保存されます。

このようにシリアライズは便利な機能ですが、
抽象クラスやインタフェースのフィールドをシリアライズできないという制約がありました。

using UnityEngine;

public class Example : MonoBehaviour
{
    // 抽象クラスなのでシリアライズできない
    [SerializeField]
    private BaseClass _baseClass;
}

[System.Serializable]
public class SomeClass : BaseClass
{
    public int _someField;
}

[System.Serializable]
public abstract class BaseClass{ }

この制約はポリモーフィズムを前提とした設計をする上で大きな制約となっていました。
しかしUnity2019.3からは次節で説明するSerializeReferenceアトリビュートを使うことでシリアライズできるようになりました。

抽象クラスやインタフェースがシリアライズできるように - SerializeReference

さて、SerializeFieldの代わりにUnity2019.3で追加されたSerializeReferenceアトリビュートを使うと、
抽象クラスやインタフェースのフィールドをシリアライズすることができます。

using UnityEngine;

[ExecuteAlways]
public class Example : MonoBehaviour
{
    // 抽象クラスのフィールドをシリアライズ
    [SerializeReference]
    private BaseClass _baseClass;

    private void OnEnable()
    {
        _baseClass = new SomeClass();
    }
}

[System.Serializable]
public class SomeClass : BaseClass
{
    public int _someField;
}

[System.Serializable]
public abstract class BaseClass{ }

上記のスクリプトをGameObjectにアタッチすると、下図のようにBaseClassのサブクラスであるSomeClass型のフィールドがInspectorに表示されます。

f:id:halya_11:20200130121340p:plain

SerializeReferenceは「参照」をシリアライズする

このSerializeReferenceは、実はSerializeFieldのようにオブジェクトのフィールドの値を直接シリアライズしているのではなく、オブジェクトの参照をシリアライズしています。
これを確認するために以下のコードを書いてみます。

using UnityEngine;

[ExecuteAlways]
public class Example : MonoBehaviour
{
    [SerializeReference]
    private SomeClass _someClass1;
    [SerializeReference]
    private SomeClass _someClass2;

    private void OnEnable()
    {
        _someClass1 = new SomeClass();
        // _someClass1の参照を_someClass2に代入
        _someClass2 = _someClass1;
    }
}

[System.Serializable]
public class SomeClass
{
    public int _someField;
}

これをGameObjectにアタッチしてInspectorを見ると、_someClass1と_someClass2の値が連動することが確認できます。
これは_someClass1と_someClass2が同じオブジェクトを参照しているためです。

初期化処理の違い

SerializeReferenceを使う際の注意点として、SerializeFieldとの初期化処理の違いがあります。

まずSerializeFieldはデフォルトコンストラクタで初期化が行われます。
つまり明示的な初期化を行わなくてもnullになることはありません。

これに対してSerializeReferenceは明示的に初期化しないとnullが代入されます。
この仕様は以下のソースコードで確認できます。

using UnityEngine;

public class Example : MonoBehaviour
{
    [SerializeField]
    private SomeClass _someClass1;
    [SerializeReference]
    private SomeClass _someClass2;

    private void Awake()
    {
        Debug.Log(_someClass1); // SomeClass
        Debug.Log(_someClass2); // Null
    }
}

[System.Serializable]
public class SomeClass
{
    public int _someField;
}

YAMLを見て理解を深める

最後に、より理解を深めるために実際にシリアライズされたYAMLを見てみます。

まずクラス型のフィールドをSerializeFieldでシリアライズすると、以下のようにフィールドの中にフィールドのマッピング入れ子にして直接書かれます。

MonoBehaviour:
  (中略)
  _someClass1:
    _someField1: 123
    _someField2: 456

これに対してSerializeReferenceでシリアライズすると、以下のようにオブジェクトの情報がreferencesの中に全て書かれます。
そしてこのIDだけがSerializeReferenceを付けたフィールドのシリアライズ情報として書き込まれています。
この仕組みにより前述の「参照のシリアライズ」が実現されています。

MonoBehaviour: 
  (中略)
  _someClass1:
    id: 0
  references:
    version: 1
    00000000:
      type: {class: SomeClass, ns: , asm: Assembly-CSharp}
      data:
        _someField1: 123
        _someField2: 456