Unity2019.3から抽象クラスやインタフェースがシリアライズ対象として指定できるようになりました。
この記事ではその使い方と注意点についてまとめます。
- ちゃんと理解するための記事を書きました
- 従来のシリアライズ - SerializeField
- 抽象クラスやインタフェースがシリアライズできるように - SerializeReference
- SerializeReferenceは「参照」をシリアライズする
- 初期化処理の違い
- YAMLを見て理解を深める
Unity2019.3.0
ちゃんと理解するための記事を書きました
SerializeReference
をしっかり理解するための記事を別記事として投稿しました。
より踏み込んだ内容となっていますので、必要に応じてこちらも参照してください。
従来のシリアライズ - SerializeField
まず従来のシリアライズとその制約についてまとめます。
フィールドをpublicにするか、SerializeFieldアトリビュートをつけるとその値はシリアライズされます。
using UnityEngine; public class Example : MonoBehaviour { public int _someInt1; [SerializeField] private int _someInt2; }
シリアライズされた値はInspectorから編集することができます。
そしてこれらの値は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に表示されます。
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