Exploring UIElements Part 4: Monster Inspector

Monster Inspector

Exploring UIElements Part 4: Monster Inspector

In this tutorial, we’ll learn more of the basics of Unity’s new UIElements framework by creating a simple custom inspector for a hypothetical Monster component.


Prerequisites

This is a beginner-level tutorial that assumes you have gone through the previous parts of the Extending the Unity Editor with UIElements series: Part 1, Part 2 and Part 3.

The Unity version used throughout this tutorial is 2019.3.12f1, but any version 2019.1 or newer should work.


What We’re Building

We’re going to build a custom inspector that can view and set properties on a hypothetical Monster component (MonoBehaviour). We’ll learn how to:

  • Override Unity’s default inspector for our component to show our UI instead
  • Create custom controls with our own VisualElement subclasses
  • Notify Unity that we’ve changed a GameObjects in the Scene
  • Dynamically add and remove VisualElements from our UI

… and more! Our goal is to end up with something that looks like this:

A preview of the tool’s final form.


Getting Started

We’ll start with a new 3D project. Before we can work on a custom inspector, we need something to inspect. Let’s create a Monster class to hold information about the baddies in our game. We’ll start with the default C# script template and pare it down:

Assets/Monster.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Monster : MonoBehaviour
{
    public string kind;
    public int maxHp;
    public List<string> attacks;

    // Start is called before the first frame update
    void Start()
    {
    }

    // Update is called once per frame
    void Update()
    {
    }
}

Unity automatically creates a default inspector for classes deriving from MonoBehaviour. Before we build a custom inspector, let’s quickly explore the inspector Unity gives us for free.


The Default Inspector

Let’s check out the default inspector for our Monster class. Create a new empty GameObject in the Scene, add the Monster component, and open the Inspector window. You should see something like this:

The default inspector for the Monster component.

Expand the Attacks field to see how Unity handles a List<string>. You can add/remove elements at the end of the list by changing the Size field – you must defocus the field by pressing Tab, clicking somewhere else, etc. for the change to take effect; Esc cancels.

Additionally, right clicking any of the item labels (like Element 1) brings up a context menu to duplicate or the delete the list item, like this:

Context menu to duplicate and delete items.

One important thing to note is that changing any of the fields causes the Scene containing our Monster GameObject to be marked as dirty, which just means “modified” or “edited”. This gives you a visual indicator that the Scene needs to be saved in the Hierarchy view, and also causes the editor to display a prompt if you try to quit before saving. Here’s an example of both of those in action:

Context menu to duplicate and delete items.

Now that we’ve seen the default inspector, let’s write our own.


Overriding the Default Inspector

Three things are required to override the default inspector for a MonoBehaviour subclass:

  1. Create a subclass of UnityEditor.Editor
  2. Tell Unity that this class should be used instead of the default inspector via the [CustomEditor(System.Type t)] attribute
  3. Override the VisualElement CreateInspectorGUI() function of the base Editor class.

Let’s try to create a minimal custom inspector. Since inspectors are used in the editor only and are not part of the game, we should create an Editor folder for all the remaining files for this tutorial.

In the Editor folder, create a new C# script named MonsterEditor. Let’s clear out the body of the class and make the three changes mentioned above.

using System.Collections;
using System.Collections.Generic;
using UnityEditor;
using UnityEngine;
using UnityEngine.UIElements;

public class MonsterEditor : MonoBehaviour
[CustomEditor(typeof(Monster))]
public class MonsterEditor : Editor
{
    // Start is called before the first frame update
    void Start()
    {
    }

    // Update is called once per frame
    void Update()
    {
    }

    public override VisualElement CreateInspectorGUI()
    {
        return new VisualElement();
    }
}

A bare new VisualElement() is kind of like an empty HTML <div>, so if we take a look at our Monster GameObject in the Inspector window, we should see an empty inspector.

An empty custom inspector.

In the previous tutorials we were subclassing EditorWindow, and in OnEnable we would instantiate our UXML template and attach the resulting UI object to the rootVisualElement of our EditorWindow.

