Unity

Unity Runtime UI – Crib Notes

Initial Setup

Every UI Document component references a UI Document asset (.uxml file) that defines the UI and a Panel Settings asset that renders it. You can connect more than one UI Document asset to a single Panel Settings asset.

Right-click Project window, and then select Create > UI Toolkit > UI Document

  1. In the Hierarchy right click the Scene, select GameObject > UI Toolkit > UI Document. This creates the following:
    • A UI Toolkit folder with a Panel Settings asset and a default runtime theme.
    • A GameObject with a UI Document component which in turn the is connected to the Panel Settings asset.
  2. In the Hierarchy select the UIDocument  then drag SimpleRuntimeUI.uxml from your Project window to the inspector (Source Asset)
  3. Create a companion script that derives from MonoBehaviour.
  4. Add SimpleRuntimeUI.cs as a component of the UIDocument GameObject.
  5. Click on the PanelSettings object Project > Assets > UI Toolkit then drag the theme style sheet (.tss) to the Panel.
  1. Use the Package Manager to install the Input System package.
  2. Select Edit > Project Settings > Player.
  3. Set Active Input Handling to Input System Package (New).
  4. Select GameObject > UI > Event System. This adds an Event System GameObjectThe fundamental object in Unity scenes, which can represent characters, props, scenery, cameras, waypoints, and more. A GameObject’s functionality is defined by the Components attached to it. More info
    See in Glossary that includes a Standalone Input Module in the Scene. The module shows an error message along with a button in the Inspector window.

Custom USS

  • Right click Project Create UI ToolkitStyle Sheet
  • Add style sheet to umxl view
    • Add a Style tag with src attribute pointing to the style sheet
    • In the UI Builder upper left corner (+▼) → Add Existing USS

Using UXML instances as templates

Any UXML file in the project is available as a library object in the UI Builder.

Editing Templates

Open in UI Builder

Library→ProjectComponent (RMB)→Open UI Builder

Close the current context and open the component in the UI Builder.

Open as Sub-Document

Hierarchy→Component (RMB)→Open Instance in Isolation

Edit the component in the UI Builder, maintaining a breadcrumb to the previous context.

Open Sub-Document in Place

Hierarchy→Component (RMB)→Open Instance in Context

Edit the component while keeping the view on the current component. Maintains a breadcrumb to the previous context.

Common Attributes

  • name
  • tooltip
  • tabindex

Accessing UXML Elements in C#

Typically a controller for a UI Element will is added to the UI Document as a component. Alternately the the UI Document can be set as a field on a MonoBehaviour instance.

Extending the MonoBehaviour class is most expedient way to control a UI component when it has only one instance. Public accessors permit access from other controllers, and should be used sparingly.

Select UI Document, in the Inspector→Add Component→Scripts.

using UnityEngine;
using UnityEngine.UIElements;

public class GameListController : MonoBehaviour {
    public string GameListID = "GameList";

    private VisualElement Root {
        get { return GetComponent<UIDocument>().rootVisualElement; }
    }

    private ListView GameList{
        get { return Root.Q<ListView>(GameListID); }
    }
}

Add event handlers to the MonoBehaviour Start method.

public void Start() {
   Root.Q<Button>(JoinButtonID).RegisterCallback<ClickEvent>(ClickJoinButton);
}
public UIDocument uiDocument1;
[SerializeReference] private UIDocument uiDocument2;
var uiDocument = GetComponent<UIDocument>();
public VisualElement Root {
    get { return GetComponent<UIDocument>().rootVisualElement; }
}

Custom Control Class for UXML Elements [source]

Create a C# class derived from the VisualElement class or a subclass.

You can initialize the control in the constructor, or when it’s added to the UI.

Event Handling

https://docs.unity3d.com/Manual/UIE-Events.html

You can register an event handler on an existing class, to handle events such as a mouse click.

Dynamically Adding Elements

ScrollView scrollView = Root.Q<ScrollView>(GameListID);
scrollView.contentContainer.Add(new Label("I done got clicked"));

External Links

UXML Elements Reference

Override VisualElement.ContainsPoint() to assign custom intersection logic. [source]

You can also create USS custom properties to style a custom control. [source]

https://docs.unity3d.com/2022.2/Documentation/Manual/UIToolkits.html

https://docs.unity3d.com/2022.2/Documentation/Manual/UIE-get-started-with-runtime-ui.html

https://docs.unity3d.com/ScriptReference/UIElements.VisualElement.html

https://docs.unity3d.com/Manual/UIE-USS.html

https://docs.unity3d.com/Manual/UIE-create-tabbed-menu-for-runtime.html

JSON Serialization in Unity

JSONUtility (Unity Class)

Unity has it’s own JSON serialization package [ref] which is very close in usage to the native C# serialization. It works with public field, or private fields marked with the ‘SerializeField’ attribute. It does not work with class properties, or fields marked ‘readonly’. To otherwise skip a field mark it with the ‘NonSerialized’ attribute.

// Will serialize
public string name;
[SerializeField] private string value;

// Will not serialize
public readonly string name;
public string name {get; set;}
[NonSerialized] int age;

The serialization method does not require type information, it will simply create json text based on the public field values. This means you do not need to deserialize into the same object you serialized from. The class it’s self must be marked with the ‘Serializable’ attribute.

using UnityEngine;

[System.Serializable]
public class PlayerState : MonoBehaviour{
    public string name;
    public int age;

    public string SaveToString(){
        return JsonUtility.ToJson(this);
    }

    // Given:
    // name = "Dr Charles"
    // age = 33
    // SaveToString returns:
    // {"name":"Dr Charles","age":33}
}

Unlike serialization, deserialization requires a type. Any values found in the JSON text that are not fields in the provided type will be ignored.

PlayerState playerState = JsonUtility.FromJson<PlayerState>(string);

JSONEncoder (Helper Class)

In order to automatically decode a json object to the correct type I have included the type name in the JSON text object. The emitted JSON has two root fields: type & instance.

using UnityEngine;
using System;

public class JSONDetails<T>{
    public T instance;
    public string type;

    public JSONDetails(T instance) {
        this.instance = instance;
        this.type = instance.GetType().ToString();
    }    
}

public class JSONEncoder {
    public string type = "";
    public JSONEncoder() { }

    public static string Serialize<T>(T anObject) {
        return JsonUtility.ToJson(new JSONDetails<T>(anObject), true);
    }

    public static object Deserialize(string json) {
        JSONEncoder wrapper = JsonUtility.FromJson<JSONEncoder>(json);
        Type g = Type.GetType(wrapper.type);
        Type t = typeof(JSONDetails<>).MakeGenericType(new Type[] { g });
        var hydrated = JsonUtility.FromJson(json, t);   
        return t.GetField("instance").GetValue(hydrated);
    }
}

At times serialization may require type information to ensure the correct type is serialized. Deserialization returns an object which can be cast as necessary. Typically this should used to detect the presence of an interface and cast accordingly.

public void ReceiveEvent(string json, string source) {
    var isEvent = JSONEncoder.Deserialize(json);

    if (isEvent is IModelEvent<M> != false) {
        IModelEvent<M> modelEvent = (IModelEvent<M>)isEvent;
        this.model = modelEvent.UpdateModel(this.model);
    }

    if (isEvent is IModelEvent<N> != false) {
        this.Broadcast(isEvent, source);
    }
}