第19章 GUI を自作する - エディター拡張入門

第19章 GUI を自作する

注: 本章は ObjectField が Sprite に対応していない時に執筆されたものです。なので目的となる「Sprite に対応したフィールドの作成」は必要のない拡張になってしまいますが、その過程で使用する技術はためになるので掲載しています。

例えば EditorGUILayout.ObjectField で Sprite のフィールドを表示した時、Texture 系であればプレビュー形式の GUI に変化しますが、Sprite はプレビュー形式にはなりません。(注: 最新バージョンの Unity ではプレビュー形式になります)

Texture2D と Sprite の GUI を並べたもの

図19.1: Texture2D と Sprite の GUI を並べたもの

void OnGUI ()
{
    GUILayoutOption[] options = new [] {
        GUILayout.Width (96),
        GUILayout.Height (96)
    };
    EditorGUILayout.ObjectField (null, typeof(Texture2D), false, options);
    EditorGUILayout.Space ();
    EditorGUILayout.ObjectField (null, typeof(Sprite), false, options);
}

プレビュー形式に対応した SpriteField を自作してみましょう。

19.1 GUI を作る

IMGUI*1 でランタイムでもエディター上でも自由に GUI を作成することが可能です。ですが NGUI や Unity4.6から搭載される uGUI を触る人が多く、ほぼ GUI クラスを使って GUI を作成するのは、エディター拡張をするときのみとなってしまいました。なので、GUI クラスにある IntField のような GUI フィールドを作成する技術はそれほど認知されていません。

そこで今回は GUI フィールドを作成するときに必要な、最低限の知識を紹介していきます。

[*1] Immediate Mode GUI - Unity4.5まで主流だった GUI システムをこう呼びます

19.2 GUIStyle

GUIStyle は GUI のスタイルを設定するもので皆さんは大抵最初の2つのパターンで使用しているかと思います。

1. GUISkin の中に複数の GUIStyle を設定して使用する

これは GUISkin を作成し、既存スタイルを変更したりカスタムスタイルとして作成できます。そして、スクリプト側で GUI.skin に自分で作成した GUISkin を代入すると一度にスタイルが適用されます。

自分で画像を用意すればボタンやトグルの画像を変更できる

図19.2: 自分で画像を用意すればボタンやトグルの画像を変更できる

2. GUI クラスの引数に指定してスタイルを変更する

これは、GUI を描画するときに既存のスタイルを使用せず引数として渡された GUIStyle を使用します。

//GUIStyle はスタイル名で指定することも可能
GUILayout.Label ("ラベル", "box");

3. スタイルを描画する

2つのパターンを紹介しました。最後にもう1つあり、これが GUI を作成するうえで必要な知識となります。

GUIStyle には GUIStyle.Draw という API が存在します。これは GUIStyle に設定されたフォントやテクスチャ、文字の色を使用して描画を行うものです。皆さんが普段目にしている GUILayout.Button のボタン画像も、GUILayout.Button の内部で GUIStyle.Draw が呼び出されています。

19.3 Event

Event クラスはすべてのイベントを管理するための機能です。

GUI に関するイベントは、さまざまな種類があり、例えばボタンをマウスでクリックするとき

  • ボタンの上にマウスがある
  • クリックされた
  • ボタンが押しっぱなしである

のイベントが発生します。この時に、イベントを適切に処理することによってボタンの動きを表現できます。

例えば「ボタンを押した」というイベントをコードにした時は、次のようになります。

if (Event.current.type == EventType.MouseDown) {

    ...ボタンを押した時の処理...

    //ボタンを押した(使用済み)として処理。
    Event.current.Use();
}

「ボタンを押した」というイベントを使用する場合、必ず Event.Use を呼び出します。そうすると今回のイベントはすでに使用されたという状態になります。

Event.Use を呼び出した後は Event.type は used となります。こうすることにより、他のイベント処理は実行されなくなり、他のイベントと競合することがなくなります。

19.4 Control ID

すべての GUI に対してコントロール ID というものが割り振られています。この ID によりそれぞれの GUI は独立したものとなります。この ID を適切に割り振らないと、「GUI.Window をマウスでドラッグした時に範囲選択ツールが動作してしまった」というような意図しない GUI の複数操作(競合)をしてしまうことになります。

