Exploring UIElements Part 1: The Renamerator

The Renamerator

Exploring UIElements Part 1: The Renamerator

In this tutorial, we’ll be learning the basics of Unity’s new UIElements framework by creating a custom editor window that makes it easy to rename several GameObjects at once – The Renamerator.

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


Prerequisites

This is a beginner-level tutorial that assumes you know the basics of using the Unity editor and C# scripting. If you can complete the official Unity Roll-a-Ball tutorial for beginners, you should be good to go.

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

This tutorial also assumes beginner-level knowledge of basic web technologies, such as HTML, XML, CSS, and the DOM.

Specifically, you should be able to roughly understand the following three snippets, which together describe a short and simple dynamic webpage:

HTML -> /some/directory/Renamerator.html

<!DOCTYPE html>
<html>
  <head>
    <title>Renamerator</title>
    <link rel="stylesheet" type="text/css" href="Renamerator.css">
  </head>
  <body>
    Find:    <input type="text" id="searchTxt">   <br/>
    Replace: <input type="text" id="replaceTxt">  <br/>
    <p id="numSelectedLbl"></p>
    <button id="renameBtn">Rename Selected</button>
    <script type="text/javascript" src="Renamerator.js"></script>
  </body>
</html>

CSS -> /some/directory/Renamerator.css

button {
    font-size: 18px;
    padding: 8px;
}

JavaScript -> /some/directory/Renamerator.js

var numSelectedLbl = document.getElementById("numSelectedLbl");
numSelectedLbl.textContent = "GameObjects Selected: ";
var searchTxt = document.getElementById("searchTxt");
var replaceTxt = document.getElementById("replaceTxt");
var btn = document.getElementById("renameBtn");
btn.onclick = showMsg;
function showMsg() {
    alert(searchTxt.value + " 🠚 " + replaceTxt.value);
}

If you were to save these three files to a directory, open Renamerator.html with your web browser, enter some text in the boxes, and click the button, you would see something like this:

An HTML page with a button and an alert.

The custom editor window we’re going to build will be closely modeled after this webpage. You will be able to click a button to rename all selected GameObjects based on a search pattern and a replacement pattern. There will also be an informative label that displays how many GameObjects are currently selected.


Introduction to UIElements

UIElements (“User Interface Elements”) is a new UI framework for Unity whose design is very strongly based on modern web technologies. It is the set to become the recommended tool for building both custom editor tooling and in-game user interfaces.

UIElements’ Relationship to HTML/CSS/JS

When writing a webpage, the structure of the page is defined using HTML, while the style of the page is defined using CSS, and the dynamic functionality of the page is defined using JS (JavaScript) – three different types of files for three different concerns.

UIElements was designed with the same three-way decoupling in mind – a different filetype for each UI concern:

  • The structure of the UI is defined in templates using UXML (Unity eXtensible Markup Language), which describes the layout of the UI using HTML-like controls in XML format.

    • Sneak peek comparison:
      HTML: <button id="someBtn">Hello</button>
      UXML: <engine:Button name="someBtn" text="Hello" />
  • The style of the UI is defined in stylesheets using USS (“Unity Style Sheets”), which is a subset of CSS plus some Unity-specific vendor extensions.

    • Sneak peek comparison:
      CSS: button { padding: 5px; }
      USS: Button { padding: 5px; }
  • The dynamic functionality of the UI is defined in scripts using C#. These scripts are also responsible for turning UXML templates into UI objects (“instantiating the template”) and adding those objects to the editor window that we created in the previous step.

    • Sneak peek comparison:
      JS: var btn = document.getElementById("someBtn"); btn.onclick = someFunction;
      C#: var btn = rootVisualElement.Q<Button>("someBtn"); btn.clicked += someFunction;

Pretty similar, aren’t they?


Getting Started

We’ll start with a new 3D project. Create a new folder named Editor in your Assets folder. This is where we’ll be putting all the files for our editor extension.

Creating an Empty Window

First up, we’re going to do the bare minimum to get a custom editor window up and running. To do this, we need a C# script that where we:

  1. Create a subclass of UnityEditor.EditorWindow.
  2. Write a static function in the subclass that calls the GetWindow function inherited from EditorWindow – this function opens or focuses the editor window.
  3. Register the new function with the Unity editor using the MenuItem attribute so we can call our new function via the menu.