For Editor subclasses, we instead instantiate and then return the resulting UI object in CreateInspectorGUI – Unity will attach insert it into the Inspector window where it belongs.

Other than this difference, using UIElements is the same for both Editor and EditorWindow subclasses. With that in mind, let’s start building our UI.


Beginning to Build the UI

We didn’t use the Create 🠚 UIElements 🠚 Editor Window template this time, so we need to manually create our USS and UXML files. We’ll focus on structure now and worry about styling later. For now, let’s create a UXML template for the structure of our UI.

Right click in the Editor folder, and choose Create 🠚 UIElements 🠚 UXML Template. Name the file MonsterEditor. Let’s temporarily add a <Label> to the template so we can verify that the template has been added to the UI in a few moments.

<?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:Label text="Custom Monster Inspector" />
</engine:UXML>

Now that we have a template, we need to build a UI from it in our C# file’s CreateInspectorGUI function. Let’s reopen Assets/Editor/MonsterEditor.cs and replace that VisualElement with an instance of our template:

    public override VisualElement CreateInspectorGUI()
    {
        return new VisualElement();
        var uxmlTemplate = AssetDatabase.LoadAssetAtPath<VisualTreeAsset>("Assets/Editor/MonsterEditor.uxml");
        var ui = uxmlTemplate.CloneTree();
        return ui;
    }

Let’s verify by selecting our Monster GameObject and checking out the Inspector window. We should see our new Label:

The new Label appears in the Inspector.


<PropertyField>: Matching the Default Inspector

There is a very powerful VisualElement subclass called <PropertyField> that can take the string name of a field/property and generate a type-appropriate editor for that field. This is another time that an example is better than an explanation – let’s reopen the UXML file, add a <PropertyField> for each of the fields in our class, and also get rid of the Label while we’re there.

    xsi:noNamespaceSchemaLocation="../../UIElementsSchema/UIElements.xsd"
>
  <engine:Label text="Custom Monster Inspector" />
    <editor:PropertyField binding-path="kind" />
    <editor:PropertyField binding-path="maxHp" />
    <editor:PropertyField binding-path="attacks" />
</engine:UXML>

Since we changed the UXML file and UXML don’t trigger an auto-refresh, you’ll need to deselect the Monster GameObject in the Hierarchy and reselect it to apply the changes we just made. Three lines of code, and we have what looks and acts exactly like the default inspector for our class:

A custom inspector that looks like the default.


Adding Elements Dynamically

Before we begin work on customizing the list editor component of our UI, let’s figure out how to add and remove VisualElements from our UI dynamically. First, a quick modification to our UXML template for testing – we’ll swap out the <PropertyField> for the attack list with a <Button> and an empty <VisualElement> container to hold the dynamic items:

  <editor:PropertyField binding-path="maxHp" />
  <editor:PropertyField binding-path="attacks" />
  <engine:Button name="addAttackBtn" text="Add Attack" />
  <engine:VisualElement name="attacksContainer" />
</engine:UXML>

Now that the UXML file is set up, let’s reopen our C# file and create a field for our container, as well as set up a click handler for the button that adds a new <Label> to the container when clicked:


[CustomEditor(typeof(Monster))]
public class MonsterEditor : Editor
{
    VisualElement attacksContainer;

    public override VisualElement CreateInspectorGUI()
    {
        var uxmlTemplate = AssetDatabase.LoadAssetAtPath<VisualTreeAsset>("Assets/Editor/MonsterEditor.uxml");
        var ui = uxmlTemplate.CloneTree();
        attacksContainer = ui.Q<VisualElement>("attacksContainer");
        var addAttackBtn = ui.Q<Button>("addAttackBtn");
        addAttackBtn.clicked += AddAttack;
        return ui;
    }

    void AddAttack()
    {
        var newAttack = new Label(System.Guid.NewGuid().ToString());
        attacksContainer.Add(newAttack);
    }
}

Reselect the Monster GameObject in your Scene, and each time you click the button, you should see a new <Label> added to the UI.

