Skip to content

User Interface System

The UI system uses Unity's UI Toolkit with a Factory-View-Host architecture for clean separation of concerns and memory safety.

Source: Assets/Scripts/UI/


Architecture

graph TD
    A[Controller] -->|game logic| B[Host]
    B -->|lifecycle, animations| C[View]
    C -->|element creation| D[UIToolkitFactory]

Components

UIToolkitFactory

Static factory class that creates pre-configured UI Toolkit elements with consistent styling and localization.

Source: UIToolkitFactory.cs

Method Returns Purpose
CreateElement<T>() T Generic element with CSS classes
CreateContainer() VisualElement Styled container
CreateButton() Button Localized button with click handler
CreateLabel() Label Localized label
CreateBoundLabel() Label Data-bound label (auto-updates)
CreateSlider() Slider Range input with callback
CreateToggle() Toggle Boolean input
CreateDropdown() DropdownField Selection input
CreateHealthBar() BarElements Container, background, fill

Fluent Extensions:

var panel = UIToolkitFactory.CreateContainer()
    .WithClasses(UIToolkitStyles.PanelBody)
    .WithPadding(20)
    .WithMargin(10)
    .WithFlexGrow(1);

BasePanelView

Abstract base class for all UI views. Views define visual structure only.

Source: BasePanelView.cs

public abstract class BasePanelView : IDisposable
{
    protected VisualElement Container;

    protected virtual void GenerateUI(VisualElement root) {}

    public virtual void Dispose()
    {
        Container?.Clear();
        Container?.RemoveFromHierarchy();
        Container = null;
    }
}

View Responsibilities:

  • Create UI element hierarchy using Factory
  • Expose elements as properties for Host binding
  • Expose events for user interactions
  • Implement Dispose for cleanup

View Restrictions:

  • No game logic
  • No external event subscriptions
  • No references to game systems

BasePanelHost

MonoBehaviour that manages View lifecycle and animations.

Source: BasePanelHost.cs

public abstract class BasePanelHost : MonoBehaviour
{
    [SerializeField] protected UIDocument uiDocument;
    [SerializeField] protected StyleSheet styleSheet;

    protected VisualElement ContentRoot;
    private ITweenable[] _tweenables;

    public abstract void Generate();
    protected abstract void Dispose();

    public void Show()
    {
        if(_tweenables == null) return;
        foreach (var tween in _tweenables)
            tween?.Show();
    }

    public void Hide()
    {
        if(_tweenables == null) return;
        foreach (var tween in _tweenables)
            tween?.Hide();
    }

    private void Awake() => _tweenables = GetComponents<ITweenable>();
    private void OnDisable() => Dispose();
}

Host Responsibilities:

  • Create and destroy Views
  • Subscribe to View events
  • Forward events to Controllers
  • Manage show/hide animations via ITweenable

Controllers

MonoBehaviours that handle game logic in response to UI events.

public class StartMenuController : MonoBehaviour
{
    [SerializeField] private StartMenuPanelHost menuHost;
    [SerializeField] private SettingsPanelHost settingsHost;

    private void OnEnable()
    {
        menuHost.Generate();
        menuHost.OnPlayClicked += HandlePlay;
        menuHost.OnSettingsClicked += HandleSettings;
    }

    private void OnDisable()
    {
        menuHost.OnPlayClicked -= HandlePlay;
        menuHost.OnSettingsClicked -= HandleSettings;
    }

    private void HandlePlay()
    {
        SceneManager.LoadScene(GameConstants.Hub);
    }

    private void HandleSettings()
    {
        settingsHost.Generate();
        settingsHost.Show();
    }
}

Data Binding

Automatic Binding

Use CreateBoundLabel for labels that auto-update when data changes.

var goldLabel = UIToolkitFactory.CreateBoundLabel(
    playerGold,
    nameof(playerGold.Value),
    UIToolkitStyles.StatValue
);

Manual Binding

For complex formatting or conditional display.

private void BindData()
{
    playerGold.OnValueChanged += UpdateGoldDisplay;
    UpdateGoldDisplay(playerGold.Value);
}

private void UnbindData()
{
    playerGold.OnValueChanged -= UpdateGoldDisplay;
}

private void UpdateGoldDisplay(int value)
{
    goldLabel.text = value >= 1000 ? $"{value/1000f:F1}K" : value.ToString();
}

Event Handling Pattern

Store callback references for proper unsubscription.

public class AudioSettingsPanelHost : BasePanelHost
{
    private Action _unbindAll;

    public void BindViewSliders(AudioSettingsPanelView view)
    {
        _unbindAll = () => { };
        _unbindAll += BindSlider(view.MasterVolume, masterVolume);
        _unbindAll += BindSlider(view.MusicVolume, musicVolume);
    }

    private static Action BindSlider(Slider slider, FloatAttribute attribute)
    {
        EventCallback<ChangeEvent<float>> callback = e => attribute.Value = e.newValue;
        slider.RegisterValueChangedCallback(callback);

        return () => slider.UnregisterValueChangedCallback(callback);
    }

    protected override void Dispose()
    {
        _unbindAll?.Invoke();
        _unbindAll = null;
        base.Dispose();
    }
}

Animation System

Hosts use ITweenable components for show/hide animations.

public class TweenTransform : MonoBehaviour, ITweenable
{
    [SerializeField] private float displayScale = 1f;
    [SerializeField] private float displayStartScale = 0.75f;
    [SerializeField] private float duration = 0.25f;

    public void Show()
    {
        transform.localScale = Vector3.one * displayStartScale;
        // Animate to displayScale with OutCubic easing
    }

    public void Hide()
    {
        // Animate to zero with InCubic easing
    }
}

Existing Panels

Panel View Host Status
Start Menu StartMenuPanelView StartMenuPanelHost Complete
Settings SettingsPanelView SettingsPanelHost Complete
Audio Settings AudioSettingsPanelView AudioSettingsPanelHost Complete
Video Settings VideoSettingsPanelView VideoSettingsPanelHost Complete
Loading Screen LoadingScreenView LoadingScreenHost Complete
Arena Intro ArenaIntroView ArenaIntroHost Complete
Boss Intro BossIntroView BossIntroHost Complete
Enemy Health Bar - EnemyHealthBar Complete

Memory Safety Rules

Rule Implementation
Always dispose Views Call Dispose in Host.OnDisable
Unsubscribe all events Use _unbindAll pattern or explicit unsubscribe
No lambda event handlers Store callback references for unsubscription
Null check in callbacks View may be disposed when callback fires
Dispose before regenerate Call Dispose before creating new View