5
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

横国ゲーム制作部Advent Calendar 2022

Day 6

インスペクタでクラス定数をenumのように選択できるようにする [Unityエディター拡張, Odin対応]

Last updated at Posted at 2022-12-06

string型の値を特定クラスの定数のみに制限してドロップダウンで選択できる属性を実装します。

はじめに

Unityのインスペクタでは、以下のような列挙型(enum)で定義されたフィールドはドロップダウンで値を選択することができます。

SampleEnumMonoBehaviour.cs
using UnityEngine;

public enum SampleEnum
{
    None,
    A,
    B,
    C,
    D,
}

public class SampleEnumMonoBehaviour : MonoBehaviour
{
    [SerializeField] private SampleEnum sampleEnum;
}

これは非常に便利な機能ですが、一方で値に意味を持たせたい場合(アセットのパスを指定するときなど)は列挙型を使わずに以下のような定数を列挙したような定義したクラスを作る場合もあると思います。

SamplePath.cs
public class SamplePath
{
    public const string PLAYER_PATH = "Assets/Prefabs/Characters/Player";
    public const string ENEMY_PATH = "Assets/Prefabs/Characters/Enemy_01";
    public const string SWORD_PATH = "Assets/Prefabs/Items/Weapons/Sword";
    // 参考:https://learn.microsoft.com/ja-jp/dotnet/csharp/fundamentals/coding-style/coding-conventions
    // C#ではアッパースネークケースを使用することは基本的にないようなので、
    // 本来はconstなフィールドも以下のようにパスカルケースで書くらしい
    // public const string SwordPath = "Assets/Prefabs/Items/Weapons/Sword";
}

string型で定義されたフィールドは基本的に手入力になるので、何もしない場合例えば上記のSamplePathの値のみに制限して入力をさせるようなことはできません。

これを以下のようにドロップダウンで入力できるようにする属性の実装を紹介します。

実装

属性DropdownConstantsAttributeの実装を示します。
通常の実装と、人気のアセットOdinに対応したものの2つを示します。

通常

DropdownConstantsAttribute.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using UnityEngine;
#if UNITY_EDITOR
using UnityEditor;
#endif

[AttributeUsage(AttributeTargets.Field)]
public class DropdownConstantsAttribute : PropertyAttribute
{
    // 定数の名前
    private readonly string[] menuNames;
    // 定数の値
    private readonly string[] menuValues;

    /// <summary>
    /// 引数で指定された型の定数のみをドロップダウンで入力できるようにする属性
    /// </summary>
    /// <param name="constantsClass">定数を取得するクラス</param>
    /// <param name="bindingFlags">取得する定数の設定用フラグ</param>
    public DropdownConstantsAttribute(Type constantsClass,
        BindingFlags bindingFlags = BindingFlags.Public | BindingFlags.Static | BindingFlags.FlattenHierarchy)
    {
        var fields = constantsClass.GetFields(bindingFlags);
        var menuDictionary = new Dictionary<string, string>(fields.Length);
        foreach (var field in fields)
        {
            if (field.FieldType == typeof(string))
            {
                menuDictionary.Add(field.Name, field.GetValue(constantsClass) as string);
            }
        }

        menuNames = menuDictionary.Keys.ToArray();
        menuValues = menuDictionary.Values.ToArray();
    }

#if UNITY_EDITOR
    [CustomPropertyDrawer(typeof(DropdownConstantsAttribute))]
    public class DropdownConstantsAttributeDrawer : PropertyDrawer
    {
        // 現在選択されている定数のインデックス
        private int index = -1;

        public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
        {
            if (property.type != "string") return;

            var targetAttribute = (DropdownConstantsAttribute)attribute;

            if (index < 0)
            {
                index = string.IsNullOrEmpty(property.stringValue)
                    ? 0
                    : Array.IndexOf(targetAttribute.menuValues, property.stringValue);
            }

            // ドロップダウンメニューを表示
            index = EditorGUI.Popup(position, label.text, index, targetAttribute.menuNames);
            property.stringValue = targetAttribute.menuValues[index];
        }
    }
#endif
}

