第17章 Handle(ハンドル) - エディター拡張入門

第17章 Handle(ハンドル)

第16章「Gizmo(ギズモ)」 にて、オブジェクトの位置や範囲、特定の要素を可視化するための仕組みを紹介しました。本章では、さらに込み入った可視化、それだけではなく「操作」を行う Handles について紹介していきます。

ハンドル は、オブジェクトに対してアクションを起こすための仕組みです。例えば 図17.1 の表示はすべてハンドルです。

これらはすべてマウスで操作できる

図17.1: これらはすべてマウスで操作できる

17.1 ゲームオブジェクトにハンドルを追加

ゲームオブジェクトに独自のハンドルを追加します。ハンドルの実装は通常 CustomEditorOnSceneGUI 関数の中で行います。

using UnityEngine;

public class Example : MonoBehaviour {

}

まずは MonoBehaviour の派生クラスを作成し、そのクラスに対するカスタムエディターを作成します。標準で実装されているハンドルを表示しないように Tools.current = Tool.None; としておきます。これでゲームオブジェクトの位置を変更するためのハンドルが作成できました。

using UnityEngine;
using UnityEditor;

[CustomEditor (typeof(Example))]
public class ExampleInspector : Editor
{
    void OnSceneGUI ()
    {
        Tools.current = Tool.None;
        var component = target as Example;


        var transform = component.transform;
        transform.position =
            Handles.PositionHandle (transform.position, transform.rotation);
    }
}

位置と回転のハンドルを同時に表示する

ハンドルを同時に複数表示することも可能です。

位置と回転のハンドルを同時表示

図17.2: 位置と回転のハンドルを同時表示

void OnSceneGUI ()
{
    var component = target as Example;

    var transform = component.transform;

    if (Tools.current == Tool.Move) {
        transform.rotation =
            Handles.RotationHandle (transform.rotation, transform.position);
    } else if (Tools.current == Tool.Rotate) {
        transform.position =
            Handles.PositionHandle (transform.position, transform.rotation);
    }
}

17.2 PositionHandle を自作する

ハンドルはさまざまなパーツを組み合わせて作成されています。基本的なものでも8種類のパーツが用意されています。

ハンドルで使えるパーツはさまざま

図17.3: ハンドルで使えるパーツはさまざま

これらのパーツを使って、PositionHandle と同じものを作成してみます。

矢印のハンドル

矢印のハンドルは、Handles.Slider を使用します。位置を「スライド」させるのに適しているので使用されています。

void OnSceneGUI ()
{
    Tools.current = Tool.None;
    var component = target as Example;
    PositionHandle (component.transform);
}

void PositionHandle (Transform transform)
{
    Handles.Slider (transform.position, transform.right); //X 軸
    Handles.Slider (transform.position, transform.up); //Y 軸
    Handles.Slider (transform.position, transform.forward); //Z 軸
}

これで、3軸の矢印のハンドルを表示することができました。ですが、まだ色がついていません。

ハンドルに色を付ける

色をつけるには、ハンドルを描画する「前」に Handles.color に値を代入します。各軸の色はすでに用意されているものを使います。

void PositionHandle (Transform transform)
{
    Handles.color = Handles.xAxisColor;
    Handles.Slider (transform.position, transform.right); //X 軸

    Handles.color = Handles.yAxisColor;
    Handles.Slider (transform.position, transform.up); //Y 軸

    Handles.color = Handles.zAxisColor;
    Handles.Slider (transform.position, transform.forward); //Z 軸
}

ハンドルで位置を移動

Slider で移動させた結果をオブジェクトへ反映します。

void OnSceneGUI ()
{
    Tools.current = Tool.None;
    var component = target as Example;
    var transform = component.transform;

    transform.position = PositionHandle (transform);
}

Vector3 PositionHandle (Transform transform)
{
    var position = transform.position;

    Handles.color = Handles.xAxisColor;
    position = Handles.Slider (position, transform.right); //X 軸

    Handles.color = Handles.yAxisColor;
    position = Handles.Slider (position, transform.up); //Y 軸

    Handles.color = Handles.zAxisColor;
    position = Handles.Slider (position, transform.forward); //Z 軸

    return position;
}

