第10章 PropertyDrawer - エディター拡張入門

第10章 PropertyDrawer

例えば、キャラクターのためのスクリプトがあり、ヒットポイントの hp 変数があるとします。

using UnityEngine;

public class Example : MonoBehaviour
{
    [SerializeField]
    int hp;
}

上記のコードは SerializeField 属性がついており、インスペクターに表示される実装となっています。

いつもよく見るインスペクター

図10.1: いつもよく見るインスペクター

この hp 変数に次の制限が付いている場合、どうすればいいでしょうか。

  • HP の上限下限は決まっている
  • この値は未調整で、値を変更しながらちょうどいい値を見つけなければいけない

これらの制限(仕様)は開発時の時点で考慮しなければいけなく、特にインスペクターで値を編集するときは、これらの制限を付けるのは標準機能であれば難しいです。

10.1 PropertyDrawer とは

Unity は、シリアライズされたデータを Unity が自動判断して適切な GUI を使い、インスペクター*1 に表示します。

PropertyDrawer はその Unity による自動判断処理をフックして、自前の GUI を使用するための技術です。これにより、特定の GUI のみをカスタマイズすることが可能です。

インスペクターに表示されるコンポーネントの GUI を変更するには CustomEditor が適しています。ですが、これはコンポーネント全体のカスタマイズになります。今回は、コンポーネントの一部である hp 変数(プロパティー)のみをカスタマイズしたいので CustomEditor ではなく PropertyDrawer を使用します。

例えば次のようなシリアライズ可能なクラスがあるとします。

[Serializable]
public class Character
{
        [SerializeField]
        string name;

        [SerializeField]
        int hp;
}

これをインスペクターに表示しようとすると コンパクトではありますがとても見にくい表示となります。

いつもの見た目

図10.2: いつもの見た目

これを PropertyDrawer によって GUI の描画をカスタマイズし、1行で表示できます。

1行に name と hp のプロパティーが表示されている

図10.3: 1行に name と hp のプロパティーが表示されている

このようにインスペクターの操作で不便だと思った部分をカスタマイズしていくことが可能です。

10.2 PropertyAttribute

PropertyAttribute は単なる Attribute を継承したクラスです。CustomEditor が コンポーネントの Editor オブジェクトを拡張するように、PropertyDrawer は、PropertyAttribute を拡張します(正確には PropertyAttribute のついたシリアライズ可能なフィールドです)。

using UnityEngine;

public class ExampleAttribute : PropertyAttribute
{
}

10.3 CustomPropertyDrawer と PropertyDrawer

CustomEditor と同じような実装方法で PropertyDrawer も拡張を行います。

PropertyDrawer を継承した派生クラスを作成し、拡張したいクラスを CustomPropertyDrawer の引数に与えます。

シリアライズ可能なクラスであれば次のクラスを作成します。

[CustomPropertyDrawer (typeof(Character))]
public class CharacterDrawer : PropertyDrawer
{
}

PropertyAttribute の派生クラスの場合も同様です。

using UnityEngine;
using UnityEditor;

[CustomPropertyDrawer (typeof(ExampleAttribute))]
public class ExampleDrawer : PropertyDrawer
{
}

あとは作成した PropertyAttribute をフィールドに追加するだけです。

using UnityEngine;

public class Hoge : MonoBehaviour
{
    [SerializeField, Example]
    int hp;
}

10.4 RangeAttribute を試す

すでに標準でいくつかの PropertyDrawer が実装されています。

その中の1つである RangeAttribute を使ってみましょう。

using UnityEngine;

public class Example : MonoBehaviour
{
    [SerializeField, Range (0, 10)]
    int hp;
}

属性として Range (0, 10) を追加するだけで 0 から 10 までスライドできる Slider を作成することができました。

図10.4:

10.5 RangeAttribute を自作する

試しに、標準実装されている RangeAttribute と同じものを作成してみます。

Range2Attribute の作成

最小値(min)と最大値(max)を保持するようにして、Attribute の使用制限を設定します。AttributeUsage についてはこちらのリンクを参照してください。

MSDN - AttributeUsageAttribute クラス

using UnityEngine;

[System.AttributeUsage (System.AttributeTargets.Field,
                               Inherited = true, AllowMultiple = false)]
public class Range2Attribute : PropertyAttribute
{
    public readonly int min;
    public readonly int max;

    public Range2Attribute (int min, int max)
    {
        this.min = min;
        this.max = max;
    }
}

Range2Drawer の作成

属性のついたフィールドは SerializedProperty 経由で扱います。 propertyType が int であれば IntSlider を使用し、int 以外であれば標準の GUI を使用します。