A message has been logged to the debug console after clicking the “Add Attack” button.

Here we just created and added a Label, but any type of VisualElement can by added in this manner. In fact, we can create our own type of VisualElement – like an editor for a monster attack list item!


Subclassing VisualElement for Custom Controls

UIElements doesn’t come with too many built-in controls, but it is easy to create your own custom controls by inheriting from VisualElement.

It’s kind of like building a tiny custom editor window we’ll stuff inside of our inspector, and it’s another thing that is easier to explain with code than prose.

Create a C# script in your Editor folder named MonsterAttackEditor. Open it, change the parent class to VisualElement, and delete everything else in the class:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UIElements;

public class MonsterAttackEditor : MonoBehaviour
public class MonsterAttackEditor : VisualElement
{
    // Start is called before the first frame update
    void Start()
    {

    }

    // Update is called once per frame
    void Update()
    {

    }
}

Now we need a constructor for our class that builds the UI using the inherited .Add(VisualElement) function. Let’s add a Label like we were doing in the AddAttack function of our main UI, but this one’s text will be set to whatever is passed in:

public class MonsterAttackEditor : VisualElement
{
    public MonsterAttackEditor(string labelText)
    {
        var label = new Label(labelText);
        Add(label);
    }
}

That’s it! We now have a custom VisualElement subclass that we can create and add to any other VisualElement. Let’s try replacing the Label in MonsterEditor.cs with our new class:

    void AddAttack()
    {
        var newAttack = new Label(System.Guid.NewGuid().ToString());
        var newAttack = new MonsterAttackEditor($"{Random.Range(0, 1000)}");
        attacksContainer.Add(newAttack);
    }

If you reselect the Monster GameObject and click the button a few times, you should see random number labels like this:

The inspector has a few labels with random numbers.

Now that we have editors for our main UI and the individual list items, we’re ready to finish the structure of our UI.


Finishing the UI Structure

Let’s take a look at what we want our inspector to look like again:

The sketch of the UI from the beginning.

Finishing the Main UI

Knowing that we’ll be dealing with the attack list items separately, the only thing missing from our main template is the label that shows how many attacks there are and has a toggle to show/hide the attack list. Let’s add that to the main template, MonsterEditor.uxml:

  <editor:PropertyField binding-path="maxHp" />
  <engine:Label name="attacksLbl" text="▼ Attacks (###)" />
  <engine:Button name="addAttackBtn" text="Add Attack" />

Note that we can use Unicode in the strings. Support depends on the font you are using for the editor. We’ll use the right and down arrow symbols, ► (U+25BA) and ▼ (U+25BC), to indicate whether the list can be, or already is, expanded. I chose these because they work with Unity’s default font, and they look similar to Unity’s default expansion toggle icon.

There is now a label that says “Attacks”.

Finishing the List Item UI

Like our main UI, we’ll use a UXML template for the structure of our attack editors. Right click in the Editor folder and choose Create 🠚 UIElements 🠚 UXML Template. Name the file MonsterAttackEditor.

The attack list items each consist of:

  • A label + text input (a <TextField>)
  • 2 buttons (<Button>)

Let’s add these to our new template file, with a placeholder label for the text field:

<?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="attackTxt" label="###" />
  <engine:Button name="duplicateBtn" text="+" />
  <engine:Button name="deleteBtn" text="X" />
</engine:UXML>

We now want to instantiate this template instead of creating a label in our corresponding C# script, MonsterAttackEditor.cs.

using System.Collections;
using System.Collections.Generic;
using UnityEditor;
using UnityEngine;
using UnityEngine.UIElements;

public class MonsterAttackEditor : VisualElement
{
    public MonsterAttackEditor(string labelText)
    {
        var label = new Label(labelText);
        var uxmlTemplate = AssetDatabase.LoadAssetAtPath<VisualTreeAsset>("Assets/Editor/MonsterAttackEditor.uxml");
        var ui = uxmlTemplate.CloneTree();
        Add(label);
        Add(ui);
    }
}

