Programming Serendipity

プログラミングを中心に種々雑多に書き留めます

クラスをインスペクタに表示するとき、見栄えをかっこよくする方法([CustomPropertyDrawer])

何かのクラスを[Serializable]してインスペクタに表示させるとき、そのままだとこうなりますよね…。

f:id:q7z:20160530215024p:plain

これを…

f:id:q7z:20160530215136p:plain

こう表示できたらカッコよくありませんか?(省スペースにもなりますし。)これのやり方を紹介します。

[CustomPropertyDrawer]を使用します。以下のスクリプトを適当なGameObjectにアタッチしてみてください。

using UnityEngine;
using UnityEditor;

public class ExampleScript : MonoBehaviour
{
    public SomeClass SomeClass;
    public SomeClass[] someClassArray;
    public ParentClass parent;
    public ParentClass[] parentArray;
}

[System.Serializable]
public class ParentClass
{
    public SomeClass some;
    public SomeClass[] array;
}

[System.Serializable]
public class SomeClass
{
    public bool active;
    public int stage;
    public int level;
    public int hp;
    public int gold;
}

#if UNITY_EDITOR
[CustomPropertyDrawer(typeof(SomeClass))]
public class SomeClassDrawer : PropertyDrawer
{
    private Rect wholeRect;
    private float partialSum;
    private SerializedProperty prop;

    public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
    {
        EditorGUIUtility.labelWidth = 80 + EditorGUI.indentLevel * 20;
        label = EditorGUI.BeginProperty(position, label, property);
        Init(position, property, label);

        var last_indent_level = EditorGUI.indentLevel;
        EditorGUI.indentLevel = 0;

        DividedField(0.1f, "active");
        DividedField(0.15f, "stage");
        DividedField(0.15f, "level", "-", 10);
        DividedField(0.3f, "hp", "hp", 44);
        DividedField(0.3f, "gold", "gold", 44);

        EditorGUI.indentLevel = last_indent_level;
        EditorGUI.EndProperty();
    }

    void Init(Rect position, SerializedProperty property, GUIContent label)
    {
        this.partialSum = 0;
        this.prop = property;
        this.wholeRect = EditorGUI.PrefixLabel(position, label);
    }

    /// <summary>
    /// 割合を指定して1行のなかにプロパティを表示
    /// </summary>
    /// <param name="widthRate">1行の中でとる幅(0, 1]</param>
    /// <param name="propertyName">表示するプロパティの名前</param>
    /// <param name="label">ラベル名</param>
    /// <param name="labelWidth">ラベルの幅</param>
    void DividedField(float widthRate, string propertyName, string label = "", float labelWidth = 0)
    {
        Debug.Assert(0 < widthRate);
        Debug.Assert(widthRate <= 1);
        Debug.Assert(!string.IsNullOrEmpty(propertyName));
        Debug.Assert(label != null);
        Debug.Assert(labelWidth >= 0);

        var width = this.wholeRect.width * widthRate;
        var rect = new Rect(this.wholeRect.x + this.partialSum, this.wholeRect.y, width, this.wholeRect.height);
        this.partialSum += width;

        EditorGUIUtility.labelWidth = Mathf.Clamp(labelWidth, 0, rect.width - 20);
        var item = this.prop.FindPropertyRelative(propertyName);
        if (item != null)
        {
            EditorGUI.PropertyField(rect, item, new GUIContent(label));
        }
        else
        {
            Debug.LogWarningFormat("Failed to find property: '{0}' in '{1}'", propertyName, this.GetType());
        }
    }
}
#endif

…もちろん、実際にはこれらのクラスは本来スクリプトは別々ですが、簡単にするため一緒にしています。(私ならUNITY_EDITOR以下をEditor>PropertyDrawer>SomeClassDrawer.csなどに分けます)

[CustomPropertyDrawer]をつけて、PropertyDrawerを継承したクラスがこの機能を持ち、実際の描画内容はOnGUI()をoverrideして実装します。

書くのに当たってCatlikeCodingこのページを大いに参考にしました。 大半のことはこのページに書かれているのですが、少し解説してみます。

EditorGUIUtility.labelWidth: 変数名のラベルの幅。プロパティの幅の内部から幅を間借りして描画されます。この数値によってプロパティの幅は変化はしません。この変数はstaticで、完全に使い回しなので、使用する直前に必ず値を入れたほうがいいでしょう。

EditorGUI.indentLevel: 配列やクラスなど、インスペクタのインデントの段階を示す変数。意図的にインデントしたいときにこの値を増減します。インデントは下がっていても描画範囲は左端全体までとられているので、変数名を表示した上でその右から描画したい場合、この変数を使って固定ラベルに割く幅を自分で計算する必要があります。

EditorGUI.BeginProperty(), EndProperty() これで囲まれた領域をひとつのプロパティとして登録し、右クリックメニューなどさまざまなプロパティ操作の対象にします。for more: Unity - Scripting API: EditorGUI.BeginProperty

EditorGUI.PrefixLabel(): 元々の変数名のラベル。配列の要素だったら"Element 0"などの文字表示が該当します。これの戻り値が、固定ラベルまで描画した後の残りの描画領域になっています。ここでは、フィールドに代入しました。

DividedField()にそれぞれ1行の中に占める割合を指定しながら描画しています。
適当に入力値チェックをして、最終的にはPropertyField()の第1引数に渡すRectの情報が描画範囲を決定しています。
ここらへんは全部手計算です。全体の幅から割合分の幅を出し、xをずらして進んだxの量を保存し、プロパティを探してそれを描画する、という感じです。
EditorGUI.PropertyFieldの第3引数に渡すGUIContentの文字列が空だとEditorGUIUtility.labelWidthに関わらず描画されないようですね。基本面倒くさいのにこういうところは便利。
ラベルの幅は、プレハブにして変更したときに太字になって幅が大きくなる対策で、少し多めにとっておくとよさそうです。

ちなみに、2行とかにしたい場合は、GetPropertyHeight()をoverrideして大きい値を返すと、OnGUI()position.heightがその値になるので、それを利用します。

        // 例:標準の4行分の高さにする(プロパティ自身の高さ4つ分と、間のスペース3つ分)
        public override float GetPropertyHeight(SerializedProperty prop, GUIContent label)
        {
            return 4 * EditorGUIUtility.singleLineHeight + 3 * EditorGUIUtility.standardVerticalSpacing;
        }

いやぁ、便利ですね。

とりあえず言いたいこと。 CatlikeCodingのJasper Flickさん素晴らしい。