State

UI Patterns

Wire Unity UI controls, paired sliders, derived status values, and a complete view-model example.

  • Slider binding
  • Two sliders
  • Derived status
  • Complete example

These examples show common Unity UI patterns that become clearer when views subscribe to state.

Common Unity UI Patterns

Slider to State

private readonly AzState<float> _volume = new(0.5f);
private readonly AzEventTokenBag _bindings = new();

private void OnEnable()
{
    volumeSlider.onValueChanged.AddListener(OnSliderChanged);
    _volume.BindTo(this, static (self, value) => self.SetSliderWithoutNotify(value)).AddTo(_bindings);
}

private void OnDisable()
{
    volumeSlider.onValueChanged.RemoveListener(OnSliderChanged);
    _bindings.DisposeAll();
}

private void OnSliderChanged(float value)
{
    _volume.SourceSet(value);
}

private void SetSliderWithoutNotify(float value)
{
    volumeSlider.SetValueWithoutNotify(value);
}

Two Sliders Bound Together

private readonly AzState<float> _modelVolume = new(0.35f);
private readonly AzState<float> _uiVolume = new(0.35f);
private IAzPausableToken _twoWay;

private void OnEnable()
{
    _twoWay = _modelVolume.BindTwoWay(_uiVolume, snapToThisOnBind: true);
}

private void OnDisable()
{
    _twoWay?.Dispose();
    _twoWay = null;
}

Derived Status State

private readonly AzState<int> _score = new(0);
private readonly AzState<string> _status = new("Ready");
private readonly AzEventTokenBag _bindings = new();

private void OnEnable()
{
    _score.SubscribeMap(
        DescribeScore,
        value => _status.SourceSet(value),
        notifyImmediately: false).AddTo(_bindings);

    _status.BindTo(SetStatusText).AddTo(_bindings);
}

private static string DescribeScore(int score)
{
    if (score >= 10) return "Score is hot";
    if (score > 0) return "Score is waking up";
    return "Score is idle";
}

Complete Example

using Azkar.Eda;
using UnityEngine;
using UnityEngine.UIElements;

public sealed class PlayerViewModel
{
    private readonly AzState<int> _health = new(100);
    private readonly AzState<int> _score = new(0);
    private readonly AzState<string> _status = new("Ready");
    private readonly AzEventTokenBag _internalBindings = new();

    public IReadOnlyAzState<int> Health => _health.AsReadOnly();
    public IReadOnlyAzState<int> Score => _score.AsReadOnly();
    public IReadOnlyAzState<string> Status => _status.AsReadOnly();

    public PlayerViewModel()
    {
        _health.Validator = value => Mathf.Clamp(value, 0, 100);

        _score.SubscribeMap(
            DescribeScore,
            value => _status.SourceSet(value),
            notifyImmediately: false).AddTo(_internalBindings);
    }

    public void Dispose()
    {
        _internalBindings.Dispose();
    }

    public void TakeDamage(int amount)
    {
        _health.SourceSet(_health.Value - amount);
    }

    public void AddScore(int amount)
    {
        _score.SourceSet(_score.Value + amount);
    }

    private static string DescribeScore(int score)
    {
        if (score >= 10) return "Score is hot";
        if (score > 0) return "Score is waking up";
        return "Score is idle";
    }
}

public sealed class PlayerHud : MonoBehaviour
{
    [SerializeField] private UIDocument document;

    private readonly AzEventTokenBag _bindings = new();
    private PlayerViewModel _model;
    private Label _healthText;
    private Label _scoreText;
    private Label _statusText;

    private void Awake()
    {
        _model = new PlayerViewModel();
    }

    private void OnEnable()
    {
        var root = document.rootVisualElement;
        _healthText = root.Q<Label>("healthText");
        _scoreText = root.Q<Label>("scoreText");
        _statusText = root.Q<Label>("statusText");

        _model.Health.SubscribeMap(
            value => $"HP: {value}",
            text => _healthText.text = text).AddTo(_bindings);

        _model.Score.SubscribeMap(
            value => $"Score: {value}",
            text => _scoreText.text = text).AddTo(_bindings);

        _model.Status.BindTo(
            text => _statusText.text = text).AddTo(_bindings);
    }

    private void OnDisable()
    {
        _bindings.DisposeAll();
    }

    private void OnDestroy()
    {
        _model?.Dispose();
    }

    public void DebugDamage()
    {
        _model.TakeDamage(10);
    }

    public void DebugScore()
    {
        _model.AddScore(1);
    }
}

This example shows the core AzState workflow:

  • the view model owns writable AzState<T>
  • public consumers receive read-only state
  • state mutation uses SourceSet
  • derived status uses SubscribeMap
  • UI uses SubscribeMap and BindTo
  • subscriptions are owned by an AzEventTokenBag