ハンドルの大きさを固定する

すでに実装されている PositionHandle は、シーンビューをズームイン・ズームアウトしてもハンドルの大きさは変わりません。これは内部で、「シーンビューのカメラ」と「ハンドル」の位置を使って、ハンドルのサイズが計算されています。これにより、いくらズームイン・ズームアウトをしても適切なサイズに拡大縮小され、見た目の長さが同じになります。

ズームイン・ズームアウトでキューブの大きさが変わってもハンドルの大きさは変わっていない

図17.4: ズームイン・ズームアウトでキューブの大きさが変わってもハンドルの大きさは変わっていない

常にハンドルの大きさを固定したいときは、Handles.Slider で大きさを指定しなければいけません。

下記コードは、引数で snap の値も必要なので snap 設定のコードも追加されています。

Vector3 snap;

void OnEnable ()
{
    //SnapSettings の値を取得する
    var snapX = EditorPrefs.GetFloat ("MoveSnapX", 1f);
    var snapY = EditorPrefs.GetFloat ("MoveSnapY", 1f);
    var snapZ = EditorPrefs.GetFloat ("MoveSnapZ", 1f);
    snap = new Vector3 (snapX, snapY, snapZ);
}

Vector3 PositionHandle (Transform transform)
{
    var position = transform.position;

    var size = 1;

    //X 軸
    Handles.color = Handles.xAxisColor;
    position =
        Handles.Slider (position, transform.right, size, Handles.ArrowCap, snap.x);

    //Y 軸
    Handles.color = Handles.yAxisColor;
    position =
        Handles.Slider (position, transform.up, size, Handles.ArrowCap, snap.y);

    //Z 軸
    Handles.color = Handles.zAxisColor;
    position =
        Handles.Slider (position, transform.forward, size, Handles.ArrowCap, snap.z);

    return position;
}
ズームアウトするにつれハンドルは小さくなっていく。ハンドルの大きさが固定

図17.5: ズームアウトするにつれハンドルは小さくなっていく。ハンドルの大きさが固定

逆に、ハンドルの見た目の大きさを同じにするには HandleUtility.GetHandleSize を使用して大きさを求めます。

var size = HandleUtility.GetHandleSize (position);

17.3 特殊なハンドル

Handles クラスには、特殊なハンドルを作成できる API がいくつか存在します。

FreeMoveHandle

3軸を意識せずに自由に動かせるハンドルを作成します。表示するパーツは RectangleCap が好ましいです。

transform.position =
    Handles.FreeMoveHandle (
        transform.position,
        transform.rotation,
        size,
        snap, Handles.RectangleCap);
四角の中でドラッグすると動かすことができる

図17.6: 四角の中でドラッグすると動かすことができる

これは、シーンビューの向きを 2D モードのような 2 軸に表示した時に便利です。こうすることで 3D オブジェクトでも X と Y 軸の位置をまとめて調整することが可能です。

シーンビューの右上のハンドルで2軸のモードにする

図17.7: シーンビューの右上のハンドルで2軸のモードにする

FreeRotateHandle

3軸を意識せずに自由に回転できるハンドルを作成します。表示するパーツは円で固定です。

transform.rotation =
    Handles.FreeRotateHandle (
        transform.rotation,
        transform.position,
        size);
どの角度から見ても円のハンドルが同じ形で表示されている

図17.8: どの角度から見ても円のハンドルが同じ形で表示されている

DrawAAPolyLine

各頂点(Vector3配列)を線で結び、表示します。

MonoBehaviour の派生クラスに Vector3 の配列を持たせ、

public class Example2 : MonoBehaviour {

    public Vector3[] vertexes;
}

CustomEditor 側で使用します。

void OnSceneGUI ()
{
    Handles.DrawAAPolyLine (component.vertexes);
}
要素順に線が引かれる

図17.9: 要素順に線が引かれる

複雑な線が引けるようになったのは良いのですが、値の設定が 図17.10 のように扱いにくいので2つ実装を行います。

値を手入力しなければいけない、また線を引く順番を変更する時に面倒

図17.10: 値を手入力しなければいけない、また線を引く順番を変更する時に面倒

PositionHandle

まずは Vector3 の値をシーンビュー上で編集できるようにハンドルを追加します。

