[Unity] インスペクタから定数シンボルを設定できるようにする

Unityに関するメモ書きです。

Unityのインスペクタ上で値を設定する上で不便に感じていたことがあります。
例えば、以下のように2つのクラス意味のある値(マジックナンバー)を設定することを考えてみます。

// 定数シンボルの定義
public class TestClass1 : MonoBehaviour {
    private int width;
    private int height;

    private void Start() {
        width = 400;
        height = 300;
    }
}
// 定数シンボルの定義
public class TestClass2 : MonoBehaviour {
    private int width;
    private int height;

    private void Start() {
        width = 400;
        height = 300;
    }
}

2つのクラスで使用するwidth、heightは同じ値である必要があるとします。
このようにスクリプト上でマジックナンバーを設定する場合、次のように定数シンボルを用いるのが一般的でしょう。

// 定数シンボルの定義
public class Constant {
    public int SMALL  = 100;
    public int MEDIUM = 200;
    public int LARGE  = 300;
}
// 定数シンボルの定義
public class TestClass1 : MonoBehaviour {
    private int width;
    private int height;

    private void Start() {
        width = Constant.SMALL;
        height = Constant.MEDIUM;
    }
}
// 定数シンボルの定義
public class TestClass2 : MonoBehaviour {
    private int width;
    private int height;

    private void Start() {
        width = Constant.MEDIUM;
        height = Constant.LARGE;
    }
}

これはスクリプトに限らず、プログラミング全般に言える話です。

次に、上記のメンバをインスペクタから設定できるように改良してみます。

// 定数シンボルの定義
public class TestClass1 : MonoBehaviour {
    public int width = Constant.SMALL;
    public int height = Constant.MEDIUM;
}
// 定数シンボルの定義
public class TestClass2 : MonoBehaviour {
    public int width = Constant.MEDIUM;
    public int height = Constant.LARGE;
}

上記クラスでアタッチされたコンポーネントは、インスタンス事に個別の値を設定できるため、以下のように一つのクラスにまとめられるでしょう。

// 定数シンボルの定義
public class TestClass : MonoBehaviour {
    public int width;
    public int height;
}

ここで、一つ問題になることがあります。
インスペクタに設定する値は(基本的に)マジックナンバーであるので、上記の例のMEDIUMの値が200から250に変わった場合、それに該当する値を設定している箇所をすべてインスペクタ上から修正する必要があります。
この修正作業はバグの温床になります。

ここで、インスペクタ上からでも定数シンボルに相当するものを設定できないか考えてみました。
最適解ではないと思いますが、ご紹介させていただきます。

実装方法

定数シンボルの定義は、独自の属性定義の機能を用いて実装します。
クラス名は「属性名 + Attribute」とし、PropertyAttributeクラスを継承する必要があります。
今回はConstant属性を独自定義してみます。

using UnityEngine;
using System.Collections.Generic;
using System.Linq;

// 定数シンボルの定義
public class ConstantAttribute : PropertyAttribute {
    // コンストラクタ
    public ConstantAttribute() { }

    // Int型の定数(★ここにシンボルを定義★)
    public enum IntEnum {
        SMALL,
        MEDIUM,
        LARGE,
    };
    // Int型の定数値(★ここに値を定義★)
    private Dictionary<IntEnum, int> intDic = new Dictionary<IntEnum, int>() {
        { IntEnum.SMALL,    100 },
        { IntEnum.MEDIUM,   200 },
        { IntEnum.LARGE,    300 },
    };
    // Int値の取得
    public int GetIntValue(IntEnum key) {
        return intDic[key];
    }
    // Int定数シンボルの取得
    public IntEnum GetIntKey(int val) {
        return intDic.FirstOrDefault(x => x.Value == val).Key;
    }
}

定数シンボル名はenumを用いて定義しています。

    // Int型の定数(★ここにシンボルを定義★)
    public enum IntEnum {
        SMALL,
        MEDIUM,
        LARGE,
    };

定数値はDictionaryを用いて定義しています。

    // Int型の定数値(★ここに値を定義★)
    private Dictionary<IntEnum, int> intDic = new Dictionary<IntEnum, int>() {
        { IntEnum.SMALL,    100 },
        { IntEnum.MEDIUM,   200 },
        { IntEnum.LARGE,    300 },
    };

上記の書き方を一本化できるスマートな方法が思いつかなかったため、2つに分けて定義しました。
より良い方法があればそちらに切り替えたいと考えているところです。

次に、定数シンボルをインスペクタから設定できるようにするエディタ拡張クラスを定義します。

using UnityEngine;
using UnityEditor;

// 定数シンボル設定GUI
[CustomPropertyDrawer(typeof(ConstantAttribute))]
public class ConstantDrawer : PropertyDrawer {
    public override void OnGUI(Rect position, SerializedProperty property, GUIContent label) {
        ConstantAttribute constAttribute = (ConstantAttribute)attribute;

        // EnumPopupによって疑似的に定数シンボルで指定する
        switch ( property.propertyType ) {
            case SerializedPropertyType.Integer:
                // Int型
                var intKey = (ConstantAttribute.IntEnum)EditorGUI.EnumPopup(position, label, constAttribute.GetIntKey(property.intValue));
                property.intValue = constAttribute.GetIntValue(intKey);

                break;

            default:
                // その他(通常表示)
                EditorGUI.PropertyField(position, property, label);
                break;
        }
    }
}

クラスには属性定義[CustomPropertyDrawer(typeof(ConstantAttribute))]を指定します。
Constant属性のエディタ拡張であることを明示するためののものです。

肝となるのは次の部分です。

                // Int型
                var intKey = (ConstantAttribute.IntEnum)EditorGUI.EnumPopup(position, label, constAttribute.GetIntKey(property.intValue));
                property.intValue = constAttribute.GetIntValue(intKey);

定数シンボル名に相当するEnum値をGUIに表示するようにし、インスペクタから設定されたEnum値から実際のInt型の値を取得し、プロパティに設定しています。

使用する側は以下のようになります。

using UnityEngine;

public class TestClass : MonoBehaviour {
    [Constant]
    public int width;
    [Constant]
    public int height;

    private void Update() {
        Debug.Log("size = (" + width + ", " + height + ")");
    }
}

これで、以下のようにインスペクタ上に定数シンボル名を表示できるようになりました。

const-from-inspector

これで、どのような意味を持った値がプロパティに設定されたのか、わかるようになりました。

しかしながら、この方法にも問題点があります。
ConstantAttributeクラスで定義した定数値を変更しても、[Constant]属性が指定されたプロパティに値が即時反映されないということです。
更に、同じ値を持つ定数値が存在するケースに対応できていません。
これらの問題を解消するためには、実行時にEnum型のプロパティを定義し、Enum値をキーにDictionaryから定数値を取得する方法が無難でしょう。

今回の方法はバッドノウハウということで紹介させていただきました。