Refresh the inspector window and verify that the UXML template we created is being loaded:

There are a couple instances of a label and 2 buttons, with placeholder label text.

While we’re here, we should set the <TextField> label to the constructor’s labelText argument.

        var ui = uxmlTemplate.CloneTree();
        var attackTxt = ui.Q<TextField>("attackTxt");
        attackTxt.label = labelText;
        Add(ui);

After refreshing again, the attack item labels should now be set to random numbers when you add items:

There are again a couple instances of a label and 2 buttons, now with custom label text.

Our structure is nearly complete now, but the editor is in some serious need of styling.


Adding Style

Before we add functionality to our attack list editor, let’s make the UI look more like our sketch. In order to add style to our UI, we need to create a USS file to hold our custom styles and embed it in our main UXML template.

First, create a USS file in your Editor folder (Create 🠚 UIElements 🠚 USS File) named MonsterEditor.uss. You can delete the empty rule included by default:

VisualElement {}

Like in Part 2 and Part 3, we’ll embed our USS file into our main UI template, MonsterEditor.uxml, with a <Style> element, and wrap the whole UI with a generic <VisualElement>:

    xsi:noNamespaceSchemaLocation="../../UIElementsSchema/UIElements.xsd"
>
  <engine:VisualElement>
    <engine:Style src="MonsterEditor.uss" />
    <editor:PropertyField binding-path="kind" />
    <editor:PropertyField binding-path="maxHp" />
    <engine:Label name="attacksLbl" text="▼ Attacks (###)" />
    <engine:Button name="addAttackBtn" text="Add Attack" />
    <engine:VisualElement name="attacksContainer" />
  </engine:VisualElement>
</engine:UXML>

Like its cousin CSS, USS rules flow down to child elements. Since we .Add() our attack list items to the VisualElement named attacksContainer above, the style rules in MonsterEditor.uss will apply to our elements in our sub-template, MonsterAttackEditor.uxml, as well.

Now that our stylesheet is embedded, we need some sort of style rule to get the elements of our list items to line up in a row.


Arranging Elements Horizontally

All of our controls have been full width and arranged in column format so far. In our mock up though, the buttons are on the same row as the text fields. To achieve this, we need to learn about how Unity arranges VisualElements.

USS uses the open source Yoga layout engine, which implements a subset of Flexbox CSS layout system.

To get multiple elements into the same row, we need to change the flex-direction property of their parent’s style to rowcolumn is the default for UIElements. First though, we should wrap the elements in our attack item UXML file in a container – a plain VisualElement will do, and we’ll give it a class of horizontalContainer so we can target it in our USS file:

    xsi:noNamespaceSchemaLocation="../../UIElementsSchema/UIElements.xsd"
>
  <engine:VisualElement class="horizontalContainer">
    <engine:TextField name="attackTxt" label="###" />
    <engine:Button name="duplicateBtn" text="+" />
    <engine:Button name="deleteBtn" text="X" />
  </engine:VisualElement>
</engine:UXML>

Now that we have a good structure set up, let’s set flex-direction property to row for elements with the class horizontalContainer in our USS file. Just like with CSS selectors, you prefix a name with a dot (.) to match by class.

.horizontalContainer { flex-direction: row; }

If you reselect the Monster GameObject and click the Add Attack button a couple times, you should see something like this:

The list items are in a row now, but the text inputs are tiny.

We now have rows, but the <TextField> inputs are tiny! They do expand as you type in them, but they don’t look good. We need to use another Flexbox property, flex-grow, to tell Unity we want our text fields to expand to take up whatever space they can:

.horizontalContainer { flex-direction: row; }
TextField { flex-grow: 1; }

The text inputs are a normal size now.


Syncing: UI List ⟷ GameObject List

This is close enough to the whiteboard drawing we had for now, so let’s move on to making our attack list functional and synced to the underlying Monster GameObject – time to reopen MonsterEditor.cs.

We’re going to be a bit lazy in how we keep our UI’s attack list in sync with our Monster’s attack list – everytime an item is added or removed from the Monster’s attack list, we’ll blow away the whole UI list and recreate it.