Let’s begin! Go into the Editor folder you just created, right click, and select Create 🠚 C# Script. I chose the name Renamerator for my script 1, but you can pick anything you’d like.

Project view showing the Editor folder with the new C# file

Open the new file with your text editor. The class needs to inherit from EditorWindow, which is in the UnityEditor namespace. We’ll also get rid of the Start and Update lifecycle methods left over from the default C# script template.

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

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

    }

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

    }
}

Now that we’ve defined our editor window, we need to write a static function to open and initialize it. All we’ll do for now is delegate to the GetWindow function inherited from EditorWindow, but this is where you could set the window’s size, change its title, etc.

public class Renamerator : EditorWindow
{
public static void OpenWindow()
    {
        GetWindow<Renamerator>();
    }
}

The last thing to do is register the function with Unity so that we can call our function by clicking a menu item. This can be done by decorating the function with the MenuItem attribute. The string argument to MenuItem determines the menu path.

    [MenuItem("Custom Tools/Renamerator")]
    public static void OpenWindow()

Believe it or not, you’ve got a working custom editor window now! After you save and Unity compiles the script, you should see a new menu item Custom Tools 🠚 Renamerator. If you click it, your new window should open. It’s a first-class editor window that you can move, resize, or dock just like the Hierarchy, Scene, or Inspector windows.

The new menu item to launch our new window.

The most minimal of custom editor windows.

Launching the Window via Hotkey

We can make our editor window easier to use by adding a hotkey to open/focus it. Just having the MenuItem attribute is actually enough to get your function to show up in the Edit 🠚 Shortcuts... window, but there’s a shortcut to creating a shortcut!

If the string argument to the MenuItem attribute ends with a special pattern, Unity will automagically2 register a shortcut for the menu item. Adding  %#t will allow us to open or focus our window by pressing Ctrl/Cmd + Shift + T. You can read more about the shortcut-shortcut syntax here.

    [MenuItem("Custom Tools/Renamerator")]
    [MenuItem("Custom Tools/Renamerator %#t")]
    public static void OpenWindow()
    {
        GetWindow<Renamerator>();
    }

The new menu item now shows the new global shortcut Shift + Ctrl + T to open/focus our window.


Adding Content to the Window

Now that we have a window, we need some content to add to it. Again sticking with the bare minimum theme, we’ll need to do the following:

  1. Create a UXML template that describes the structure of our UI.
  2. Add a function to our editor window C# script to:
    1. Load the UXL template into a C# object.
    2. Create a UI object from the template object.
    3. Add the resulting object to the editor window.

Creating a UXML Template

We’ll start by using Unity’s built-in template for new UXML files. In your Editor folder, right click and select Create 🠚 UIElements 🠚 UXML Template. I chose to name my template Renamerator to stay consistent with the C# script, but any name is fine.

The Editor folder with the new UXML file.

Open up the new UXML file with your text editor and let’s walk through what’s inside.

UXML files are valid XML documents, and like all XML documents, the first line of the file should be an XML declaration (although it is technically optional):

<?xml version="1.0" encoding="utf-8"?>

Next comes the document’s root element, which for UXML documents must be a <UXML> element, defined in the UnityEngine.UIElements namespace, aliased to engine here.

<engine:UXML
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:engine="UnityEngine.UIElements"
    xmlns:editor="UnityEditor.UIElements"
    xsi:noNamespaceSchemaLocation="../../UIElementsSchema/UIElements.xsd"
>

And finally, good citizens always close their tags.

</engine:UXML>

Note that there is no content in the default template, just a blank slate with a bit of boilerplate – this is exactly what we wanted to start with, so there’s no cleanup necessary before we start work on our UI.

Adding a Button to the Template

Referring back to our webpage example earlier, our HTML file defined the button like this: <button id="renameBtn">Rename Selected</button>. One way UXML differs from HTML is that UXML elements are not allowed to have any text content – they are either self-closing or only contain other UXML elements. Instead, attributes are used to hold text content. With that in mind, let’s add a Button to our UXML template, which is also in the engine namespace:

    xsi:noNamespaceSchemaLocation="../../UIElementsSchema/UIElements.xsd"
>
  <engine:Button name="renameBtn" text="Rename Selected" />
