Exploring UIElements Part 3: The Renamerator II

The Renamerator II

Exploring UIElements Part 3: The Renamerator II

In this tutorial, we’ll learn more of the basics of Unity’s new UIElements framework by continuing work on The Renamerator from Part 1, adding support for regular expressions, confirmation before the rename, a preview of the changes, and more – The Renamerator 2.

Update: Check out Part 4 of the series once you’re finished with this one.


Prerequisites

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

It also assumes you know the basics of regular expressions. We won’t be doing anything wizardly, just simple character classes and capture groups. If you understand the gist of the following, you should be fine: Regex.Replace("abc 123", @"(\w+) (\d+)", @"$2_$1"); // returns "123_abc"

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


A Quick Recap of Part 1

In Part 1, we used UIElements to build The Renamerator, a custom editor window to batch rename GameObjects. Here’s an image of The Renamerator in action:

7 selected objects have just been renamed with the Renamerator custom tool from Part 1.


What We’re Building

In this tutorial, we’ll be building a new custom editor window for renaming GameObjects, more powerful, stylish, and user-friendly than the original Renamerator. We’ll learn how to:

  • Use the <Toggle>, UIElements’ checkbox component
  • Show a confirmation dialog to prevent accidental renames
  • Disable a VisualElement to prevent user interaction
  • Add a scroll bar to support content larger than the window

… 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. Create a new folder named Editor in your Assets folder. As in the previous tutorials, all of our files will reside in this folder.

Like we did in Part 2, we’ll be using Unity’s built-in UIElements editor window template to get started quickly.

Creating an Editor Window ASAP

Go into the Editor folder you just created, right click, and select Create ­čáÜ UIElements ­čáÜ Editor Window. I chose the name Renamerator2, but you can pick anything you’d like.

The create UI editor window template, about to click Confirm.

After clicking Confirm, three files should be created, and the new editor window should open.

The create UI editor window template, about to click Confirm.

Let’s quickly clean up the files that Unity created to leave ourselves a blank slate, very similar to what we did in the Cleaning Up the Defaults section of Part 2.


Cleaning Up the Defaults

Renamerator2.cs

First, let’s rename the function that opens the window for clarity, change the menu path and add a hotkey:

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

public class Renamerator2 : EditorWindow
{
    [MenuItem("Window/UIElements/Renamerator2")]
    [MenuItem("Custom Tools/The Renamerator II %#t")]
    public static void ShowExample()
    public static void OpenWindow()
    {
        Renamerator2 wnd = GetWindow<Renamerator2>();
        wnd.titleContent = new GUIContent("Renamerator2");
    }

Next, let’s clean up OnEnable by deleting everything but the part that loads the UXML template, and we’ll make a few cosmetic changes to what’s left for brevity and clarity:


    public void OnEnable()
    {
        // Each editor window contains a root VisualElement object
        VisualElement root = rootVisualElement;

        // VisualElements objects can contain other VisualElement following a tree hierarchy.
        VisualElement label = new Label("Hello World! From C#");
        root.Add(label);

        // Import UXML
        var visualTree = AssetDatabase.LoadAssetAtPath<VisualTreeAsset>("Assets/Editor/Renamerator2.uxml");
        var uxmlTemplate = AssetDatabase.LoadAssetAtPath<VisualTreeAsset>("Assets/Editor/Renamerator2.uxml");
        VisualElement labelFromUXML = visualTree.CloneTree();
        var ui = visualTree.CloneTree();
        root.Add(labelFromUXML);
        rootVisualElement.Add(ui);

        // A stylesheet can be added to a VisualElement.
        // The style will be applied to the VisualElement and all of its children.
        var styleSheet = AssetDatabase.LoadAssetAtPath<StyleSheet>("Assets/Editor/Renamerator2.uss");
        VisualElement labelWithStyle = new Label("Hello World! With Style");
        labelWithStyle.styleSheets.Add(styleSheet);
        root.Add(labelWithStyle);
    }
}

At this point, we should be able to refresh our editor window and see the following:

The editor window now only has a single, unstyled label.

We’re done with the C# file for now – we’ll come back to it when we’re ready to start adding functionality to our window. Let’s move on to the USS file next.


Renamerator2.uss