コントロール ID の生成/取得

int id = GUIUtility.GetControlID(FocusType.Passive, rect);

この場合はキーボード以外のフォーカスを受け付け、特定の範囲(rect)内のコントロール ID を生成/取得しています。

コントロール ID と描画部分の紐付け

EditorStyles.objectFieldThumb.Draw(rect, content, id);

GUIStyle.Draw にコントロール ID を渡すことによって、スタイルの描画は渡したコントロール ID によって制御されることになります。

現在フォーカス中のコントロール ID

現在、どのコントロール ID にフォーカスが当てられているかを知るには GUIUtility.hotControl を使用します。またはキーボードのフォーカスを知るには GUIUtility.keyboardControl となります。また、GUIUtility.hotControl に代入することで強制的にコントロール ID を切り替えることが可能です。

19.5 ObjectPicker

オブジェクトピッカーは特定のオブジェクトやアセットを選択するためのウィンドウです。ObjectField のようなオブジェクトの参照を持つ GUI の場合はこのオブジェクトピッカーがあるとユーザーが操作を行うときに便利になります。

オブジェクトピッカーの表示は EditorGUIUtility.ShowObjectPicker で行うことが可能です。

そして、オブジェクトピッカーで選択中のオブジェクト・選択されたオブジェクトを把握するために、Event のコマンド名(Event.commandname)が ObjectSelectorUpdatedEditorGUIUtility.GetObjectPickerObject を組み合わせて使用することになります。

//コマンド名が ObjectSelectorUpdated でオブジェクトピッカーが
//現在コントロール中の GUI によるものであった場合
if (evt.commandName == "ObjectSelectorUpdated"
    && id == EditorGUIUtility.GetObjectPickerControlID())
{
    //オブジェクトピッカーで選択中のオブジェクトを取得
    sprite = EditorGUIUtility.GetObjectPickerObject() as Sprite;
    //GUI を再描画
    HandleUtility.Repaint();
}

if (GUI.Button(buttonRect, "select", EditorStyles.objectFieldThumb.name + "Overlay2"))
{
    //現在のコントロール ID に対するオブジェクトピッカーを表示する
    EditorGUIUtility.ShowObjectPicker<Sprite>(sprite, false, "", id);
    //オブジェクトピッカーを表示するイベントを発行したのでイベントの Use を実行する
    evt.Use();
}

19.6 DragAndDrop

ObjectField のようにドラッグ&ドロップしてオブジェクトの参照を格納したい場合があります。その時には DragAndDrop を使用します。DragAndDrop ではオブジェクトをドラッグする処理と、ドロップする処理の2つにわかれます。ObjectField と似た GUI を作成するときは、ヒエラルキーやプロジェクトブラウザーではすでにドラッグする処理が実装済みなので、GUI へドロップする処理のみを実装すればよいです。

//マウス位置が GUI の範囲内であれば
if (rect.Contains(evt.mousePosition))
{
    switch (evt.type)
    {
        //ドラッグ中
        case EventType.DragUpdated:
        case EventType.DragPerform:

            //ドラッグしているのが参照可能なオブジェクトの場合
            if (DragAndDrop.objectReferences.Length == 1)

                //オブジェクトを受け入れる
                DragAndDrop.AcceptDrag();

            //ドラッグしているものを現在のコントロール ID と紐付ける
            DragAndDrop.activeControlID = id;

            //このオブジェクトを受け入れられるという見た目にする
            DragAndDrop.visualMode = DragAndDropVisualMode.Generic;
            break;

        //ドラッグ終了 = ドロップ
        case EventType.DragExited:

            //ドロップしているのが参照可能なオブジェクトの場合
            if (DragAndDrop.objectReferences.Length == 1)
            {
                var reference = DragAndDrop.objectReferences[0] as Sprite;
                if (reference != null)
                {
                    sprite = reference;
                    HandleUtility.Repaint();
                }
            }
            break;
    }
}

19.7 HandleUtility.Repaint

HandleUtility.Repaint は現在のビューを再描画するものです。