void OnSceneGUI ()
{
    var vertexes = component.vertexes;

    for (int i = 0; i < vertexes.Length; i++) {

        vertexes [i] = Handles.PositionHandle (vertexes [i], Quaternion.identity);
    }

    Handles.DrawAAPolyLine (vertexes);
}

ハンドルが大きいと感じる場合は、本章で自作した PositionHandle を使用してみます。

各点に PositionHandle が表示された

図17.11: 各点に PositionHandle が表示された

ReorderableList

インスペクターでの表示を、ReorderableList で行います。ReorderableList を使用することで、要素の追加と並び替えがとても楽になります。詳しくは 第14章「ReorderbleList」 をご覧ください。

using UnityEngine;
using UnityEditor;
using UnityEditorInternal;

[CustomEditor (typeof(Example2))]
public class ExampleInspector2 : Editor
{
    ReorderableList reorderableList;
    Example2 component;

    void OnEnable ()
    {
        Tools.current = Tool.None;
        component = target as Example2;
        reorderableList = new ReorderableList (component.vertexes, typeof(Vector3));

        reorderableList.drawElementCallback = (rect, index, isActive, isFocused) => {
            component.vertexes [index] =
                EditorGUI.Vector3Field (rect, GUIContent.none,
                                          component.vertexes [index]);
        };

        reorderableList.onAddCallback = (list) => {
            ArrayUtility.Add (ref component.vertexes, Vector3.zero);
            ActiveEditorTracker.sharedTracker.ForceRebuild ();
        };
        reorderableList.onRemoveCallback = (list) => {
            ArrayUtility.Remove (ref component.vertexes,
                                    component.vertexes [list.index]);
            ActiveEditorTracker.sharedTracker.ForceRebuild ();
        };
        reorderableList.onChangedCallback = (list) => SceneView.RepaintAll ();
    }

    public override void OnInspectorGUI ()
    {
        reorderableList.DoLayoutList ();
    }
}

17.4 GUI の描画(2D)

ナビゲーションウィンドウを開いている時に右下に GUI が表示されている

図17.12: ナビゲーションウィンドウを開いている時に右下に GUI が表示されている

ハンドルは 3D 空間に特化した可視化ですが、2D 空間に対しての描画もできます。

3D 空間と 2D 空間

この2つの描画する仕組みは別になっているため少しだけ気をつけなければいけない部分があります。2D 向けの GUI を描画するには Handles.BeginGUIHandles.EndGUI を呼びださなければいけません。

void OnSceneGUI ()
{
    Handles.BeginGUI ();

    GUILayout.Button ("Button", GUILayout.Width (50));

    Handles.EndGUI ();
}
左上にボタンが表示された

図17.13: 左上にボタンが表示された

Handles.BeginGUI を GUI.Scope で表現する

GUI.Scope の機能を使って Handles.BeginGUI と EndGUI の記述を省くことで、いくらかコードの可読性が上がるかもしれません。

まずは HandleGUIScope を作成します。

public class HandleGUIScope : GUI.Scope
{
    public HandleGUIScope ()
    {
        Handles.BeginGUI ();
    }

    protected override void CloseScope ()
    {
        Handles.EndGUI ();
    }
}

あとは次のように使用するだけです。

void OnSceneGUI ()
{
    using (new HandleGUIScope ()) {
        GUILayout.Button ("Button", GUILayout.Width (50));
    }
}

ウィンドウの表示

GUI クラスの使い方は、インスペクターや他のウィンドウでの使い方と変わりません。試しにウィンドウを表示してみます。

左上に GUI のウィンドウが表示される

図17.14: 左上に GUI のウィンドウが表示される

int windowID = 1234;
Rect windowRect;

void OnSceneGUI ()
{
    Handles.BeginGUI ();

    windowRect = GUILayout.Window (windowID, windowRect, (id) => {

        EditorGUILayout.LabelField ("Label");

        EditorGUILayout.ToggleLeft ("Toggle", false);

        GUILayout.Button ("Button");

        GUI.DragWindow();

    }, "Window", GUILayout.Width (100));

    Handles.EndGUI ();
}
第16章 Gizmo(ギズモ) 第18章 HierarchySort