【Unity】【エディタ拡張】Custom AttributeとCustom Property Drawerを組み合わせたらInspectorがうまく表示されなくなった件と対応方法

久々にエディタ拡張で盛大にハマったので自分用にメモしておきます。
Custom AttributeとCustom Property Drawerを組み合わせたらInspectorがうまく表示されなくなった件とその対応方法です。

Unity2018.3.1

問題点

いま、SomeClassというSerializableなクラスと、そのインスタンスシリアライズしたExampleクラスがあるとします。

public class Example : MonoBehaviour
{
    public SomeClass _example;
}

[System.Serializable]
public struct SomeClass
{
    [SerializeField]
    private int _someField;
}

Inspectorはこんな感じに表示されます。まあ普通です。

f:id:halya_11:20190117224209p:plain

次にSomeClassCustomPropertyDrawerを定義してInspectorの見え方を変えたとします。

using UnityEngine;
using UnityEditor;

[CustomPropertyDrawer(typeof(SomeClass))]
public class SomeClassDrawer : PropertyDrawer
{
    private float LineHeight { get { return EditorGUIUtility.singleLineHeight + EditorGUIUtility.standardVerticalSpacing; } }
    
    public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
    {
        var someProp = property.FindPropertyRelative("_someField");

        var fieldRect = position;
        fieldRect.height = LineHeight;

        using (new EditorGUI.PropertyScope(fieldRect, label, property)) 
        {
            var preIndent = EditorGUI.indentLevel;
            EditorGUI.indentLevel = 0;

            // プロパティを描画
            EditorGUI.PropertyField(fieldRect, someProp, new GUIContent("拡張テスト"));
            
            EditorGUI.indentLevel = preIndent;
        }
    }

    public override float GetPropertyHeight(SerializedProperty property, GUIContent label)
    {
        return LineHeight;
    }
}

Inspectorは次のように変わります。フィールド名を日本語にしただけです。

f:id:halya_11:20190117224429p:plain

さらにここで、PropertyAttributeを一つ作ります。

using UnityEngine;
using UnityEditor;

[System.AttributeUsage(System.AttributeTargets.Field | System.AttributeTargets.Property, AllowMultiple = true)]
public class ExampleAttribute : PropertyAttribute {    
}

#if UNITY_EDITOR
[CustomPropertyDrawer(typeof(ExampleAttribute))]
public class ExampleAttributeDrawer : PropertyDrawer
{
    public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
    {
        EditorGUI.PropertyField(position, property, label, true);
    }
    
    public override float GetPropertyHeight (SerializedProperty property, GUIContent label)
    {
        return EditorGUI.GetPropertyHeight(property);
    }
}
#endif

そしてこのアトリビュートを最初のSomeClassインスタンスに指定します。

public class Example : MonoBehaviour
{
    [Example]
    public SomeClass _example;
}

すると、せっかく拡張したInspectorの表示が元に戻ってしまいます。

f:id:halya_11:20190117224652p:plain

まあ一つのフィールドに対してPropertyDrawerを二つ使ってしまっているのでそれはそうかなという感じです。
アトリビュートによるPropertyDrawerのほうが優先されてしまった感じです。

ただこれでは、上記のようにしてインスペクタを拡張したフィールドに
そのフィールドの可視状態を指定するアトリビュートを設定するときなどに困ってしまいます。

なので解決してみます。

解決方法

色々試した結果、スマートには解決できなかったのですが解決方法を紹介します。
まず、CustomPropertyDrawerの対象クラスとDrawerのインスタンスとの紐づけを行うstaticなクラスを定義します。

using UnityEditor;
using System.Collections.Generic;

public static class PropertyDrawerDatabase
{
    private  static Dictionary<System.Type, PropertyDrawer> _drawers = new Dictionary<System.Type, PropertyDrawer>();

    static PropertyDrawerDatabase()
    {
        _drawers = new Dictionary<System.Type, PropertyDrawer>();
        
        // クラスと対応するPropertyDrawerを登録しておく
        _drawers.Add(typeof(SomeClass), new SomeClassDrawer());
    }

    public static PropertyDrawer GetDrawer(System.Type fieldType)
    {
        PropertyDrawer drawer;
        return _drawers.TryGetValue(fieldType, out drawer) ? drawer : null;
    }
}

Typeを与えれば適したPropertyDrawerを返してくれます。
次にアトリビュートPropertyDrawerを下記のように変えます。

[CustomPropertyDrawer(typeof(ExampleAttribute))]
public class ExampleAttributeDrawer : PropertyDrawer
{
    public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
    {
        var drawer = PropertyDrawerDatabase.GetDrawer(fieldInfo.FieldType);
        drawer.OnGUI(position, property, label);
    }
    
    public override float GetPropertyHeight (SerializedProperty property, GUIContent label)
    {
        var drawer = PropertyDrawerDatabase.GetDrawer(fieldInfo.FieldType);
        return drawer.GetPropertyHeight(property, label);
    }
}

Drawerを先ほどのPropertyDrawerDatabaseから取得するようにしました。
以上で対応は完了です。

できないことと参考

下記の記事でやっているようなproperty情報のキャッシュはできないです。(やるとnullが発生しました)

light11.hatenadiary.com

もうちょっとしっかりとした実装をしたければ、下記のアセットを参考にしたり、
やりたい事次第では導入を検討するのがよさそうです。

www.asset-sale.net