using UnityEditor;
using UnityEngine;

[CustomPropertyDrawer (typeof(Range2Attribute))]
internal sealed class RangeDrawer : PropertyDrawer
{
    public override void OnGUI (Rect position,
                      SerializedProperty property, GUIContent label)
    {
        Range2Attribute range2 = (Range2Attribute)attribute;

        if (property.propertyType == SerializedPropertyType.Integer)
            EditorGUI.IntSlider (position, property, range2.min, range2.max, label);
        else
            EditorGUI.PropertyField (position, property, label);
    }
}

10.6 Range2Attribute を使用する

以上で実装が終わったので Range2Attribute を使用してみます。 int 以外では扱えないことを確認するために string に対しても属性を付加しています。

using UnityEngine;

public class Example : MonoBehaviour
{
    [SerializeField, Range2 (0, 10)]
    int hp;

    [SerializeField, Range2 (0, 10)]
    string str;
}

10.7 さまざまな PropertyDrawer

Unity に標準実装されているものは 第2章「標準で使えるエディター拡張機能」 で紹介しています。ここからは私が今までに作成した PropertyDrawer を紹介していきます。

Angle

真ん中の数字は InputField なので値を入力して変更できる

図10.5: 真ん中の数字は InputField なので値を入力して変更できる

API として 図10.5 のようにノブ(取っ手)を表示する EditorGUILayout.Knob があります。ですが PropertyDrawer では EditorGUILayout の使用は禁止されているので使うことができません。内部的には EditorGUI.Knob が実装されており、リフレクションを使って呼び出すことによって使用が可能になります。

private readonly MethodInfo knobMethodInfo = typeof(EditorGUI).GetMethod("Knob",
       BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Static);

private float Knob(Rect position, Vector2 knobSize,
                      float currentValue, float start,
                      float end, string unit,
                      Color backgroundColor, Color activeColor,
                      bool showValue)
{
    var controlID = GUIUtility.GetControlID("Knob".GetHashCode(),
                                              FocusType.Native, position);

    var invoke = knobMethodInfo.Invoke(null, new object[] {
        position, knobSize, currentValue,
        start, end, unit, backgroundColor,
        activeColor, showValue,
        controlID });
    return (float)(invoke ?? 0);
}

GetPropertyHeight

デフォルトの GUI の高さ (EditorGUIUtility.singleLineHeight) から変更したいときは GetPropertyHeight をオーバーライドします。

[CustomPropertyDrawer(typeof(AngleAttribute))]
public class AngleDrawer : PropertyDrawer
{
  private AngleAttribute angleAttribute { get { return (AngleAttribute)attribute; } }

  public override void OnGUI (Rect position,
                    SerializedProperty property, GUIContent label)
  {
    //略
  }

  //戻り値として返した値が GUI の高さとして使用されるようになる
  public override float GetPropertyHeight(SerializedProperty property,
                                                            GUIContent label)
  {
      var height = base.GetPropertyHeight(property, label);

      var floatType = property.propertyType != SerializedPropertyType.Float;

      return floatType ? height : angleAttribute.knobSize + 4;
  }
}

AnimatorParameter

Animator ウィンドウにあるパラメータ名をタイプセーフにフィールドにアタッチできます。取得できるパラメーターは同じゲームオブジェクトにアタッチされている Animator Controller のパラメーターになります。

using UnityEngine;

[RequireComponent(typeof(Animator))]
public class AnimatorParameterExample : MonoBehaviour
{
    //すべてのタイプのパラメーターを取得
    [AnimatorParameter]
    public string param;

    //Float のみ
    [AnimatorParameter(AnimatorParameterAttribute.ParameterType.Float)]
    public string floatParam;

    //Int のみ
    [AnimatorParameter(AnimatorParameterAttribute.ParameterType.Int)]
    public string intParam;

    //Bool のみ
    [AnimatorParameter(AnimatorParameterAttribute.ParameterType.Bool)]
    public string boolParam;

    //Trigger のみ
    [AnimatorParameter(AnimatorParameterAttribute.ParameterType.Trigger)]
    public string triggerParam;
}

同オブジェクトのコンポーネントを取得

今回は同じゲームオブジェクトにアタッチされている Animator コンポーネントから AnimatorController を取得しています。

コンポーネントは SerializedProperty -> SerializedObject -> Component の順で取得することが可能です。

AnimatorController GetAnimatorController(SerializedProperty property)
{
    var component = property.serializedObject.targetObject as Component;

    if (component == null)
    {
        Debug.LogException(new InvalidCastException("Couldn't cast targetObject"));
    }

    var anim = component.GetComponent<Animator>();

    if (anim == null)
    {
        var exception = new MissingComponentException("Missing Aniamtor Component");
        Debug.LogException(exception);
        return null;
    }

    return anim.runtimeAnimatorController as AnimatorController;
}

