In this tutorial, we’ll be continue learning the basics of Unity’s new UIElements framework by creating a custom editor window that makes it easy to spread out several GameObjects into a grid pattern – Gridify.
Update: Check out Part 3 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 of the Extending the Unity Editor with UIElements series.
The Unity version used throughout this tutorial is 2019.3.12f1, but any version 2019.1 or newer should work.
What We’re Building
In this tutorial, we’ll be building a new custom editor window for moving GameObjects around in the Scene, specifically to spread them out into a grid pattern. We’ll learn how to:
- Use Unity’s built-in custom editor window template.
- Get numeric input from the user with the
<FloatField>
element. - Give your window a custom icon.
- Integrate with the Unity editor’s
Undo
system. 1
… and more! Our goal is to end up with something that looks like this:
Getting Started
We’ll start with a new 3D project. Create a new folder named Editor
in your Assets folder. As in Part 1, all of our files will reside in this folder.
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 Gridify
, but you can pick anything you’d like.
Immediately after clicking Confirm
, three files should be created and a new editor window should open. Unity just created a UXML template, a USS file for styling, and a C# file to glue everything together.
Like the window we made in Part 1, it’s a first-class editor window that you can move, resize, or dock just like the Hierarchy, Scene, or Inspector windows. It’s a bit faster to get a UIElements-ready custom editor window up and running this way compared to our approach in the previous tutorial, but there is a bit of junk that we need to clean up before we start working on Gridify.
Cleaning Up the Defaults
Before we start working on our feature, let’s walk through the files that Unity just created and clean them up while we’re at it. We’ll start with the C# file:
Gridify.cs
First off are some imports, the class declaration where we subclass EditorWindow
, and the function that opens the window and sets the title. Let’s change the menu path and rename the function for clarity:
using UnityEditor;
using UnityEngine;
using UnityEngine.UIElements;
using UnityEditor.UIElements;
public class Gridify : EditorWindow
{
[MenuItem("Window/UIElements/Gridify")]
[MenuItem("Custom Tools/Gridify")]
public static void ShowExample()
public static void OpenWindow()
{
Gridify wnd = GetWindow<Gridify>();
wnd.titleContent = new GUIContent("Gridify");
}
We should also add a hotkey to open our editor window quickly since we’ll need to refresh the window everytime we change our UXML files. You can take a look at the Unity docs for a reminder of how the hotkey syntax works.
public class Gridify : EditorWindow
{
[MenuItem("Custom Tools/Gridify)]
[MenuItem("Custom Tools/Gridify %#g")]
public static void OpenWindow()
The only other function in the file is OnEnable
, which sets up the UI when the window is opened. Let’s walk through it and modify it to suit the UI we will be building.
First off, a local variable is created to store a reference to the rootVisualElement
property. This saves a few keystrokes later on in the function, but isn’t strictly necessary, so let’s get rid of it:
public void OnEnable()
{
// Each editor window contains a root VisualElement object
VisualElement root = rootVisualElement;
The next section creates a <Label>
and attaches it to the root. We’ll be describing our UI in a UXML template, so we can delete this as well.
// VisualElements objects can contain other VisualElement following a tree hierarchy.
VisualElement label = new Label("Hello World! From C#");
root.Add(label);
The following section loads the UXML template file created earlier, creates an element representing our UI from it, and attaches the new element to the root. This is what we did in Part 1, and is also what we want to do here, so we’ll leave it in, just making a few cosmetic changes for brevity and clarity.
// Import UXML
var visualTree = AssetDatabase.LoadAssetAtPath<VisualTreeAsset>("Assets/Editor/Gridify.uxml");
var uxmlTemplate = AssetDatabase.LoadAssetAtPath<VisualTreeAsset>("Assets/Editor/Gridify.uxml");
VisualElement labelFromUXML = visualTree.CloneTree();
var ui = uxmlTemplate.CloneTree();
root.Add(labelFromUXML);
rootVisualElement.Add(ui);
Finally, the sample code shows an example of loading a USS file and applying the resulting stylesheet to a VisualElement
before adding it to the UI. We’ll be using a different method to apply our USS file to our UI, so let’s get rid of this entire section:
// 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/Gridify.uss");
VisualElement labelWithStyle = new Label("Hello World! With Style");
labelWithStyle.styleSheets.Add(styleSheet);
root.Add(labelWithStyle);
}
}
OnEnable
is now a slim 3 lines, and we’re done with the C# file for now. At this point, we should be able to refresh our editor window and see the following:
We’re done with the C# file for now. Let’s move on to the USS file next.
Gridify.uss
The Unity style sheet file is short and sweet. It uses two standard supported CSS properties, font-size
and color
, as well as one Unity vendor extension property, -unity-font-style
, which is similar to the unsupported font-weight
CSS property.
Label {
font-size: 20px;
-unity-font-style: bold;
color: rgb(68, 138, 255);
}
You can read more about USS syntax and supported properties in the official Unity USS documentation. We’ll leave this file alone for the moment and take a look at the UXML file that Unity generated for us.
Gridify.uxml
Like the USS file, the default UXML file is pretty trim already. It only has the required boilerplate for a UXML template and a single <Label>
.
<?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:UXML>
In Part 1, we loaded our USS file and attached it to our UI in C#, similar to the code we just removed from our C# file. USS files can also be applied to any VisualElement
in a UXML file, except for the root <engine:UXML>
element 4, by including a <Style>
element as a child element.
Practically, this means we need to wrap our UI in a <VisualElement>
and add a <Style>
element as a child of that instead of the root. Using a <VisualElement>
like this for grouping is similar to wrapping a portion of an HTML file in a <div>
for styling purposes. For the src
attribute, we’ll use the relative path to the USS file.
xsi:noNamespaceSchemaLocation="../../UIElementsSchema/UIElements.xsd"
>
<engine:VisualElement>
<engine:Style src="Gridify.uss" />
<engine:Label text="Hello World! From UXML" />
</engine:VisualElement>
</engine:UXML>
If you refresh the editor window now, you should see that the stylesheet has been applied to the sibling <Label>
element our UI.
Now that we are confident that our stylesheet is being applied to our UI, we can finish cleaning up the default files. Let’s remove the <Label>
:
<engine:Style src="Gridify.uss" />
<engine:Label text="Hello World! From UXML" />
</engine:VisualElement>
At this point, if you refresh the editor window, you should be left with an empty window:
The last thing we need to do before we can begin working on our feature is to clean up the USS file that we looked at earlier.
Back to Renamerator2.uss
Obliterate the contents of the file, we’ll style things later:
Label {
font-size: 20px;
-unity-font-style: bold;
color: rgb(68, 138, 255);
}
And with that, we’re ready to start working on our gridify 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:
It looks like all we need is:
- 1 numeric field
- 1 button
We’ve already used the <VisualElement>
subclass <Button>
when we were building the Renamerator in Part 1. For the numeric field, we will use another built-in subclass, the <FloatField>
, which is like the <TextField>
, but only allows numeric input.
Unlike the <TextField>
and <Button>
, which are in the UnityEngine.UIElements
namespace (aliased to engine
), the <FloatField>
is in the UIEditor.UIElements
namespace (aliased to editor
). With that in mind, let’s build our UI:
<engine:Style src="Gridify.uss" />
<editor:FloatField name="spread" label="Spread Distance" />
<engine:Button name="gridifyBtn" text="Gridify Selected" />
</engine:VisualElement>
Refresh the window to verify.
And with that, our UI’s structure is complete. It does look a little odd though.
Detour: The UIElements Debugger
Diagnosing Style Issues
You may be wondering why the <FloatField>
's label text is large and blue when our stylesheet doesn’t mention the FloatField
element. UIElements comes with a powerful debugger that we can use to diagnose this issue, similar to the developer tools offered by most major web browsers. You can access it by right clicking your editor window tab and choosing UIElements Debugger
from the context menu.
The debugging window displays the structure of your UI on the left side, and the selected element’s properties on the right side. Hovering an item on the left side highlights the corresponding element on your window’s UI.
The Pick Element
button performs this search in reverse – after clicking it, you hover an element on your UI, and it highlights the element in the debugging window. Let’s use it find out what’s going on with our UI by clicking Pick Element
, then moving our cursor to hover the “Spread Distance” text:
Now we can see that the <FloatField>
is actually a compound element, made up of a <Label>
and a <FloatInput>
, and our USS file has a rule for Label
elements. Looking at the right side of the debugger window, the last entry of the Matching Selectors
section verifies that we’re matching the Label
selector on line 1 of our USS file.
Live Editing the UI
Not only can we inspect our UI with the debugger, but we can also use it to modify our UI on the fly. If you scroll down the right side of the debugging window, you’ll see that most things are editable, such as the element’s Text
, color
, and font-size
. Try editing some of the values, and you should see the changes reflected on your UI immediately.
These changes will persist until you refresh the window.
Styling the UI
Before we start moving GameObjects around, let’s open up our USS file and make some small changes to our UI’s style.
Maintaining a Consistent Style
Now that we know the Label
selector applies to our FloatField
, let’s modify it to get rid of the color and bold, and reduce the font size a bit as well:
Label {
font-size: 20px;
font-size: 16px;
-unity-font-style: bold;
color: rgb(68, 138, 255);
}
The <Label>
now has larger text than the other parts of our UI, the <FloatInput>
and the <Button>
. We’d like to bump up the font size of the other elements to match. Like CSS, several USS selectors can share the same rule by separating the selectors with a comma. Let’s try to reuse our style for the other elements:
Button,
FloatInput,
Label {
font-size: 16px;
}
If we want our style to apply to all of our UI, we can use the universal selector, the asterisk. Let’s refactor our USS to use that instead:
Button,
FloatInput,
Label {
* {
font-size: 16px;
}
I don’t like the way the button looks – it’s short and stretched all the way across the window. Let’s give it a bit of margin and padding to let it stand out more:
* {
font-size: 16px;
}
Button {
margin: 10px 30px;
padding: 5px;
}
The last thing our whiteboard sketch has that our UI doesn’t is an awesome hand-drawn icon. We’ll have to go back to our C# file for that.
A Custom Icon
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 GridifyIcon
:
Your image may not preview correctly, instead display a black box, like this:
The icon will work in this state, but it is a bit annoying to not have a preview of the image in the Project view. You can fix it by selecting the image and changing the texture type of the image to “Sprite (2D and UI)” in the Inspector window.
After applying this change, the thumbnail should display correctly.
To use the icon, we must load it and pass it to the GUIContent
constructor:
Renamerator2 wnd = GetWindow<Gridify>();
var icon = (Texture2D)EditorGUIUtility.Load("Assets/Editor/GridifyIcon.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:
Adding Functionality
Now that we’re done with the structure and style of our UI, it’s time to focus on functionality. Everything from here on out will be in the C# file.
Setting Up a Click Handler
First, let’s create a skeleton for our function that will be called when we click the gridify button:
rootVisualElement.Add(ui);
}
void GridifySelected()
{
Debug.Log("[Gridify]");
}
}
Next, let’s find the button in OnEnable
using the Q
query function and register our function to be called when the button is clicked:
rootVisualElement.Add(ui);
var btn = ui.Q<Button>("gridifyBtn");
btn.clicked += GridifySelected;
}
Clicking the button should now log to the debug console:
Getting User Input - <FloatField>
Before we start moving GameObjects around, we should see how far the user wants to move them. Like the TextField
in the previous tutorial, the FloatField
gives access to the input text via the value
property.
Let’s add a field to store a reference to the FloatField
…
public class Gridify : EditorWindow
{
FloatField spread;
[MenuItem("Window/UIElements/Gridify %#g")]
}
… wire it up in OnEnable
…
rootVisualElement.Add(ui);
spread = ui.Q<FloatField>("spread");
var btn = ui.Q<Button>("gridifyBtn");
… and check its value in the gridify function.
void GridifySelected()
{
Debug.Log("[Gridify]");
Debug.Log($"[Gridify] Spreading {spread.value} units.");
}
Type anything in the field and verify that it’s echoed to the console when you click the button:
Moving GameObjects
Alright, time to actually do some real work! In Part 1, we learned how to access the selected GameObjects via Selection.gameObjects
. To move these objects, we can access the Transform
component of each GameObject and set its position
.
One Dimension
Let’s first line up objects along the X axis, then move onto a grid afterwards. We’ll use a humble for
loop to iterate over the selected objects and move them, starting from (0,0,0):
First, some convenience variables:
void GridifySelected()
{
var selected = Selection.gameObjects;
var numObjs = selected.Length;
var start = new Vector3(0, 0, 0);
Debug.Log($"[Gridify] Spreading {spread.value} units.");
Now the loop – for each item, we need to calculate how far to move the object, and then move it.
var start = new Vector3(0, 0, 0);
Debug.Log($"[Gridify] Spreading {spread.value} units.");
Debug.Log($"[Gridify] Spreading {numObjs} objects {spread.value} units.");
for (var i = 0; i < numObjs; i++)
{
var xOffset = i * spread.value;
var pos = start;
pos.x += xOffset;
selected[i].transform.position = pos;
}
}
Let’s test it out! Create several cubes (or anything) in your scene, select and few, and gridify them! You should see something like this:
Two Dimensions
In this section, we’ll modify our code to arrange the selected GameObjects in a square-ish grid pattern.
We want the smallest square that’s larger than the number of GameObjects we have selected. For example, if we had 8 objects, we’d want to arrange them in a 3x3 (9-element) square. We can calculate the side lengths by rounding up the square root of the number of elements, guaranteeing a square large enough to hold everything. For example, again assuming 8 objects: the square root of 8 is about 2.82, which rounds up to 3, so we would choose a side length of 3 and try to fill out a 3x3 square.
var start = new Vector3(0, 0, 0);
var sideLength = Mathf.CeilToInt(Mathf.Sqrt(numObjs));
Debug.Log($"[Gridify] Spreading {numObjs} objects {spread.value} units.");
Now that we know how long the sides are, we will arrange the object in rows along the X axis, increasing the Z component everytime we get to a new row. Thus, the X component will have a cyclic pattern, like 0,1,2,0,1,2,0,1,2...
, and the Z component will increase by 1 per sideLength
objects, like 0,0,0,1,1,1,2,2,2,...
. These patterns correspond to the the modulus (%
) and integer division (/
) operators, respectively.
for (var i = 0; i < numObjs; i++)
{
var xOffset = i * spread.value;
var xOffset = (i % sideLength) * spread.value;
var zOffset = (i / sideLength) * spread.value;
var pos = start;
pos.x += xOffset;
pos.z += zOffset;
selected[i].transform.position = pos;
}
That’s it! Now when we press the button, the GameObjects should snap into a grid formation instead of a line:
Finishing Touches
Our editor window is pretty awesome, but it has one significant problem. The Unity editor is unaware of what our custom editor window is doing, and that means that it can’t help us undo our changes. The Renamerator from Part 1 had this same issue. However, there is a solution.
Adding Undo/Redo Support
To get first-class Undo/Redo support, all we need to do is tell Unity what objects we are changing. Unity’s built-in Undo
class has a RecordObjects
function that takes an Object[]
of objects to watch and a string
name for the operation that you are performing.
The only catch regarding this function is that the objects are only shallowly monitored. This means that first-level properties are recorded, but properties of properties are ignored. Given some GameObject g
, a change to g.name
would be recorded, but a change to g.transform.position
would not be – in short, more than one .
means it won’t work.
What this means for us is that instead of passing in the array of GameObjects that we have (Selection.gameObjects
), we need to generate an array of their Transforms to monitor instead. We’ll use LINQ’s Select
and ToArray
functions, so first we need to import LINQ:
using System.Linq;
using UnityEditor;
And then we can get a Transform[]
to pass to Undo.RecordObjects
:
var sideLength = Mathf.CeilToInt(Mathf.Sqrt(numObjs));
var transforms = selected.Select(x => x.transform).ToArray();
Undo.RecordObjects(transforms, $"[Gridify] Moved {numObjs} objects");
Debug.Log($"[Gridify] Spreading {numObjs} objects {spread.value} units.");
And with that done, we can gridify without reservation, because our changes will show up on the main Unity Edit
menu like so:
Wrap Up
Final Code: https://github.com/exploringunity/gridify
This time we started with just a whiteboard drawing and a dream, and now we have a first-class custom editor window powered by UIElements that can do a snap GameObjects into a grid with just the click of a button.
Here are some of the things we covered:
- Using the UIElements debugger
- Integrating with Unity’s
Undo
system - Programmatically moving GameObjects around the scene
- Giving your editor window an icon
What’s Next?
In Part 3 of this series, we’ll learn more about UIElements by continuing to work on The Renamerator from Part 1, adding support for regular expressions, confirmation before the rename, a preview of the changes, and more. See you next time!
-
If you tried out The Renamerator from Part 1, you may have noticed that you can’t undo the rename using
Edit 🠚 Undo
. We’ll learn how to provide undo support in this part. ↩︎ -
Kenney.nl is awesome, check them out! In their own words: “Free game assets, no strings attached. We’ve created thousands of sprites, 3D models and sound effects which you can use in your projects. The generous public domain license allows any kind of use, even commercial!” ↩︎
-
Funny enough, programmatically planting trees with just a click is already a built-in Unity feature. ↩︎
-
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). ↩︎