カスタムエディターウィンドウを使うと、独自のエディターやワークフローを実装して Unity を拡張することができます。 このガイドでは、コードによるエディターウィンドウの作成、ユーザー入力への対応、UI のサイズの可変にする方法、ホットリロードの処理について説明します。
このチュートリアルでは、プロジェクト内のすべてのスプライトを検索して表示し、リストとして表示するスプライトブラウザーを作成します。リストの中のスプライトを選択すると、ウィンドウの右側に画像が表示されます。
完成した例は、エディターウィンドウスクリプト のセクションで見ることができます。
このガイドは、Unity を使い慣れていて、UI Toolkit を使い慣れてはいない開発者のためのものです。Unity と C# スクリプティングについて基本的な知識があることが推奨されます。
このガイドでは、以下の概念も参照しています。
本ガイドで使用するコントロール
このガイドでは、以下を行います。
ヒント |
---|
エディターウィンドウのスクリプトを作成するために必要なコードは、Unity エディターで生成することができます。Project ウィンドウで右クリックして、Create > UI Toolkit > Editor Window の順に選択します。このガイドでは UXML と USS のチェックボックスを無効にしてください。また、上図のように、ファイルの先頭に using ディレクティブを追加える必要があるかもしれません。 |
エディターウィンドウは、プロジェクト内の C# スクリプトで作成します。カスタムエディターウィンドウは、EditorWindow
クラスから派生したクラスです。
新しいスクリプトファイル MyCustomEditor.cs
を Assets/Editor フォルダーに作成します。そのスクリプトに以下のコードを貼り付けます。
using System.Collections.Generic;
using System.Linq;
using UnityEditor;
using UnityEngine;
using UnityEngine.UIElements;
public class MyCustomEditor : EditorWindow
{
}
ノート |
---|
これは、UnityEditor 名前空間を含むエディター専用ウィンドウであるため、ファイルは Editor フォルダー の下、またはエディター専用の Assembly Definition (アセンブリ定義) 内に置く必要があります。 |
新しいエディターウィンドウを開くには、エディターメニューにエントリーを作成する必要があります。
MenuItem
属性を静的メソッドに加えます。この例では、静的メソッドの名前は ShowMyEditor()
です。
この ShowMyEditor()
の内部では、EditorWindow.GetWindow() メソッドを呼び出して、ウィンドウを作成し表示します。このメソッドは、EditorWindow オブジェクトを返します。ウィンドウのタイトルを設定するには、EditorWindow.titleContent プロパティを変更します。
前の手順で作成した MyCustomEditor
クラスの中に以下の関数を加えます。
[MenuItem("Tools/My Custom Editor")]
public static void ShowMyEditor()
{
// This method is called when the user selects the menu item in the Editor
EditorWindow wnd = GetWindow<MyCustomEditor>();
wnd.titleContent = new GUIContent("My Custom Editor");
}
Unity エディターのメニュー Tools > My Custom Editor を選び、新しいウィンドウを開いてテストします。
UI Toolkit は CreateGUI メソッドを使用してエディター UI にコントロールを加え、Unity はウィンドウを表示する必要があるときに CreateGUI
メソッドを自動的に呼び出します。このメソッドは Awake
や Update
のようなメソッドと同じように働きます。
ビジュアルツリーにビジュアルエレメントを追加することで、UIにUIコントロールを追加することができます。VisualElement.Add() メソッドは、既存のビジュアルエレメントに子を追加するために使用されます。エディタウィンドウの ビジュアルツリー には、rootvisualElement
プロパティでアクセスできます。
まず初めに CreateGUI()
関数をカスタムエディタークラスに加え、‘Hello’ ラベルを加えます。
public void CreateGUI()
{
rootVisualElement.Add(new Label("Hello"));
}
ノート |
---|
スプライトのリストを表示するには、AssetDatabase 関数を使用して、プロジェクト内のすべてのスプライトを検索します。 |
CreateGUI() 内のコード
を以下のコードに置き換えて、プロジェクト内のすべてのスプライトを列挙します。
public void CreateGUI()
{
// Get a list of all sprites in the project
var allObjectGuids = AssetDatabase.FindAssets("t:Sprite");
var allObjects = new List<Sprite>();
foreach (var guid in allObjectGuids)
{
allObjects.Add(AssetDatabase.LoadAssetAtPath<Sprite>(AssetDatabase.GUIDToAssetPath(guid)));
}
}
スプライトブラウザーを作るために、トップレベルのビジュアル要素は、TwoPaneSplitView になります。このコントロールは、利用可能なウィンドウスペースを、固定サイズとフレキシブルなサイズの 2 つのペインに分割します。ウィンドウのサイズを変更すると、固定サイズのペインは同じサイズのままで、フレキシブルなペインだけがサイズ変更されます。
TwoPaneSplitView
コントロールを動作させるためには、ちょうど 2 つの子要素を持つ必要があります。CreateGUI()
の内部にコードを加えて TwoPaneSplitview
を作成し、異なるコントロール用のプレースホルダーとして 2 つの子要素を加えます。
// Create a two-pane view with the left pane being fixed with
var splitView = new TwoPaneSplitView(0, 250, TwoPaneSplitViewOrientation.Horizontal);
// Add the view to the visual tree by adding it as a child to the root element
rootVisualElement.Add(splitView);
// A TwoPaneSplitView always needs exactly two child elements
var leftPane = new VisualElement();
splitView.Add(leftPane);
var rightPane = new VisualElement();
splitView.Add(rightPane);
下の画像は、カスタムウィンドウに2つの空のパネルを配置したものです。仕切りバーは移動できます。
スプライトブラウザーの場合、左ペインは、プロジェクトで見つかったすべてのスプライトの名前を含むリストになります。ListView コントロールは、 VisualElement
から派生しています。そのため、空白の VisualElement
の代わりに ListView
を使用するようにコードを修正するのは簡単です。
CreateGUI()
関数内のコードを修正して、 ListView
コントロールを、 VisualElement
の代わりに左ペインに作成します。
public void CreateGUI()
{
...
var leftPane = new ListView();
splitView.Add(leftPane);
...
}
ListView コントロールは、選択可能なアイテムのリストを表示します。可視領域をカバーするのに十分な要素のみを作成するように最適化されており、リストをスクロールするとビジュアル要素をプールして再利用します。これにより、多数のアイテムを持つリストであっても、パフォーマンスを最適化し、メモリフットプリントを低く抑えることができます。
これを利用するためには、ListView
を以下のように適切に初期化する必要があります。
リストの各要素に複雑な UI 構造を作ることもできますが、この例ではシンプルなテキストラベルを使ってスプライトの名前を表示します。
一番下の CreateGUI()
関数にコードを追加し、 ListView
を初期化します。
public void CreateGUI()
{
...
// Initialize the list view with all sprites' names
leftPane.makeItem = () => new Label();
leftPane.bindItem = (item, index) => { (item as Label).text = allObjects[index].name; };
leftPane.itemsSource = allObjects;
}
下の画像は、スクロール可能なリストビューと選択可能な項目を備えたエディターウィンドウです。
参考までに、以下は、 CreateGUI()
関数の現在のコードをそのまま掲載しています。
public void CreateGUI()
{
// Get a list of all sprites in the project
var allObjectGuids = AssetDatabase.FindAssets("t:Sprite");
var allObjects = new List<Sprite>();
foreach (var guid in allObjectGuids)
{
allObjects.Add(AssetDatabase.LoadAssetAtPath<Sprite>(AssetDatabase.GUIDToAssetPath(guid)));
}
// Create a two-pane view with the left pane being fixed with
var splitView = new TwoPaneSplitView(0, 250, TwoPaneSplitViewOrientation.Horizontal);
// Add the panel to the visual tree by adding it as a child to the root element
rootVisualElement.Add(splitView);
// A TwoPaneSplitView always needs exactly two child elements
var leftPane = new ListView();
splitView.Add(leftPane);
var rightPane = new VisualElement();
splitView.Add(rightPane);
// Initialize the list view with all sprites' names
leftPane.makeItem = () => new Label();
leftPane.bindItem = (item, index) => { (item as Label).text = allObjects[index].name; };
leftPane.itemsSource = allObjects;
}
左ペインのリストからスプライトを選択すると、その画像が右ペインに表示される必要があります。そのためには、 ListView
が、ユーザが選択したときに呼び出すことのできるコールバック関数を用意する必要があります。 ListView
コントロールは、この目的のために onSelectionChange
プロパティを持っています。
コールバック関数は、ユーザーが選択したアイテムを含むリストを受け取ります。 ListView
を複数選択できるように設定することも可能ですが、デフォルトでは選択モードは1つのアイテムに限定されています。
ユーザーが左ペインのリストから選択を変更したときのコールバック関数を追加します。
public void CreateGUI()
{
...
// React to the user's selection
leftPane.onSelectionChange += OnSpriteSelectionChange;
}
private void OnSpriteSelectionChange(IEnumerable<object> selectedItems)
{
}
ノート |
---|
ウィンドウを失い、メニューで再表示されないような場合は、Window > Panels > Close all floating panels を使って、すべてのフローティングパネルを閉じるか、ウィンドウレイアウトをリセットしてください。 |
選択したスプライトの画像をウィンドウの右側に表示するためには,この関数は, TwoPaneSplitView
の右側のペインにアクセスする必要があります。このコントロールをクラスのメンバー変数にしておけば、コールバック関数内でアクセスできるようになります。
CreateGUI()
の中で作成した rightPane
をメンバー変数にします。
private VisualElement m_RightPane;
public void CreateGUI()
{
...
m_RightPane = new VisualElement();
splitView.Add(m_RightPane);
...
}
TwoPaneSplitView
を参照すると、右ペインには flexedPane
プロパティからアクセスできます。
右ペインに新しい Image
コントロールを作成する前に、VisualElement.Clear()
を使って前の画像を削除します。この メソッドは、既存のビジュアル要素からすべての子を削除します。
右ペインの以前のコンテンツをすべて消去し、選択したスプライト用に新しい Image (画像) コントロールを作成します。
private void OnSpriteSelectionChange(IEnumerable<object> selectedItems)
{
// Clear all previous content from the pane
m_RightPane.Clear();
// Get the selected sprite
var selectedSprite = selectedItems.First() as Sprite;
if (selectedSprite == null)
return;
// Add a new Image control and display the sprite
var spriteImage = new Image();
spriteImage.scaleMode = ScaleMode.ScaleToFit;
spriteImage.sprite = selectedSprite;
// Add the Image control to the right-hand pane
m_RightPane.Add(spriteImage);
}
ノート |
---|
First() メソッドを selectedItems パラメーターで使用するために、必ず using System.Linq; をファイルの先頭に記述してください。 |
エディターでスプライトブラウザーをテストします。下の画像は、カスタムエディターウインドウが動作しているところです。
エディターウィンドウは、最小と最大の許容寸法内でサイズ変更が可能です。これらの寸法は、C# でウィンドウを作成する際に、EditorWindow.minSize およびEditorWindow.maxSize プロパティに書き込むことで設定できます。ウィンドウのサイズが変わらないようにするには、両方のプロパティに同じ寸法を割り当てます。
ShowMyEditor() 関数の一番下に以下の行を追加することで、カスタムエディターウィンドウのサイズを制限します。
[MenuItem("Tools/My Custom Editor")]
public static void ShowMyEditor()
{
// This method is called when the user selects the menu item in the Editor
EditorWindow wnd = GetWindow<MyCustomEditor>();
wnd.titleContent = new GUIContent("My Custom Editor");
// Limit size of the window
wnd.minSize = new Vector2(450, 200);
wnd.maxSize = new Vector2(1920, 720);
}
ウィンドウの寸法が小さすぎて UI 全体を表示できない場合、ScrollView
要素を使用してウィンドウのスクロールを提供する必要があり、そうしないとコンテンツにアクセスできなくなる可能性があります。
左ペインの ListView
は、内部的に ScrollView
を使用していますが、右ペインは、通常の VisualElement
です。これを ScrollView
コントロールに交換すると、ウィンドウが小さすぎて画像全体を元のサイズに収めることができない場合、自動的にスクロールバーが表示されます。
右ペイン VisualElement
を、双方向スクロールが可能な ScrollView
に交換します。
public void CreateGUI()
{
...
m_RightPane = new ScrollView(ScrollViewMode.VerticalAndHorizontal);
splitView.Add(m_RightPane);
...
}
下の画像は,スプライトブラウザーのウィンドウにスクロールバーをつけたものです.
新しいサイズ制限をテストするために、カスタムエディタのウィンドウを閉じて、再度開いてみてください。
ノート |
---|
Unity 2021.2 以降では、ウィンドウがドッキングされているときに、minSize と maxSize のプロパティを尊重しません。これにより、ユーザーは制限なくドッキング領域のサイズを変更することができます。ScrollView をトップレベルの要素の 1 つとして作成し、その中にすべての UI を配置することで、UI をできるだけ対応可能にするように検討してください。 |
適切なエディターウィンドウは、ユニティエディターで起こるホットリロードのワークフローとうまく連動しなければなりません。C# ドメインのリロードは、スクリプトが再コンパイルされるときや、ユーザーが再生モードに入るときに発生します。このトピックについては、スクリプトのシリアル化 のページで詳しく説明しています。
前に作成したエディターウィンドウでこの動作を確認するには、スプライトブラウザーを開き、スプライトを選択してから再生モードに入ります。ウィンドウがリセットされ、選択したものが消えます。
VisualElement
オブジェクトはシリアル化できないため、Unity で再ロードが発生するたびに UI を再作成する必要があります。つまり、再ロードが完了した後に、CreateGUI()
メソッドが再び呼び出されることになります。これにより、EditorWindow
クラスに必要なデータを格納することで、再ロード前の UI の状態を復元することができます。
メンバー変数を追加して、現在選択されているインデックスをスプライトリストに保存します。
public class MyCustomEditor : EditorWindow
{
[SerializeField] private int m_SelectedIndex = -1;
....
}
ユーザーが選択を行うと、リストビューの新しい選択インデックスがこのメンバー変数内に格納されます。CreateGUI()
関数内で UI を作成する際に、この選択インデックスを復元することができます。
CreateGUI()
関数の最後に、選択したリストのインデックスを保存し復元するコードを追加します。
public void CreateGUI()
{
...
// Restore the selection index from before the hot reload
leftPane.selectedIndex = m_SelectedIndex;
// Store the selection index when the selection changes
leftPane.onSelectionChange += (items) => { m_SelectedIndex = leftPane.selectedIndex; };
}
リストからスプライトを選択して再生モードに入ると、ホットリロードのテストができます。
以下は、このガイドを使って作成したエディターウィンドウの最終的なスクリプトです。このコードをコピーして、Assets/Editor フォルダ内の MyCustomEditor.cs
というファイルに直接貼り付けてテストすることができます。
using System.Collections.Generic;
using System.Linq;
using UnityEditor;
using UnityEngine;
using UnityEngine.UIElements;
public class MyCustomEditor : EditorWindow
{
[SerializeField] private int m_SelectedIndex = -1;
private VisualElement m_RightPane;
[MenuItem("Tools/My Custom Editor")]
public static void ShowMyEditor()
{
// This method is called when the user selects the menu item in the Editor
EditorWindow wnd = GetWindow<MyCustomEditor>();
wnd.titleContent = new GUIContent("My Custom Editor");
// Limit size of the window
wnd.minSize = new Vector2(450, 200);
wnd.maxSize = new Vector2(1920, 720);
}
public void CreateGUI()
{
// Get a list of all sprites in the project
var allObjectGuids = AssetDatabase.FindAssets("t:Sprite");
var allObjects = new List<Sprite>();
foreach (var guid in allObjectGuids)
{
allObjects.Add(AssetDatabase.LoadAssetAtPath<Sprite>(AssetDatabase.GUIDToAssetPath(guid)));
}
// Create a two-pane view with the left pane being fixed with
var splitView = new TwoPaneSplitView(0, 250, TwoPaneSplitViewOrientation.Horizontal);
// Add the panel to the visual tree by adding it as a child to the root element
rootVisualElement.Add(splitView);
// A TwoPaneSplitView always needs exactly two child elements
var leftPane = new ListView();
splitView.Add(leftPane);
m_RightPane = new ScrollView(ScrollViewMode.VerticalAndHorizontal);
splitView.Add(m_RightPane);
// Initialize the list view with all sprites' names
leftPane.makeItem = () => new Label();
leftPane.bindItem = (item, index) => { (item as Label).text = allObjects[index].name; };
leftPane.itemsSource = allObjects;
// React to the user's selection
leftPane.onSelectionChange += OnSpriteSelectionChange;
// Restore the selection index from before the hot reload
leftPane.selectedIndex = m_SelectedIndex;
// Store the selection index when the selection changes
leftPane.onSelectionChange += (items) => { m_SelectedIndex = leftPane.selectedIndex; };
}
private void OnSpriteSelectionChange(IEnumerable<object> selectedItems)
{
// Clear all previous content from the pane
m_RightPane.Clear();
// Get the selected sprite
var selectedSprite = selectedItems.First() as Sprite;
if (selectedSprite == null)
return;
// Add a new Image control and display the sprite
var spriteImage = new Image();
spriteImage.scaleMode = ScaleMode.ScaleToFit;
spriteImage.sprite = selectedSprite;
// Add the Image control to the right-hand pane
m_RightPane.Add(spriteImage);
}
}