DisableAttribute

プロパティーをインスペクター上で編集不可にします。インスペクターに表示したいが、値を変更させたくない時に使います。

using UnityEngine;

public class DisableExample : MonoBehaviour
{
    [Disable]
    public string hoge = "hoge";

    [Disable]
    public int fuga = 1;

    [Disable]
    public AudioType audioType = AudioType.ACC;
}

実装の方法は簡単です。

BeginDisabledGroup と EndDisabledGroup、または DisabledGroupScope を使って PropertyField を囲むだけで実装できます。

インスペクターで編集できなくなったといっても、インスペクターを Debug モードにすると編集できますし、スクリプトから値の編集ができるので注意してください。

public override void OnGUI(Rect position,
                             SerializedProperty property, GUIContent label)
{
    EditorGUI.BeginDisabledGroup(true);
    EditorGUI.PropertyField(position, property, label);
    EditorGUI.EndDisabledGroup();
}

EnumLabel

Enum の表示名を変更します。

using UnityEngine;

public class EnumLabelExample : MonoBehaviour
{
    public enum Example
    {
        [EnumLabel("テスト")]
        HIGH,
        [EnumLabel("その2")]
        LOW
    }

    [EnumLabel("例")]
    public Example test = Example.HIGH;
}

GUI を表示するときに EnumLabel に渡した文字列を使って Popup を表示しています。上記の例のように test 変数にも属性を付けないと適用されません。これは PropertyAttribute がフィールドについていないとイベントが発火しないためです。

Popup

Attribute に渡したパラメーターを使って Popup を表示します。値を Popup で選択できるようになります。

using UnityEngine;
using System.Collections;

public class PopupExample : MonoBehaviour
{
    [Popup("Hoge","Fuga","Foo","Bar")]
    public string popup;

    [Popup(1,2,3,4,5,6)]
    public int popup2;

    [Popup(1.5f,2.3f,3.4f,4.5f,5.6f,6.7f)]
    public float popup3;
}

値の持ち方を object にしているので複数のタイプをサポートしています。

public class PopupAttribute : PropertyAttribute
{
    public object[] list;

    //引数は object 配列
    public PopupAttribute (params object[] list)
    {
        this.list = list;
    }
}

ですがこうすると Popup から選択した値を代入するときに少し苦労します。

SerializedProperty に値を代入するには property.stringValueproperty.intValueproperty.floatValue というようにそれぞれの変数へと代入しなければいけません。

PreviewTexture

テクスチャのプレビューを表示します。

using UnityEngine;

public class PreviewTextureAttributeExample : MonoBehaviour
{
    //60秒キャッシュする
    [PreviewTexture(60)]
    public string textureURL = "https://www.hogehoge.com/image.png";

    [PreviewTexture]
    public Texture hoge;
}

テクスチャの描画には GUIStyle を使う

テクスチャの描画には EditorGUI.DrawPreviewTexture を使用しますが、PropertyDrawer では 描画タイミングの関係でテクスチャが表示/非表示を繰り返してチカチカしてしまう問題が発生します。この問題があるため代案として GUIStyle を使ってスタイルの背景としてテクスチャを描画します。

void DrawTexture(Rect position, Texture2D texture)
{
    float width = Mathf.Clamp(texture.width,
                              position.width * 0.7f,
                              position.width * 0.7f);

    var rect = new Rect(position.width * 0.15f,
                        position.y + 16,
                        width,
                        texture.height * (width / texture.width));

    if (style == null)
    {
        style = new GUIStyle();
        style.imagePosition = ImagePosition.ImageOnly;
    }

    style.normal.background = texture;
    GUI.Label(rect, "", style);
}

SceneName

有効なシーン名を Popup で選択できます。Popup で表示されるものは Build Settings の Scene In Build に含まれているシーン名です。

using UnityEngine;

public class SceneNameExample : MonoBehaviour
{
    [SceneName]
    public string sceneName;

    //無効状態のシーンも表示する
    [SceneName(false)]
    public string sceneName2;
}

EditorBuildSettings.scenes でシーンの一覧を取得

シーンは EditorBuildSettings.scenes 変数で管理されています。ただし Build Settings の Scene In Build にシーンを登録していないと一覧に含まれないので注意してください。

[*1] 実際は、インスペクターに限らず OnGUI を使う場所だとどこでも Unity が自動判断して適切な GUI を表示します。

第9章 CustomEditor 第11章 ProjectWindowUtil