この記事は Unity Advent Calendar 2017 9日目の記事です。
前の記事は @fuqunaga さんによる 「ComputeShaderで巨大ライフゲームを作る」 でした。
ComputeShaderで多量のオブジェクトを描画したゲームは視覚によるインパクトが非常に強いので、どこかで試してみたい。
実行環境
- Unity 2017.2.0p1
背景
もともとこの記事を書く前は Mapbox Unity SDK について記載する予定でした。
こちらは記載を進めるうちに徐々にUnityから内容が離れていったためお蔵入りとなりましたが
Mapbox Unity SDK
を触る内に少し困った自体に遭遇しました。
コンポーネントから任意のオブジェクトを辿る際に、元々どのオブジェクトを参照していたか分からなくなることです。
今回はこの現象の解消と、普段あまり触らないエディタ拡張の素振りのために
"戻る" 機能 を実装してみようと思います。
エディタで選択中のオブジェクトを取得する
- 今エディタで選択しているオブジェクトには Selection を用いることでアクセスできる
-
Selection.objects に現在選択中のオブジェクトが格納される
-
Selection.objects
ではObject[]
を取得できる - 配列なのはエディタ上で複数のオブジェクトを同時に選択できるため
-
- Selection.selectionChanged にコールバック関数を登録することで、 オブジェクトを選択するたびに実行される
📝 コンパイル終了後に処理を実行する
-
InitializeOnLoad
もしくはInitializeOnLoadMethod
属性をクラスに対して適応する- 起動時エディタースクリプト実行 を参照
using UnityEditor;
using UnityEngine;
[InitializeOnLoad]
class ShowSelection
{
static ShowSelection()
{
Selection.selectionChanged += () =>
{
foreach (var obj in Selection.objects)
{
Debug.Log(obj.name);
}
};
}
}
Inspector上の表示内容を切り替える
- Inspectorには今エディタで選択されているオブジェクトの内容が表示される
-
Selection.objects
にinspector上に表示したいオブジェクトを代入することで表示を切り替えることができる - 下記は独自定義したウィンドウを表示し、オブジェクトを指定して
Set Selection
を押すことで、対象のオブジェクトをinspectorに表示するというもの
📝 Menuに項目として追加する
- MenuItem 属性を付けたメソッドはメニュー上から呼ぶことができる
📝 Windowの描画を行う
-
EditorWindow.Show() を任意のフックポイントで呼び出す必要がある
- 今回は
MenuItem
属性を付けたメソッド内で指定し、メニュー経由で表示
- 今回は
📝 Window内に要素の描画する
- EditorWindow.OnGUI() メソッド内に要素を記載する
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() で要素を格納する
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つ前の要素に戻る
- Stack.Pop() で先頭の要素を削除しつつ、返却する
- 先頭には現在Inspectorに表示中のオブジェクトが表示されている
- 1つ前のオブジェクトを取得するために2回Popを実行している
-
Stack.Peek() (削除を行わない)でなく
Stack.Pop()
(削除を行う) なのはSelection.objects
代入時に再度追加されるのを見越すため - この辺りはいくつか実装アプローチがあって、もう少し可読性が高いものがありそう
-
Stack.Peek() (削除を行わない)でなく
- 1つ前のオブジェクトを取得するために2回Popを実行している
📝 ショートカットキーを指定する
-
MenuItem
の引数にはメニューのパスを文字列で指定する - 文字列にはショートカットキーを含めることができる
-
%a
: (Win)Ctrl/(mac)Cmd + a -
#&x
: Shift + Alt + x -
_LEFT
: ← のみ
-
- Unity標準で指定されているショートカットとの重複に注意
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;
}
}
履歴をウィンドウで閲覧する
📝 Selection更新時にWindowの中身を更新する
- EditorWindow.OnSelectionChange() メソッドはSelection更新時に呼ばれる
-
EditorWindow.Repaint() を呼び出すことで
OnGUI
に指定した内容を再度描画する
📝 Window内にスクロールバーを表示する
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を使うのも良いでしょう。
最後に
明日は @Fuji0k0 さんによる「CustomShaderGUIによるBlend Mode指定」のお話です!楽しみですね!