Execution Order & Best Practices
Understanding how GameEvent executes callbacks and manages event flow is crucial for building reliable, performant event-driven systems. This guide covers execution order, common patterns, pitfalls, and optimization strategies.
π― 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 N: healthEvent.Raise(15f)
βββΊ SaveGame() [0.1ms]
βββΊ UpdateUI() [0.3ms]
βββΊ ShowWarning() [0.2ms]
Total: 0.6ms, 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 N: bossDefeatedEvent.Raise()
βββΊ stopBossMusicEvent.Raise() β
βββΊ playVictoryMusicEvent.Raise() β
βββΊ spawnLootEvent.Raise() β (Failed!)
βββΊ showVictoryUIEvent.Raise() β (Still runs!)
βββΊ saveCheckpointEvent.Raise() β (Still runs!)
All in same frame, failures are isolated
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:
Frame 0: cutsceneStartEvent raised
βββΊ fadeOutEvent.Raise()
Frame 60: (1 second later)
βββΊ hideUIEvent.Raise()
Frame 90: (0.5 seconds later)
βββΊ playCutsceneEvent.Raise()
Frame 390: (5 seconds later)
βββΊ fadeInEvent.Raise()
Frame 450: (1 second later)
βββΊ showUIEvent.Raise()
Total: ~7.5 seconds across multiple frames
Failure Scenario:
// If playCutsceneEvent fails:
Frame 0: cutsceneStartEvent β
Frame 60: fadeOutEvent β
Frame 90: hideUIEvent β
Frame 390: playCutsceneEvent β FAILED!
π Chain stops here!
fadeInEvent and showUIEvent NEVER execute
π‘ 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
{
[SerializeField] private 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
{
[SerializeField] private 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
{
[SerializeField] private GameEvent<float> 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
{
[SerializeField] private GameEvent<string> 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β
Each listener adds overhead. 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
- Remember to call ExecuteChainEvents() for chains
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