Unity
UnityDay 9

[Unity] Editor拡張でInspectorの"戻る"を実現する

この記事は Unity Advent Calendar 2017 9日目の記事です。
前の記事は @fuqunaga さんによる 「ComputeShaderで巨大ライフゲームを作る」 でした。
ComputeShaderで多量のオブジェクトを描画したゲームは視覚によるインパクトが非常に強いので、どこかで試してみたい。

実行環境

  • Unity 2017.2.0p1

背景

もともとこの記事を書く前は Mapbox Unity SDK について記載する予定でした。
こちらは記載を進めるうちに徐々にUnityから内容が離れていったためお蔵入りとなりましたが
Mapbox Unity SDK を触る内に少し困った自体に遭遇しました。

コンポーネントから任意のオブジェクトを辿る際に、元々どのオブジェクトを参照していたか分からなくなることです。

inspector_prev1.gif

今回はこの現象の解消と、普段あまり触らないエディタ拡張の素振りのために
"戻る" 機能 を実装してみようと思います。

エディタで選択中のオブジェクトを取得する

inspector_prev3.gif

  • 今エディタで選択しているオブジェクトには Selection を用いることでアクセスできる
  • Selection.objects に現在選択中のオブジェクトが格納される
    • Selection.objectsでは Object[] を取得できる
    • 配列なのはエディタ上で複数のオブジェクトを同時に選択できるため
  • Selection.selectionChanged にコールバック関数を登録することで、 オブジェクトを選択するたびに実行される

📝 コンパイル終了後に処理を実行する

ShowSelection.cs
using UnityEditor;
using UnityEngine;

[InitializeOnLoad]
class ShowSelection
{
    static ShowSelection()
    {
        Selection.selectionChanged += () =>
        {
            foreach (var obj in Selection.objects)
            {
                Debug.Log(obj.name);
            }
        };
    }
}

Inspector上の表示内容を切り替える

inspector_prev2.gif

  • Inspectorには今エディタで選択されているオブジェクトの内容が表示される
  • Selection.objects にinspector上に表示したいオブジェクトを代入することで表示を切り替えることができる
  • 下記は独自定義したウィンドウを表示し、オブジェクトを指定して Set Selection を押すことで、対象のオブジェクトをinspectorに表示するというもの

📝 Menuに項目として追加する

  • MenuItem 属性を付けたメソッドはメニュー上から呼ぶことができる

📝 Windowの描画を行う

  • EditorWindow.Show() を任意のフックポイントで呼び出す必要がある
    • 今回は MenuItem 属性を付けたメソッド内で指定し、メニュー経由で表示

📝 Window内に要素の描画する

SetSelection.cs
using UnityEditor;
using UnityEngine;

public class SetSelection : EditorWindow
{
    private Object obj;

    [MenuItem("Window/SetSelection")]
    public static void OpenWindow()
    {
        EditorWindow.GetWindow(typeof(SetSelection)).Show();
    }

    void OnGUI()
    {
        obj = EditorGUILayout.ObjectField("Object", obj, typeof(Object), true);
        if (obj != null)
        {
            if (GUILayout.Button("Set Selection"))
            {
                Selection.objects = new[] {obj};
            }
        }
    }
}

オブジェクト選択時に内容を保持する

  • オブジェクトが切り替わったタイミングで選択していたオブジェクトを履歴に追加する
  • オブジェクトは Stack に格納する
    • Stack はFILO(先入れ後出し)を実現するためのジェネリックコレクション
    • 順番を考慮して格納したり、取り出したりするときに便利
  • Stack.Push() で要素を格納する
InspactorHistory.cs
using System.Collections.Generic;
using UnityEditor;
using UnityEngine;

public class InspactorHistory
{
    private static Stack<Object[]> history;

    [InitializeOnLoadMethod]
    public static void Init()
    {
        history = new Stack<Object[]>();
        Selection.selectionChanged += () => Set(Selection.objects);
    }

    public static void Set(Object[] objects)
    {
        if (objects.Length == 0) return;
        history.Push(objects);
    }
}

1つ前の要素に戻る

