Version: 2021.3
言語: 日本語
ランタイム用タブメニューの作成

カスタムエディターウィンドウの作成

カスタムエディターウィンドウを使うと、独自のエディターやワークフローを実装して Unity を拡張することができます。 このガイドでは、コードによるエディターウィンドウの作成、ユーザー入力への対応、UI のサイズの可変にする方法、ホットリロードの処理について説明します。

このチュートリアルでは、プロジェクト内のすべてのスプライトを検索して表示し、リストとして表示するスプライトブラウザーを作成します。リストの中のスプライトを選択すると、ウィンドウの右側に画像が表示されます。

完成した例は、エディターウィンドウスクリプト のセクションで見ることができます。

カスタムスプライトブラウザー
カスタムスプライトブラウザー

要件

このガイドは、Unity を使い慣れていて、UI Toolkit を使い慣れてはいない開発者のためのものです。Unity と C# スクリプティングについて基本的な知識があることが推奨されます。

このガイドでは、以下の概念も参照しています。

コンテンツ

本ガイドで使用するコントロール

このガイドでは、以下を行います。

  • エディターウィンドウスクリプトを作成する
  • ウィンドウを開くためのメニュー項目を作成する
  • ウィンドウに UI コントロールを加える
  • ユーザーの選択に応答するコードを書く
  • UI のサイズを変更可能にする 
  • エディターのホットリロードに対応する

エディターウィンドウスクリプトを作成する

ヒント
エディターウィンドウのスクリプトを作成するために必要なコードは、Unity エディターで生成することができます。Project ウィンドウで右クリックして、Create > UI Toolkit > Editor Window の順に選択します。このガイドでは UXML と USS のチェックボックスを無効にしてください。また、上図のように、ファイルの先頭に using ディレクティブを追加える必要があるかもしれません。

エディターウィンドウは、プロジェクト内の C# スクリプトで作成します。カスタムエディターウィンドウは、EditorWindow クラスから派生したクラスです。

新しいスクリプトファイル MyCustomEditor.csAssets/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() です。

Inside ShowMyEditor(), call the EditorWindow.GetWindow() method to create and display the window. It returns an EditorWindow object. To set the window title, change the EditorWindow.titleContent property.

前の手順で作成した 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 コントロールをウィンドウに加える

UI Toolkit は CreateGUI メソッドを使用してエディター UI にコントロールを加え、Unity はウィンドウを表示する必要があるときに CreateGUI メソッドを自動的に呼び出します。このメソッドは AwakeUpdate のようなメソッドと同じように働きます。

ビジュアルツリーにビジュアルエレメントを追加することで、UIにUIコントロールを追加することができます。VisualElement.Add() メソッドは、既存のビジュアルエレメントに子を追加するために使用されます。エディタウィンドウの ビジュアルツリー には、rootvisualElement プロパティでアクセスできます。

まず初めに CreateGUI() 関数をカスタムエディタークラスに加え、‘Hello’ ラベルを加えます。

public void CreateGUI()
{
  rootVisualElement.Add(new Label("Hello"));
}
「Hello」ラベルの付いたウィンドウ
「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つの空のパネルを配置したものです。仕切りバーは移動できます。

2 つの分割されたペインを持つウィンドウ
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;
}

下の画像は、スクロール可能なリストビューと選択可能な項目を備えたエディターウィンドウです。

スプライト名を表示したListView
スプライト名を表示したListView

参考までに、以下は、 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; をファイルの先頭に記述してください。

エディターでスプライトブラウザーをテストします。下の画像は、カスタムエディターウインドウが動作しているところです。

スプライトブラウザの動作
スプライトブラウザの動作

UIをリサイズ可能にする

エディターウィンドウは、最小と最大の許容寸法内でサイズ変更が可能です。これらの寸法は、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 以降では、ウィンドウがドッキングされているときに、minSizemaxSize のプロパティを尊重しません。これにより、ユーザーは制限なくドッキング領域のサイズを変更することができます。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; };
}

リストからスプライトを選択して再生モードに入ると、ホットリロードのテストができます。

エディターウィンドウの例

The code below is the final script of the Editor window created during this guide. You can paste the code directly into a file called MyCustomEdtior.cs inside the Assets/Editor folder to see it in the Unity Editor.

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);
  }
}

ランタイム用タブメニューの作成