Version: 2022.2
Create a custom Editor window
SerializedObject data binding

Create a Custom Inspector

While Unity generates a default inspector for your MonoBehaviours and ScriptableObjects, there are good reasons to write a custom inspector, such as:

  • Create a more user-friendly representation of script properties.
  • Organize and group properties together.
  • Display or hide sections of the UI depending on the user’s choices.
  • Provide additional information about the meaning of individual settings and properties.

Creating custom inspectors using UI Toolkit is similar to using Immediate Mode GUI (IMGUI), but UI Toolkit has several advantages, such as automatic data binding and automatic undo support. Where IMGUI creates the UI for the inspector entirely through script, UI Toolkit allows you to build the UI via script, visually in UI Builder, or a combination of both.

您可以在本页底部此处找到本指南的最终源代码。

In this guide, you’ll create a custom inspector for a MonoBehaviour class, using both scripts and UXML (using UI Builder) to create the UI. The custom inspector will also feature a custom property drawers.

先决条件

This guide is for developers familiar with Unity, but new to UI Toolkit. It’s recommended to have a basic understanding of Unity and C# scripting.

This guide also references the following concepts:

Content

Topics covered:

In this guide, you’ll do the following:

  • Creating a new MonoBehaviour
  • Creating a custom inspector script
  • Using UXML inside a custom inspector
  • Undo and data binding
  • Creating a default inspector
  • Property fields
  • Creating a custom property drawer

Create a new MonoBehaviour

To begin, you need to create a custom class that you can create a custom inspector for, which is either a MonoBehaviour or a ScriptableObject. This guide works with a MonoBehaviour script that represents a simple car with properties, such as model and color.

Create a new script file Car.cs inside Assets/Scripts and copy the following code into it.

using UnityEngine;

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

Create a new GameObject in the scene and attach the Car script component to it.

Default inspector for the Car object
Default inspector for the Car object

Create a custom inspector script

To create a custom inspector for any serialized object, you need to create a class deriving from the Editor base class, and add the CustomEditor attribute to it. This attribute lets Unity know which class this custom inspector represents. The workflow for this in UI Toolkit is identical to that in Immediate Mode GUI (IMGUI).

Create a file Car_Inspector.cs inside Assets/Scripts/Editor and copy the following code into it.

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

[CustomEditor(typeof(Car))]
public class Car_Inspector : Editor
{
}
Note
The custom inspector file must be inside the Editor folder, or inside an Editor-only assembly definition. Attempting to create standalone builds will fail, as the UnityEditor namespace isn’t available.

If you select your GameObject with the Car component at this point, Unity will still display the default inspector. You need to override CreateInspectorGUI() inside your Car_Inspector class to replace the default inspector.

The CreateInspectorGUI() function builds the visual tree for the inspector. The function needs to return a VisualElement containing the UI. The implementation of CreateInspectorGUI() below creates a blank new VisualElement and adds a label to it.

Override the CreateInspectorGUI() function inside your Car_Inspector script and copy the code below.

public override VisualElement CreateInspectorGUI()
{
  // Create a new VisualElement to be the root of our inspector UI
  VisualElement myInspector = new VisualElement();

  // Add a simple label
  myInspector.Add(new Label("This is a custom inspector"));

  // Return the finished inspector UI
  return myInspector;
}
Custom inspector with a label
Custom inspector with a label

Use UXML inside a custom inspector

UI Toolkit allows you to add UI controls in two ways:

  • Implementing a script
  • Loading a UXML file containing a pre-made UI tree.

This section will be using the UI Builder to create a UXML file containing the UI, and use code to load and instantiate the UI from the UXML file.

Open the UI Builder via the menu Window > UI Toolkit > UI Builder and create a new Visual Tree Asset using the File > New menu entry inside the UI Builder.

Custom inspector with a label
Custom inspector with a label

UI Toolkit offers additional controls types when you’re using it to create Editor windows and custom inspectors. By default, these Editor-only controls aren’t visible in UI Builder. To make them available, you need to enable the checkbox Editor Extension Authoring.

Select the <unsaved file>*.uxml in the Hierarchy view in the UI Builder and enable the Editor Extension Authoring checkbox.

Custom inspector with a label
Custom inspector with a label
Note
If you use UI Toolkit to create Editor windows and custom inspectors, you can enable this setting by default in Project Settings > UI Builder.

To add a control to the UI, select it from the Library and drag it into the Hierarchy above. You don’t need to adjust the position or size of the new control unless you want to modify the automatic layout. By default, the label uses the entire width of the available panel and the height adjusts to the chosen font size.

Add a label control to the visual tree by dragging it from the Library to the Hierarchy.

Custom inspector with a label
Custom inspector with a label

You can change the text inside the label by selecting it and changing the text in the element’s inspector on the right side of the UI Builder editor.

Custom inspector with a label
Custom inspector with a label

When the UI Builder saves a visual tree, it saves it as a Visual Tree Asset in the UXML format. You can learn more about this on the UXML documentation page.