We’ll leave styling things for later – for now, just delete everything and leave the file empty:

Label {
    font-size: 20px;
    -unity-font-style: bold;
    color: rgb(68, 138, 255);
}

Let’s move on to the UXML file.


Renamerator2.uxml

Let’s delete the default content, the <Label>. Then, like in Part 2, we’ll embed our USS file into our UI template with a <Style> element, wrapped with a generic <VisualElement>. 1

<?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="Hello World! From UXML" />
  <engine:VisualElement>
    <engine:Style src="Renamerator2.uss" />
  </engine:VisualElement>
</engine:UXML>

At this point, if you refresh the editor window, you should be left with an empty window:

The editor window is now empty.

And with that, we’re ready to start working on our rename feature. We’ll begin by building the basic structure of our UI.


Building the UI

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

A sketch of the UI we are about to build.

From top to bottom, it looks like we need something like the following:

  • 2 text fields
  • 1 checkbox
  • 1 label
  • 1 button
  • Several labels

We’ve already used the <VisualElement> subclasses <TextField>, <Label>, and <Button> when we were building the original Renamerator in Part 1. The only new thing we need to learn is how to create a checkbox. For that, we will use another built-in subclass, the <Toggle>.

Let’s start fleshing out the UI from the top down. Open the UXML file and add the “Find” and “Replace” text fields:

    <engine:Style src="Renamerator2.uss" />
    <engine:TextField name="searchTxt" label="Find:" />
    <engine:TextField name="replaceTxt" label="Replace:" />
  </engine:VisualElement>

Refresh the window to verify.

There are now two text fields in the window.

Now for the checkbox. All we need to do is add a <Toggle>:

    <engine:TextField name="replaceTxt" label="Replace:" />
    <engine:Toggle name="useRegex" label= "Regex" />
  </engine:VisualElement>

There is now a checkbox in the window.

Moving on to the label and button:

    <engine:Toggle name="useRegex" label= "Regex" />
    <engine:Label name="numSelectedLbl" text="# Selected: NNN" />
    <engine:Button name="renameBtn" text="Rename Selected" />
  </engine:VisualElement>

There is now a label and a button in the window.

The final thing we had on our mock UI was a preview of the rename, with a line for each object to be renamed. Let’s just add a single <Label> for now:

    <engine:Button name="renameBtn" text="Rename Selected" />
    <engine:Label name="previewLbl" text="PREVIEW" />
  </engine:VisualElement>

Alright, we’re finished with the structure of our UI. Let’s refresh the window once more to double check our work, and then we’ll move on to adding functionality.

There is now a preview label under the button.


Basic Rename Functionality

In this section, we’ll add support for the same basic string.Replace-type renaming that the original Renamerator had. We’ll approach this incrementally by:

  1. Adding a click handler to the rename button.
  2. Getting user input from the find and replace fields.
  3. Displaying information on the selected GameObjects.
  4. Modifying the click handler to rename the selected GameObjects.

Since these are all functional changes, we shouldn’t need to touch the UXML or USS files – all our changes will be in our editor window C# script. This section is basically the same as Part 1, so we’ll mostly be skipping the theory and just cranking out code.

A Click Handler

Let’s start by adding a click handler to the button. First we need a function to define what happens when the button is clicked. For now, we’ll just log to the debug console:

        rootVisualElement.Add(ui);
    }

    void RenameSelected()
    {
        Debug.Log("[Renamerator2]");
    }
}

Now we need to find the rename button using the query function, Q, and then add our new function to its click handlers.

        rootVisualElement.Add(ui);
        var renameBtn = ui.Q<Button>("renameBtn");
        renameBtn.clicked += RenameSelected;
    }

    void RenameSelected()

The rename button should now log a message when clicked.

A debug message was printed to the console after the button was clicked.


Getting User Input - <TextField>

Next we need to be able to get user input from the text fields. First, we’ll add some fields to hold references to the TextFields from our UI.

public class Renamerator2 : EditorWindow
{
    TextField searchTxt;
    TextField replaceTxt;

    [MenuItem("Custom Tools/The Renamerator II %#t")]

Then we initialize those fields in OnEnable.


