Design choices

Choosing The Right Primitive

Decide when to use events, priority events, ref payloads, state, lifecycle tokens, and tracking.

  • Event or state
  • Priority dispatch
  • Value vs ref payloads
  • Unity lifecycle patterns

Azkar EDA gives you a few intentionally small building blocks. The important choice is not "which one is most powerful?" It is "what shape is my problem?"

Deep references:

Quick Decision Table

Something happened and listeners may react

Use
AzEvent
Example

Player died, button clicked, wave started.

Something happened with a value

Use
AzEvent<T>
Example

Damage dealt, item collected, score changed as a notification.

Something happened with a large struct payload

Use
AzEventRef<T>
Example

Combat hit result, generated sample data, physics snapshot.

Reactions must run in explicit order

Use
AzPriorityEvent
Example

Input phase, simulation phase, presentation phase.

Ordered reactions carry data

Use

AzPriorityEvent<T> or AzPriorityEventRef<T>

Example

Ordered damage modifiers, validation phases, layered UI refresh.

A value has current meaning

Use
AzState<T>
Example

Health, selected target, current settings, quest progress.

You need cleanup ownership

Use

AzEventToken or AzEventTokenBag

Example

MonoBehaviour enable and disable subscriptions.

You need editor visibility

Use

Tracking

Example

Debugging live handlers, publishers, state values, and graph connections.

Event Or State?

Ask this first:

Does the value still matter after the moment has passed?

If no, use an event.

using Azkar.Eda;

public static class CombatEvents
{
    public static readonly AzEvent<int> DamageApplied = new();
}

If yes, use state.

using Azkar.Eda;

public static class PlayerState
{
    public static readonly AzState<int> Health = new(100);
}

Common event-shaped messages:

  • "The player clicked this button."
  • "The wave started."
  • "This enemy was defeated."
  • "This command was requested."

Common state-shaped values:

  • "The current health is 72."
  • "The selected target is enemy 4."
  • "The settings menu is open."
  • "The active objective is FindKey."

If late subscribers need the current value, use state.

AzEvent Or AzPriorityEvent?

Use AzEvent when listener order is not part of correctness.

using Azkar.Eda;

public static class UiEvents
{
    public static readonly AzEvent ScreenOpened = new();
}

Use AzPriorityEvent when order is part of the design.

using Azkar.Eda;

public static class SimulationEvents
{
    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 SimulationReader : MonoBehaviour
{
    private readonly AzEventTokenBag _tokens = new();

    private void OnEnable()
    {
        SimulationEvents.Tick.Subscribe(ReadInput, TickPriority.Input).AddTo(_tokens);
        SimulationEvents.Tick.Subscribe(UpdateSimulation, TickPriority.Simulation).AddTo(_tokens);
        SimulationEvents.Tick.Subscribe(UpdateView, TickPriority.Presentation).AddTo(_tokens);
    }

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

    private void ReadInput() { }
    private void UpdateSimulation() { }
    private void UpdateView() { }
}

AzPriorityEvent slots are numbered 1 through 9. Lower slots run first. If two subscribers use the same priority slot, do not design logic that depends on which one runs first. Give them separate slots if order matters.

Value Payload Or Ref Payload?

Use normal value payloads for most code:

using Azkar.Eda;

public static class InventoryEvents
{
    public static readonly AzEvent<string> ItemAdded = new();
}

Use ref payloads for large structs or hot paths where copying matters:

using Azkar.Eda;

public readonly struct HitReport
{
    public readonly int TargetId;
    public readonly float Damage;

    public HitReport(int targetId, float damage)
    {
        TargetId = targetId;
        Damage = damage;
    }
}

public static class CombatEvents
{
    public static readonly AzEventRef<HitReport> HitResolved = new();
}

Ref event handlers should treat the payload as read-only. The Ref<T> event types are intended for struct payloads.

Main Thread And Off Thread

Be conservative in Unity:

  • Touch UnityEngine.Object, scene objects, UI, transforms, and most Unity APIs on the main thread.
  • Prefer invoking gameplay-facing events on the main thread.
  • If worker code produces data, queue the result and publish from the main thread.
  • Use state writer methods such as SourceSet when you need distinct-until-changed state updates.

Threaded code should move plain data between threads, not Unity object references.

using System.Collections.Concurrent;
using UnityEngine;
using Azkar.Eda;

public sealed class MainThreadEventPump : MonoBehaviour
{
    private readonly ConcurrentQueue<int> _scores = new();

    public void EnqueueScoreFromWorker(int score)
    {
        _scores.Enqueue(score);
    }

    private void Update()
    {
        while (_scores.TryDequeue(out int score))
        {
            GameEvents.ScoreChanged.Invoke(score);
        }
    }
}

Common Unity Lifecycle Patterns

For scene object listeners, subscribe in OnEnable and dispose in OnDisable.

using UnityEngine;
using Azkar.Eda;

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

    private void OnEnable()
    {
        EnemyEvents.Spawned.Subscribe(OnEnemySpawned).AddTo(_tokens);
    }

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

    private void OnEnemySpawned(int enemyId) { }
}

For objects that subscribe once and live until destroyed, Awake plus OnDestroy is also fine.

using UnityEngine;
using Azkar.Eda;

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

    private void Awake()
    {
        AppEvents.Quitting.Subscribe(OnQuitting).AddTo(_tokens);
    }

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

    private void OnQuitting() { }
}

For temporary UI screens, let the screen own its bag. When the screen closes, dispose the bag.