UIElements Tutorial for Unity: Getting Started

In this Unity tutorial, you’ll learn how to use Unity’s UIElements to create complex, flexible editor windows and tools to add to your development pipeline. By Ajay Venkat.

Leave a rating/review
Download materials
Save for later
Share
You are currently viewing page 4 of 5 of this article. Click here to view the first page.

Adding Functionality to the Editor Window

It’s great that you have a good-looking editor window, but there’s no point if you can’t interact with it. Open the PresetWindow.cs and create the following empty methods:

private void SetupControls()
{

}

private void PopulatePresetList()
{

}

private void LoadPreset(int elementID)
{

}

private void BindControls()
{

}
  1. SetupControls(): Adds actions to the buttons so something happens when you click them.
  2. PopulatePresetList(): Populates the List view with VisualElements that represent the presets.
  3. LoadPreset(int elementID): Given an ID, the script will load a preset into the editor window.
  4. BindControls(): Binds the selected preset’s values to the editor window controls.

Setting up the ObjectField

Before you populate these methods, you need to set up the object field.

At the top of the PresetWindow.cs class, add the following private variables:

private PresetObject presetManager;
private SerializedObject presetManagerSerialized;
private Preset selectedPreset;
private ObjectField presetObjectField;

These are references that you’ll use to modify the presets. The SerializedObject binds the values.

At the end of the OnEnable() method, add the following code:

// 1
presetObjectField = root.Q<ObjectField>("ObjectField");

// 2
presetObjectField.objectType = typeof(PresetObject);

// 3
presetObjectField.RegisterCallback<ChangeEvent<UnityEngine.Object>>(e =>
{
    if (presetObjectField.value != null)
    {
        // 4
        presetManager = (PresetObject)presetObjectField.value;
        presetManagerSerialized = new SerializedObject(presetManager);
    }

    PopulatePresetList();
            
});

PopulatePresetList();
SetupControls();

There’s a lot going on here, so take a look at the code step-by-step:

  1. Makes a query to find the ObjectField, searching through the hierarchy starting from the root element.
  2. Sets the ObjectField‘s objectType to PresetObject, which is a pre-made script containing the presets.
  3. Registers a ChangeEvent callback to the presetObjectField, so when anything changes, the code within it will run.
  4. Creates a SerializedObject from the value of the presetObjectField and calls PopulatePresetList.

Visualization of the callbacks

Since UIElements run using Retained Mode GUI, you can’t check for a change in the ObjectField every frame. That’s why you use callbacks. This is a more efficient style of rendering a GUI in application programming.

You can register callbacks to any VisualElement and run a certain piece of code when one occurs. Find a complete list of all callbacks in the Unity3d docs.

Now that you’ve finished your ObjectField, your next step is to get your buttons ready to go.

Setting up Buttons

Get started by inserting the following into SetupControls():

// 1
Button newButton = rootVisualElement.Q<Button>("NewButton");
Button clearButton = rootVisualElement.Q<Button>("ClearButton");
Button deleteButton = rootVisualElement.Q<Button>("DeleteButton");

// 2
newButton.clickable.clicked += () => 
{
    if (presetManager != null)
    {
        Preset newPreset = new Preset();
        presetManager.presets.Add(newPreset);

        EditorUtility.SetDirty(presetManager);
                
        PopulatePresetList();
        BindControls();
    }
};

// 3
clearButton.clickable.clicked += () =>
{
    if (presetManager != null && selectedPreset != null)
    {
        selectedPreset.color = Color.black;
        selectedPreset.animationSpeed = 1;
        selectedPreset.objectName = "Unnamed Preset";
        selectedPreset.isAnimating = true;
        selectedPreset.rotation = Vector3.zero;
        selectedPreset.size = Vector3.one;
    }
};

// 4
deleteButton.clickable.clicked += () =>
{
    if (presetManager != null && selectedPreset != null)
    {
        presetManager.presets.Remove(selectedPreset);
        PopulatePresetList();
        BindControls();
    }
};