It’s a bit inefficient, but it allows us to write some very short and simple code to get syncing working – make it work now, optimize later!

The Sync Function

To sync our UI list to our list of GameObjects, we first need a reference to the GameObject that is being inspected. Unity provides this to subclasses of Editor via the inherited target property, which we can cast to our Monster class. This is the object that we need to stay in sync with.

Let’s create a field in to hold our target monster and initialize it:

[CustomEditor(typeof(Monster))]
public class MonsterEditor : Editor
{
    Monster targetMonster;
    VisualElement attacksContainer;

    public override VisualElement CreateInspectorGUI()
    {
        targetMonster = (Monster)target;
        var uxmlTemplate = AssetDatabase.LoadAssetAtPath<VisualTreeAsset>("Assets/Editor/MonsterEditor.uxml");

Now that we have our target monster, we’re ready to write a sync function. Every VisualElement has a Clear() function that removes all children elements. We can use this to reset the list, then create a new list item for each of monster’s attacks.

We’ll use a for loop instead of a foreach loop because we want to label each of the list items with it’s list index. We should also call this new sync function at the end of CreateInspectorGUI() to initialize the attacks UI:


        addAttackBtn.clicked += AddAttack;
        SyncAttacksUI();
        return ui;
    }

    void SyncAttacksUI()
    {
        attacksContainer.Clear();
        for (var i = 0; i < targetMonster.attacks.Count; i++)
        {
            var newAttack = new MonsterAttackEditor($"#{i}");
            attacksContainer.Add(newAttack);
        }
    }

    void AddAttack()

We should also set the label that displays the number of attacks during the sync. For this, we’ll add a field to hold the label, initialize it in CreateInspectorGUI(), and set it in SyncAttacksUI(). We’ll worry about the expanded/contracted icon later.

    Monster targetMonster;
    Label attacksLbl;
    VisualElement attacksContainer;

        attacksContainer = ui.Q<VisualElement>("attacksContainer");
        attacksLbl = ui.Q<Label>("attacksLbl");
        var addAttackBtn = ui.Q<Button>("addAttackBtn");


    void SyncAttacksUI()
    {
        attacksLbl.text = $"▼ Attacks ({targetMonster.attacks.Count})";
        attacksContainer.Clear();

Now the number of attacks should be shown in the list header, and the list items should be labeled with their index also:

The attacks header and items are labelled correctly.

Sync: Adding Elements and Undo/Redo

Now we can make the AddAttack function actually add an attack to our monster! We just need to add an empty attack to the target monster’s attack list and call the sync function to rebuild our list UI.

We should also use Undo.RecordObject to tell Unity that we are editing a GameObject in the Scene – that way the Scene gets marked as dirty and the user is reminded to save their changes if they try to quit before saving.

    void AddAttack()
    {
        var newAttack = new MonsterAttackEditor($"{Random.Range(0, 1000)}");
        attacksContainer.Add(newAttack);
        Undo.RecordObject(targetMonster, "Add Monster Attack");
        targetMonster.attacks.Add(string.Empty);
        SyncAttacksUI();
    }

Unfortunately, if you actually try to Undo, it doesn’t look like anything happened. The new attack does get removed from the underlying Monster object, but our UI doesn’t know that the undo occurred, so it stays the same.

To fix this issue, we can register with Unity to be notified everytime an Undo or Redo occurs by adding a callback to the built-in Undo.undoRedoPerformed.

For our callback, we’ll use the SyncAttacksUI function. It does exactly what we want – rebuild the UI to match the state of the Monster. Let’s register our function with the Undo system. Also, as good citizens, we should unregister our callback when our UI goes away via the OnDisable() lifecycle method provided by the Editor class.

        addAttackBtn.clicked += AddAttack;
        SyncAttacksUI();
        Undo.undoRedoPerformed += SyncAttacksUI;
        return ui;
    }

    void OnDisable()
    {
        Undo.undoRedoPerformed -= SyncAttacksUI;
    }

