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>orAzPriorityEventRef<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
AzEventTokenorAzEventTokenBag- 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
SourceSetwhen 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.