inspector_prev4.gif

  • Stack.Pop() で先頭の要素を削除しつつ、返却する
  • 先頭には現在Inspectorに表示中のオブジェクトが表示されている
    • 1つ前のオブジェクトを取得するために2回Popを実行している
      • Stack.Peek() (削除を行わない)でなく Stack.Pop() (削除を行う) なのは Selection.objects 代入時に再度追加されるのを見越すため
      • この辺りはいくつか実装アプローチがあって、もう少し可読性が高いものがありそう

📝 ショートカットキーを指定する

  • MenuItem の引数にはメニューのパスを文字列で指定する
  • 文字列にはショートカットキーを含めることができる
    • %a : (Win)Ctrl/(mac)Cmd + a
    • #&x : Shift + Alt + x
    • _LEFT : ← のみ
  • Unity標準で指定されているショートカットとの重複に注意
StackSelection.cs
using System.Collections.Generic;
using UnityEditor;
using UnityEngine;

public class InspactorHistory
{
    private static Stack<Object[]> history;

    [InitializeOnLoadMethod]
    public static void Init()
    {
        history = new Stack<Object[]>();
        Selection.selectionChanged += () => Set(Selection.objects);
    }

    public static void Set(Object[] objects)
    {
        if (objects.Length == 0) return;
        history.Push(objects);
    }

    // Cmd + Alt + b
    [MenuItem("Edit/Inspector/Prev %&b")]
    public static void Prev()
    {
        if (history.Count <= 1) return;

        // XXX 現在表示されている分を削除した上で、1つ前の履歴を取得し、遷移する
        history.Pop();
        Move(history.Pop());
    }

    public static void Move(Object[] objects)
    {
        Selection.objects = objects;
    }

    public static Stack<Object[]> GetHistory()
    {
        return history;
    }
}

履歴をウィンドウで閲覧する

inspector_prev5.gif

📝 Selection更新時にWindowの中身を更新する

📝 Window内にスクロールバーを表示する

InspactorHistoryWindow.cs
using UnityEditor;
using UnityEngine;

public class InspactorHistoryWindow : EditorWindow
{
    private Vector2 scrollPosition;

    [MenuItem("Window/Inspector/History")]
    public static void Open()
    {
        EditorWindow.GetWindow(typeof(InspactorHistoryWindow)).Show();
    }

    void OnGUI()
    {
        this.RenderHistory();
    }

    void OnSelectionChange()
    {
        Repaint();
    }

    private void RenderHistory()
    {
        GUILayout.Label("History", EditorStyles.boldLabel);
        scrollPosition = EditorGUILayout.BeginScrollView(scrollPosition);
        foreach (Object[] objects in InspactorHistory.GetHistory())
        {
            this.RenderHistoryCell(objects);
        }
        GUILayout.EndScrollView();
    }

    private void RenderHistoryCell(Object[] objects)
    {
        if (objects.Length == 0) return;

        EditorGUILayout.BeginHorizontal(GUIStyle.none);
        if (GUILayout.Button("SELECT", GUILayout.MaxWidth(60), GUILayout.MaxHeight(15)))
        {
            InspactorHistory.Move(objects);
        }
        GUILayout.Label(this.GetObjectName(objects), EditorStyles.label);
        GUILayout.FlexibleSpace();
        EditorGUILayout.EndHorizontal();
    }

    private string GetObjectName(Object[] objects)
    {
        string name = objects[0].name;
        return objects.Length > 1 ? System.String.Format("{0} ...", name) : name;
    }
}

今回得た学び

  • Selection の挙動
  • Selection の更新に追従した処理の実行方法
  • MenuItem のショートカットキーの指定方法
  • 独自Windowに対するスクロールバーの表示方法

ここまで書いておいてなんですが...

今回の作成したものはコンパイルの度に初期化される等、改良点が多く残るものです。
アセットでもっといい感じのいっぱいあるので、積極的にアセット使っていきましょう!
Inspector Navigatorは無料で使用できますし
History Inspectorを使うのも良いでしょう。

image.png
良さそう!

最後に

明日は @Fuji0k0 さんによる「CustomShaderGUIによるBlend Mode指定」のお話です!楽しみですね!