</engine:UXML>

Now that we have something in our template, let’s get it added to our window. We’ll finish fleshing out the rest of the UI a little bit later. Reopen the C# editor window script.

From a <Button> in the Template to a Button on the UI

The EditorWindow class supports the OnEnable() lifecycle method, which means if your subclass has an OnEnable() function, it will be called when the window is loaded. This is the function where you set up your UI contents, wire up event handlers, etc. Let’s add it and test it out.

        GetWindow<Renamerator>();
    }

    void OnEnable()
    {
        Debug.Log("[Renamerator] OnEnable()");
    }
}

Save these changes and open your editor window. You should see the message logged to the debug console.

Opening our window prints a message.

Quick Aside: Hot Reloading

If you happened to have had the editor window still open from before, you may have seen the message log to the debug console immediately after saving the changes above without having to reopen the window. This is because Unity supports hot reloading on changes to editor scripts.

In practice, the hot reloading has been somewhat fragile for me in the face of compilation errors and exceptions, so if don’t see your changes applied immediately, try closing and reopening your custom window (which I’ll refer to as “refreshing” the window hereafter).

Note that changes to UXML files are not automatically reloaded, so you will definitely have to refresh the window after changing your template. Good thing we set up that hotkey earlier!

Back to the <Button>

As I mentioned at the beginning of this section, the three things we need to do to go from template to actual UI are loading the template, creating a UI object from the template, and adding the resulting object to the editor window.

We can use the AssetDatabase class’ LoadAssetAtPath to load our UXML template just like any other file in our Assets folder. The resulting object is of type VisualTreeAsset, which is an object that represents the parsed template in memory. We can call CloneTree() on it to get a UI object that we can place into our editor window. And while we’re at it, let’s remove the debug message:

    void OnEnable()
    {
        Debug.Log("[Renamerator] OnEnable()");
        var template = AssetDatabase.LoadAssetAtPath<VisualTreeAsset>("Assets/Editor/Renamerator.uxml");
        var ui = template.CloneTree();
    }

You won’t see a change to your editor window yet if you refresh, because we still haven’t added the UI object to our editor window. To do this, we need a reference to the root element of the editor window, which is sort of like the <body> of an HTML document. That reference is available to subclasses of EditorWindow via the inherited property named rootVisualElement. To add our UI object, we use its appropriately named Add function:

        var ui = template.CloneTree();
        rootVisualElement.Add(ui);
    }

That’s it! If hot reloading is working and you still have the editor window open, you may see the button appear right after saving the changes above. Otherwise open/refresh the window and behold the beautiful baby button:

Our editor window’s new button.

We now have a button in our window, but it’s not very pretty, and it doesn’t do anything. Let’s add some style now, and we’ll add functionality afterwards. You know what they say: “Make it look good now – we can try to make it work after we sell it.” 3


Styling the Window Content

Creating a Stylesheet

To style our window’s content, we’ll follow steps similar to how we added content in the previous section. We’ll need to:

  1. Create a USS file that defines our UI’s style.
  2. Modify the OnEnable function of our editor window C# script to also:
    1. Load the USS file into a C# object.
    2. Apply the resulting object to the editor window.

We’ll start by using Unity’s built-in template for new USS files. In your Editor folder, right click and select Create 🠚 UIElements 🠚 USS File. I once again went with Renamerator for consistency, but again any name is fine.

The Editor folder with the new USS file.

Open the new USS file in your text editor, and let’s go over its contents. The file has only a single selector with no declarations. VisualElement is the base type for everything in UIElements, much like <html>, <img>, and <div> are all DOM elements, so this particular USS rule is like the UIElements equivalent of CSS’s univeral selector (* {}).

VisualElement {}

Let’s remove this rule and replace it with some style for our button. Again referring back to our webpage example from the beginning, we had this CSS: button { font-size: 18px; padding: 8px; }. The USS equivalent of this CSS is actually the same, except we are targeting Buttons (note the capital B):

VisualElement {}
Button {
    font-size: 18px;
    padding: 8px;
}

Applying the Stylesheet

