Version: 2023.1
言語: 日本語
カスタムエディターウィンドウの作成
SerializedObject のデータバインディング

カスタムインスペクターの作成

Unity は MonoBehaviours と ScriptableObject に対してデフォルトのインスペクターを生成しますが、カスタムインスペクターの作成には、次のような理由があります。

  • スクリプトのプロパティをよりユーザーが使いやすい表示にする。
  • プロパティを整理し、グループ化する。
  • ユーザーの選択で、UI の各セクションを表示または非表示にする。
  • 個々の設定やプロパティの意味について、追加情報を提供する。

UI Toolkit を使用したカスタムインスペクターの作成は、IMGUI (Immediate Mode GUI) と似ていますが、UI Toolkit には自動データバインディングや自動の “元に戻す” サポートなど、いくつかの利点があります。IMGUI がインスペクターの UI をすべてスクリプトで作成するのに対し、UI Toolkit ではスクリプト、UI Builder で視覚的な方法、またはその両方の組み合わせで UI を作成することができます。

このガイドの最終的なソースコードは、このページの 下の方 にあります。

このガイドでは、MonoBehaviour クラスのカスタムインスペクターを作成します。スクリプトと UXML (UI Builder を使用) の両方を使用して、UI を作成します。また、カスタムインスペクターには、カスタムプロパティのドローワーも用意されています。

要件

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

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

コンテンツ

以下のトピックを説明します。

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

  • 新しい MonoBehaviour を作成する
  • カスタムインスペクターのスクリプトを作成する
  • カスタムインスペクター内で UXML を使用する
  • Undo (取り消し) とデータバインディング
  • デフォルトインスペクターを作成する
  • プロパティフィールド
  • カスタムプロパティドローワーを作成する

新しい MonoBehaviour の作成

始めに、カスタムインスペクターを作成するために、MonoBehaviourScriptableObject のいずれかのカスタムクラスを作成する必要があります。このガイドでは、モデルや色などのプロパティを持つ単純な車を表す MonoBehaviour スクリプトを使います。

Assets/Scripts 内に新しいスクリプトファイル Car.cs を作成し、以下のコードをコピーしてください。

using UnityEngine;

public class Car : MonoBehaviour
{
  public string m_Make = "Toyota";
  public int m_YearBuilt = 1980;
  public Color m_Color = Color.black;
}

シーン内に新しいゲームオブジェクトを作成し、Car スクリプトコンポーネントをそれにアタッチします。

Car オブジェクトのデフォルトインスペクター
Car オブジェクトのデフォルトインスペクター

カスタムインスペクタースクリプトの作成

任意のシリアライズされたオブジェクトのカスタムインスペクターを作成するには、Editor 基本クラスから派生したクラスを作成し、CustomEditor 属性をそのクラスに加える必要があります。この属性によって、Unity はこのカスタムインスペクターがどのクラスを表しているのかを知ることができます。UI Toolkit でのこのワークフローは、Immediate Mode GUI (IMGUI) と同じです。

Assets/Scripts/Editor 内に Car_Inspector.cs ファイルを作成し、それに以下のコードをコピーします。

using UnityEditor;
using UnityEditor.UIElements;
using UnityEngine.UIElements;

[CustomEditor(typeof(Car))]
public class Car_Inspector : Editor
{
}
ノート
カスタムインスペクターファイルは Editor フォルダー内、または Editor-only アセンブリ定義内になければなりません。スタンドアロンビルドを作成しようとすると、UnityEditor 名前空間が利用できないため失敗します。

この時点で Car コンポーネントを持つゲームオブジェクトを選択すると、Unity はデフォルトのインスペクターを表示したままです。Car_Inspector クラス内の CreateInspectorGUI() をオーバーライドして、デフォルトのインスペクターを置き換える必要があります。

CreateInspectorGUI() 関数は、インスペクターのビジュアルツリーを構築します。この関数は、UI を含む VisualElement を返す必要があります。下の CreateInspectorGUI() の実装では、新しい VisualElement を作成し、それにラベルを加えます。

Car_Inspector スクリプト内の CreateInspectorGUI() 関数をオーバーライドし、それに以下のコードをコピーします。