Odin版

DropdownConstantsAttribute.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using UnityEngine;
#if UNITY_EDITOR
using Sirenix.OdinInspector.Editor;
using UnityEditor;
#endif

[AttributeUsage(AttributeTargets.Field)]
public class DropdownConstantsAttribute : Attribute
{
    private readonly string[] menuNames;
    private readonly string[] menuValues;
    private readonly bool viewValue;

    public DropdownConstantsAttribute(Type constantsClass, bool viewValue = false,
        BindingFlags bindingFlags = BindingFlags.Public | BindingFlags.Static | BindingFlags.FlattenHierarchy)
    {
        this.viewValue = viewValue;
        var fields = constantsClass.GetFields(bindingFlags);
        var menuDictionary = new Dictionary<string, string>(fields.Length);
        foreach (var field in fields)
        {
            if (field.FieldType == typeof(string))
            {
                menuDictionary.Add(field.Name, field.GetValue(constantsClass) as string);
            }
        }

        menuNames = menuDictionary.Keys.ToArray();
        menuValues = menuDictionary.Values.ToArray();
    }

#if UNITY_EDITOR
    [DrawerPriority(DrawerPriorityLevel.AttributePriority)]
    private class DropdownConstantsAttributeDrawer : OdinAttributeDrawer<DropdownConstantsAttribute, string>
    {
        private int index;

        protected override void Initialize()
        {
            if (string.IsNullOrEmpty(ValueEntry.SmartValue))
            {
                index = 0;
                ValueEntry.SmartValue = Attribute.menuValues.Length > 0 ? Attribute.menuValues[0] : "";
            }
            else
            {
                index = Array.IndexOf(Attribute.menuValues, ValueEntry.SmartValue);
            }
        }

        protected override void DrawPropertyLayout(GUIContent label)
        {
            index = EditorGUILayout.Popup(label, index, Attribute.menuNames);
            ValueEntry.SmartValue = Attribute.menuValues[index];
            if (Attribute.viewValue)
            {
                GUI.enabled = false;
                EditorGUILayout.LabelField($"{Attribute.menuNames[index]} :", Attribute.menuValues[index]);
                GUI.enabled = true;
            }
        }
    }
#endif
}

使い方

以下のようにSerializeFieldなどと併用して使用します。
引数でドロップダウンで表示したい定数が定義されているクラスの型を指定します。

SamplePathMonoBehaviour.cs
using UnityEngine;

public class SamplePathMonoBehaviour : MonoBehaviour
{
    [SerializeField] private string path;
    [SerializeField] [DropdownConstants(typeof(SamplePath))]
    private string dropdownPath;
}

これで前述のように定数名をドロップダウンで表示して選択できるようになります。

注意点

注意点がいくつかあります。
まず、表示するのは定数名ですが、実際にフィールドに格納されているのは定数の値(画像のPLAYER_PATHならAssets/Prefabs/Characters/Player)です。
定数の値もインスペクタに表示したい場合は、LabelFieldなどを使って下に表示する、マウスオーバーした時にツールチップとして表示するようにするなど少し工夫が必要です。
Odin版ではラベルを下に表示する方法で対応しているのでご覧ください。
また当たり前の話ですが、これはインスペクタ上から値を設定する場合は制限として機能しますが、スクリプトからの値変更される場合の制限にはなりません。
そして、BindingFlagsを引数で受け取れるようにすることで取得する定数をある程度制御できるようにしていますが、静的な定数以外を想定していない実装になっているためこれでInstanceなどの指定を行うと例外が発生します。
アクセス修飾の指定程度にとどめて使用してください。

おわりに

いかがだったでしょうか。
BGMやSEの配置場所を指定するときなどにも使えると思うので、ぜひ作ってみてください。

5
3
3

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?