The UXML below displays the code generated by UI Builder from the previous steps:

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

Save the visual tree you created under Asset > Script > Editor as Car_Inspector_UXML.uxml, using the File menu in UI Builder.

To use the UXML file you created inside your custom inspector, you need to load and clone it inside the CreateInspectorGUI() function and add it to the visual tree. To do this, you use the CloneTree method. You can pass any VisualElement as a parameter to act as a parent for the created elements.

Modify the CreateInspectorGUI() function to clone the visual tree inside the UXML file and use it in your custom inspector.

public override VisualElement CreateInspectorGUI()
{
  // Create a new VisualElement to be the root of our inspector UI
  VisualElement myInspector = new VisualElement();

  // Add a simple label
  myInspector.Add(new Label("This is a custom inspector"));

  // Load and clone a visual tree from UXML
  VisualTreeAsset visualTree = AssetDatabase.LoadAssetAtPath<VisualTreeAsset>("Assets/Scripts/Editor/Car_Inspector_UXML.uxml");
  visualTree.CloneTree(myInspector);

  // Return the finished inspector UI
  return myInspector;
}

The inspector for the car component now displays two created labels: one through script, and one through UI Builder/UXML.

Custom inspector with two labels label
Custom inspector with two labels label

The code must load the Visual Tree Asset (UXML) file to clone the visual tree, and uses a hard-coded path and filename. However, hard-coded files aren’t recommended, because if the file properties change, such as file path or name, it might invalidate the code.

The better solution to access the Visual Tree Asset is to use a reference to the asset file. The GUID inside the meta file stores the file reference. If you rename or move the file, the GUID remains unchanged, and Unity will still be able to find and load the file from its new location.

For Prefabs and ScriptableObjects, you can assign references to other files in the Editor. For script files, Unity allows setting a Default Reference. If you declare public fields of type VisualTreeAsset in a window class, the Inspector offers the ability to drag references onto the corresponding object fields. This means that any new instance of the Car_Inspector class populates with the references set to the corresponding VisualTreeAsset object. This is the recommended way of assigning UXML files to custom inspectors and Editor window scripts.

Create a public variable for a VisualTreeAsset in your script, and assign the Car_Inspector_UXML.uxml file as a default reference in the Editor.

public VisualTreeAsset m_InspectorXML;
Custom inspector with two labels label
Custom inspector with two labels label
Note
Default references only work in the Editor. They do not work with runtime components in standalone builds using the AddComponent() method.

With the default reference set, you no longer need to load the VisualTreeAsset using the LoadAssetAtPath function. Instead, you can use CloneTree directly on the reference to your UXML file.

This reduces the code inside the CreateInspectorGUI() method to 3 lines.

public VisualTreeAsset m_InspectorXML;

public override VisualElement CreateInspectorGUI()
{
  // Create a new VisualElement to be the root of our inspector UI
  VisualElement myInspector = new VisualElement();

  // Load from default reference
  m_InspectorXML.CloneTree(myInspector);

  // Return the finished inspector UI
  return myInspector;
}

Undo and data binding

The purpose of this custom inspector is to display all properties of the Car class. When the user modifies any of the UI controls, the values inside the instance of the Car class should also change. To do so, you need to add UI controls to the visual tree and connect them to the individual properties of the class.

UI Toolkit supports linking of UI controls to serialized properties with a SerializedObject data binding. A control bound to a serialized property displays the current value of a property and updates the property value if the user changes it in the UI. You don’t have to write code that retrieves a value from a control and writes it back to the property.

Add a TextField control for the car’s m_Make property to the inspector using UI Builder.

Adding a text field to the UI
Adding a text field to the UI

To bind a control to a serialized property, assign the property to the binding-path field of the control. You can do this in code, UXML or UI Builder. The property matches by name, so make sure to check your spelling.

Bind the new TextField to the m_Make property in UI Builder.

Binding a property to a control in UI Builder
Binding a property to a control in UI Builder

Below is the UXML code for the inspector UI, including the data binding attribute.

<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>

When you set the binding path of a control, you tell the control the name of the serialized property it should be linking to. But the control still needs to receive and instance of the serialized object that the property belongs to. You can use the VisualElement.Bind method to bind a serialized object such as a MonoBehaviour to an entire Visual Tree, and the individual controls will bind to the appropriate properties on that object.

When writing a custom inspector, binding is automatic. CreateInspectorGUI() does an implicit bind after you return your visual tree. To learn more, see SerializedObject data binding.

Custom inspector showing a text field
Custom inspector showing a text field

Because UI Toolkit is working with serialized properties, there is no additional code needed to support Undo/Redo functionality. It’s automatically supported.

Property fields

To display properties of the Car class, you must add a control for each field. The control must match the property type so that it can be bound. For example, an int should be bound to an Integer field or an Integer Slider.

Instead of adding a specific control based on the property type, you can also make use of the generic PropertyField control. This control works for most types of serialized properties, and generates the default inspector UI for this property type.

Add a PropertyField control for the m_YearBuilt and the m_Color properties of the Car class. Assign the binding path for each and fill in the Label text.

