UIElements or IMGUI for dynamic custom inspectors

I want to build a custom inspector that renders a list of objects and contains a button. Pressing the button should add a new element to the list. The inspector should render the list including the new element afterwards.

Since the UI is changing (one more element is rendered each time the button is pressed) I am unsure if using UIElements is a good approach. As far as I understood in UIElements you define the UI elements in CreateInspectorGUI, which is called the first time that the inspector is drawn. But you don’t define what happens each frame. This is more like the approach in IMGUI.

Would IMGUI be more suitable for this or can I still work with UIElements?

I hope that somebody more into these topics can nudge me in the right direction. :slight_smile:

Thanks!

Hey @MogwaiHawk,

Edit: I wrote an in-depth tutorial on how to create dynamic custom inspectors like the one below. You can check it out at Exploring UIElements Part 4: Monster Inspector.


I would definitely recommend UIElements for this. You are correct that you define your UI elements in CreateInspectorGUI. That is also where you can set up event handlers to make your UI dynamic. In your example, you could add a click handler to your button that instantiates some sort of VisualElement and uses the Add function to place the element on your UI. You can use the Remove or Clear functions to get rid of elements that are no longer needed.


Below is a simple, but complete, example I wrote to experiment with the idea. Hopefully it illustrates some of the concepts you are asking about. The idea is that you have a Potion class for your game that has a Title and a list of Effects, and you want a custom inspector for it using UIElements.

Here’s the default inspector for our Potion class:

159431-default-inspector.png

And a preview of the final result:

159430-uielements-list-demo.png


Assets/Potion.cs

using System.Collections.Generic;
using UnityEngine;

public class Potion : MonoBehaviour
{
    public string title;
    public List<string> effects;
}

Assets/Editor/PotionEditor.cs

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

[CustomEditor(typeof(Potion))]
public class PotionEditor : Editor
{
    Potion potion;
    VisualElement effectEditorsContainer;
    Label sizeLbl;

    void OnEnable()
    {
        potion = (Potion)target;
        if (potion.effects == null) { potion.effects = new List<string>(); }
    }

    public override VisualElement CreateInspectorGUI()
    {
        var mainTemplate = AssetDatabase.LoadAssetAtPath<VisualTreeAsset>("Assets/Editor/PotionEditor.uxml");
        var ui = mainTemplate.Instantiate();

        sizeLbl = ui.Q<Label>("EffectsListSize");
        effectEditorsContainer = ui.Q("EffectsList");
        var addEffectBtn = ui.Q<Button>("AddEffectBtn");
        addEffectBtn.clicked += AddNewEffect;

        RefreshUI();

        return ui;
    }

    void AddEffect(int idx)
    {
        var effectUI = new PotionEffectEditor(potion, idx, DeleteEffect);
        effectEditorsContainer.Add(effectUI);
    }

    void AddNewEffect()
    {
        potion.effects.Add(string.Empty);
        EditorUtility.SetDirty(potion);
        RefreshUI();
    }

    void DeleteEffect(PotionEffectEditor effectEditor)
    {
        potion.effects.RemoveAt(effectEditor.idx);
        EditorUtility.SetDirty(potion);
        RefreshUI();
    }

    void RefreshUI()
    {
        effectEditorsContainer.Clear();
        foreach (var idx in Enumerable.Range(0, potion.effects.Count))
        {
            AddEffect(idx);
        }
        sizeLbl.text = $"       Size: {potion.effects.Count}";
    }
}

Assets/PotionEditor.uxml

<?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:VisualElement>
    <engine:Style src="PotionEditor.uss" />
    <engine:TextField name="PotionTitle" label="Title" binding-path="title" />
    <engine:Label name="EffectsListHeader" text=" Effects" />
    <engine:Label name="EffectsListSize" text="SIZE: ###" />
    <engine:Button name="AddEffectBtn" text="Add New Effect" />
    <engine:VisualElement name="EffectsList">
    </engine:VisualElement>
  </engine:VisualElement>
</engine:UXML>

Assets/PotionEditor.uss

.horizontalContainer { flex-direction: row; }
 #AddEffectBtn { margin: 0 3px 0 20px; }
 #DeleteBtn { color: red; }
 #Effect { flex-grow: 1; }

Assets/PotionEffectEditor.cs

using UnityEditor;
using UnityEngine.UIElements;
using System;

public class PotionEffectEditor : VisualElement
{
    public Potion potion;
    public int idx;
    Action<PotionEffectEditor> deleteCallback;

    public PotionEffectEditor(Potion potion_, int idx_, Action<PotionEffectEditor> deleteCallback_)
    {
        idx = idx_;
        potion = potion_;
        deleteCallback = deleteCallback_;

        var template = AssetDatabase.LoadAssetAtPath<VisualTreeAsset>("Assets/Editor/PotionEffectEditor.uxml");
        var ui = template.Instantiate();

        var effectField = ui.Q<TextField>("Effect");
        effectField.label = $"      Element {idx}";
        effectField.value = potion.effects[idx];
        effectField.RegisterValueChangedCallback(x => UpdatePotionEffect(x));

        var deleteBtn = ui.Q<Button>("DeleteBtn");
        deleteBtn.clicked += DeleteEffect;

        Add(ui);
    }

    void DeleteEffect() { deleteCallback(this); }

    void UpdatePotionEffect(ChangeEvent<string> change)
    {
        potion.effects[idx] = change.newValue;
        EditorUtility.SetDirty(potion);
    }
}

Assets/Editor/PotionEffectEditor.uxml

<?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:VisualElement class="horizontalContainer">
    <engine:TextField name="Effect" />
    <engine:Button name="DeleteBtn" text="X" />
  </engine:VisualElement>
</engine:UXML>

Hope this helps!