Here’s what this code does:

  1. Creates three button references by querying the rootVisualElement for the buttons you created in the PresetWindow.uxml.
  2. Attaches the code to let the newButton add a new empty preset to the presetManager.
  3. Attaches the code to clear the settings of the selectedPreset to the clearButton.
  4. Sets the code to allow the deleteButton to delete the selectedPreset.

Logic behind the new buttons

Unlike other VisualElements, the button doesn’t use RegisterCallback to detect clicks. Instead, it has a custom property called clicked that takes an action.

Congratulations, you now have three working buttons!

three clickable buttons wired up

Right now you can click them, and they execute the relevant methods, but the methods don’t contain any functional code yet. You’ll fix that next! :]

Populating the List View

Now that the buttons actually call methods, it’s time to show a list of all the presets in the List view.

Add this to the top of PresetWindow.cs so you can use the Dictionary type:

using System.Collections.Generic;

Next, insert the following into PopulatePresetList():

// 1
ListView list = (ListView)rootVisualElement.Q<ListView>("ListView");
list.Clear();
        
// 2
Dictionary<Button, int> buttonDictionary = new Dictionary<Button, int>();

// 3
if (presetManager == null)
{
    return;
}
  1. Find the ListView using queries, then clear it to prevent duplicates from any potential previously executed PopulatePresetList() calls.
  2. Create a dictionary to monitor the index of the button, tracking what preset it belongs to.
  3. Code safety. Return early if the presetManager is null for any reason.

With the first bit of code added to the PopulatePresetList() method, add the rest to complete it:

for (int i = 0; i < presetManager.presets.Count; i++) {
    // 1
    VisualElement listContainer = new VisualElement();
    listContainer.name = "ListContainer";

    Label listLabel = new Label(presetManager.presets[i].objectName);
       
    Button listButton = new Button();
    listButton.text = "L";

    // 2
    listLabel.AddToClassList("ListLabel");
    listButton.AddToClassList("ListButton");
    
    listContainer.Add(listLabel);
    listContainer.Add(listButton);
            
    // 3
    list.Insert(list.childCount, listContainer);

    if (!buttonDictionary.ContainsKey(listButton))
    {
        buttonDictionary.Add(listButton, i);
    }


    // 4
    listButton.clickable.clicked += () => 
    {
        if (presetObjectField.value != null)
        {
            LoadPreset(buttonDictionary[listButton]);
        }
    };

    // 5
    if (selectedPreset == presetManager.presets[buttonDictionary[listButton]])
    {
        listButton.style.backgroundColor = new StyleColor(new 
        Color(0.2f,0.2f,0.2f,1f));

    }
}

What you’re essentially doing here is looping through all the presets stored in presetManager and creating a new VisualElement. At each iteration, you add to the list, styling the VisualElements and attaching actions to the buttons of each element along the way.

Don’t worry if it looks overwhelming, here’s a breakdown:

  1. Create a container using an empty VisualElement and create a label with the preset’s name. Add another button to load the preset.
  2. Add the label and button to their respective classes to style them. The styles for these elements are in PresetTemplate.uss. You then add the elements as children of listContainer.
  3. Insert the listContainer at the end of the list.
  4. Register a click event to the button to load the preset. You use the buttonDictionary to load the correct preset, which corresponds to the button you press.
  5. If the loop is currently on the selectedPreset, then highlight the button with a color change to symbolize its selection.

The LoadPreset(int elementID) method doesn’t do anything right now. To change that, add the following:

if (presetManager != null)
{
    selectedPreset = presetManager.presets[elementID];
    presetManager.currentlyEditing = selectedPreset;

    PopulatePresetList();

    if (selectedPreset != null)
    {
        BindControls();
    }
}
else
{
    PopulatePresetList();
}

This code simply sets the selectedPreset to the preset of the button you clicked and refreshes the editor.

Before you go any further, save the PresetWindow.cs and return to the editor. Test the editor by dragging the Preset Object GameObject from the Hierarchy, into the editor window’s object field slot.

The editor window is almost complete

Take a moment to appreciate how far you’ve come! There’s only one more thing left to do: Attach the fields to the preset.