public override VisualElement CreateInspectorGUI()
{
  // インスペクター UI のルートとなる新しい VisualElement を作成します。
  VisualElement myInspector = new VisualElement();

  // 簡単なラベルを加えます。
  myInspector.Add(new Label("This is a custom inspector"));

  // インスペクター UI を返します。
  return myInspector;
}
ラベル付きカスタムインスペクター
ラベル付きカスタムインスペクター

カスタムインスペクターで UXML を使用

UI Toolkit では、2 つの方法で UI コントロールを加えることができます。

  • スクリプトの実装
  • あらかじめ作成された UI ツリーを含む UXML ファイルをロード

このセクションでは、UI Builder を使用して UI を含む UXML ファイルを作成し、コードを使って UXML ファイルから UI をロードしてインスタンス化します。

メニュー Window > UI Toolkit > UI Builder で UI Builder を開き、UI Builder のメニュー File > New を使って、新しい Visual Tree アセットを作成し ます。

ラベル付きカスタムインスペクター
ラベル付きカスタムインスペクター

UI Toolkit を使用してエディターウィンドウやカスタムインスペクターを作成する場合、追加のコントロールタイプが提供されます。デフォルトでは、これらのエディター専用コントロールは UI Builder では表示されません。これらを利用できるようにするには、チェックボックス Editor Extension Authoring を有効にする必要があります。

Hierarchy ビューで <unsaved file>*.uxml を選択し、Editor Extension Authoring のチェックボックスを有効にします。

ラベル付きカスタムインスペクター
ラベル付きカスタムインスペクター
ノート
UI Toolkit を使用してエディターウィンドウとカスタムインスペクターを作成する場合、Project Settings > UI Builder でこの設定をデフォルトで有効にすることができます。

UI にコントロールを追加するには、 Library からコントロールを選択し、上記の Hierarchy にドラッグします。自動レイアウトを変更しない限り、新しいコントロールの位置や大きさを調整する必要はありません。デフォルトでは、ラベルは利用可能なパネルの幅全体を使用し、高さは選択されたフォントサイズに調整されます。

ラベルコントロールを Library から Hierarchy にドラッグして、ビジュアルツリーに加えます。

ラベル付きカスタムインスペクター
ラベル付きカスタムインスペクター

ラベル内のテキストを変更するには、ラベルを選択し、UI Builder エディターの右側にある要素のインスペクターでテキストを変更します。

ラベル付きカスタムインスペクター
ラベル付きカスタムインスペクター

UI Builder がビジュアルツリーを保存する場合、Visual Tree アセット として UXML 形式で保存されます。これについては、UXML ドキュメント を参照してください。

下の UXML は、これまでの手順で UI Builder が生成したコードを表示したものです。

<ui:UXML xmlns:ui="UnityEngine.UIElements" xmlns:uie="UnityEditor.UIElements" editor-extension-mode="True">
    <ui:Label text="Label created in UI Builder" />
</ui:UXML>

Asset > Script > Editor で作成したビジュアルツリーを、UI Builder の File メニューを使用して Car_Inspector_UXML.uxml として保存します。

作成した UXML ファイルをカスタムインスペクター内で使用するには、CreateInspectorGUI() 関数内でロードして複製し、ビジュアルツリーに加える必要があります。これを行うには、CloneTree メソッドを使用します。作成された要素の親として動作するように、任意の VisualElement をパラメーターとして渡すことができます。

CreateInspectorGUI() 関数を修正して UXML ファイル内のビジュアルツリーを複製し、カスタムインスペクターで使用します。

public override VisualElement CreateInspectorGUI()
{
  // インスペクター UI のルートとなる新しい VisualElement を作成します。
  VisualElement myInspector = new VisualElement();

  // 簡単なラベルを加えます。
  myInspector.Add(new Label("This is a custom inspector"));

  // UXML のビジュアルツリーをロードして複製します。
  VisualTreeAsset visualTree = AssetDatabase.LoadAssetAtPath<VisualTreeAsset>("Assets/Scripts/Editor/Car_Inspector_UXML.uxml");
  visualTree.CloneTree(myInspector);

  // インスペクター UI を返します。
  return myInspector;
}

