Unityのエディタ拡張でSerializeReferenceでシリアライズする型の派生型をInspectorから選択できるようにする方法についてまとめました。
Unity2020.3.15f2
やりたいこと
Unity2019.3で追加されたSerializeReferenceアトリビュートを使うと、インターフェースや抽象クラスの派生型の情報をシリアライズできます。
ただしこれはSerializeFieldのようにInspectorから何かを設定・調整できるものではなく、スクリプトを通して操作を行う必要があります。
以下のようにドロップダウンで派生型を選択してインスタンスをアサインしてくれたら便利そうです。
本記事ではこれをエディタ拡張で実装する方法についてまとめます。
実装
それでは早速ソースコードです。
using System; using UnityEngine; #if UNITY_EDITOR using System.Collections.Generic; using System.Linq; using System.Reflection; using UnityEditor; using UnityEngine.Assertions; #endif [AttributeUsage(AttributeTargets.Field)] public sealed class SelectableSerializeReferenceAttribute : PropertyAttribute { } #if UNITY_EDITOR [CustomPropertyDrawer(typeof(SelectableSerializeReferenceAttribute))] public sealed class SelectableSerializeReferenceAttributeDrawer : PropertyDrawer { private readonly Dictionary<string, PropertyData> _dataPerPath = new Dictionary<string, PropertyData>(); private PropertyData _data; private int _selectedIndex; private void Init(SerializedProperty property) { if (_dataPerPath.TryGetValue(property.propertyPath, out _data)) { return; } _data = new PropertyData(property); _dataPerPath.Add(property.propertyPath, _data); } public override void OnGUI(Rect position, SerializedProperty property, GUIContent label) { Assert.IsTrue(property.propertyType == SerializedPropertyType.ManagedReference); Init(property); var fullTypeName = property.managedReferenceFullTypename.Split(' ').Last(); _selectedIndex = Array.IndexOf(_data.DerivedFullTypeNames, fullTypeName); using (var ccs = new EditorGUI.ChangeCheckScope()) { var selectorPosition = position; var indent = EditorGUI.indentLevel; EditorGUI.indentLevel = 0; selectorPosition.width -= EditorGUIUtility.labelWidth; selectorPosition.x += EditorGUIUtility.labelWidth; selectorPosition.height = EditorGUIUtility.singleLineHeight; var selectedTypeIndex = EditorGUI.Popup(selectorPosition, _selectedIndex, _data.DerivedTypeNames); if (ccs.changed) { _selectedIndex = selectedTypeIndex; var selectedType = _data.DerivedTypes[selectedTypeIndex]; property.managedReferenceValue = selectedType == null ? null : Activator.CreateInstance(selectedType); } EditorGUI.indentLevel = indent; } EditorGUI.PropertyField(position, property, label, true); } public override float GetPropertyHeight(SerializedProperty property, GUIContent label) { Init(property); if (string.IsNullOrEmpty(property.managedReferenceFullTypename)) { return EditorGUIUtility.singleLineHeight; } return EditorGUI.GetPropertyHeight(property, true); } private class PropertyData { public PropertyData(SerializedProperty property) { var managedReferenceFieldTypenameSplit = property.managedReferenceFieldTypename.Split(' ').ToArray(); var assemblyName = managedReferenceFieldTypenameSplit[0]; var fieldTypeName = managedReferenceFieldTypenameSplit[1]; var fieldType = GetAssembly(assemblyName).GetType(fieldTypeName); DerivedTypes = TypeCache.GetTypesDerivedFrom(fieldType).Where(x => !x.IsAbstract && !x.IsInterface) .ToArray(); DerivedTypeNames = new string[DerivedTypes.Length]; DerivedFullTypeNames = new string[DerivedTypes.Length]; for (var i = 0; i < DerivedTypes.Length; i++) { var type = DerivedTypes[i]; DerivedTypeNames[i] = ObjectNames.NicifyVariableName(type.Name); DerivedFullTypeNames[i] = type.FullName; } } public Type[] DerivedTypes { get; } public string[] DerivedTypeNames { get; } public string[] DerivedFullTypeNames { get; } private static Assembly GetAssembly(string name) { return AppDomain.CurrentDomain.GetAssemblies() .SingleOrDefault(assembly => assembly.GetName().Name == name); } } } #endif
SerializeReferenceでシリアライズされている値を書き換えるには、上記のようにSerializedProperty.managedReferenceValue
にインスタンスをアサインします。
また、SerializedProperty.managedReferenceFullTypename
からはシリアライズされているインスタンスの型の情報が、
SerializedProperty.managedReferenceFieldTypename
からはSerializeReferenceをつけているフィールドの型の情報が得られます。
それ以外は特に難しいことはやっていません。
派生型を取得するのに以下の記事で説明しているTypeCacheを使用しています。
また、PropertyDrawerのインスタンスは複数のプロパティで使いまわされることを意識した実装にしないとリストにSerializeReferenceを付けたときなどにバグるので注意してください。
PropertyDrawerの基本的な実装は以下の記事にまとめています。
使ってみる
それでは実際にこれを使ってみます。
まずインターフェースと抽象クラスとそれを実装した派生型を用意します。
using System; using UnityEngine; using Object = UnityEngine.Object; public interface IInterface { } [Serializable] public abstract class InterfaceImpl : IInterface { } [Serializable] public class Foo : InterfaceImpl { [SerializeField] private int _testValue; } [Serializable] public class Bar : IInterface { [SerializeField] private Object _testObj; }
次にこれらをシリアライズしたMonoBehaviourを用意します。
using UnityEngine; public class Example : MonoBehaviour { [SerializeReference, SelectableSerializeReference] public IInterface testInterface; [SerializeReference, SelectableSerializeReference] public IInterface[] testInterfaces; [SerializeReference, SelectableSerializeReference] public InterfaceImpl[] testAbstractClasses; }
これを適当なGameObjectにアタッチします。
正常に動作していることを確認できました。