        rootVisualElement.Add(ui);
        searchTxt = ui.Q<TextField>("searchTxt");
        replaceTxt = ui.Q<TextField>("replaceTxt");
        var renameBtn = ui.Q<Button>("renameBtn");

And finally, we can modify the click callback function to print the values from the text fields as a quick test.


    void RenameSelected()
    {
        var search = searchTxt.value;
        var replace = replaceTxt.value;
        Debug.Log("[Renamerator2]");
        Debug.Log($"[Renamerator2] Renaming: {search} -> {replace}");
    }

If you type something into the find and replace fields and click the button, you should see a message logged to the console similar to this:

A debug message displays the contents of the text fields.


Counting Selected GameObjects

We covered using Selection class in Part 1. We’ll use its gameObjects and selectionChanged properties to keep our label in sync with the count of selected GameObjects.

We will need to set the label both when the window opens, and when the selection is changed. First off, we need a reference to the label:

public class Renamerator2 : EditorWindow
{
    Label numSelectedLbl;
    TextField searchTxt;

Now let’s write a function to set the label based on the current selection:

        renameBtn.clicked += RenameSelected;
    }

    void UpdateNumSelectedLabel()
    {
        numSelectedLbl.text = $"# Selected: {Selection.gameObjects.Length}";
    }

    void RenameSelected()

And now we’ll look up and initialize the label, and we’ll register the function with the Selection class so that it’s called everytime the user changes what GameObjects are selected.

        renameBtn.clicked += RenameSelected;
        numSelectedLbl = ui.Q<Label>("numSelectedLbl");
        UpdateNumSelectedLabel();
        Selection.selectionChanged += UpdateNumSelectedLabel;
    }

    void UpdateNumSelectedLabel()

And as good citizens, we should clean up after ourselves – we’ll unregister our callback when our window is closed in the OnDisable lifecycle method provided by EditorWindow.

        Selection.selectionChanged += UpdateNumSelectedLabel;
    }

    public void OnDisable()
    {
        Selection.selectionChanged -= UpdateNumSelectedLabel;
    }

    void UpdateNumSelectedLabel()

All that’s left is to modify our click handler to actually loop over and rename the selected GameObjects. We’ll also update the log message to give us a little more information on what happened.

    void RenameSelected()
    {
        var search = searchTxt.value;
        var replace = replaceTxt.value;
        var numSelected = Selection.gameObjects.Length;
        var logMsg = $"Renaming { numSelected } objs: { search } -> { replace }";
        Debug.Log($"[Renamerator2] Renaming: {search} -> {replace}");
        Debug.Log($"[Renamerator2] " + logMsg);
        foreach (var gameObj in Selection.gameObjects)
        {
            gameObj.name = gameObj.name.Replace(search, replace);
        }
    }

Create several GameObjects in your scene, select a few, and try renaming them. You should see something like this:

7 GameObjects have been renamed with the click of a button.

Congratulations! The Renamerator 2 has now achieved feature parity with The Renamerator from Part 1. From here on, it just gets better!


Beyond the Basics

This section is all about functionality, so keep the C# script open.

Undo Support

Let’s quickly add Undo support for our feature, like we did in the “Adding Undo/Redo Support” section of Part 2. We’ll again use the Undo.RecordObjects(object[] objectsToUndo, string name) function, but this time we can pass Selection.gameObjects directly to the function, because we’re modifying the first-level (shallow) name property of each GameObject.

        Debug.Log($"[Renamerator2] " + logMsg);
        Undo.RecordObjects(Selection.gameObjects, "Rename Selected GameObjects");
        foreach (var gameObj in Selection.gameObjects)

That’s it! One line, and we’ve now got first-class undo and redo support, just like any built-in action in the Unity editor.

Our action now shows up under Edit ­čáÜ Undo.


Confirmation Dialog

What’s better than being able to undo accidents? Preventing accidents from happening in the first place! In this section, we’ll add a confirmation pop-up to give the user a chance to confirm/cancel the rename.

To present the user with a confirmation dialog, we will use the DisplayDialog function from the EditorUtility class. The parameters are strings for the pop-up’s title, body, ok button, and cancel button. It returns true if the user selected the ok button, false if they chose the cancel button or closed the popup. We’ll exit the function with an early return if the user doesn’t confirm.

        var numSelected = Selection.gameObjects.Length;
        const string title = "Rename Selected GameObjects";
        var msg = $"Are you sure you want to rename the {numSelected} selected GameObjects?";
        var doIt = EditorUtility.DisplayDialog(title, msg, "Rename", "Cancel");
        if (!doIt) { return; }
        var logMsg = $"Renaming { numSelected } objs: { search} -> { replace }";