car コンポーネントのインスペクターに、スクリプトによるものと UI Builder/UXML によるものの 2 つの作成済みラベルが表示されます。

2 つのラベルを持つカスタムインスペクターラベル
2 つのラベルを持つカスタムインスペクターラベル

このコードでは、ビジュアルツリーを複製するために、Visual Tree アセット (UXML) ファイルをロードする必要があり、ハードコードされたパスとファイル名を使用します。ただし、ハードコードされたファイルは推奨されません。なぜなら、ファイルのパスや名前など、ファイルのプロパティが変更されると、コードが無効になる可能性があるためです。

Visual Tree アセットにアクセスするためのより良い解決策は、アセットファイルへの参照を使用することです。メタファイル内の GUID は、ファイルの参照を保存します。ファイル名を変更したり移動したりしても GUID は変更されず、Unity はその新しい場所からファイルを見つけてロードすることができます。

プレハブと ScriptableObject の場合、エディターで他のファイルへの参照を割り当てることができます。スクリプトファイルの場合、Unity では、Default Reference を設定することができます。 window クラスの VisualTreeAsset 型のパブリックフィールドを宣言すると、Inspector では、対応するオブジェクトフィールドに参照をドラッグする機能が可能です。これは、Car_Inspector クラスの新しいインスタンスには、対応する VisualTreeAsset オブジェクトへの参照が設定されることを意味します。この方法は、カスタムインスペクターやエディターウィンドウのスクリプトに UXML ファイルを割り当てるために推奨される方法です。

スクリプト内に VisualTreeAsset のパブリック変数を作成し、Car_Inspector_UXML.uxml ファイルをエディターのデフォルト参照として割り当てます。

public VisualTreeAsset m_InspectorXML;
2 つのラベルを持つカスタムインスペクターラベル
2 つのラベルを持つカスタムインスペクターラベル
ノート
デフォルトの参照はエディターでのみ機能します。AddComponent() メソッドを使用したスタンドアロンビルドのランタイムコンポーネントでは機能しません。

デフォルトの参照を使用すると、LoadAssetAtPath 関数を使用して VisualTreeAsset をロードする必要がなくなります。その代わりに、CloneTree を UXML ファイルへの参照に直接使用することができます。

これにより、 CreateInspectorGUI() メソッド内のコードを 3 行に減らすことができます。

public VisualTreeAsset m_InspectorXML;

public override VisualElement CreateInspectorGUI()
{
 // インスペクター UI のルートとなる、新しい VisualElement を作成します。
  VisualElement myInspector = new VisualElement();

 // デフォルトの参照からロードします
  m_InspectorXML.CloneTree(myInspector);

 // インスペクターの UI を返します
  return myInspector;
}

Undo (取り消し) とデータバインディング

このカスタムインスペクターの目的は、Car クラスのすべてのプロパティを表示することです。ユーザーが UI コントロールのいずれかを変更すると、Car クラスのインスタンス内の値も変更される必要があります。そのためには、ビジュアルツリーに UI コントロールを加えて、クラスの個々のプロパティに接続する必要があります。

UI Toolkit は、SerializedObject データバインディング を使用して、UI コントロールとシリアライズされたプロパティの関連付けをサポートします。シリアル化したプロパティにバインドされたコントロールはプロパティの現在値を表示し、ユーザーが UI でそれを変更すると、プロパティ値を更新します。コントロールから値を取得し、それをプロパティに表示するコードを書く必要はありません。

UI Builder を使って、車の m_Make プロパティの TextField コントロールをインスペクターに加えます。

UI にテキストフィールドを加える
UI にテキストフィールドを加える

コントロールとシリアライズされたプロパティを結びつけるには、そのプロパティをコントロールの binding-path フィールドに割り当てます。この作業は、コード、UXML、UI Builder で行うことができます。プロパティは、名前によって一致します。そのため、綴りを確認するようにしてください。

新しい TextField を、UI Builder の m_Make プロパティにバインドします。

