⚙️

【Unity,UIToolKit】Enum選択に応じてインスペクターの入力フィールドを動的に切り替える【Custom Editor】

に公開
  • UI Toolkit, Custom Editor で、
    列挙型に応じて Inspector の入力フィールドを切り替える方法を解説するよ~
  • 使用した Unity のバージョンは 2022.3.9f

何がしたいのか

  • float param 変数に対してインスペクターから入力ができる状況
  • 選択に応じて受け付ける型を int, float と切り替えたい

結論、何ができたか

  • 列挙型(mode)に応じて、ラベルと入力できる型を動的に切り替え
  • あくまで入力内容は param へ行く

必要なプログラム

データ側

SampleData.cs
using UnityEngine;

public class SampleData : MonoBehaviour
{
    public enum Mode
    {
        Int,
        Float,
    }

    public Mode mode;
    public float param;  //mode に応じて Inspector の UI を切り替え
}

エディタ拡張側

  • 今回は uGUI ではなく UIToolKit を用いている
SampleDataEditor.cs
using UnityEditor;
using UnityEditor.UIElements;
using UnityEngine;
using UnityEngine.UIElements;

[CustomEditor(typeof(SampleData))]
public class SampleDataEditor : Editor
{
    SerializedProperty mode;
    SerializedProperty param;
    VisualElement paramContainer;

    //インスペクターの UI 構築
    public override VisualElement CreateInspectorGUI()
    {
        var root = new VisualElement();

        //プロパティ取得
        mode  = serializedObject.FindProperty("mode");
        param = serializedObject.FindProperty("param");

        //モード選択フィールド
        var modeField = new PropertyField(mode);
        //値変更時に呼び出す関数をセット
        modeField.RegisterValueChangeCallback(_ => UpdateParamField());
        root.Add(modeField);

        //切り替え先コンテナ
        paramContainer = new VisualElement();
        root.Add(paramContainer);

        //バインド, 初回描画
        root.Bind(serializedObject);
        UpdateParamField();

        return root;
    }

    //mode に応じて paramContainer 内を再生成する
    void UpdateParamField()
    {
        //既存 UI を全消去し、最新値を取得する
        paramContainer.Clear();
        serializedObject.Update();

        switch ((SampleData.Mode)mode.enumValueIndex)
        {
            case SampleData.Mode.Int:
                var iField = new IntegerField("Int Value");
                iField.value = Mathf.RoundToInt(param.floatValue);
                iField.RegisterValueChangedCallback(evt => param.floatValue = evt.newValue);
                paramContainer.Add(iField);
                break;

            case SampleData.Mode.Float:
                var fField = new FloatField("Float Value");
                fField.value = param.floatValue;
                fField.RegisterValueChangedCallback(evt => param.floatValue = evt.newValue);
                paramContainer.Add(fField);
                break;
        }

        //SerializedProperty の変更を確定
        serializedObject.ApplyModifiedProperties();
    }
}

実装のポイント

  • インスペクターの操作に合わせて即時 UI 更新

    • modeField.RegisterValueChangeCallback(_ => UpdateParamField());
    • モード変更と同時に param 入力欄を再生成し、誤入力を防止
  • float → int へ切り替え時の丸め処理

    • Mathf.RoundToInt(param.floatValue)
    • 前回までの小数値を整数へ変換(小数のまま param に残存しないようにする)

動作イメージ

補足


入力内容に他意はない

  • IntegerField, FloatField に文字列を入れてもちゃんと弾いてくれます

実際の開発で導入した例

背景

  • サウンドライブラリでリソース(AudioClip)のキャッシュ機能を実装していた
  • LRU(Least Recently Used), TTL(Time To Live) などとキャッシュ方式毎にクラスを実装、
    SoundCacheFactory クラスで外部から指定方式のものを生成する形だった
SoundCacheFactory.cs
public static class SoundCacheFactory
{
    public enum Type
    {
        LeastRecentlyUsed,
        TimeToLive,
        Random
    }

    /// <summary>
    /// キャッシュ方式とパラメータからISoundCacheを生成する
    /// </summary>
    public static ISoundCache Create(Type t, float param)
    {
        return t switch
        {
            Type.LeastRecentlyUsed    => CreateLRU(param),
            Type.TimeToLive           => CreateTTL(param),
            Type.Random               => CreateRandom((int)param),
            _           => throw new ArgumentOutOfRangeException(nameof(t))
        };
    }

    public static ISoundCache CreateLRU(float idleTimeThreshold) {}
    public static ISoundCache CreateTTL(float ttlSeconds) {}
    public static ISoundCache CreateRandom(int maxCacheCount) {}
}
  • また、プリセット機能としてエディタ上でのキャッシュ方式選択, param 入力も実装していた

    こんな感じ

問題

  • param が何のパラメータかエディタ上ではわからない

結果

  • 選択に応じてラベル(param の部分)と入力できる型が変わるようにした
  • Ramdom は int, それ以外は float を受け付ける

    撮影した時期はキャッシュ方式を一部略称で表示していた

参考

Discussion