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:
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" />
- Sneak peek comparison:
-
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; }
- Sneak peek comparison:
-
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;
- Sneak peek comparison:
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:
- Create a subclass of
UnityEditor.EditorWindow
. - Write a static function in the subclass that calls the
GetWindow
function inherited fromEditorWindow
– this function opens or focuses the editor window. - 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.
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.
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>();
}
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:
- Create a UXML template that describes the structure of our UI.
- Add a function to our editor window C# script to:
- Load the UXL template into a C# object.
- Create a UI object from the template object.
- 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.
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.
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:
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:
- Create a USS file that defines our UI’s style.
- Modify the
OnEnable
function of our editor window C# script to also:- Load the USS file into a C# object.
- 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.
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 Button
s (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 VisualElement
s, 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.
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.
Finishing the UI Template
Adding Text Inputs and Labels
Let’s take a look at our webpage again for a reminder of our goal:
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.
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:
- We need to be able to check the text inputs’ values at the time that the button is clicked.
- Instead of placeholder text, our label should display how many GameObjects are selected and stay in sync with the actual user selection.
- 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 TextField
s 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.
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 TextField
s, 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.
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:
- Loop over everything in
Selection.gameObjects
and use thestring.Replace
function to set theirname
property according to theTextField
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!
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
, andTextField
- 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!