While Unity generates a default inspector for your MonoBehaviours and ScriptableObjects, there are good reasons to write a custom inspector, such as:
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:
Topics covered:
In this guide, you’ll do the following:
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.
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;
}
UI Toolkit allows you to add UI controls in two ways:
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.
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.
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.
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.
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.
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;
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;
}
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 data binding. Controls that are bound to a serialized property automatically display the current value of a property, and they also automatically update 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, like you would in IMGUI.
Add a TextField
control for the car’s m_Make
property to the inspector using UI Builder.
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.
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="<not set>" 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 the documentation page on Data binding.
Because UI Toolkit is working with serialized properties, there is no additional code needed to support Undo/Redo functionality. It’s automatically supported.
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.
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.
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.
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="<not set>" 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;
}
For more information on property drawers, please see the documentation of PropertyDrawer
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:
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);
Below you can find the full source code for all files created in this guide.
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];
}
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;
}
}
<ui:UXML xmlns:ui="UnityEngine.UIElements" xmlns:uie="UnityEditor.UIElements" editor-extension-mode="True">
<ui:TextField label="Make of the car" text="<not set>" 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>
[System.Serializable]
public class Tire
{
public float m_AirPressure = 21.5f;
public int m_ProfileDepth = 4;
}
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;
}
}