Save the changes above and test it out. Now when you click the rename button a dialog should pop up, and the rename should only occur if you confirm.

A confirmation dialog for the rename.


Disabling a VisualElement

Another sanity check we could perform to increase user-friendliness is to check that there is at least one GameObject selected. We could abort with an early return in RenameSelected, like we did with the confirmation dialog above if the user cancels. However, another option we have is to disable the button whenever nothing is selected, preventing us from needing to abort in the first place.

First, we’ll need to hoist our renameBtn variable in OnEnable to a field, so that we can access it in other functions later.

public class Renamerator2 : EditorWindow
{
    Button renameBtn;
    Label numSelectedLbl;

We’re already looking up the button in OnEnable, we just need to get rid of the variable and set the field instead.

        replaceTxt = ui.Q<TextField>("replaceTxt");
        var renameBtn = ui.Q<Button>("renameBtn");
        renameBtn = ui.Q<Button>("renameBtn");
        renameBtn.clicked += RenameSelected;

Now that we have a reference to our button, we need to keep it in sync with the user selection. Our label already stays in sync with the selection, so we’ll hijack that function. We’ll rename the function to be more generic and add logic to enable/disable the button, using the SetEnabled function that VisualElement and all of its subclasses have.

        numSelectedLbl = ui.Q<Label>("numSelectedLbl");
        UpdateNumSelectedLabel();
        SyncUIWithSelection();
        Selection.selectionChanged += UpdateNumSelectedLabel;
        Selection.selectionChanged += SyncUIWithSelection;

    }
    public void OnDisable()
    {
        Selection.selectionChanged -= UpdateNumSelectedLabel;
        Selection.selectionChanged -= SyncUIWithSelection;

    }

    void UpdateNumSelectedLabel()
    void SyncUIWithSelection()
    {
        var numSelected = Selection.gameObjects.Length;
        numSelectedLbl.text = $"# Selected: {Selection.gameObjects.Length}";
        numSelectedLbl.text = $"# Selected: {numSelected}";
        renameBtn.SetEnabled(numSelected > 0);
    }

Hopefully you are using your editor’s refactoring tools to help with function rename operations like this! For example, Ctrl+R Ctrl+R is the default shortcut for renaming in Visual Studio on Windows.

After you save these changes, the button should be disabled when you have no GameObjects selected in the Hierarchy/Scene windows.

No GameObjects are selected and the button is disabled.


Getting User Input - <Toggle>

We’re going to start working toward supporting regex, but first, let’s check whether or not the user wants to use regex. Getting the value of a <Toggle> is about the same as how we got the value of our <TextField>s earlier. We look it up with the UI’s Q function and inspect the value property. Checked is true, and unchecked is false.

First we add a field for the toggle.

    TextField replaceTxt;
    Toggle useRegex;

    [MenuItem("Custom Tools/The Renamerator II %#t")]

Then we look it up in OnEnable.

        replaceTxt = ui.Q<TextField>("replaceTxt");
        useRegex = ui.Q<Toggle>("useRegex");
        var renameBtn = ui.Q<Button>("renameBtn");

And finally, we check its value in RenameSelected. We’ll just log a message to the debug console for now.


        foreach (var gameObj in Selection.gameObjects)
        {
            if (useRegex.value)
            {
                Debug.Log("useRegex is checked.");
            }
            else
            {
                gameObj.name = gameObj.name.Replace(search, replace);
            }
        }

If you leave the regex checkbox unchecked, the behavior should be the same as before. If you check the box before clicking the rename button, you should see something like this:

The debug log indicates that the useRegex checkbox is checked.

Now that we’re respecting the checkbox, let’s power up our search and replace capabilities with regular expressions.


Regular Expressions

This one is easy! All we need to do to support regular expressions is to pass the user’s input to Regex.Replace in useRegex branch of the RenameSelected method.

            if (useRegex.value)
            {
                Debug.Log("useRegex is checked.");
                gameObj.name = Regex.Replace(gameObj.name, search, replace);
            }