現在の「ビュー」を少し言い換えると、「EditorWindow や ScriptableWizard など GUI を描画するウィンドウ」となり、それらの再描画になります。

もっと詳しく言い換えると「GUIView を継承しているビュー」を再描画します。

機能的には EditorWindow.Repaint と変わりません。

プレビュー表示のような GUI の見た目を変更する場合、何も対策を行わなければ EditorWindow などの GUIView が再描画処理に頼ることになり GUI は最新の描画を保つことができません。これを GUI 側から再描画をリクエストするのが HandleUtility.Repaint となります。

まとめると、再描画処理が行われるのは次の3パターンです。

  • GUIView上で Event の Repaint が発行された時
  • GUIView上にマウスを載せる
  • GUIView にフォーカスを当てる
  • GUIView の Repaint をリクエストした時
  • EditorWindow.Repaint など
  • HandleUtility.Repaint を発行した時

19.8 GUIUtility.ExitGUI

GUI では描画に関する Event を2種類発行しています。GUI のレイアウトを構成する「Layout」と再描画を行う「Repaint」です。これらのイベント以外で描画を変更するような処理を加えた場合、GUI のレイアウトが崩れたとこになり Unity 側はエラーと判断し、GUI が正しく描画されなくなります。

この現象は、「GUI 関数の引数で使う要素を配列で保持しているときに、特定の要素を配列から削除」するときによく起こります。

これを回避するためには、GUIUtility.ExitGUI を呼び出し、以降の GUI の描画に関する処理はすべて無視することができます。こうすることで問題なく次フレームの「Layout」と「Repaint」処理が発行され、エラーは発生しません。

19.9 GUILayout 対応

GUILayout の仕組みは GUILayoutUtility.GetRect で Rect 情報を取得し描画することになります。

ですが一番手っ取り早い方法があります。

public static Sprite SpriteField(Sprite sprite, params GUILayoutOption[] options)
{
    EditorGUILayout.LabelField("", "", options);
    var rect = GUILayoutUtility.GetLastRect();
    return CustomEditorGUI.SpriteField(rect, sprite);
}

このようにすでに実装されている GUILayout の API を発行した後、GUILayoutUtility.GetLastRect を行うことで簡単に Rect 情報を取得することが可能です。

これで、SpriteField を作成するための知識が揃いました。細かな説明を加えながらコードを組み立てていきましょう。

19.10 SpriteField 関数を作成する

まずは CustomEditorGUI クラスを作成し、その中に SpriteField のメソッドを追加します。

using UnityEditor;
using UnityEngine;

public class CustomEditorGUI
{
    public static Sprite SpriteField (Rect rect, Sprite sprite)
    {
    }
}

つぎに、GUIStyle.Draw を使って背景を描画します。GUIStyle.Draw は EventType.Repaint の時のみ実行なので気をつけてください。

using UnityEditor;
using UnityEngine;

public class CustomEditorGUI
{
    public static Sprite SpriteField (Rect rect, Sprite sprite)
    {
        var evt = Event.current;
        if (evt.type == EventType.Repaint) {
            //サムネ形式の背景を描画
            EditorStyles.objectFieldThumb.Draw (rect, GUIContent.none, id, false);

            if (sprite) {
                //スプライトからプレビュー用のテクスチャを取得
                var spriteTexture = AssetPreview.GetAssetPreview (sprite);

                if (spriteTexture) {
                    spriteStyle.normal.background = spriteTexture;
                    //スプライトを描画
                    spriteStyle.Draw (rect, false, false, false, false);
                }
            }
        }
    }
}

つぎはドラッグ&ドロップの処理です。 GUI 上にマウスがあるときのみ処理を行うので Rect.Contains でマウスの位置を監視します。

using UnityEditor;
using UnityEngine;

