【Unity】UI ToolkitでCustomPropertyDrawerを作るときの注意点【エディター拡張】

# 自己紹介
最近Unity関係で頼られることが多くなって嬉しい大学生。
Xもフォローしてね。

https://x.com/KaidoAtsuya

# 今回の記事について
今回は、IMGUIを使ったことがない人向けにUI Toolkitで実装できるPropertyDrawerを実装できるようにする記事です。
私もIMGUIを触ったことがない状態で、UI Toolkitで実装しました。

## なにつくったの？
今回は、`SerializedReference`を選択できる`SubClassSelector`AttributeをUI Toolkitで実装していきます。
`SubClassSelector`はインターフェースをシリアライズできる

### 元ネタ様

https://qiita.com/tsukimi_neko/items/7922b2433ed4d8616cce

https://github.com/mackysoft/Unity-SerializeReferenceExtensions/tree/main

# CustomPropertyDrawerとは？
`CustomPropertyDrawer`とは、Unityのインスペクターでの挙動を変えられるようになるエディター拡張です。

`PropertyDrawer`クラスを継承して実装していきます。
```Csharp
public class SubclassSelectorPropertyDrawer : PropertyDrawer
{
    // 処理を書いていく
}
```

## UI Toolkitを用いたPropertyDrawer
UI Toolkitを用いて制作する場合は`CreatePropertyGUI()`を使います。

```Csharp
public class SubclassSelectorPropertyDrawer : PropertyDrawer
{
    public override VisualElement CreatePropertyGUI(SerializedProperty property)
    {
        // VisualElementをreturnするとそれがそのインスペクターになる。
    }
}
```
# PropertyDrawerを実装する注意点まとめ
## １．保存されない問題
ただ、値を変更しても保存されません。
AssetDatabase.SaveAssets();を使用すれば保存されます。
```Csharp
property.managedReferenceValue = Activator.CreateInstance(type);
property.serializedObject.ApplyModifiedProperties();
AssetDatabase.SaveAssets();
```
## ２．タイプを取得したい
継承した`PropertyDrawer`クラスに`fieldInfo`という変数が存在し、それから取得する方法が出来ます。

## ３．Attributeの実装に使うクラス
Attributeの実装に使うクラスは`Attribute`クラスではなく`PropertyAttribute`クラスのようです。

# UI Toolkitで実装する際の注意点まとめ
## １．幅が合わない問題
### 問題の概要
下記の画像の下の部分が今回実装したものなのですが、幅がズレています。
これを修正します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3083521/936c01af-e27a-4a1f-1ffd-56014aa64a28.png)

### 修正方法
以下のようにUSSを適用してあげると良いようです。
```csharp
var element = new VisualElement();

// 何かの処理

element.AddToClassList(BaseField<object>.alignedFieldUssClassName);

// 何かの処理

return element;
```
`BaseField<T>`のTの部分はなんでもよいようです。
特に大きく処理が変わらなかったため、`Object`としています。

誰か違いを知っている人がいれば教えてください。

# 今回作ったソースコード
```Csharp:PropertyDrawer
using System;
using System.Collections.Generic;
using System.Linq;
using UnityEditor;
using UnityEngine;
using UnityEngine.UIElements;

[CustomPropertyDrawer(typeof(SubClassSelectorAttribute))]
public class SubclassSelectorPropertyDrawer : PropertyDrawer
{
    public override VisualElement CreatePropertyGUI(SerializedProperty property)
    {
        var dropdown = new DropdownField(property.name);
        dropdown.AddToClassList(BaseField<object>.alignedFieldUssClassName);
        
        // ドロップダウンの要素を指定
        var types = GetAllInterface().ToList();
        dropdown.choices = types.Select(x => x.Name).ToList();
        dropdown.value = types.First(x => x.Name == property.managedReferenceValue.GetType().Name).Name;

        dropdown.RegisterValueChangedCallback(e =>
        {
            var type = types.First(x => e.newValue == x.Name);
            property.managedReferenceValue = Activator.CreateInstance(type);
            property.serializedObject.ApplyModifiedProperties();
            AssetDatabase.SaveAssets();
        });
        
        return dropdown;
    }

    private IEnumerable<Type> GetAllInterface()
    {
        // Interfaceの型を取得する
        var interfaceType = fieldInfo.FieldType.FullName;
        var assemblies = AppDomain.CurrentDomain.GetAssemblies();

        // インターフェースを継承しているクラスの一覧を取得する。
        foreach (var assembly in assemblies)
        {
            var types = assembly.GetTypes()
                .Where(t => t.GetInterface(interfaceType) is not null && t.IsClass);
            
            foreach (var type in types)
            {
                yield return type;
            }
        }
    }
}
```

```Csharp:Attribute
using System;
using UnityEngine;

[AttributeUsage(AttributeTargets.Field)]
public class SubClassSelectorAttribute : PropertyAttribute
{
    
}
```

# 最後に
元ネタ様からすると機能不足ですが、今回使用したいものとしては十分でした。
元ネタ様はUI Toolkitを使っていなかったので、記事にしてみました。