チリペヂィア

リンクフリー。サンプルコードなどは関連記事内でライセンスについて明示されない限り商用利用なども自由に行って構いませんが、自己責任でお願いします。またこれら日記内容の著作権自体は放棄していません。引用部分については引用元の権利に従ってください。

PropertyDrawerにあらためて挑戦

前回の記事なんですが…間違って消しちゃいました。

前回の記事

PropertyDrawerのカスタマイズでした。「RangeAttributeにInfinityとかNaNを設定する事で、片側リミットを設けられないのはいかがなものか」と思ったので自作する話だったんですが、記事を書いた後、あらためてインスペクタの描画カスタマイズを掘り下げてみると、エディタカスタマイズは現状まだ発展途上でかなりダーティな仕様と判明。

ボロがあると分かってて同じ記事を書くのもアレだしどーしよっかなーと、一度書いちゃったものは仕方ないのでまた別の方向からちまちま積みなおしてみます。

ArgumentException: Getting control 0's position in a group with only 0 controls when doing Repaint

まず最初に、この例外の対策方法から。なんでか素のMonoBehaviorクラスに対してそのメンバにPropertyDrawerで属性を与え、なおかつLayoutロジックをちょっとでも使用すると例外をスローします。例えば以下のコードはなんら問題が無さそうに見えてしっかり例外を吐きます。

using UnityEngine;

public class NewBehaviourScript : MonoBehaviour {
    [MyFloat]
    public float test;
}

public class MyFloatAttribute : PropertyAttribute {}
using UnityEngine;
using UnityEditor;

[CustomPropertyDrawer(typeof(MyFloatAttribute))]
public class MyFloatAttributeDrawer : PropertyDrawer
{
	public override void OnGUI (Rect position, SerializedProperty prop, GUIContent label)
	{
		EditorGUILayout.FloatField(label, prop.floatValue);
	}

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

PropertyDrawerはもともとGetPropertyHeight()、つまり「あらかじめ高さを取得するメソッドが存在する」設計になっています。これがいわゆる素のインスペクタ表示の場合、GetPropertyHeight()を完全に当て込んだ処理(通常のOnGUI処理フローとはちょっと違う)になっていて、コントロール描画するほど領域を伸ばしていくGUILayout処理がうまく動かないようなのです(EditorType.LayoutイベントでもOnGUI呼ばれない…?)。

ただ、使えない事は無いのです。一番最初に標準インスペクタ処理をキャンセルしてGUILayout処理を始めてしまえば、途中でGUILayout処理をするのはもちろん、固定サイズのGUIコントロール描画属性を与えてもどちらでもうまく処理できます。そのための一番簡単な方法が、Editorクラスを定義してしまう事です。毎回Editorを定義するのがイヤだから属性プログラミングじゃないの?というツッコミはあるんですが、いまのところ仕方ないみたいです。とりあえずEditor属性は継承クラスにも影響させる事ができるので、汎用Editorクラスを作ってなんとかします。

では、汎用性において問題の無さそうな、Editor.OnInspectorGUI()のデフォルト実装であるDrawDefaultInspector(SerializedObject obj)を、MonoDevelopやらILSpyなどでチェックしてみます。

internal static bool DoDrawDefaultInspector(SerializedObject obj)
{
	EditorGUI.BeginChangeCheck();
	obj.Update();
	SerializedProperty iterator = obj.GetIterator();
	bool enterChildren = true;
	while (iterator.NextVisible(enterChildren))
	{
		EditorGUILayout.PropertyField(iterator, true, new GUILayoutOption[0]);
		enterChildren = false;
	}
	obj.ApplyModifiedProperties();
	return EditorGUI.EndChangeCheck();
}

なるほどこんな実装になっているようです。ただしこのコードだと、さりげなく邪魔なわりに役に立たない一行目「スクリプト自身を入力するフィールド」が表示されちゃってちょっとアレなのでこれを消してみましょう。

/// <summary>DrawDefaultInspector()相当の処理を呼び出すための基底クラス。</summary>
public abstract class LikeControlBehavior : MonoBehaviour
{
}
[CustomEditor(typeof(LikeControlBehavior), true)] // 第二引数をtrueにして継承させます。
public class LikeControlBehaviorEditor : Editor
{
    public override void OnInspectorGUI()
    {
        DrawDefaultInspector(serializedObject, false);
    }

    /// <summary>DrawDefaultInspector相当の処理</summary>
    /// <param name="isDrawFirstScriptField">falseに設定すると、最初のScriptフィールドメンバを表示しません。</param>
    public static void DrawDefaultInspector(SerializedObject obj, bool isDrawFirstScriptField)
    {
        obj.Update();
        SerializedProperty iterator = obj.GetIterator();
        if (iterator.NextVisible(true))
        {
            if (isDrawFirstScriptField) EditorGUILayout.PropertyField(iterator, true, new GUILayoutOption[0]);
            while (iterator.NextVisible(false)) EditorGUILayout.PropertyField(iterator, true, new GUILayoutOption[0]);
        }
        obj.ApplyModifiedProperties();
    }
}

あとは、LikeControlBehaviorクラスを継承する事で、とりあえずGUILayoutだろうと固定サイズGUIだろうと属性プログラミングできるようになります(*1)。

// これで動く!
public class NewBehaviourScript : LikeControlBehavior {
    [MyFloat]
    public float test;
    [Range(0f, 100f)]
    public float test2;
}

ちょっと体裁が崩れるけど固定幅で描画される通常のRange属性も大丈夫です。

*1:ていうかこの仕様どうなんだよwと思うのですが、「なんでかUnity4からこうなった、どうしてこうなった、いちおーバグ報告したけどマイナーアップでスルーされた」みたいなスレも見かけましたが、NewGUI搭載もモタついているくらいですし、実際これからどうなることやらちょっとよくわかりません。個人的には根拠無くゲーム画面用にNewGUI、エディタカスタム用に旧GUIが残るんじゃないかなとは思ってるんですが。