Creating a UIElements Custom Inspector in Unity
Introduction
Have you ever worked on a Unity project wishing the inspectors could do more than just set the values of exposed variables? Have you ever wished there was a better way to interact with that data and even automate some of the construction of your scenes using that data inside of the editor? Well if you have, then you’ve no doubt come across the wonderful world of editor extension.
The creation of editor extensions in Unity used to be done with the use of IMGUI, however Unity has made this process even more approachable and flexible with the release of UIElements, Unity’s retained-mode UI toolkit.
In order to demonstrate some of the new workflows for creating editor extensions using UIElements, this tutorial will detail the implementation of a custom inspector for representing and manipulating nested scriptable objects.
We’ll use the scenario of a star system containing multiple planets as an example for this tutorial and by the end of it you’ll have made a custom inspector. I’ve made the project files available if you’d like to play along.
The project files also include a way to use the scriptable objects we’ll define in this tutorial to populate the scene with game objects, which is not covered in this post.
Why create a custom inspector?
A default inspector in Unity will only give you a very basic way to interact with the variables a class has. It will not display any fields for properties and will also not allow you to edit the variables of any object references your class may have.
For the most part, this isn’t an issue but sometimes you may want to represent the data your classes contain in a more visual and meaningful manner or have the values of some of your variables dynamically change the values of others, or perhaps you’d like to create buttons that can automate processes inside of the editor to simplify development in your project. These are the kinds of benefits a custom inspector can bring.
In our scenario, we’ll create a custom inspector to enable us to edit all of our star system and planet data in a single centralised place.
The default inspector for our StarSystem scriptable objects will look like this:
With a custom inspector, we can change how all of the data our objects contain are presented, but more than that we can also directly interact with that data and even define more complex functionality.
Ultimately this tutorial will create a custom inspector that will look like this:
In addition to presenting the planets that a star system has, this custom inspector will allow us to easily add and remove planets from our star system and directly change the values of variables those planets have from within the star system inspector.
Why use Scriptable Objects?
ScriptableObject
is a serializable class that is great for storing data and is commonly used in inventory systems, config files and storing game object properties.
Because ScriptableObjects
can be saved as asset files in your project, they can be referenced and shared by game objects in any scene. This makes them an excellent candidate for storing our StarSystem
and Planet
data.
The usefulness of ScriptableObjects
doesn’t stop there, though; talks given by Richard Fine and Ryan Hipple cover more about how you can leverage the power of ScriptableObjects
in your projects.
Creating the Scriptable Objects
The setup for our two Scriptable Objects is quite straightforward, requiring only two objects:
- A
StarSystem
object that has a sprite and a collection of planet objects. - A
Planet
object that describes what a single planet looks like.
Create the following two script files inside your scripts folder:
StarSystem.cs
[CreateAssetMenu(fileName = "New Star System", menuName = "Star System")] public class StarSystem : ScriptableObject { public Sprite sprite; public List<Planet> planets; public float scale; private void OnEnable() { if (planets == null) planets = new List<Planet>(); } }
Planet.cs
public class Planet : ScriptableObject { public Sprite sprite; public float scale; public float speed; public float distance; }
The [CreateAssetMenu(...)]
attribute allows us to easily create custom assets of our class from the create menu:
From this menu, create a new StarSystem
scriptable object asset.
Creating the Editor Files
Note: All editor scripts must be placed in a specially named Editor folder in order for them to work correctly. For more details see: Unity Special Folder Names
Now we will get to the meat of this tutorial – the creation of the custom inspector. Before we begin, let’s quickly get an idea about the structure of our custom inspector:
When selecting a StarSystem
Scriptable Object asset, we want our custom inspector to appear in the inspector window. This inspector will display the details of the star system but will also contain a list of sub-inspectors for each of the Planets
the StarSystem
has.
Now with that out of the way, we can create all the editor files our StarSystem
custom inspector will need.
Right-click in your project window and select Create/UIElements Editor Window
:
After selecting this, you will be presented with a dialog box in which you can specify which editor files you wish to create and their associated names:
Note: By default, this new UIElements editor will be an Editor Window, we will change this so that it will be a custom inspector later in this tutorial.
Now we have all the files we will need for our main StarSystem
custom inspector. We will repeat this process to create all the editor files required for our Planet
custom sub-inspector.
After this, we should have the following 6 files:
So what are these files we have just created? Well in UIElements there are 3 types of files that each work together to define the look and functionality of your editor:
- The C# file is responsible for the logic of the editor and is also responsible for loading in the styles and templates from the other two editor files.
- The UXML file is responsible for the structure of the editor and can be used to define templates for different UI elements. The format of this file is inspired from HTML, XAML and XML.
- The USS file is responsible for the styling of the editor and will dictate the look and feel of the editor. The format of this file is very similar to CSS.
StarSystemEditor
UXML
Open your StarSystemEditor.uxml file; this file will define the structure of our Star System editor.
For the StarSystem
custom inspector, we will be populating our StarSystemEditor.uxml file so that its elements fall into one of two different sections:
Edit your file until it mirrors the below:
StarSystemEditor.uxml
<?xml version="1.0" encoding="utf-8"?> <engine:UXML xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:engine="UnityEngine.UIElements" xmlns:editor="UnityEditor.UIElements" xsi:noNamespaceSchemaLocation="../../../UIElementsSchema/UIElements.xsd" > <!-- Star System --> <engine:VisualElement class="container"> <engine:Label text="Star System" class="heading"/> <engine:VisualElement class="spriteContainer"> <engine:VisualElement class="spriteBackground"/> <engine:VisualElement name="systemSprite" class="spriteImage"/> </engine:VisualElement> <editor:ObjectField name="systemSpriteField" label="Sprite" allow-scene-objects="false"/> <editor:FloatField name="starScale" label="Star Scale"/> </engine:VisualElement> <!-- Planets --> <engine:VisualElement class="container"> <engine:Label text="Planets" class="heading"/> <!-- planetlist will contain the Planet sub-inspectors --> <engine:VisualElement name="planetList"/> <engine:Button text="Add new planet" name="btnAddNew"/> <engine:Button text="Remove all planets" name="btnRemoveAll"/> </engine:VisualElement> </engine:UXML>
In UIElements, all of the existing elements are defined in one of two namespaces:
UnityEngine.UIElements
– Elements defined as part of the Unity Runtime.UnityEditor.UIElements
– Elements defined as part of the Unity Editor.
To simplify things we define these namespaces to the prefixes engine
and editor
respectively so that we can easily specify the elements we require.
You’ll see that we’ve separated our elements into two container elements that correspond with our layout; both of these container elements are of the type VisualElement
.
Many of these elements have classes and names associated with them and these will be important later. The classes will be used to apply styling to the various elements and the names will be used to identify the elements when connecting up the actual functionality.
USS
Now open your StarSystemEditor.uss file. Here we’ll define the styles that will be used by our Star System custom inspector.
Edit your file until it mirrors the below:
StarSystemEditor.uss
.container { background-color: #A9A9A9; margin: 5, 10, 5, 0; padding: 5; border-width: 3; border-radius: 10; border-color: #909090; } .spriteContainer { width: 120; height: 120; left: 150; margin-bottom: 2; } .spriteImage { position: absolute; -unity-background-scale-mode: scale-and-crop; margin: 10; width: 100; height: 100; border-radius: 10; } .spriteBackground { position: absolute; background-color: #000000; border-width: 3; border-color: #444444; width: 120; height: 120; border-radius: 10; } .heading{ margin: 5, 0, 10; font-size: 20; -unity-font-style: bold; -unity-text-align: middle-center; color: #4B4B4B; }
For those of you familiar with CSS you should feel right at home. The classes defined in this file are used in our StarSystemEditor.uxml file but some of them will also be used in our Planet
sub-editor later in this tutorial.
Unity’s documentation on the USS-SupportedProperties offers information on all the currently available properties so we will not be going into any detail about what these do here. However, I will say that it’s worthwhile to make use of the UIElements Debugger when determining what property values to change.
The debugger can be found within the dropdown list right above the scene view:
CS
Open your StarSystemEditor.cs file. This file defines the functionality of the editor.
Clear out all the example code until your file mirrors the following:
StarSystemEditor.cs
using UnityEditor; using UnityEngine; using UnityEngine.UIElements; using UnityEditor.UIElements; [CustomEditor(typeof(StarSystem))] public class StarSystemEditor : Editor { public void OnEnable() { starSystem = (StarSystem)target; rootElement = new VisualElement(); // Load in UXML template and USS styles, then apply them to the root element. VisualTreeAsset visualTree = AssetDatabase.LoadAssetAtPath<VisualTreeAsset>("Assets/Scripts/Editor/Star System Editor/StarSystemEditor.uxml"); visualTree.CloneTree(rootElement); StyleSheet stylesheet = AssetDatabase.LoadAssetAtPath<StyleSheet>("Assets/Scripts/Editor/Star System Editor/StarSystemEditor.uss"); rootElement.styleSheets.Add(stylesheet); } ...
There are a few things of particular importance to note in the changes:
- Firstly we’ve added the
[CustomEditor(typeof(StarSystem))]
attribute to the class, this informs Unity which object the class should act as an editor for. - Secondly, the class no longer derives from
EditorWindow
but insteadEditor
, this change is important as we are creating a custom inspector and not an editor window.
Within the OnEnable
method, we define the root element; this visual element will contain all of our other elements. We’ll also load in our UXML template and USS styles and apply them to our root element. This will mean that all the children of our root element can have access to the styles that were defined in our StarSystemEditor.uss file.
Next, append the following to your file:
StarSystemEditor.cs
... public override VisualElement CreateInspectorGUI() { #region Fields // Find the visual element with the name "systemSprite" and make it display the star system sprite if it has one. VisualElement systemSprite = rootElement.Query<VisualElement>("systemSprite").First(); systemSprite.style.backgroundImage = starSystem.sprite ? starSystem.sprite.texture : null; // Find an object field with the name "systemSpriteField", set that it only accepts objects of type Sprite, // set its initial value and register a callback that will occur if the value of the filed changes. ObjectField spriteField = rootElement.Query<ObjectField>("systemSpriteField").First(); spriteField.objectType = typeof(Sprite); spriteField.value = starSystem.sprite; spriteField.RegisterCallback<ChangeEvent<Object>>( e => { starSystem.sprite = (Sprite)e.newValue; systemSprite.style.backgroundImage = starSystem.sprite.texture; // Set StarSystem as being dirty. This tells the editor that there have been changes made to the asset and that it requires a save. EditorUtility.SetDirty(starSystem); } ); FloatField scaleField = rootElement.Query<FloatField>("starScale").First(); scaleField.value = starSystem.scale; scaleField.RegisterCallback<ChangeEvent<float>>( e => { starSystem.scale = e.newValue; EditorUtility.SetDirty(starSystem); } ); #endregion #region Display Planet Data // Store visual element that will contain the planet sub-inspectors. planetList = rootElement.Query<VisualElement>("planetList").First(); UpdatePlanets(); #endregion #region Buttons // Assign methods to the click events of the two buttons. Button btnAddPlanet = rootElement.Query<Button>("btnAddNew").First(); btnAddPlanet.clickable.clicked += AddPlanet; Button btnRemoveAllPlanets = rootElement.Query<Button>("btnRemoveAll").First(); btnRemoveAllPlanets.clickable.clicked += RemoveAll; #endregion return rootElement; } ...
Here we’ve overridden the CreateInspectorGUI
method. This method returns the root VisualElement
of the inspector. All the elements that make up our custom inspector must be children of this root element.
In order to get references to the elements that were defined earlier in our StarSystemEditor.uxml file, we make use of UQuery. UQuery is a set of extension methods in Unity for retrieving elements from any UIElements visual tree. For anyone that knows JQuery, UQuery will feel quite familiar.
StarSystemEditor.cs
... #region Display Planet Data Functions public void UpdatePlanets() { planetList.Clear(); // Create and add a PlanetSubEditor to our planetList container for each planet. foreach (Planet planet in starSystem.planets) { //PlanetSubEditor planetSubEditor = new PlanetSubEditor(this, planet); //planetList.Add(planetSubEditor); } } #endregion ...
At this point in the tutorial we haven’t yet made any changes to our PlanetSubEditor.cs file so we’ll leave the code in the foreach commented out for now so that we don’t get compile errors.
Next we’ll add the functions our inspector will use to add and remove Planets
from our StarSystem
object.
StarSystemEditor.cs
... #region Button Functions // Create a new Planet that is a child to the StarSystem asset. Save the assets to disk and update the Planet sub-inspectors. private void AddPlanet() { Planet planet = ScriptableObject.CreateInstance<Planet>(); planet.name = "New Planet"; starSystem.planets.Add(planet); AssetDatabase.AddObjectToAsset(planet, starSystem); AssetDatabase.SaveAssets(); AssetDatabase.Refresh(); UpdatePlanets(); } // Remove all the planets from the StarSystem asset, save the changes and then remove all the Planet sub-inspectors. private void RemoveAll() { if (EditorUtility.DisplayDialog("Delete All Planets", "Are you sure you want to delete all of the planets this star system has?", "Delete All", "Cancel")) { for (int i = starSystem.planets.Count - 1; i >= 0; i--) { AssetDatabase.RemoveObjectFromAsset(starSystem.planets[i]); starSystem.planets.RemoveAt(i); } AssetDatabase.SaveAssets(); AssetDatabase.Refresh(); UpdatePlanets(); } } // Remove a specified Planet from the StarSystem asset, save the changes and update the Planet sub-inspectors. public void RemovePlanet(Planet planet) { starSystem.planets.Remove(planet); AssetDatabase.RemoveObjectFromAsset(planet); AssetDatabase.SaveAssets(); AssetDatabase.Refresh(); UpdatePlanets(); } #endregion }
Now we can successfully add and remove planets from our StarSystem
but we’ll need to complete our PlanetSubEditor
to be able to manipulate our Planets
from this inspector.
PlanetSubEditor
UXML
Open your PlanetSubEditor.uxml file. As with our star system editor, this file will define the structure of our planet sub-editor. Edit the file until it reflects the following:
PlanetSubEditor.uxml
<?xml version="1.0" encoding="utf-8"?> <engine:UXML xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:engine="UnityEngine.UIElements" xmlns:editor="UnityEditor.UIElements" xsi:noNamespaceSchemaLocation="../../../UIElementsSchema/UIElements.xsd" > <engine:TextField name="planetName" label="Planet Name"></engine:TextField> <engine:VisualElement class="spriteContainer"> <engine:VisualElement class="spriteBackground"/> <engine:VisualElement name="planetSpriteDisplay" class="spriteImage"/> </engine:VisualElement> <editor:ObjectField name="planetSprite" label="Sprite" allow-scene-objects="false"/> <editor:FloatField name="planetScale" label="Planet Scale"/> <editor:FloatField name="planetDistance" label="Distance From Star (Light Seconds)"/> <editor:FloatField name="planetSpeed" label="Planet Speed"/> <engine:Button text="Remove planet" name="btnRemove"/> </engine:UXML>
As you can see this file contains tags that define input fields for each of the variables our Planet
scriptable object has. It also contains a visual element that’ll be used to display the sprite of the planet and a button that’ll be used to remove the Planet
if the user desires.
USS
Now open your PlanetSubEditor.uss file and change its content until it looks like this:
PlanetSubEditor.uss
.planetSubeditor { background-color: #6A6A6A; margin: 5, 15; padding: 5; border-width: 3; border-radius: 10; border-color: #444444; }
CS
Finally open your PlanetSubEditor.cs file. Remove all of the example code and add the following:
PlanetSubEditor.cs
using UnityEditor; using UnityEngine; using UnityEngine.UIElements; using UnityEditor.UIElements; public class PlanetSubEditor : VisualElement { Planet planet; StarSystemEditor starSystemEditor; public PlanetSubEditor(StarSystemEditor starSystemEditor, Planet planet) { this.starSystemEditor = starSystemEditor; this.planet = planet; VisualTreeAsset visualTree = AssetDatabase.LoadAssetAtPath<VisualTreeAsset>("Assets/Scripts/Editor/Star System Editor/PlanetSubEditor.uxml"); visualTree.CloneTree(this); StyleSheet stylesheet = AssetDatabase.LoadAssetAtPath<StyleSheet>("Assets/Scripts/Editor/Star System Editor/PlanetSubEditor.uss"); this.styleSheets.Add(stylesheet); this.AddToClassList("planetSubeditor"); ...
The first thing to note is that PlanetSubEditor
is derived from VisualElement
. This means that we define our own visual element that can be added to the root element in our StarSystemEditor
.
Unlike the StarSystemEditor
where the elements and logic were added in the CreateInspectorGUI
method, for the PlanetSubEditor
we’ll be defining our logic in the constructor as the logic will be executed when a PlanetSubEditor
is created:
Just as before we’ll load in our UXML template and USS styles and apply them to our root element (which in this case is the class itself as it is derived from VisualElement
).
PlanetSubEditor.cs
#region Fields TextField nameField = this.Query<TextField>("planetName").First(); nameField.value = planet.name; nameField.RegisterCallback<ChangeEvent<string>>( e => { planet.name = (string)e.newValue; EditorUtility.SetDirty(planet); } ); // Sprite is displayed the same way as in the Star System Inspector VisualElement planetSpriteDisplay = this.Query<VisualElement>("planetSpriteDisplay").First(); planetSpriteDisplay.style.backgroundImage = planet.sprite ? planet.sprite.texture : null; ObjectField spriteField = this.Query<ObjectField>("planetSprite").First(); spriteField.objectType = typeof(Sprite); spriteField.value = planet.sprite; spriteField.RegisterCallback<ChangeEvent<Object>>( e => { planet.sprite = (Sprite)e.newValue; planetSpriteDisplay.style.backgroundImage = planet.sprite.texture; EditorUtility.SetDirty(planet); } ); FloatField scaleField = this.Query<FloatField>("planetScale").First(); scaleField.value = planet.scale; scaleField.RegisterCallback<ChangeEvent<float>>( e => { planet.scale = e.newValue; EditorUtility.SetDirty(planet); } ); FloatField distanceField = this.Query<FloatField>("planetDistance").First(); distanceField.value = planet.distance; distanceField.RegisterCallback<ChangeEvent<float>>( e => { planet.distance = e.newValue; EditorUtility.SetDirty(planet); } ); FloatField speedField = this.Query<FloatField>("planetSpeed").First(); speedField.value = planet.speed; speedField.RegisterCallback<ChangeEvent<float>>( e => { planet.speed = e.newValue; EditorUtility.SetDirty(planet); } ); #endregion #region Buttons Button btnAddPlanet = this.Query<Button>("btnRemove").First(); btnAddPlanet.clickable.clicked += RemovePlanet; #endregion } #region Button Functions private void RemovePlanet() { if (EditorUtility.DisplayDialog("Delete Planet", "Are you sure you want to delete this planet?", "Delete", "Cancel")) starSystemEditor.RemovePlanet(planet); } #endregion }
The rest of this sub-inspector is functionally very similar to what we have already seen in the Star System inspector.
When all is done you should be left with a inspector that looks like this:
Now we have an inspector that allows us to more easily interact with our data!
If you’ve downloaded the project files you can see how we can take this functionality one step further by actually using this data to manipulate objects in our scene.
To see this in action simply select the StarSystemController
game object in the demo scene and in the inspector window click the “Update Star System” button.
The Star System game object will be updated to reflect the data stored in the StarSystem
scriptable object.
Caveats
There are a few things to know about with the current implementation of this inspector:
- There is no undo/redo functionality. There are ways to add this functionality but it is outside of the scope of this tutorial. More information can be found on Unity’s Scripting API – Undo page.
- Renaming planets does not rename them in the project window until the star system asset is updated with an addition or removal of a planet, or if the editor restarts. I’m not certain how to resolve the issue, but regardless it is minor.
Conclusion
Custom editors can really bolster the development of your projects within Unity and allow you to automate a lot of otherwise laborious functionality, more than that though it can provide a powerful and user-friendly way for your teammates to interact with the various systems you create in your projects. UIElements has made the process of creating editor extensions more accessible and in some regards more powerful to boot.
This tutorial really only acts as a primer to the world of editor scripting with UIElements, the actual scope of the functionality in this tutorial’s custom editor was very simple but I hope that the processes we went through to create it was insightful and will aid you in your own editor scripting endeavours.
Header image by Graham
Holtshausen on Unsplash