【Unity】【エディタ拡張】SerializeReferenceでシリアライズする型の派生型をInspectorから選択できるようにする

Unityのエディタ拡張でSerializeReferenceでシリアライズする型の派生型をInspectorから選択できるようにする方法についてまとめました。

Unity2020.3.15f2

やりたいこと

Unity2019.3で追加されたSerializeReferenceアトリビュートを使うと、インターフェースや抽象クラスの派生型の情報をシリアライズできます。

light11.hatenadiary.com

ただしこれはSerializeFieldのようにInspectorから何かを設定・調整できるものではなく、スクリプトを通して操作を行う必要があります。
以下のようにドロップダウンで派生型を選択してインスタンスアサインしてくれたら便利そうです。

f:id:halya_11:20211127160419p:plain
ドロップダウンで派生型を選択

本記事ではこれをエディタ拡張で実装する方法についてまとめます。

実装

それでは早速ソースコードです。

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を使用しています。

light11.hatenadiary.com

また、PropertyDrawerのインスタンスは複数のプロパティで使いまわされることを意識した実装にしないとリストにSerializeReferenceを付けたときなどにバグるので注意してください。
PropertyDrawerの基本的な実装は以下の記事にまとめています。

light11.hatenadiary.com

使ってみる

それでは実際にこれを使ってみます。
まずインターフェースと抽象クラスとそれを実装した派生型を用意します。

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にアタッチします。

f:id:halya_11:20211127161725g:plain
結果

正常に動作していることを確認できました。

関連

light11.hatenadiary.com

light11.hatenadiary.com

light11.hatenadiary.com