Now that we’ve got a stylesheet, we need to load it up and attach it to our UI. We’ll again use AssetDatabase.LoadAssetAtPath, but this time the type of the object returned will be a StyleSheet. All VisualElements, including the rootVisualElement and our instantiated template (the ui variable), have a styleSheets property with a function called Add. Reopen the editor window C# script and add the following to the OnEnable function:

        rootVisualElement.Add(ui);
        var styleSheet = AssetDatabase.LoadAssetAtPath<StyleSheet>("Assets/Editor/Renamerator.uss");
        ui.styleSheets.Add(styleSheet);
    }

Refresh your editor window if needed and now the button should have larger text and some padding. If you edit the numbers and save, you should see the changes reflected immediately, as USS files also support hot reloading.

The editor window button is now styled.

Quick Aside: Linking a USS Stylesheet Directly to a UXML Template

In our webpage example, we applied the stylesheet to the page via a link, specifically: <link rel="stylesheet" type="text/css" href="renamerator.css">. UIElements supports a similar way to link USS files into UXML templates using the <Style> tag, found in the engine namespace. For our UI, we could have added this to our UXML file instead of adding the style sheet via C# above:

    xsi:noNamespaceSchemaLocation="../../UIElementsSchema/UIElements.xsd"
>
    <engine:Style src="Renamerator.uss" />
    <engine:Button name="renameBtn" text="Rename Selected" />

Adding Dynamic Functionality

Handling Clicks

It’s time that we make our button actually do something when clicked. Once again referring back to our webpage example from the start, we had the following couple lines of JavaScript: var btn = document.getElementById("renameBtn"); btn.onclick = showMsg;, where showMsg was a function with no parameters that logged something to the console. We’re going to translate this to UIElements.

There’s nothing that we need to modify about our UI template or style for this, it’s a purely functional change. Thanks to UIElements’ design philosophy of separation of concerns, our changes will be local to our editor window C# script – no need to touch the UXML or USS files.

Open up the C# file, and let’s write a simple function that logs to the debug console:

        ui.styleSheets.Add(styleSheet);
    }

    void RenameSelected()
    {
        Debug.Log("Renaming GameObjects");
    }
}

Now we need to find the button and have it call our function when clicked. VisualElement subclasses inherit a method named Q (“Query”) that finds children elements based on their name attribute, kind of like DOM elements’ getElementById function. Once we’ve found the Button we’re looking for, we’ll register our function with it’s clicked property.

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

    void RenameSelected()

Refresh your window, click the button, and behold your glorious log message.

Clicking the button logs a message.


Finishing the UI Template

Adding Text Inputs and Labels

Let’s take a look at our webpage again for a reminder of our goal:

Our web page design mock up from earlier.

What we’re missing are two text inputs and some text. Since this is a change to the structure of our UI, the changes will be local to our UXML template. UIElements comes with a couple built-in components we can use for this. We’ve already seen the <Button>, and now we’ll use two other built-in components from the same namespace, <TextField> and <Label>.

    xsi:noNamespaceSchemaLocation="../../UIElementsSchema/UIElements.xsd"
>
    <engine:TextField name="searchTxt" label="Find" />
    <engine:TextField name="replaceTxt" label="Replace" />
    <engine:Label name="numSelectedLbl" text="### SELECTED" />
    <engine:Button name="renameBtn" text="Rename Selected" />

Now refresh your editor window to see the changes – remember that changes to UXML files do not trigger an automatic refresh of the UI like C# and USS files.

The design is complete.

And with that done, our final task is to finish up the functionality of our editor window. We’re almost there!


Finishing the Functionality

Let’s make a quick list of what we have to finish up:

  1. We need to be able to check the text inputs’ values at the time that the button is clicked.
  2. Instead of placeholder text, our label should display how many GameObjects are selected and stay in sync with the actual user selection.
  3. The rename button should actually rename the selected GameObjects when clicked.

Since these changes don’t involve changing the structure or style of the UI, our changes will be local to the editor window C# script.

Getting User Input

We can find the TextFields the same way we found the Button earlier – via the query function, Q. We’ll create a couple properties to hold references to the fields, and then set them in the OnEnable function.

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

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

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

    void RenameSelected()

We still have our button set up to log to the debug console. Let’s modify that function to test out our TextField references:


    void RenameSelected()
    {
        Debug.Log("Renaming GameObjects");
        Debug.Log($"Renaming: {searchTxt.value} -> {replaceTxt.value}");
    }