public class CustomEditorGUI
{
    public static Sprite SpriteField (Rect rect, Sprite sprite)
    {
        var evt = Event.current;

        //... 背景描画のコードは略 ...


        if (rect.Contains (evt.mousePosition)) {

            switch (evt.type) {

            case EventType.DragUpdated:
            case EventType.DragPerform:

                if (DragAndDrop.objectReferences.Length == 1)
                    DragAndDrop.AcceptDrag ();

                DragAndDrop.visualMode = DragAndDropVisualMode.Generic;

                break;
            case EventType.DragExited:

                if (DragAndDrop.objectReferences.Length == 1) {
                    var reference = DragAndDrop.objectReferences [0] as Sprite;
                    if (reference != null) {
                        sprite = reference;
                        HandleUtility.Repaint ();
                    }
                }
                break;
            }
        }
    }
}

つぎに、描画範囲に対してのコントロール ID を取得します。今回は Tab キーを押すことで別の GUI 要素へ移動する実装を行います。その時には、FocusType.Keyboard を使って ID を作成します。

using UnityEditor;
using UnityEngine;

public class CustomEditorGUI
{
    public static Sprite SpriteField (Rect rect, Sprite sprite)
    {
          var id = GUIUtility.GetControlID (FocusType.Keyboard, rect);
    }
}

また、Draw の引数にコントロール ID が使用できます。これは、ドラッグ&ドロップと関係があります。現在ドラッグ中で、マウスが GUI 上にある場合は on は true となり、GUI にフォーカスがあたっている状態を表現することができます。

var on = DragAndDrop.activeControlID == id;
EditorStyles.objectFieldThumb.Draw (rect, GUIContent.none, id, on);

.
.
.

switch (evt.type)
{
case EventType.DragUpdated:
case EventType.DragPerform:
    DragAndDrop.activeControlID = id;

最後に オブジェクトピッカー を実装します。

まずは、オブジェクトピッカーを表示するための小さなボタンを右下に表示します。

ただ、ボタンを表示するのではなく、キーボード操作でもボタンを押せるようにします。

var buttonRect = new Rect (rect);
//加工
buttonRect.x += buttonRect.width * 0.5f;
buttonRect.width *= 0.5f;
buttonRect.y += rect.height - 16;
buttonRect.height = 16;

//キーを押した時
//エンターキーである時
//そして操作しているのがこの GUI であるとき
var hitEnter = evt.type == EventType.KeyDown
            && (evt.keyCode == KeyCode.Return || evt.keyCode == KeyCode.KeypadEnter)
            && EditorGUIUtility.keyboardControl == id;

//ボタンを押した時、またはエンターキーを押した時にオブジェクトピッカーを表示
if (GUI.Button (buttonRect, "select", EditorStyles.objectFieldThumb.name + "Overlay2")
     || hitEnter) {
    //どの GUI で表示したか判断できるようにコントロール ID を渡す
    EditorGUIUtility.ShowObjectPicker<Sprite> (sprite, false, "", id);
    evt.Use ();
    GUIUtility.ExitGUI ();
}

最後にオブジェクトピッカーで選択したものを取得して実装は完了です。

//オブジェクトピッカーがこの GUI のためのものであるか判断
if (evt.commandName == "ObjectSelectorUpdated"
    && id == EditorGUIUtility.GetObjectPickerControlID ()) {

    sprite = EditorGUIUtility.GetObjectPickerObject () as Sprite;

    //描画するスプライトが変更されたので再描画
    HandleUtility.Repaint ();
}

GUILayout ようにもクラスを作成します。

public class CustomEditorGUILayout
{
    public static Sprite SpriteField (Sprite sprite, params GUILayoutOption[] options)
    {
        EditorGUILayout.LabelField ("", "", options);
        var rect = GUILayoutUtility.GetLastRect ();
        return CustomEditorGUI.SpriteField (rect, sprite);
    }
}

作成したものは次のように使用します。

using UnityEditor;
using UnityEngine;

public class NewBehaviourScript : EditorWindow
{
    [MenuItem("Window/SpriteEditor")]
    static void Open()
    {
        GetWindow<NewBehaviourScript>();
    }

    private Sprite sprite1,sprite2;
    void OnGUI()
    {
        sprite1 = CustomEditorGUI.SpriteField(new Rect(134, 1, 128, 128), sprite1);
        sprite2 = CustomEditorGUILayout.SpriteField(sprite2,
                                      GUILayout.Width(128), GUILayout.Height(128));
    }
}
第18章 HierarchySort 第20章 Overwriter