Adoption

Migration Guide

Move from C# events, manual ordered phases, property-change pairs, and manual cleanup into Azkar EDA patterns gradually.

  • C# event migration
  • Priority phase migration
  • State migration
  • Cleanup migration

This guide shows how to move common Unity patterns into Azkar EDA without changing the shape of your game all at once.

Deep references:

C# Events To AzEvent

Before:

using System;
using UnityEngine;

public static class ScoreEvents
{
    public static event Action<int> ScoreChanged;

    public static void RaiseScoreChanged(int score)
    {
        ScoreChanged?.Invoke(score);
    }
}

After:

using Azkar.Eda;

public static class ScoreEvents
{
    public static readonly AzEvent<int> ScoreChanged = new();
}
using UnityEngine;
using Azkar.Eda;

public sealed class ScoreWriter : MonoBehaviour
{
    public void Publish(int score)
    {
        ScoreEvents.ScoreChanged.Invoke(score);
    }
}

Listener migration:

using UnityEngine;
using Azkar.Eda;

public sealed class ScoreListener : MonoBehaviour
{
    private readonly AzEventTokenBag _tokens = new();

    private void OnEnable()
    {
        ScoreEvents.ScoreChanged.Subscribe(OnScoreChanged).AddTo(_tokens);
    }

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

    private void OnScoreChanged(int score) { }
}

The important change is ownership. Instead of relying on -= with the same delegate instance, keep the returned token in a bag.

Ordered Manual Phases To AzPriorityEvent

Before:

using System.Collections.Generic;
using UnityEngine;

public sealed class ManualTick : MonoBehaviour
{
    private readonly List<System.Action> _input = new();
    private readonly List<System.Action> _simulation = new();
    private readonly List<System.Action> _presentation = new();

    private void Update()
    {
        foreach (var action in _input) action();
        foreach (var action in _simulation) action();
        foreach (var action in _presentation) action();
    }
}

After:

using Azkar.Eda;

public static class TickBus
{
    public static readonly AzPriorityEvent Tick = new();
}

public static class TickPriority
{
    public const byte Input = 1;
    public const byte Simulation = 5;
    public const byte Presentation = 9;
}
using UnityEngine;
using Azkar.Eda;

public sealed class TickDriver : MonoBehaviour
{
    private void Update()
    {
        TickBus.Tick.Invoke();
    }
}
using UnityEngine;
using Azkar.Eda;

public sealed class TickSubscriber : MonoBehaviour
{
    private readonly AzEventTokenBag _tokens = new();

    private void OnEnable()
    {
        TickBus.Tick.Subscribe(ReadInput, TickPriority.Input).AddTo(_tokens);
        TickBus.Tick.Subscribe(Simulate, TickPriority.Simulation).AddTo(_tokens);
        TickBus.Tick.Subscribe(Present, TickPriority.Presentation).AddTo(_tokens);
    }

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

    private void ReadInput() { }
    private void Simulate() { }
    private void Present() { }
}

Use priority slots 1 through 9 as named phases. Lower slots run first. If same-slot order matters, split the slot into separate named priorities.

Property Plus Event To AzState

Before:

using System;

public sealed class PlayerModel
{
    public event Action<int> HealthChanged;

    public int Health { get; private set; } = 100;

    public void SetHealth(int health)
    {
        if (Health == health)
        {
            return;
        }

        Health = health;
        HealthChanged?.Invoke(Health);
    }
}

After:

using Azkar.Eda;

public sealed class PlayerModel
{
    public readonly AzState<int> Health = new(100);

    public void SetHealth(int health)
    {
        Health.SourceSet(health);
    }
}

Listener:

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

public sealed class HealthHud : MonoBehaviour
{
    [SerializeField] private UIDocument document;
    [SerializeField] private PlayerModel model;

    private readonly AzEventTokenBag _tokens = new();
    private Label _label;

    private void OnEnable()
    {
        _label = document.rootVisualElement.Q<Label>("healthLabel");
        model.Health.Subscribe(OnHealthChanged).AddTo(_tokens);
    }

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

    private void OnHealthChanged(int health)
    {
        _label.text = health.ToString();
    }
}

Use state when subscribers need the current value immediately, not just future changes.

Cleanup Migration

Before:

using UnityEngine;

public sealed class OldListener : MonoBehaviour
{
    private void OnEnable()
    {
        ScoreEvents.ScoreChanged += OnScoreChanged;
    }

    private void OnDisable()
    {
        ScoreEvents.ScoreChanged -= OnScoreChanged;
    }

    private void OnScoreChanged(int score) { }
}

After:

using UnityEngine;
using Azkar.Eda;

public sealed class NewListener : MonoBehaviour
{
    private readonly AzEventTokenBag _tokens = new();

    private void OnEnable()
    {
        ScoreEvents.ScoreChanged.Subscribe(OnScoreChanged).AddTo(_tokens);
    }

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

    private void OnScoreChanged(int score) { }
}

This is especially helpful when old code used lambdas:

using UnityEngine;
using Azkar.Eda;

public sealed class LambdaListener : MonoBehaviour
{
    private readonly AzEventTokenBag _tokens = new();

    private void OnEnable()
    {
        ScoreEvents.ScoreChanged
            .Subscribe(score => Debug.Log($"Score: {score}"))
            .AddTo(_tokens);
    }

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

With tokens, you do not need to recreate the exact lambda to remove it.

Recommended Migration Order

  1. Convert one small C# event to AzEvent<T>.
  2. Update its listeners to use AzEventTokenBag.
  3. Add tracking attributes to the bus and one handler.
  4. Convert one property plus change event to AzState<T>.
  5. Convert one manual ordered workflow to AzPriorityEvent.
  6. Run the scene and inspect the tracking window.
  7. Only then migrate broader systems.