UI Builder でコントロールにプロパティをバインドする
UI Builder でコントロールにプロパティをバインドする

下は、インスペクター UI の UXML コードで、データバインディング属性も含みます。

<ui:UXML xmlns:ui="UnityEngine.UIElements" xmlns:uie="UnityEditor.UIElements" editor-extension-mode="True">
    <ui:TextField label="Make of the car" text="&lt;not set&gt;" binding-path="m_Make" />
</ui:UXML>

コントロールのバインディングパスを設定するとき、コントロールにリンクすべきシリアル化されたプロパティの名前を伝えます。しかし、コントロールは、そのプロパティが属するシリアル化されたオブジェクトのインスタンスも受け取る必要があります。VisualElement.Bind メソッドを使用して、 MonoBehaviour などのシリアル化されたオブジェクトを Visual Tree 全体にバインドすることができ、個々のコントロールはそのオブジェクト上の適切なプロパティにバインドされます。

カスタムインスペクターを書くと、バインディングは自動的に行われます。CreateInspectorGUI() は、ビジュアルツリーを返した後に暗示的なバインドを行います。詳しくは、SerializedObject のデータバインディング を参照してください。

テキストフィールドを表示するカスタムインスペクター
テキストフィールドを表示するカスタムインスペクター

UI Toolkit はシリアル化されたプロパティを使うので、Undo/Redo (元に戻す/やり直し) 機能をサポートするために追加のコードは必要ありません。自動的にサポートされます。

プロパティフィールド

Car クラスのプロパティを表示するには、各フィールドのコントロールを追加する必要があります。コントロールは、プロパティのタイプと一致するため、バインドすることができます。例えば、int は Integer フィールドまたは Integer スライダーにバインドされます。

プロパティタイプに基づく特定のコントロールを追加する代わりに、汎用的な PropertyField コントロールを利用することも可能です。このコントロールは、ほとんどのタイプのシリアル化されたプロパティで動作し、このプロパティタイプのデフォルトインスペクター UI を生成します。

PropertyField コントロールを、Car クラスの m_YearBuiltm_Color プロパティに加えます。それぞれにバインドパスを割り当て、Label のテキストを入力します。

UI Builder でプロパティフィールドを追加する
UI Builder でプロパティフィールドを追加する

PropertyField の利点は、スクリプト内部で変数の型を変更すると、インスペクターの UI が自動的に調整されることです。ただし、ビジュアルツリーがシリアライズされたオブジェクトにバインドされ、UI Toolkit がプロパティタイプを決定するまで必要なコントロールタイプは不明なので、UI Builder 内でコントロールのプレビューを取得することはできません。

プロパティフィールドを持つカスタムインスペクター
プロパティフィールドを持つカスタムインスペクター

カスタムプロパティドローワーの作成

カスタムプロパティドローワーは、カスタムの serializable クラスのためのカスタムインスペクター UI です。その serializable クラスが他のシリアライズされたオブジェクトの一部である場合、カスタム UI はインスペクターにそのプロパティを表示します。UI Toolkit では、PropertyField コントロールは、フィールドのカスタムプロパティドローワーが存在する場合、それを表示します。

Assets/Scripts 内に新しいスクリプトファイル Tire.cs を作成し、それに以下のコードをコピーしてください。

[System.Serializable]
public class Tire
{
  public float m_AirPressure = 21.5f;
  public int m_ProfileDepth = 4;
}

以下のコードのように、 Car クラスに Tire のリストを追加します。

public class Car : MonoBehaviour
{
  public string m_Make = "Toyota";
  public int m_YearBuilt = 1980;
  public Color m_Color = Color.black;

  // この車には 4 つのタイヤがあります
  public Tire[] m_Tires = new Tire[4];
}

PropertyField コントロールは、すべての標準的なプロパティタイプで動作しますが、カスタムのシリアライズ可能なクラスと配列もサポートします。車のタイヤのプロパティを表示するには、UI Builder で別の PropertyField を追加して m_Tires にバインドします。

m_Tires プロパティの PropertyField コントロールを加えます。

PropertyField コントロールを使用して配列を表示する
PropertyField コントロールを使用して配列を表示する