    void SyncAttacksUI()

Undo should now work as expected after clicking the Add Attack button. And for a bonus, Redo support comes for free with Undo.

There is an entry to undo and redo adding an attack.


Sync: Editing Items

It’s time to make the things we type in the text inputs persist. Our MonsterAttackEditor elements need two things to sync with the underlying target monster:

  1. A reference to the target monster to edit.
  2. Their list index, so they initialize to and modify the correct item in the monster attack list.

Let’s pass these 2 things in to the constructor. Since we’re passing in the index now, we no longer need the labelText argument – the label can figure out its own text. First, let’s change MonsterEditor.cs, since after this we’ll be in MonsterAttackEditor.cs for a bit.


        for (var i = 0; i < targetMonster.attacks.Count; i++)
        {
            var newAttack = new MonsterAttackEditor($"#{i}");
            var newAttack = new MonsterAttackEditor(targetMonster, i);
            attacksContainer.Add(newAttack);
        }

The solution won’t compile until we fix the constructor in MonsterAttackEditor.cs. Let’s add some fields to store the monster and index, change the parameters, and change the way the label is set.


public class MonsterAttackEditor : VisualElement
{
    Monster monster;
    int attackIdx;

    public MonsterAttackEditor(string labelText)
    public MonsterAttackEditor(Monster monster_, int attackIdx_)
    {
        var uxmlTemplate = AssetDatabase.LoadAssetAtPath<VisualTreeAsset>("Assets/Editor/MonsterAttackEditor.uxml");
        var ui = uxmlTemplate.CloneTree();
        var attackTxt = ui.Q<TextField>("attackTxt");
        attackTxt.label = labelText;
        attackTxt.label = $"#{attackIdx}";
        Add(ui);
    }
}

Now we can properly initialize the text input value too:

        attackTxt.label = $"#{attackIdx}";
        attackTxt.value = monster.attacks[attackIdx];
        Add(ui);

What we need next is a way to update the monster object when the text input is changed. The TextField class provides a function called RegisterCallback<EventType> we can use for this – you tell it what event you want to handle and provide it a function, and it will call your function when that event occurs.

We’ll use the BlurEvent , which is called when the text field lost focus (after the user hit Enter, Tab, clicked outside the field, etc.). We need to:

  • Store a reference to our TextField so we can access it outside of the constructor.
  • Create a function that takes a BlurEvent, and…
    • Tells Unity we are about to edit a GameObject.
    • Updates the appropriate attack on the target Monster GameObject.
  • Register our new function with our TextField.

Let’s give it a shot:

public class MonsterAttackEditor : VisualElement
{
    TextField attackTxt;
    Monster monster;
…
        var ui = uxmlTemplate.CloneTree();
        var attackTxt = ui.Q<TextField>("attackTxt");
        attackTxt = ui.Q<TextField>("attackTxt");
        attackTxt.RegisterCallback<BlurEvent>(HandleAttackChanged);
        attackTxt.label = $"#{attackIdx}";
        attackTxt.value = monster.attacks[attackIdx];
        Add(ui);
    }

    void HandleAttackChanged(BlurEvent evt)
    {
        Undo.RecordObject(monster, "Rename Monster Attack");
        monster.attacks[attackIdx] = attackTxt.value;
    }
}

At this point, the list item editor is starting to become useful. You can add new attacks and give them names, and your changes will apply to the GameObject and persist.

The attacks header and items are labelled correctly.


Sync: Deleting Items

Deleting from the list is similar to adding to the list. We tell Unity we’re changing the object (RecordObject), delete the list item, and sync the UI. Let’s write a function in MonsterEditor.cs to do that, given some list index:

        SyncAttacksUI();
    }

    void DeleteAttack(int attackIdx)
    {
        Undo.RecordObject(targetMonster, $"Delete Monster Attack #{attackIdx}");
        targetMonster.attacks.RemoveAt(attackIdx);
        SyncAttacksUI();
    }
}