And that’s it! Try it out! Here’s an example of switching “Game” and “Object” and adding a space between them in the names of several selected objects:

Several objects have been renamed with a regex find and replace.


Basic Rename Preview

It’s nice to be able to see a preview of your rename operation, especially when working with regex. Let’s start with a preview for the non-regex case since it’s simpler. It may seem like we need more than the one <Label> that we put in our UXML template for previews, but we can actually just stuff all of the previews into a single string. Let’s implement previews for the non-regex rename.

First off, we need a field for the label.

    Label numSelectedLbl;
    Label previewLbl;
    TextField searchTxt;

Followed by setting the reference in OnEnable:

        numSelectedLbl = ui.Q<Label>("numSelectedLbl");
        previewLbl = ui.Q<Label>("previewLbl");
        SyncUIWithSelection();

Next we need a function that sets the preview label. We’ll add some local variables to keep the lines shorter for the rest of the function to come, as well as a sanity check to prevent errors.

        renameBtn.SetEnabled(numSelected > 0);
    }

    void UpdatePreview()
    {
        var search = searchTxt.value;
        var replace = replaceTxt.value;
        var gameObjs = Selection.gameObjects;
        if (search == "") { previewLbl.text = ""; return; }
    }

    void RenameSelected()

With that in place, there’s what we need to do:

  1. Get a list (IEnumerable) of all the unmodified names.
  2. Get a list of all the names after replacement.
  3. Zip together the two lists above into one list of (unmodified -> modified) strings
  4. Join all the preview strings into a single, newline-separated preview string.

The implementation is basically a line-for-line translation of these steps:

        if (search == "") { previewLbl.text = ""; return; }
        var oldNames = gameObjs.Select(go => go.name);
        var newNames = gameObjs.Select(go => go.name.Replace(search, replace));
        var nameChanges = oldNames.Zip(newNames, (x, y) => $"{x} -> {y}");
        previewLbl.text = string.Join("\n", nameChanges);
    }

The last thing we need is to have this function be called when our window opens, when the selection changes, and everytime either of our text fields change. We already are hooked into the selection changing, so we’ll piggyback off that, which will also initialize the label, since SyncUIWithSelection is called in OnEnable.

    void SyncUIWithSelection()
    {
        UpdatePreview();
        var numSelected = Selection.gameObjects.Length;

Finally, the TextField class has a function called RegisterValueChangedCallback that takes another function as an argument and calls it everytime its value is changed. We’ll set up these event handlers at the end of OnEnable:

        Selection.selectionChanged += SyncUIWithSelection;
        searchTxt.RegisterValueChangedCallback(x => UpdatePreview());
        replaceTxt.RegisterValueChangedCallback(x => UpdatePreview());
    }

There is now a preview of the rename at the bottom of the window.


Regex Rename Preview

Supporting a preview when the regex option is exactly the same as the basic preview, except for how the new names are generated. Instead of just guarding against an empty find string, we also need to worry about invalid search and replacement patterns. We’ll branch based on the regex option and use Regex.Replace inside of a try/catch to handle invalid inputs.



        var oldNames = gameObjs.Select(go => go.name);
        var newNames = gameObjs.Select(go => go.name.Replace(search, replace));
        System.Collections.Generic.IEnumerable<string> newNames;
        if (useRegex.value)
        {
            newNames = gameObjs.Select(go =>
            {
                try { return Regex.Replace(go.name, search, replace); }
                catch { return "REGEX ERROR"; }
            });
        }
        else
        {
            newNames = gameObjs.Select(go => go.name.Replace(search, replace));
        }
        var nameChanges = oldNames.Zip(newNames, (x, y) => $"{x} -> {y}");

Et voilà! Try entering a regex and you should see a live preview as you type:

The regex rename preview is correct.

If your regex is not valid, the preview will let you know:

The previews all say “regex error”.


Finishing Touches

Let’s take one last look at the sketch of our UI to compare to what we have so far:

The same sketch from earlier previewing the tool’s final form.

A Nicer Window Tab

While we still have the C# file open, let’s change our menu’s title to look more like the sketch by giving it an icon, and we’ll change the 2 to a ┬▓ since the title supports Unicode. The GUIContent constructor that we call in the OpenWindow function has an overload that accepts a Texture for the icon.