現在のインスペクター UI 用に生成された Car_Inspector_UXML.uxml の UXML コードは以下のとおりです。

<ui:UXML xmlns:ui="UnityEngine.UIElements" xmlns:uie="UnityEditor.UIElements" editor-extension-mode="True">
    <ui:TextField label="Make of the car" text="&lt;not set&gt;" binding-path="m_Make" />
    <uie:PropertyField label="Year Built" binding-path="m_YearBuilt" />
    <uie:PropertyField binding-path="m_Color" label="Paint Color" />
    <uie:PropertyField binding-path="m_Tires" label="Tires" />
</ui:UXML>

カスタムプロパティードローワーを使うと、リスト内の個々の Tire (タイヤ) 要素の外観をカスタマイズすることができます。カスタムプロパティドローワーは Editor 基礎クラスから派生するのではなく、PropertyDrawer クラスから派生します。カスタムプロパティの UI を作成するには、CreatePropertyGUI メソッドをオーバーライドする必要があります。

フォルダー Assets/Scripts/Editor 内に新しいスクリプト Tire_PropertyDrawer.cs を作成し、その中に以下のコードをコピーします。

using UnityEditor;
using UnityEditor.UIElements;
using UnityEngine.UIElements;

[CustomPropertyDrawer(typeof(Tire))]
public class Tire_PropertyDrawer : PropertyDrawer
{
  public override VisualElement CreatePropertyGUI(SerializedProperty property)
  {
    // プロパティ UI のルートとなる新しい VisualElement を作成します。
    var container = new VisualElement();

    // C# を使用してドロワー UI を作成します。
    // ...

    //  UI を返します。
    return container;
  }
}

カスタマイズされたインスペクターのように、コードと UXML を使用してプロパティの UI を作成することができます。この例では、コードを使用してカスタム UI を作成します。

CreatePropertyGUI メソッドを以下のように拡張して、Tire クラスのプロパティドローワー用のカスタム UI を作成します。

public override VisualElement CreatePropertyGUI(SerializedProperty property)
{
  // プロパティ UI のルートとなる新しい VisualElement を作成します。
  var container = new VisualElement();

  // C# を使用してドロワー UI を作成します。
  var popup = new UnityEngine.UIElements.PopupWindow();
  popup.text = "Tire Details";
  popup.Add(new PropertyField(property.FindPropertyRelative("m_AirPressure"), "Air Pressure (psi)"));
  popup.Add(new PropertyField(property.FindPropertyRelative("m_ProfileDepth"), "Profile Depth (mm)"));
  container.Add(popup);

  // UI を返します。
  return container;
}
カスタムプロパティドローワーを使用したインスペクター
カスタムプロパティドローワーを使用したインスペクター

プロパティドローワーの詳細については、 PropertyDrawer のドキュメントを参照してください。

ノート: Unity は IMGUI でデフォルトインスペクターを作成するため、デフォルトインスペクター内でのカスタムプロパティドローワーの使用はサポートしていません。カスタムプロパティドローワーを作成したい場合は、このガイドで TireCar に行ったように、そのプロパティを使用するクラスのカスタムインスペクターも作成する必要があります。

デフォルトインスペクターの作成

カスタムインスペクターを開発する際、デフォルトインスペクターへのアクセスを保っておくと便利です。UI Toolkit を使えば、デフォルトのインスペクターの UI をカスタム UI に簡単に加えることができます。

UI Builder で UI に Foldout (折りたたみ) コントロールを追加し Default_Inspector と名付け、ラベルテキストを割り当てます。

デフォルトインスペクターの折りたたみ表示
デフォルトインスペクターの折りたたみ表示

UI Builder を使用して折りたたみを作成しますが、インスペクターは作成しません。デフォルトのインスペクターのコンテンツは、インスペクタースクリプトの内部で生成され、コードを通して折りたたみコントロールにアタッチされます。

UI Builder で作成した折りたたみにデフォルトのインスペクター UI を添付するには、折りたたみへの参照を取得する必要があります。インスペクターのビジュアルツリーから、折りたたみのビジュアル要素を取得できます。これは、API の UQuery ファミリーを使用して行います。UI 内の個々の要素は、名前、USS クラス、タイプ、またはこれらの属性の組み合わせで取得できます。

