Programming Serendipity

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

インスペクタにboolの配列を出すとき、チェックボックスを横に並べて表示させる方法

boolの配列って普通こうなりますよね。 f:id:q7z:20160614204433p:plain

それを…

f:id:q7z:20160614204437p:plain

こうすると見やすくありませんか。これを紹介します。

以下のスクリプトをEditor>PropertyDrawerフォルダにでも入れます。

using UnityEngine;
using UnityEditor;

[CustomPropertyDrawer(typeof(SmartBoolArrayAttribute))]
public class SmartBoolArrayDrawer : PropertyDrawer
{
    /// <summary>インデント段階を保存</summary>
    private int lastIndentLevel;


    /// <summary>1行におく要素の数</summary>
    private const int ItemsInLine = 10;

    /// <summary>ラベルの幅</summary>
    private const int LabelWidth = 40;


    public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
    {
        BeginProperty(position, property, label);

        int index = GetIndex(property);
        var targetRect = position;
        if (index % ItemsInLine == 0)
        {
            var labelRect = targetRect;
            labelRect.width = LabelWidth;
            labelRect.height = 18;
            EditorGUI.LabelField(labelRect, index.ToString());
        }
        targetRect.y += -(index % ItemsInLine) * 2;
        targetRect.x += LabelWidth + index % ItemsInLine * ((position.width - LabelWidth) / ItemsInLine);
        targetRect.width = 18;
        targetRect.height = 18;
        EditorGUI.PropertyField(targetRect, property, new GUIContent());


        EndProperty();
    }

    public override float GetPropertyHeight(SerializedProperty property, GUIContent label)
    {
        return 0;
    }

    int GetIndex(SerializedProperty property)
    {
        var elem = property;
        var path = elem.propertyPath;
        var num = path.Substring(path.LastIndexOf('[') + 1);
        num = num.Substring(0, num.Length - 1);
        int index = 0;
        if (!int.TryParse(num, out index))
        {
            Debug.LogWarningFormat("Failed to parse int from: {0}, originally: {1}", num, path);
        }

        return index;
    }

    /// <summary>
    /// プロパティの開始
    /// </summary>
    /// <param name="position">OnGUIと同じ</param>
    /// <param name="property">OnGUIと同じ</param>
    /// <param name="label">OnGUIと同じ</param>
    protected void BeginProperty(Rect position, SerializedProperty property, GUIContent label)
    {
        EditorGUIUtility.labelWidth = 80 + EditorGUI.indentLevel * 20;
        label = EditorGUI.BeginProperty(position, label, property);

        this.lastIndentLevel = EditorGUI.indentLevel;
        EditorGUI.indentLevel = 0;
    }

    /// <summary>
    /// プロパティの終了
    /// </summary>
    protected void EndProperty()
    {
        EditorGUI.indentLevel = this.lastIndentLevel;
        EditorGUI.EndProperty();
    }
}

そして、次のようなファイル(大抵は属性をまとめたAttributes.csなど)を用意してEditorフォルダ以外の普通のスクリプトフォルダに置きます。

using UnityEngine;

class SmartBoolArrayAttribute : PropertyAttribute {}

すると、

using UnityEngine;
using UnityEditor;

public class ExampleScript : MonoBehaviour
{
    [SmartBoolArray]
    public bool[] flags;
}

このようにすることで先ほどのことができます。

雑な解説

  • 基本的なことは前回の1行プロパティドロアーの時と共通ですが、今回は属性にCustomPropertyDrawerを適用します。
  • 属性は[SmartBoolArrayAttirbute]と指定してもいいですが、PropertyAttributeを継承したクラス名がAttributeで終わる場合、その属性を指定する側ではAttributeは省略可能です。というか、Unity標準の[Range(0, 1)]とかも元々は[RangeAttribute(0, 1)]です。
  • 配列に指定することを想定しているので、OnGUIに配列オブジェクトが渡ってくると思っていましたが、実際には、要素が1個ずつ(Element 0, Element 1, のように=インスペクタの1行分ずつ?)渡ってきます。
  • 今回はflagsという名前の配列ですが、この場合要素0番目のproperty.propertyPathにはflags.Array.data[0]という形式で文字列が入っているので、そこから要素番号を取得しています。(もっといい方法があればいいのですが)
  • 追記:↑はproperty.displayName(これも文字列で、こちらは"Element 0", "Element 1"のように格納)から取り出すこともできます。
  • GetPropertyHeight()0でoverrideするという男気あふれる事をしていますが、これによって要素ごとに自動で位置を下げず、横に並べるということを実現しています。
  • しかし、EditorGUI.PropertyFieldにわたすRectには幅高さを持たせないと、クリックしても反応しなくなってしまいます。
  • 高さを0にしても若干下がるらしいので、y座標を少し上にあげています。
  • 横を20個にする場合は少し工夫が必要?GetPropertyHeight()をマイナスにする必要があるかもしれないが…といったところで力尽きた。

素敵なbool配列ライフを!