Refresh the window, type some text into each box, and click the button. You should see a message logged to the debug console with the contents of both text boxes.

The text inputs are logged to the debug console when the button is clicked.

Dynamic Labels and the Selection Class

Our goal for this section is to get our Label to stay in sync with the GameObjects we have selected in the Hierarchy/Scene windows. Like the TextFields, we’ll create a property for our Label and find it using the Q query function.

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


        renameBtn.clicked += RenameSelected;
        numSelectedLbl = ui.Q<Label>("numSelectedLbl");
        searchTxt = ui.Q<TextField>("searchTxt");

The Unity editor exposes a Selection class that contains information about what objects are selected via a GameObject[] property named gameObjects. Now that we have a reference to our label, let’s replace the placeholder text with an actual count of how many GameObjects are selected in OnEnable.

        numSelectedLbl = ui.Q<Label>("numSelectedLbl");
        var numSelected = Selection.gameObjects.Length;
        numSelectedLbl.text = $"GameObjects Selected: {numSelected}";
        searchTxt = ui.Q<TextField>("searchTxt");

Close the custom editor window. Create several empty GameObjects in your scene (Ctrl+D is the default shortcut duplicating GameObjects), then select some number of them and reopen the editor window. The Label text should reflect how many items you have selected.

7 items are selected, and the label says there are 7 items selected.

However, if you change the selection, the label will still show whatever it did when you opened the window, because we are only setting the value in OnEnable. To stay in sync, the Selection class exposes a selectionChanged property where we can register a function to be called when the GameObject selection changes.

We’ll refactor setting the label text into a function, then register the new function with the Selection class.

        Debug.Log($"Renaming: {searchTxt.value} -> {replaceTxt.value}");
    }

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


        numSelectedLbl = ui.Q<Label>("numSelectedLbl");
        var numSelected = Selection.gameObjects.Length;
        numSelectedLbl.text = $"GameObjects Selected: {numSelected}";
        UpdateNumSelectedLabel();
        Selection.selectionChanged += UpdateNumSelectedLabel;
        searchTxt = ui.Q<TextField>("searchTxt");

Now when you change your selection, the label should stay in sync.

We didn’t need to ever unregister the click handler for our UI button, because the button gets destroyed along with our window. However, the Selection class is global and sticks around as our editor window comes and goes. As good citizens, we should clean up after ourselves. The EditorWindow supports the OnDisable lifecycle method which is where we can unregister our callback.

        replaceTxt = ui.Q<TextField>("replaceTxt");
    }

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

    void RenameSelected()

Almost done, we just need to actually do the rename now.

Renaming GameObjects

Here’s what we need to do when we click the button in order to rename all the selected GameObjects:

  1. Loop over everything in Selection.gameObjects and use the string.Replace function to set their name property according to the TextField values.

Really, that’s it! We’ll also make the log message just a bit more informative as well.


    void RenameSelected()
    {
        Debug.Log($"Renaming: {searchTxt.value} -> {replaceTxt.value}");
        Debug.Log($"Renaming {Selection.gameObjects.Length} GameObjects: " +
                  $"{searchTxt.value} -> {replaceTxt.value}");
        foreach (var gameObj in Selection.gameObjects)
        {
            gameObj.name = gameObj.name.Replace(searchTxt.value, replaceTxt.value);
        }
    }

And now for the moment of truth… Save the changes, refresh the window, and try it out!

7 selected objects have just been renamed with the new custom tool.


Wrap Up

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

We started with just a little webpage and a dream, and now we have a first-class custom editor window powered by UIElements that can do a bulk rename of GameObjects with just the click of a button.

Here are some of the things we covered:

  • The foundations of UIElements – UXML, USS, and C#
  • Basic controls – Button, Label, and TextField
  • Dynamic functionality – Setting labels, collecting user input, and reacting to clicks
  • The Selection class and programmatically renaming GameObjects

What’s Next?

In Part 2 of this series, we’ll learn more about UIElements by continuing to work on The Renamerator, adding support for regular expressions, confirmation before the rename, custom undo behavior, a preview of the changes, better styling, and more. See you next time!


  1. Naming things is hard. “IDubThee” was actually my first choice, but it started to sound like a C# interface name after a while, so I changed it. ↩︎

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

  3. I really hope they don’t say that. ↩︎