Drag a PNG format image that you want to be your icon into your Editor folder – I made my own 32x32 icon using GIMP and named it RenameratorIcon:

The Renamerator icon, a capital letter R in a triangle.

The Editor folder now has a PNG image file.

You may need to change the texture type of the image to “Sprite (2D and UI)” in the Inspector window if it isn’t displaying properly.

The texture type has been changed from Default to Sprite 2D and UI.

To use the icon, we must load it and pass it to the GUIContent constructor:

        Renamerator2 wnd = GetWindow<Renamerator2>();
        var icon = (Texture2D)EditorGUIUtility.Load("Assets/Editor/RenameratorIcon.png");
        wnd.titleContent = new GUIContent("Renamerator2");
        wnd.titleContent = new GUIContent("Renamerator\u00B2", icon);

If you save the changes above and refresh the editor window, you should have a fancy looking window tab like this:

The window title tab now has an icon and a unicode superscript number two.


A Bigger & Bolder Button

Let’s add some styling to our button to make it stand out. For this, we’ll need to work in our USS file that we cleared out earlier. Let’s make the following changes:

  • Add some space around the button.
  • Add some space around the contents of the button.
  • Make the text bold.

We can use the standard CSS properties margin and padding for the spacing, but Unity handles text differently than browsers, so we’ll need to use the Unity vendor extension property -unity-font-style to get bold text. Let’s try it out:

Button {
    margin: 5px 20px;
    padding: 5px;
    -unity-font-style: bold;
}

You may not need to refresh the editor window, since saving changes to USS files usually triggers an automatic refresh. Check out the button’s new style:

The button now has an increased margin and padding, and the font is bold.


More Spacing

The labels could use some breathing room on the left side too, so let’s give them a little margin-left:

    -unity-font-style: bold;
}

Label {
    margin-left: 5px;
}

Save the changes and let’s look at the result.

The labels all have some extra space on the left now.


Targeted Styling

The last change we made affected all the labels on our UI. We can look up a specific element by its name attribute in USS like we can by id in CSS – using # as a prefix. To find an element with name="foo", you would use the selector #foo. Let’s change the font style of our label that displays the number of selected GameObjects to be italicized:

    margin-left: 5px;
}

#numSelectedLbl {
    -unity-font-style: italic;
}

The numSelected label is now italicized.


Scrollable Content

Our UI is now as good as the whiteboard drawing we started with, but there’s one thing the drawing didn’t capture – you might have a lot of GameObjects selected, so many that even if you maximize the editor window, you still can’t see the entire preview.

UIElements has a built-in solution for this, the VisualElement subclass <ScrollView>. This element provides a scrollbar that adjusts to the element’s contents. Let’s open our UXML file and use one to wrap our preview label:

    <engine:Button name="renameBtn" text="Rename Selected" />
    <engine:ScrollView>
      <engine:Label name="previewLbl" text="PREVIEW" />
    </engine:ScrollView>
  </engine:VisualElement>

That’s it, refresh the editor window since we changed the UXML file, and we automagically2 have a scrollbar!

The preview label has a scroll bar now for overflow.

We could keep improving The Renamerator┬▓ forever, but we’ve accomplished our initial goal and more, so let’s stop here for now.


Wrap Up

Final Code: https://github.com/exploringunity/renamerator2

Again we started with just a whiteboard drawing and a dream, and now we have a first-class custom editor window driven by UIElements that can do a user-friendly, regex-powered, bulk rename of GameObjects with just the click of a button.

Here are some of the things we covered:

  • Getting yes/no user input with the built-in Toggle control
  • More USS – How to target a specific VisualElement for styling
  • Displaying a confirmation pop-up dialog to the user
  • Handling content larger than your window with a ScrollView

What’s Next?

In Part 4 of this series, we’ll learn how to use UIElements to create a basic custom inspector for our classes. See you next time!


  1. When I wrote this, the Unity documentation on UXML templates said that “Unity does not support <Style> elements under the root <UXML> element.” That is true for 2019.3.12f1, but seems to have been fixed in Unity 2020.1.0b3 (beta version). ↩︎

  2. “Automagically” is still one of my favorite words.. ↩︎