Execution Order & Best Practices
Apply the correct execution order and lifecycle rules for listeners, schedules, priorities, conditions, Triggers, and Chains.
For programmers building reliable, high-performance event flow who want to avoid the common failure modes.Register listeners where their targets become active and remove them when the targets stop listening. When several callbacks share an event, check their execution order before making one callback depend on another.
Execution Orderβ
Visual Timelineβ
When myEvent.Raise() is called, execution follows this precise order:
myEvent.Raise() π
β
βββ 1οΈβ£ Basic Listeners (FIFO Order)
β β
β βββΊ OnUpdate() π
β β β Executed
β β
β βββΊ OnRender() π¨
β β Executed
β
βββ 2οΈβ£ Priority Listeners (High β Low)
β β
β βββΊ [Priority 100] Critical() β‘
β β β Executed First
β β
β βββΊ [Priority 50] Normal() π
β β β Executed Second
β β
β βββΊ [Priority 0] LowPriority() π
β β Executed Last
β
βββ 3οΈβ£ Conditional Listeners (Priority + Condition)
β β
β βββΊ [Priority 10] IfHealthLow() π
β β
β βββΊ Condition Check: health < 20?
β β βββΊ β
True β Execute Listener
β β βββΊ β False β Skip Listener
β β
β βββΊ (Next conditional checked...)
β
βββ 4οΈβ£ Persistent Listeners (Cross-Scene)
β β
β βββΊ GlobalLogger() π
β β Always Executes (DontDestroyOnLoad)
β
βββ 5οΈβ£ Trigger Events (Parallel - Fan Out) π
β β
β βββββββΊ lightOnEvent.Raise() π‘
β β (Executes independently)
β β
β βββββββΊ soundEvent.Raise() π
β β (Executes independently)
β β
β βββββββΊ particleEvent.Raise() β¨
β (Executes independently)
β
β β οΈ If one fails, others still execute
β
βββ 6οΈβ£ Chain Events (Sequential - Strict Order) π
β
βββΊ fadeOutEvent.Raise() π
β Success
β
βββΊ β±οΈ Wait (duration/delay)
β
βββΊ loadSceneEvent.Raise() πΊοΈ
β Success
β
βββΊ β±οΈ Wait (duration/delay)
β
βββΊ fadeInEvent.Raise() π
β Success
π If ANY step fails β Chain STOPS
Execution Characteristicsβ
| Stage | Pattern | Timing | Failure Behavior | Use Case |
|---|---|---|---|---|
| Basic Listeners | Sequential | Same frame, synchronous | Continue to next | Standard callbacks |
| Priority Listeners | Sequential (sorted) | Same frame, synchronous | Continue to next | Ordered processing |
| Conditional Listeners | Sequential (filtered) | Same frame, synchronous | Skip if false, continue | State-dependent logic |
| Persistent Listeners | Sequential | Same frame, synchronous | Continue to next | Cross-scene systems |
| Trigger Events | Parallel | Same frame, independent | Others unaffected | Side effects, notifications |
| Chain Events | Sequential | Multi-frame, blocking | Chain stops | Cutscenes, sequences |
Key Differences Explainedβ
- Listeners (1-4)
- Triggers (5)
- Chains (6)
Characteristics:
- Execute synchronously in the current frame
- Run one after another in defined order
- Each listener is independent
- Failure in one listener doesn't stop others
Example:
healthEvent.AddListener(UpdateUI); // Runs 1st
healthEvent.AddPriorityListener(SaveGame, 100); // Runs 2nd (higher priority)
healthEvent.AddConditionalListener(ShowWarning,
health => health < 20); // Runs 3rd (if condition true)
healthEvent.Raise(15f);
// Order: SaveGame() β UpdateUI() β ShowWarning() (if health < 20)
Timeline:
πΌοΈ Frame 1024
π healthEvent.Raise(15.0f)
β
βββΊ πΎ SaveGame() β±οΈ 0.1ms
βββΊ π₯οΈ UpdateUI() β±οΈ 0.3ms
βββΊ β οΈ ShowWarning() β±οΈ 0.2ms
β
π Total Cost: 0.6ms | β‘ Status: Synchronous (Same Frame)
Characteristics:
- Execute in parallel (fan-out pattern)
- All triggers fire independently
- One trigger's failure doesn't affect others
- Still synchronous, but logically parallel
Example:
// When boss dies, trigger multiple independent events
bossDefeatedEvent.AddTriggerEvent(stopBossMusicEvent, priority: 100);
bossDefeatedEvent.AddTriggerEvent(playVictoryMusicEvent, priority: 90);
bossDefeatedEvent.AddTriggerEvent(spawnLootEvent, priority: 50);
bossDefeatedEvent.AddTriggerEvent(showVictoryUIEvent, priority: 40);
bossDefeatedEvent.AddTriggerEvent(saveCheckpointEvent, priority: 10);
bossDefeatedEvent.Raise();
// All 5 events fire, sorted by priority, but independently
// If spawnLootEvent fails, others still execute
Timeline:
πΌοΈ Frame 2048
π bossDefeatedEvent.Raise()
β
βββΊ π stopBossMusicEvent.Raise() β
Success
βββΊ π playVictoryMusicEvent.Raise() β
Success
βββΊ π spawnLootEvent.Raise() β Failed! (Exception Isolated)
βββΊ π showVictoryUIEvent.Raise() β
Executed (Resilient)
βββΊ π saveCheckpointEvent.Raise() β
Executed (Resilient)
β
π Result: 4/5 Success | π‘οΈ Status: Fault-Tolerant (Isolated Failure)
Characteristics:
- Execute sequentially with blocking
- Strict order: A β B β C
- Supports delays between steps
- Entire chain stops if any step fails
Example:
// Cutscene sequence
cutsceneStartEvent.AddChainEvent(fadeOutEvent, delay: 0f, duration: 1f);
cutsceneStartEvent.AddChainEvent(hideUIEvent, delay: 0f, duration: 0.5f);
cutsceneStartEvent.AddChainEvent(playCutsceneEvent, delay: 0f, duration: 5f);
cutsceneStartEvent.AddChainEvent(fadeInEvent, delay: 0f, duration: 1f);
cutsceneStartEvent.AddChainEvent(showUIEvent, delay: 0f, duration: 0f);
// Execute the chain
cutsceneStartEvent.Raise();
Timeline:
πΌοΈ T+0.0s | Frame 0
π cutsceneStartEvent.Raise()
βββΊ π¬ fadeOutEvent.Raise() β
Initiated
β (Ξ 1.0s Delay)
βΌ
πΌοΈ T+1.0s | Frame 60
βββΊ π₯οΈ hideUIEvent.Raise() β
Executed
β (Ξ 0.5s Delay)
βΌ
πΌοΈ T+1.5s | Frame 90
βββΊ ποΈ playCutsceneEvent.Raise() β
Executed
β (Ξ 5.0s Delay)
βΌ
πΌοΈ T+6.5s | Frame 390
βββΊ π¬ fadeInEvent.Raise() β
Executed
β (Ξ 1.0s Delay)
βΌ
πΌοΈ T+7.5s | Frame 450
βββΊ π₯οΈ showUIEvent.Raise() β
Finalized
π Total Timeline: ~7.5s | ποΈ Total Span: 450 Frames
Failure Scenario:
πΌοΈ T+0.0s | Frame 0
π cutsceneStartEvent.Raise() β
Initiated
β (Ξ 1.0s)
βΌ
πΌοΈ T+1.0s | Frame 60
π fadeOutEvent.Raise() β
Executed
β (Ξ 0.5s)
βΌ
πΌοΈ T+1.5s | Frame 90
π hideUIEvent.Raise() β
Executed
β (Ξ 5.0s)
βΌ
πΌοΈ T+6.5s | Frame 390
π playCutsceneEvent.Raise() β CRITICAL FAILURE!
π [ CIRCUIT BREAKER ACTIVE ]
β οΈ Logical chain halted to prevent state desync.
β© fadeInEvent.Raise() π« NEVER EXECUTED
β© showUIEvent.Raise() π« NEVER EXECUTED
Best Practicesβ
1. Listener Managementβ
Always Unsubscribeβ
Memory leaks are the #1 issue with event systems. Always clean up listeners.
- β Bad
- β Good
public class PlayerController : MonoBehaviour
{
[GameEventDropdown] public GameEvent onPlayerDeath;
void Start()
{
onPlayerDeath.AddListener(HandleDeath);
}
// Object destroyed but listener remains in memory!
// This causes memory leaks and potential crashes
}
public class PlayerController : MonoBehaviour
{
[GameEventDropdown] public GameEvent onPlayerDeath;
void OnEnable()
{
onPlayerDeath.AddListener(HandleDeath);
}
void OnDisable()
{
// Always unsubscribe to prevent memory leaks
onPlayerDeath.RemoveListener(HandleDeath);
}
void HandleDeath()
{
Debug.Log("Player died!");
}
}
Use OnEnable/OnDisable Patternβ
The OnEnable/OnDisable pattern is the recommended approach for Unity.
public class HealthUI : MonoBehaviour
{
[GameEventDropdown] public SingleGameEvent healthChangedEvent;
void OnEnable()
{
// Subscribe when active
healthChangedEvent.AddListener(OnHealthChanged);
}
void OnDisable()
{
// Unsubscribe when inactive
healthChangedEvent.RemoveListener(OnHealthChanged);
}
void OnHealthChanged(float newHealth)
{
// Update UI
}
}
Benefits:
- Automatic cleanup when object is disabled/destroyed
- Listeners only active when needed
- Prevents duplicate subscriptions
- Works with object pooling
2. Schedule Managementβ
Store Handles for Cancellationβ
Always store ScheduleHandle if you need to cancel later.
- β Bad
- β Good
public class PoisonEffect : MonoBehaviour
{
void ApplyPoison()
{
// Can't cancel this later!
poisonEvent.RaiseRepeating(damagePerTick, 1f, repeatCount: 10);
}
void CurePoison()
{
// No way to stop the poison!
// It will keep ticking for all 10 times
}
}
public class PoisonEffect : MonoBehaviour
{
private ScheduleHandle _poisonHandle;
void ApplyPoison()
{
// Store the handle
_poisonHandle = poisonEvent.RaiseRepeating(
damagePerTick,
1f,
repeatCount: 10
);
}
void CurePoison()
{
// Can cancel the poison effect
if (poisonEvent.CancelRepeating(_poisonHandle))
{
Debug.Log("Poison cured!");
}
}
void OnDisable()
{
// Clean up on disable
poisonEvent.CancelRepeating(_poisonHandle);
}
}
Multiple Schedules Patternβ
When managing multiple schedules, use a collection.
public class BuffManager : MonoBehaviour
{
[GameEventDropdown] public StringGameEvent buffTickEvent;
private Dictionary<string, ScheduleHandle> _activeBuffs = new();
public void ApplyBuff(string buffName, float interval, int duration)
{
// Cancel existing buff if any
if (_activeBuffs.TryGetValue(buffName, out var existingHandle))
{
buffTickEvent.CancelRepeating(existingHandle);
}
// Apply new buff
var handle = buffTickEvent.RaiseRepeating(
buffName,
interval,
repeatCount: duration
);
_activeBuffs[buffName] = handle;
}
public void RemoveBuff(string buffName)
{
if (_activeBuffs.TryGetValue(buffName, out var handle))
{
buffTickEvent.CancelRepeating(handle);
_activeBuffs.Remove(buffName);
}
}
void OnDisable()
{
// Cancel all buffs
foreach (var handle in _activeBuffs.Values)
{
buffTickEvent.CancelRepeating(handle);
}
_activeBuffs.Clear();
}
}
3. Trigger and Chain Managementβ
Use Handles for Safe Removalβ
Always use handles to avoid removing other systems' triggers/chains.
- β Risky
- β Safe
public class DoorSystem : MonoBehaviour
{
void SetupDoor()
{
doorOpenEvent.AddTriggerEvent(lightOnEvent);
}
void Cleanup()
{
// DANGER: Removes ALL triggers to lightOnEvent
// Even those registered by other systems!
doorOpenEvent.RemoveTriggerEvent(lightOnEvent);
}
}
public class DoorSystem : MonoBehaviour
{
private TriggerHandle _lightTriggerHandle;
void SetupDoor()
{
// Store the handle
_lightTriggerHandle = doorOpenEvent.AddTriggerEvent(lightOnEvent);
}
void Cleanup()
{
// Only removes YOUR specific trigger
doorOpenEvent.RemoveTriggerEvent(_lightTriggerHandle);
}
}
Organizing Multiple Triggers/Chainsβ
Use a structured approach for complex systems.
public class CutsceneManager : MonoBehaviour
{
// Store all handles for cleanup
private readonly List<ChainHandle> _cutsceneChains = new();
private readonly List<TriggerHandle> _cutsceneTriggers = new();
void SetupCutscene()
{
// Build cutscene sequence
var chain1 = startEvent.AddChainEvent(fadeOutEvent, duration: 1f);
var chain2 = startEvent.AddChainEvent(playVideoEvent, duration: 5f);
var chain3 = startEvent.AddChainEvent(fadeInEvent, duration: 1f);
_cutsceneChains.Add(chain1);
_cutsceneChains.Add(chain2);
_cutsceneChains.Add(chain3);
// Add parallel triggers for effects
var trigger1 = startEvent.AddTriggerEvent(stopGameplayMusicEvent);
var trigger2 = startEvent.AddTriggerEvent(hideCrosshairEvent);
_cutsceneTriggers.Add(trigger1);
_cutsceneTriggers.Add(trigger2);
}
void SkipCutscene()
{
// Clean up all chains
foreach (var chain in _cutsceneChains)
{
startEvent.RemoveChainEvent(chain);
}
_cutsceneChains.Clear();
// Clean up all triggers
foreach (var trigger in _cutsceneTriggers)
{
startEvent.RemoveTriggerEvent(trigger);
}
_cutsceneTriggers.Clear();
}
}
4. Priority Usageβ
Guidelines for Priority Valuesβ
Use a consistent priority scale across your project.
// Define priority constants
public static class EventPriority
{
public const int CRITICAL = 1000; // Absolutely must run first
public const int HIGH = 100; // Important systems
public const int NORMAL = 0; // Default priority
public const int LOW = -100; // Can run later
public const int CLEANUP = -1000; // Final cleanup tasks
}
// Usage
healthEvent.AddPriorityListener(SavePlayerData, EventPriority.CRITICAL);
healthEvent.AddPriorityListener(UpdateHealthBar, EventPriority.HIGH);
healthEvent.AddPriorityListener(PlayDamageSound, EventPriority.NORMAL);
healthEvent.AddPriorityListener(UpdateStatistics, EventPriority.LOW);
Priority Anti-Patternsβ
- β Avoid
- β Best Practice
// Don't use random or inconsistent priorities
healthEvent.AddPriorityListener(SystemA, 523);
healthEvent.AddPriorityListener(SystemB, 891);
healthEvent.AddPriorityListener(SystemC, 7);
// Don't overuse priority when order doesn't matter
uiClickEvent.AddPriorityListener(PlaySound, 50);
uiClickEvent.AddPriorityListener(PlayParticle, 49);
// These don't need priority, use basic listeners!
// Use priorities only when order matters
saveGameEvent.AddPriorityListener(ValidateData, 100); // Must validate first
saveGameEvent.AddPriorityListener(SerializeData, 50); // Then serialize
saveGameEvent.AddPriorityListener(WriteToFile, 0); // Finally write
// Use basic listeners when order doesn't matter
buttonClickEvent.AddListener(PlaySound);
buttonClickEvent.AddListener(ShowFeedback);
buttonClickEvent.AddListener(LogAnalytics);
5. Conditional Listenersβ
Effective Condition Designβ
Keep conditions simple and fast.
- β Expensive
- β Efficient
// Don't do expensive operations in conditions
enemySpawnEvent.AddConditionalListener(
SpawnBoss,
() => {
// Bad: Complex calculations in condition
var enemies = FindObjectsOfType<Enemy>();
var totalHealth = enemies.Sum(e => e.Health);
var averageLevel = enemies.Average(e => e.Level);
return totalHealth < 100 && averageLevel > 5;
}
);
// Cache state, make conditions simple checks
private bool _shouldSpawnBoss = false;
void UpdateGameState()
{
// Update cached state occasionally, not every frame
_shouldSpawnBoss = enemyManager.TotalHealth < 100
&& enemyManager.AverageLevel > 5;
}
void Setup()
{
// Simple, fast condition check
enemySpawnEvent.AddConditionalListener(
SpawnBoss,
() => _shouldSpawnBoss
);
}
Common Pitfallsβ
1. Memory Leaksβ
Problem: Not unsubscribing listeners when objects are destroyed.
Symptoms:
- Increasing memory usage over time
- Errors about destroyed objects
- Callbacks executing on null references
Solution:
// Always use OnEnable/OnDisable pattern
void OnEnable() => myEvent.AddListener(OnCallback);
void OnDisable() => myEvent.RemoveListener(OnCallback);
2. Lost Schedule Handlesβ
Problem: Creating schedules without storing handles.
Symptoms:
- Cannot cancel repeating events
- Events continue after object is destroyed
- Resource waste from unneeded executions
Solution:
private ScheduleHandle _handle;
void StartTimer()
{
_handle = timerEvent.RaiseRepeating(1f);
}
void StopTimer()
{
timerEvent.CancelRepeating(_handle);
}
3. Broad Removal Impactβ
Problem: Using target-based removal instead of handle-based removal.
Symptoms:
- Other systems' triggers/chains get removed unexpectedly
- Hard-to-debug issues where events stop firing
- Cross-system coupling and fragility
Solution:
// Store handles, remove precisely
private TriggerHandle _myTrigger;
void Setup()
{
_myTrigger = eventA.AddTriggerEvent(eventB);
}
void Cleanup()
{
eventA.RemoveTriggerEvent(_myTrigger); // Safe!
}
4. Recursive Event Raisesβ
Problem: Event listener raises the same event, causing infinite loop.
Symptoms:
- Stack overflow exceptions
- Unity freezes
- Exponential execution growth
Example:
// β DANGER: Infinite recursion!
void Setup()
{
healthEvent.AddListener(OnHealthChanged);
}
void OnHealthChanged(float health)
{
// This triggers OnHealthChanged again!
healthEvent.Raise(health - 1); // β INFINITE LOOP
}
Solution:
// β
Use a flag to prevent recursion
private bool _isProcessingHealthChange = false;
void OnHealthChanged(float health)
{
if (_isProcessingHealthChange) return; // Prevent recursion
_isProcessingHealthChange = true;
// Safe to raise here now
if (health <= 0)
{
deathEvent.Raise();
}
_isProcessingHealthChange = false;
}
Performance Optimizationβ
1. Minimize Listener Countβ
Even though the code has been highly optimized, there will still be some overhead for each listener. Consolidate when possible.
- β Inefficient
- β Optimized
// Multiple listeners for related operations
healthEvent.AddListener(UpdateHealthBar);
healthEvent.AddListener(UpdateHealthText);
healthEvent.AddListener(UpdateHealthIcon);
healthEvent.AddListener(UpdateHealthColor);
// Single listener handles all UI updates
healthEvent.AddListener(UpdateHealthUI);
void UpdateHealthUI(float health)
{
// Batch all UI updates together
healthBar.value = health / maxHealth;
healthText.text = $"{health:F0}";
healthIcon.sprite = GetHealthIcon(health);
healthColor.color = GetHealthColor(health);
}
2. Avoid Heavy Operations in Listenersβ
Keep listeners lightweight. Move heavy work to coroutines/async.
- β Blocking
- β Async
void OnDataLoaded(string data)
{
// Bad: Blocks execution for all subsequent listeners
var parsed = JsonUtility.FromJson<LargeData>(data);
ProcessComplexData(parsed); // Takes 50ms
SaveToDatabase(parsed); // Takes 100ms
}
void OnDataLoaded(string data)
{
// Good: Start async processing, don't block
StartCoroutine(ProcessDataAsync(data));
}
IEnumerator ProcessDataAsync(string data)
{
// Parse
var parsed = JsonUtility.FromJson<LargeData>(data);
yield return null;
// Process
ProcessComplexData(parsed);
yield return null;
// Save
SaveToDatabase(parsed);
}
3. Cache Delegate Allocationsβ
Avoid creating new delegate allocations every frame.
- β Allocations
- β Cached
void OnEnable()
{
// Creates new delegate allocation every time
updateEvent.AddListener(() => UpdateHealth());
}
void OnEnable()
{
// Reuses same method reference, no allocation
updateEvent.AddListener(UpdateHealth);
}
void UpdateHealth()
{
// Implementation
}
Summary Checklistβ
Use this checklist when working with GameEvent:
Listener Managementβ
- Always unsubscribe in OnDisable
- Use OnEnable/OnDisable pattern
- Cache delegate references when possible
- Keep listeners lightweight
Schedule Managementβ
- Store ScheduleHandle when you need cancellation
- Cancel schedules in OnDisable
- Use collections for multiple schedules
- Clean up on object destruction
Trigger/Chain Managementβ
- Use handles for safe removal
- Store handles in collections for cleanup
- Choose triggers for parallel, chains for sequential
- Chains run automatically when the source event is raised
Performanceβ
- Consolidate related listeners
- Move heavy work to coroutines/async
- Use simple, fast conditions
- Avoid recursive event raises
Priority & Conditionsβ
- Use consistent priority scale
- Only use priority when order matters
- Keep conditions simple and cached
- Document priority dependencies