Unityで参照をシリアライズするSerializeReferenceアトリビュートについてまとめました。
Unity2019.4.0
はじめに
SerializeReference
アトリビュートはUnity2019.3で追加された、参照をシリアライズするための機能です。
うまく使えば非常に便利な機能なのですが、使いこなすには仕様をしっかり理解する必要があります。
また、値をシリアライズする従来のSerializeField
との性質の違いも把握する必要があります。
本記事ではこのSerializeReference
の基本的な知識をまとめた上で実用的な活用方法について考えます。
注意点:結構バグってた
以下のフォーラムを見ると、SerializeReference
については結構バグの報告が多い印象です。
https://forum.unity.com/threads/serializereference-attribute.678868/
修正は随時行われているようなので、微妙なUnityのバージョン違いで挙動が異なることがあると思います。
Unity2019.3の前半あたりは結構まだバグが多いかもしれません。
本記事中でももし古い内容などありましたらご指摘いただけますと幸いです。
以前書いた記事
なお以前SerializeReference
について以下の記事を書きましたが、あくまで触りだけだったので本記事できちんとまとめます。
SerializeFieldを復習する
SerializeReference
について学ぶ前に、まずは以前からあるSerializeField
の挙動について簡単にまとめます。
プリミティブ型・構造体をシリアライズできる
SerializeField
を付けるとprivateなプリミティブ型、構造体を明示的にシリアライズ対象にできます。
ためしにいくつかのプリミティブ型や構造体にSerializeField
を付けたコンポーネントを作ってみます。
using System; using UnityEngine; public class Example : MonoBehaviour { [SerializeField] private int _field1; // プリミティブ [SerializeField] private Vector3 _field2; // Unity定義の構造体 [SerializeField] private ExampleStruct _field3; // 自作の構造体 [Serializable] // 自作の構造体にはSerializableアトリビュートをつける public struct ExampleStruct { public int exampleInt; public string exampleString; } }
このコンポーネントを適当なGameObjectにアタッチすると、SceneやPrefabに値がシリアライズされます。
またそれらの値は以下のようにInspectorから編集可能になります。
UnityEngine.Objectの派生型の参照をシリアライズできる
また、UnityEngine.Objectから派生した型のインスタンスに関してはその参照をシリアライズすることができます。
using UnityEngine; public class Example : MonoBehaviour { [SerializeField] private GameObject _field1; [SerializeField] private MeshRenderer _field2; [SerializeField] private Example _field3; }
このコンポーネントを適当なGameObjectにアタッチすると以下のように各オブジェクトの参照を代入できることが確認できます。
非UnityEngine.Objectなクラスのインスタンスは値としてシリアライズできる
UnityEngine.Objectではないクラスのインスタンスも、Serializable
アトビリュートが付いていればシリアライズすることができます。
ただしこの場合、参照ではなくフィールドの値をシリアライズします。
つまり、同じインスタンスを違うフィールドに代入しても、デシリアライズされた際にそれらは別々のインスタンスになってしまいます。
この挙動を確認するコードを以下に示します。
using System; using UnityEditor; using UnityEditor.SceneManagement; using UnityEngine; public class Example : MonoBehaviour { [SerializeField] private ExampleInner _inner1; [SerializeField] private ExampleInner _inner2; [MenuItem("Example/SerializeTest")] public static void SerializeTest() { var example = FindObjectOfType<Example>(); if (example == null) return; var gameObject = example.gameObject; // _inner1と_inner2に同じインスタンスを代入する var inner = new ExampleInner { exampleInt = 1234, exampleString = "Example" }; example._inner1 = inner; example._inner2 = inner; EditorUtility.SetDirty(example); // 参照が同じか判定する Debug.Log($"判定1: {example._inner1 == example._inner2} ({example._inner1.GetHashCode()} : {example._inner2.GetHashCode()})"); // シリアライズ&デシリアライズするためシーンを切り替えて戻す var scenePath = gameObject.scene.path; EditorSceneManager.NewScene(NewSceneSetup.EmptyScene, NewSceneMode.Single); EditorSceneManager.OpenScene(scenePath); // 参照が同じか改めて判定する example = FindObjectOfType<Example>(); Debug.Log($"判定2: {example._inner1 == example._inner2} ({example._inner1.GetHashCode()} : {example._inner2.GetHashCode()})"); } // シリアライズ対象のクラス [Serializable] public class ExampleInner { public int exampleInt; public string exampleString; } }
これをシーン上の適当なGameObjectにアタッチしてExample > SerializeTest
を実行すると以下のようなログが出力されます。
デシリアライズされると二つのオブジェクトが全く異なるインスタンスになっていることが確認できました。
ちなみにこうしてシリアライズしたフィールドはnullにならず必ずインスタンスが代入された状態になるという仕様があります。
抽象クラスやインタフェースには非対応
さてSerializeField
は非UnityEngine.Objectの抽象クラス型やインタフェース型をシリアライズ対象にすることはできません。
ただしUnityEngine.Object派生型の抽象クラスについてはシリアライズ対象にできます。
using UnityEngine; public class Example : MonoBehaviour { // これらはシリアライズできない [SerializeField] private ExampleAbstract _field1; [SerializeField] private IExample _field2; // これらはシリアライズできる [SerializeField] private ExampleAbstractBehaviour _field3; } public abstract class ExampleAbstract { } public interface IExample { } public abstract class ExampleAbstractBehaviour : MonoBehaviour { }
Genericな型はシリアライズできる(Unity2020.1から)
なおUnity2020.1からはGenericな型をSerializeFieldでシリアライズできるようになりました。
using System; using UnityEngine; public class Example : MonoBehaviour { // Genericなクラスのインスタンスをシリアライズ可能に [SerializeField] private ExampleGeneric<int> _example; } // SerializableでGenericなクラス [Serializable] public class ExampleGeneric<T> { public T _testField; }
SerializeReferenceの仕様を理解する
それでは次にSerializeReference
の仕様を見ていきます。
SerializeReferenceは非UnityEngine.Objectのための機能
さてまず前提として、SerializeReference
はUnityEngine.Objectを継承した型ではない型の参照をシリアライズするためのものです。
前述の通り、SerializeField
はUnityEngine.Objectの派生型の参照をシリアライズできるのでそっちを使えばOKです。
仮にSerializeReference
アトリビュートを付けたInterface型にUnityEngine.Objectを代入しようとすると以下のようなエラーが出力されます。
Fields with [SerializeReference] cannot serialize objects that derive from Unity.Object
参照をシリアライズする
さてSerializeReference
はSerializeField
とは異なり、参照情報を含めてシリアライズします。
これを確認するために、SerializeField
のときに使ったコードをSerializeReference
に書き換えてみます。
using System; using UnityEditor; using UnityEditor.SceneManagement; using UnityEngine; public class Example : MonoBehaviour { [SerializeReference] private ExampleInner _inner1; [SerializeReference] private ExampleInner _inner2; [MenuItem("Example/SerializeTest")] public static void SerializeTest() { var example = FindObjectOfType<Example>(); if (example == null) return; var gameObject = example.gameObject; // _inner1と_inner2に同じインスタンスを代入する var inner = new ExampleInner { exampleInt = 1234, exampleString = "Example" }; example._inner1 = inner; example._inner2 = inner; EditorUtility.SetDirty(example); // 参照が同じか判定する Debug.Log($"判定1: {example._inner1 == example._inner2} ({example._inner1.GetHashCode()} : {example._inner2.GetHashCode()})"); // シリアライズ&デシリアライズするためシーンを切り替えて戻す var scenePath = gameObject.scene.path; EditorSceneManager.NewScene(NewSceneSetup.EmptyScene, NewSceneMode.Single); EditorSceneManager.OpenScene(scenePath); // 参照が同じか改めて判定する example = FindObjectOfType<Example>(); Debug.Log($"判定2: {example._inner1 == example._inner2} ({example._inner1.GetHashCode()} : {example._inner2.GetHashCode()})"); } // シリアライズ対象のクラス [Serializable] public class ExampleInner { public int exampleInt; public string exampleString; } }
これをシーン上の適当なGameObjectにアタッチしてExample > SerializeTest
を実行すると以下のようなログが出力されます。
同じインスタンスを二つのフィールドにシリアライズしてデシリアライズしても、それらは同じインスタンスを示すことがわかりました。
抽象型やインタフェース型のフィールドをシリアライズできる
SerializeReference
アトリビュートは抽象クラスやインタフェース型のフィールドに付けることもできます。
これらのフィールドにインスタンスを代入すると実際の型の情報がシリアライズされます。
using System; using UnityEditor; using UnityEngine; public class Example : MonoBehaviour { // インタフェース [SerializeReference] private IExampleInner _inner1; // 抽象クラス [SerializeReference] private ExampleInnerBase _inner2; [MenuItem("Example/SerializeTest")] public static void SerializeTest() { var example = FindObjectOfType<Example>(); if (example == null) return; // _inner1と_inner2に同じインスタンスを代入する var inner = new ExampleInner { exampleInt = 1234, exampleString = "Example" }; example._inner1 = inner; example._inner2 = inner; EditorUtility.SetDirty(example); } public interface IExampleInner { } public abstract class ExampleInnerBase { } [Serializable] public class ExampleInner : ExampleInnerBase, IExampleInner { public int exampleInt; public string exampleString; } }
Inspectorにも実際の型に対応したプロパティが表示されます。
参照を二つのMonoBehaviour間で共有することはできない
SerializeReference
でシリアライズした参照情報を二つのMonoBehaviour間で使いまわすことはできません。
ScriptableObject
にシリアライズした時も同様で、二つのScriptableObject間で使いまわすことはできません。
nullを表現できる
上述の通り、SerializeField
には、このアトリビュートを付けたフィールドはnullにならないという仕様がありました。
これに対してSerializeReference
を付けたフィールドはnullを表現できます。
Genericを使ったクラスのインスタンスには使えない
Genericを使ったクラスのインスタンスをシリアライズしようとしてもできません。
そのようなケースではさらにそれを継承する非Genericなクラスを定義する必要があります。
using System; using UnityEditor; using UnityEngine; public class Example : MonoBehaviour { // シリアライズできない [SerializeReference] private ExampleInnerBase _inner1; [MenuItem("Example/SerializeTest")] public static void SerializeTest() { var example = FindObjectOfType<Example>(); if (example == null) return; // シリアライズできる var inner = new ExampleInnerInt { example = 1234, exampleString = "Example" }; // シリアライズできない /* var inner = new ExampleInner<int> { example = 1234, exampleString = "Example" }; */ example._inner1 = inner; EditorUtility.SetDirty(example); } public abstract class ExampleInnerBase { } public class ExampleInner<T> : ExampleInnerBase { public T example; public string exampleString; } [Serializable] public class ExampleInnerInt : ExampleInner<int> { } }
SerializeReferenceの使いどころ
さてこのように意外と複雑なSerializeReference
ですが、実際どのような活用方法があるのでしょうか。
その① グラフ構造やツリー構造をシリアライズする
参照をシリアライズするという特徴からまず考えられるのはグラフ構造やツリー構造です。
このようにお互いに参照関係を持つ構造はまさにSerializeReference
の使いどころといえます。
ScriptableObjectとSerializeField
でも実現可能ですが、ノードの数が多いと辛いです。
その② エディタの入力インタフェースを手間なく切り替えられる
次の使いどころとして、入力インタフェースが簡単に切り替えられる点があります。
例えばいま、スキルの効果値を入力することを考えます。
スキルの種類には攻撃とバフがあり、攻撃の場合には攻撃力を入力できます。
バフの場合はバフの種類(攻撃力、防御力など)と効果値、そして効果時間を入力できます。
これをSerializeReference
を使って実装してみます。
using System; using UnityEditor; using UnityEngine; public class Example : MonoBehaviour { [SerializeReference] private Skill _skill; [MenuItem("CONTEXT/Example/Set Attack Skill")] public static void SetAttackSkill(MenuCommand command) { var example = (Example) command.context; example._skill = new AttackSkill(); EditorUtility.SetDirty(example); } [MenuItem("CONTEXT/Example/Set Buff Skill")] public static void SetBuffSkill(MenuCommand command) { var example = (Example) command.context; example._skill = new BuffSkill(); EditorUtility.SetDirty(example); } } [Serializable] public abstract class Skill { public float value; } [Serializable] public class AttackSkill : Skill { } [Serializable] public class BuffSkill : Skill { public BuffType buffType; public float duration; } public enum BuffType { Attack, Defense }
インスタンスの型を切り替えるとそれに応じて入力フィールドも変更されていることが確認できます。
その③ 振る舞いをエディタから変更できる
インスタンスが差し替えられるということは以下のように振る舞いも差し替えられます。
using System; using UnityEngine; public class Example : MonoBehaviour { [SerializeReference] private Speaker _speaker; public void Start() { _speaker?.Speak(); } } [Serializable] public abstract class Speaker { public string word; public abstract void Speak(); } [Serializable] public class NormalSpeaker : Speaker { public override void Speak() => Debug.Log($"{word}"); } [Serializable] public class LoudSpeaker : Speaker { public override void Speak() => Debug.Log($"{word}{word}{word}"); }
これを実行するとシリアライズされているインスタンスの種類に応じたSpeakメソッドが呼ばれます。
ちなみにこちらもScriptableObjectでも実現はできますが、振る舞いの種類ごとにScriptableObjectを作らなければならなくなります。