Skip to content

UI Development

Choosing a UI System

The modern, retained-mode UI system. Uses UXML for layout and USS for styling, similar to HTML/CSS. Best for:

  • Main menus, settings screens, HUD overlays
  • Editor tools and custom inspectors
  • Data-heavy interfaces (inventories, skill trees, dashboards)
  • Projects that need source-control-friendly UI definitions

Canvas/UGUI (Legacy, still supported)

The older immediate-style system using GameObjects and Canvas components. Still valid for:

  • World-space UI (floating health bars, interaction prompts)
  • Projects already using Canvas extensively
  • Quick prototypes where visual layout in the scene view is preferred

UI Toolkit

Document Structure

A UI Toolkit panel needs a UIDocument component referencing a UXML file:

public class MainMenuController : MonoBehaviour
{
    [SerializeField] private UIDocument _document;

    private Button _startButton;
    private Button _settingsButton;

    private void OnEnable()
    {
        var root = _document.rootVisualElement;
        _startButton = root.Q<Button>("start-button");
        _settingsButton = root.Q<Button>("settings-button");

        _startButton.clicked += OnStartClicked;
        _settingsButton.clicked += OnSettingsClicked;
    }

    private void OnDisable()
    {
        _startButton.clicked -= OnStartClicked;
        _settingsButton.clicked -= OnSettingsClicked;
    }

    private void OnStartClicked() => SceneManager.LoadScene("Game");
    private void OnSettingsClicked() => ShowSettingsPanel();
}

USS Styling

.main-menu {
    flex-direction: column;
    align-items: center;
    justify-content: center;
    height: 100%;
    background-color: rgba(0, 0, 0, 0.8);
}

.menu-button {
    width: 300px;
    height: 60px;
    margin: 10px;
    font-size: 24px;
    -unity-font-style: bold;
    background-color: rgb(40, 40, 40);
    color: rgb(220, 220, 220);
    border-radius: 8px;
}

.menu-button:hover {
    background-color: rgb(60, 60, 60);
}

Runtime Data Binding (Unity 6)

Runtime binding connects a property of a plain C# object (the data source) to a property of a visual element. Mark source members with [CreateProperty], set the element's dataSource, and add a DataBinding with SetBinding. Implementing INotifyBindablePropertyChanged lets the binding system update only the bindings whose property changed:

using System;
using System.Runtime.CompilerServices;
using Unity.Properties;
using UnityEngine;
using UnityEngine.UIElements;

public class PlayerStats : INotifyBindablePropertyChanged
{
    float _health = 100f;

    public event EventHandler<BindablePropertyChangedEventArgs> propertyChanged;

    [CreateProperty]
    public float Health
    {
        get => _health;
        set
        {
            if (Mathf.Approximately(_health, value)) return;
            _health = value;
            Notify();
        }
    }

    void Notify([CallerMemberName] string property = "") =>
        propertyChanged?.Invoke(this, new BindablePropertyChangedEventArgs(property));
}

public class HealthHud : MonoBehaviour
{
    [SerializeField] UIDocument _document;

    public PlayerStats Stats { get; } = new PlayerStats();

    void OnEnable()
    {
        var bar = _document.rootVisualElement.Q<ProgressBar>("health-bar");
        bar.dataSource = Stats;
        bar.SetBinding(nameof(ProgressBar.value), new DataBinding
        {
            dataSourcePath = new PropertyPath(nameof(PlayerStats.Health)),
            bindingMode = BindingMode.ToTarget
        });
    }
}

dataSource is inherited by child elements, so you can set it once on a container and bind many children. Bindings can also be authored in UI Builder or UXML. Use a ScriptableObject as the data source to share state between screens.

Custom Controls with [UxmlElement]

The deprecated UXMLFactory pattern is replaced by attribute-based registration:

[UxmlElement]
public partial class RadialProgress : VisualElement
{
    [UxmlAttribute]
    public float Progress { get; set; }

    public RadialProgress()
    {
        generateVisualContent += OnGenerateVisualContent;
    }

    private void OnGenerateVisualContent(MeshGenerationContext ctx)
    {
        // Custom drawing logic
    }
}

Canvas/UGUI

Canvas Render Modes

  • Screen Space - Overlay: Renders on top of everything. No camera required. Use for HUD.
  • Screen Space - Camera: Renders as if a fixed distance from a camera. Supports post-processing.
  • World Space: Exists in 3D space. Use for in-game screens, floating labels, VR interfaces.

Layout Components

  • HorizontalLayoutGroup: Arrange children in a row
  • VerticalLayoutGroup: Arrange children in a column
  • GridLayoutGroup: Arrange children in a grid
  • ContentSizeFitter: Auto-size to content
  • LayoutElement: Override layout sizing per element

Anchoring for Responsive UI

Set anchors to handle different screen resolutions: - Anchor to corners for elements that should stay at screen edges - Anchor to center for centered elements - Stretch anchors for elements that should scale with screen size

Performance

  • Minimize Canvas rebuilds: separate dynamic and static elements onto different Canvases
  • Use Canvas Groups to toggle visibility instead of enabling/disabling GameObjects
  • Mark static UI elements with "Raycast Target: false" if they do not need click detection
  • Use TextMeshPro for ALL text rendering; never use the legacy Text component

TextMeshPro

Always use TextMeshPro (TMP) for text:

using TMPro;

[SerializeField] private TMP_Text _scoreText;

private void UpdateScore(int score)
{
    _scoreText.SetText("Score: {0}", score);
}

Use SetText with format parameters instead of string concatenation to avoid garbage collection.