Now the question is, how do we get the delete button to call this function? We could look up the button and set up a click handler right after we create the list items in SyncAttacksUI(), but we’d like our custom VisualElement subclasses to be self-contained – optimally, we just pass some data to the constructor, and it’s ready to go.

So, let’s leave the click handling up to the MonsterAttackEditor internals, and instead hand it a callback (an Action) for when the delete button is pressed.

        for (var i = 0; i < targetMonster.attacks.Count; i++)
        {
            var newAttack = new MonsterAttackEditor(targetMonster, i);
            var newAttack = new MonsterAttackEditor(targetMonster, i, DeleteAttack);
            attacksContainer.Add(newAttack);

For this to compile, we need to jump over to MonsterAttackEditor.cs and change the constructor declaration to match.

    int attackIdx;
    Action<int> deleteCallback;

    public MonsterAttackEditor(Monster monster_, int attackIdx_)
    public MonsterAttackEditor(Monster monster_,
                              int attackIdx_,
                              Action<int> deleteCallback_)
    {
        monster = monster_;
        attackIdx = attackIdx_;
        deleteCallback = deleteCallback_;
        var uxmlTemplate = AssetDatabase.LoadAssetAtPath<VisualTreeAsset>("Assets/Editor/MonsterAttackEditor.uxml");

Finally, we create a click handler, find our button, and register it:

        attackTxt.value = monster.attacks[attackIdx];
        var deleteBtn = ui.Q<Button>("deleteBtn");
        deleteBtn.clicked += DeleteAttack;
        Add(ui);
…
        monster.attacks[attackIdx] = attackTxt.value;
    }

    void DeleteAttack() { deleteCallback(attackIdx); }
}

Refresh the inspector and try adding, editing, and deleting a few items. The UI, GameObject, and undo menu should all stay in sync:


Sync: Duplicating Items

We’ll treat duplicating attacks much the same way we did deleting attacks. Here’s the plan:

  1. MonsterEditor.cs - Write a function to duplicate items.
  2. MonsterEditor.cs - Pass the new function as a callback to the constructor for MonsterAttackEditor items.
  3. MonsterAttackEditor.cs - Update the MonsterAttackEditor constructor to take a duplicate callback.
  4. MonsterAttackEditor.cs - Set up the click handler. For this we need to:
    1. Create a click handler that just calls the duplicate callback.
    2. Find the duplicate button.
    3. Register the click handler with the button.

Let’s take care of MonsterEditor.cs first:

        for (var i = 0; i < targetMonster.attacks.Count; i++)
        {
            var newAttack = new MonsterAttackEditor(targetMonster, i, DeleteAttack);
            var newAttack = new MonsterAttackEditor(targetMonster, i, DeleteAttack, DuplicateAttack);
            attacksContainer.Add(newAttack);
…
        SyncAttacksUI();
    }

    void DuplicateAttack(int attackIdx)
    {
        Undo.RecordObject(targetMonster, $"Duplicating Monster Attack #{attackIdx}");
        targetMonster.attacks.Insert(attackIdx, targetMonster.attacks[attackIdx]);
        SyncAttacksUI();
    }
}

And now, MonsterAttackEditor.cs:

    Action<int> deleteCallback;
    Action<int> duplicateCallback;

    public MonsterAttackEditor(Monster monster_,
                              int attackIdx_,
                              Action<int> deleteCallback_)
                              Action<int> deleteCallback_,
                              Action<int> duplicateCallback_)
    {
        monster = monster_;
        attackIdx = attackIdx_;
        deleteCallback = deleteCallback_;
        duplicateCallback = duplicateCallback_;
        var uxmlTemplate = AssetDatabase.LoadAssetAtPath<VisualTreeAsset>("Assets/Editor/MonsterAttackEditor.uxml");
…
        deleteBtn.clicked += DeleteAttack;
        var duplicateBtn = ui.Q<Button>("duplicateBtn");
        duplicateBtn.clicked += DuplicateAttack;
        Add(ui);
…
    void DeleteAttack() { deleteCallback(attackIdx); }
    void DuplicateAttack() { duplicateCallback(attackIdx); }
}