Adding a property field in UI Builder
Adding a property field in UI Builder

The advantage of a PropertyField is the inspector UI will automatically adjust when you change the variable type inside your script. However, you can’t get a preview of the control inside the UI Builder, since the control type needed is unknown until the visual tree is bound to a serialized object, and UI Toolkit can determine the property type.

Custom inspector with property fields
Custom inspector with property fields

Create a custom property drawer

A custom property drawer is a custom inspector UI for a custom serializable class. If that serializable class is part of another serialized object, the custom UI displays that property in the inspector. In UI Toolkit, the PropertyField control displays the custom property drawer for a field if one exists.

Create a new script Tire.cs in Assets/Scripts and copy the following code into the file:

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

Add a list of Tire to the Car class as shown in the code below:

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

  // This car has four tires
  public Tire[] m_Tires = new Tire[4];
}

The PropertyField control works with all standard property types, but it also supports custom serializable classes and arrays. To display the properties of the car’s tires, add another PropertyField in UI Builder and bind it to m_Tires.

Add a PropertyField control for the m_Tires property.

Using a PropertyField control to display an array
Using a PropertyField control to display an array

You can find the UXML code from Car_Inspector_UXML.uxml generated for the current inspector UI below:

<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>

A custom property drawer allows you to customize the look of the individual Tire elements in the list. Instead of deriving from the Editor base class, custom property drawers derive from the PropertyDrawer class. To create UI for the custom property, you need to override the CreatePropertyGUI method.

Create a new script Tire_PropertyDrawer.cs inside the folder Assets/Scripts/Editor and copy the code below into it.

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

[CustomPropertyDrawer(typeof(Tire))]
public class Tire_PropertyDrawer : PropertyDrawer
{
  public override VisualElement CreatePropertyGUI(SerializedProperty property)
  {
    // Create a new VisualElement to be the root the property UI
    var container = new VisualElement();

    // Create drawer UI using C#
    // ...

    // Return the finished UI
    return container;
  }
}

You can use code and UXML to create the UI for the property, like in a customized inspector. This examples uses code to create the custom UI.

Create custom UI for the Tire class property drawer by extending the CreatePropertyGUI method as shown below.

public override VisualElement CreatePropertyGUI(SerializedProperty property)
{
  // Create a new VisualElement to be the root the property UI
  var container = new VisualElement();

  // Create drawer UI using C#
  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);

  // Return the finished UI
  return container;
}
Inspector using a custom property drawer
Inspector using a custom property drawer

For more information on property drawers, please see the documentation of PropertyDrawer.

Note: Unity does not support the use of custom property drawers within default inspectors, as Unity makes default inspectors with IMGUI. If you wish to create a custom property drawer, you must also create a custom inspector for the class that uses that property, as this guide does for Tire and Car.

Create a default inspector

During development of a custom inspector it’s helpful to keep access to the default inspector. With UI Toolkit, it’s simple to add the default inspector UI to your custom UI.

Add a Foldout control to your UI in UI Builder, name it Default_Inspector and assign a label text:

Foldout for the Default Inspector
Foldout for the Default Inspector

You will use UI Builder to create the foldout, but not the inspector. The content of the default inspector generates inside the inspector script and attaches to the foldout control via code.

To attach the default inspector UI to the foldout you created in the UI Builder, you must obtain a reference to it. You can retrieve the visual element of the foldout from the visual tree of your inspector. This is done using the UQuery family of APIs. You can retrieve individual elements inside your UI by name, USS class or by type, or a combination of these attributes.

Get a reference to the Foldout control inside your CreateInspectorGUI method, using the name you set in UI Builder.

// Get a reference to the default inspector foldout control
VisualElement inspectorFoldout = myInspector.Q("Default_Inspector");

The FillDefaultInspector method of the InspectorElement creates a visual tree with a default inspector for a given serialized object and attaches it to the parent visual element passed into the method as a parameter.

Create and attach a default inspector to the foldout using the code below:

// Attach a default inspector to the foldout
InspectorElement.FillDefaultInspector(inspectorFoldout, serializedObject, this);
Foldout with the Default Inspector
Foldout with the Default Inspector

Final scripts

Below you can find the full source code for all files created in this guide.

Car.cs

using UnityEngine;

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

  // This car has four tires
  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()
  {
    // Create a new VisualElement to be the root of our inspector UI
    VisualElement myInspector = new VisualElement();

    // Load from default reference
    m_InspectorXML.CloneTree(myInspector);

    // Get a reference to the default inspector foldout control
    VisualElement inspectorFoldout = myInspector.Q("Default_Inspector");

    // Attach a default inspector to the foldout
    InspectorElement.FillDefaultInspector(inspectorFoldout, serializedObject, this);

    // Return the finished inspector 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)
  {
    // Create a new VisualElement to be the root the property UI
    var container = new VisualElement();

    // Create drawer UI using C#
    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);

    // Return the finished UI
    return container;
  }
}
Create a custom Editor window
SerializedObject data binding