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

この記事は [Unity Advent Calendar 2017](https://qiita.com/advent-calendar/2017/unity) 9日目の記事です。
前の記事は @fuqunaga さんによる [「ComputeShaderで巨大ライフゲームを作る」](https://qiita.com/fuqunaga/private/e0c93c0f419bdec4ce0e) でした。
ComputeShaderで多量のオブジェクトを描画したゲームは視覚によるインパクトが非常に強いので、どこかで試してみたい。

## 実行環境
- Unity 2017.2.0p1

## 背景
もともとこの記事を書く前は [Mapbox Unity SDK](https://www.mapbox.com/unity/) について記載する予定でした。
こちらは記載を進めるうちに徐々にUnityから内容が離れていったためお蔵入りとなりましたが
`Mapbox Unity SDK` を触る内に少し困った自体に遭遇しました。

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

![inspector_prev1.gif](https://qiita-image-store.s3.amazonaws.com/0/102664/fa6f6fad-d814-aa5b-246f-22ae3ab1754e.gif)

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


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

![inspector_prev3.gif](https://qiita-image-store.s3.amazonaws.com/0/102664/89d17985-02c9-8fa8-81f5-33cb354291a5.gif)

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

### 📝 コンパイル終了後に処理を実行する
- `InitializeOnLoad` もしくは `InitializeOnLoadMethod ` 属性をクラスに対して適応する
  - [起動時エディタースクリプト実行](https://docs.unity3d.com/ja/current/Manual/RunningEditorCodeOnLaunch.html) を参照

```csharp: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](https://qiita-image-store.s3.amazonaws.com/0/102664/991f77f5-2728-3e13-f014-96c1b7913ac3.gif)

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

### 📝 Menuに項目として追加する
- [MenuItem](https://docs.unity3d.com/ScriptReference/MenuItem.html) 属性を付けたメソッドはメニュー上から呼ぶことができる

### 📝 Windowの描画を行う
- [EditorWindow.Show()](https://docs.unity3d.com/ScriptReference/EditorWindow.Show.html) を任意のフックポイントで呼び出す必要がある
  - 今回は `MenuItem` 属性を付けたメソッド内で指定し、メニュー経由で表示

### 📝 Window内に要素の描画する
- [EditorWindow.OnGUI()](https://docs.unity3d.com/ScriptReference/EditorWindow.OnGUI.html) メソッド内に要素を記載する

```csharp: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](https://msdn.microsoft.com/ja-jp/library/3278tedw(v=vs.110).aspx) に格納する
  - `Stack` はFILO(先入れ後出し)を実現するためのジェネリックコレクション
  - 順番を考慮して格納したり、取り出したりするときに便利
- [Stack.Push()](https://msdn.microsoft.com/ja-jp/library/system.collections.stack.push(v=vs.110).aspx) で要素を格納する

```csharp: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](https://qiita-image-store.s3.amazonaws.com/0/102664/23751cc2-878c-7757-fded-2fc0ba9544f2.gif)

- [Stack.Pop()](https://msdn.microsoft.com/ja-jp/library/system.collections.stack.pop(v=vs.110).aspx) で先頭の要素を削除しつつ、返却する
- 先頭には現在Inspectorに表示中のオブジェクトが表示されている
  - 1つ前のオブジェクトを取得するために2回Popを実行している
      - [Stack.Peek()](https://msdn.microsoft.com/ja-jp/library/system.collections.stack.peek(v=vs.110).aspx) (削除を行わない)でなく `Stack.Pop()` (削除を行う) なのは `Selection.objects` 代入時に再度追加されるのを見越すため
      - _この辺りはいくつか実装アプローチがあって、もう少し可読性が高いものがありそう_

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

```csharp: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](https://qiita-image-store.s3.amazonaws.com/0/102664/eedb88bb-aa4a-7133-7a84-9f0064dd1cca.gif)

### 📝 Selection更新時にWindowの中身を更新する
- [EditorWindow.OnSelectionChange()](https://docs.unity3d.com/ScriptReference/EditorWindow.OnSelectionChange.html) メソッドはSelection更新時に呼ばれる
- [EditorWindow.Repaint()](https://docs.unity3d.com/ScriptReference/EditorWindow.Repaint.html) を呼び出すことで `OnGUI` に指定した内容を再度描画する

### 📝 Window内にスクロールバーを表示する
- [EditorGUILayout.BeginScrollView()](https://docs.unity3d.com/ScriptReference/EditorGUILayout.BeginScrollView.html) の項目を参照

```csharp: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](https://assetstore.unity.com/packages/tools/utilities/inspector-navigator-26181)は無料で使用できますし
[History Inspector](https://assetstore.unity.com/packages/tools/utilities/history-inspector-44279)を使うのも良いでしょう。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/102664/1909f5fb-a36c-1a07-8b82-314a2e8ce4e2.png)
良さそう！

## 最後に
明日は [@Fuji0k0](https://qiita.com/Fuji0k0) さんによる[「CustomShaderGUIによるBlend Mode指定」](https://qiita.com/Fuji0k0/items/4f4bfa552e5c7967ac60)のお話です！楽しみですね！