UI Development¶
Choosing a UI System¶
UI Toolkit (Recommended for new projects)¶
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.