Refresh the inspector and go crazy – all the buttons should work now.

The first attack has been duplicated.


Finishing Touches

We have a fairly functional custom inspector now, but there are a couple more things that we can polish to make it even better. First off, we should implement the expand/contract feature that we’ve been teasing since we first put the ▼ character in our UI. Reopen MonsterEditor.cs.

Generic Click Handling & Showing/Hiding VisualElements

Like CSS, we can hide elements in USS by setting the display style property to none. The other option for display is flex, which refers to Flexbox that we mentioned earlier. A VisualElement's styles can be manipulated programatically through its style property.

Let’s keep track of whether our list is expanded or contracted with a bool and add a click handler to the list header label to toggle the attack container’s display style. Unlike the Button class, Label doesn’t have a clicked property for attaching handlers. Instead, we need to use the generic RegisterCallback function that all VisualElement subclasses have.

We’ll use the RegisterCallback<MouseDownEvent>, which calls a function you pass in when the element is clicked. The function you pass in must take a parameter of type MouseDownEvent, but you don’t have to use the parameter in your function body if you don’t need to.

public class MonsterEditor : Editor
{
    bool attacksExpanded = true;
    Monster targetMonster;
…
        attacksLbl = ui.Q<Label>("attacksLbl");
        attacksLbl.RegisterCallback<MouseDownEvent>(ToggleAttacksListDisplay);
        var addAttackBtn = ui.Q<Button>("addAttackBtn");
…
        SyncAttacksUI();
    }

    void ToggleAttacksListDisplay(MouseDownEvent evt)
    {
        DisplayStyle newDisplayStyle;
        if (attacksExpanded)
        {
            newDisplayStyle = DisplayStyle.None;
        }
        else
        {
            newDisplayStyle = DisplayStyle.Flex;
        }
        attacksContainer.style.display = newDisplayStyle;
        attacksExpanded = !attacksExpanded;
    }
}

Now clicking the attack list header label hides and shows the list.

There are 3 list items, but they are hidden.  The icon makes it seem like the list is still expanded though.

We should also keep the label itself in sync, changing the expanded/contracted symbol both here and when we sync the UI after a change to the list. Since we now are setting the label in 2 places, let’s also refactor quickly and extract a function to update the label:

    void SyncAttacksUI()
    {
        attacksLbl.text = $"▼ Attacks ({targetMonster.attacks.Count})";
        UpdateAttackListLabel();
…
        attacksExpanded = !attacksExpanded;
        UpdateAttackListLabel();
    }

    void UpdateAttackListLabel()
    {
        var icon = attacksExpanded ? "▼" : "►";
        attacksLbl.text = $"{icon} Attacks ({targetMonster.attacks.Count})";
    }
}

The icon should now match the expanded/contracted state of the list.

The icon matches the list expansion state now.


Some Final Style

Let’s take a final look at what we’re trying to build again.

The whiteboard drawing from the beginning.

In the default inspector, and in our drawing, the attack list header label is further to the left. Let’s reopen our style sheet file, MonsterEditor.uss, and adjust the margin.

.horizontalContainer { flex-direction: row; }
TextField { flex-grow: 1; }
#attacksLbl { margin-left: -12px; }

Here is our final UI, with the list contracted:

The attack label is now left aligned.

And here it is with the list expanded:

The list is expanded and everything looks ok.

Close enough!


Wrap Up

Final Code: https://github.com/exploringunity/monster-inspector

Yet again we started with just a whiteboard drawing and a dream, and now we have a custom inspector powered by UIElements that handles editing both the simple and dynamic fields of our class.

Here are some of the things we covered:

  • Creating custom VisualElement subclasses
  • Dynamically adding/removing elements to/from our UI
  • Binding UI elements to GameObject fields
  • Arranging elements using flex-direction and flex-grow

What’s Next?

In Part 5 of this series, we’ll be building a custom editor window that can save and restore the position, rotation, and scale of groups of GameObjects. See you next time!