Foldout コントロールへの参照を、 CreateInspectorGUI メソッド内で、UI Builder で設定した名前を使用して取得します。

// デフォルトインスペクターの折りたたみコントロールへの参照を取得します
VisualElement inspectorFoldout = myInspector.Q("Default_Inspector");

InspectorElementFillDefaultInspector メソッドは、指定されたシリアル化されたオブジェクトのデフォルトインスペクターを持つビジュアルツリーを作成し、それをパラメーターとしてメソッドに渡された親ビジュアル要素にアタッチします。

以下のコードでデフォルトインスペクターを作成し、折りたたみ (Foldout) にアタッチします。

// デフォルトインスペクターを Foldout (折りたたみ) にアタッチします
InspectorElement.FillDefaultInspector(inspectorFoldout, serializedObject, this);
デフォルトインスペクターを持つ折りたたみ
デフォルトインスペクターを持つ折りたたみ

最終的なスクリプト

以下に、このガイドで作成したすべてのファイルの完全なソースコードを掲載します。

Car.cs

using UnityEngine;

public class Car : MonoBehaviour
{
  public string m_Make = "Toyota";
  public int m_YearBuilt = 1980;
  public Color m_Color = Color.black;

  // この車には 4 つのタイヤがあります
  public Tire[] m_Tires = new Tire[4];
}

Car_Inspector.cs

using UnityEditor;
using UnityEditor.UIElements;
using UnityEngine.UIElements;

[CustomEditor(typeof(Car))]
public class Car_Inspector : Editor
{
  public VisualTreeAsset m_InspectorXML;

  public override VisualElement CreateInspectorGUI()
  {
    // インスペクター UI のルートとなる、新しい VisualElement を作成します
    VisualElement myInspector = new VisualElement();

    // デフォルトの参照からロードします
    m_InspectorXML.CloneTree(myInspector);

    // デフォルトインスペクターの折りたたみコントロールへの参照を取得します
    VisualElement inspectorFoldout = myInspector.Q("Default_Inspector");

    // デフォルトインスペクターを Foldout にアタッチします
    InspectorElement.FillDefaultInspector(inspectorFoldout, serializedObject, this);

    // インスペクターの UI を返します
    return myInspector;
  }
}

Car_Inspector_UXML.uxml

<ui:UXML xmlns:ui="UnityEngine.UIElements" xmlns:uie="UnityEditor.UIElements" editor-extension-mode="True">
    <ui:TextField label="Make of the car" text="&lt;not set&gt;" binding-path="m_Make" />
    <uie:PropertyField label="Year Built" binding-path="m_YearBuilt" />
    <uie:PropertyField binding-path="m_Color" label="Paint Color" />
    <uie:PropertyField binding-path="m_Tires" label="Tires" />
    <ui:Foldout text="Default Inspector" name="Default_Inspector" />
</ui:UXML>

Tire.cs

[System.Serializable]
public class Tire
{
  public float m_AirPressure = 21.5f;
  public int m_ProfileDepth = 4;
}

Tire_Property.cs

using UnityEditor;
using UnityEditor.UIElements;
using UnityEngine.UIElements;

[CustomPropertyDrawer(typeof(Tire))]
public class Tire_PropertyDrawer : PropertyDrawer
{
  public override VisualElement CreatePropertyGUI(SerializedProperty property)
  {
    //プロパティ UI のルートとなる新しい VisualElement を作成します
    var container = new VisualElement();

    // C# を使用してドロワー UI を作成します
    var popup = new UnityEngine.UIElements.PopupWindow();
    popup.text = "Tire Details";
    popup.Add(new PropertyField(property.FindPropertyRelative("m_AirPressure"), "Air Pressure (psi)"));
    popup.Add(new PropertyField(property.FindPropertyRelative("m_ProfileDepth"), "Profile Depth (mm)"));
    container.Add(popup);

    // UI を返します
    return container;
  }
}
カスタムエディターウィンドウの作成
SerializedObject のデータバインディング