Skip to main content

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​

StagePatternTimingFailure BehaviorUse Case
Basic ListenersSequentialSame frame, synchronousContinue to nextStandard callbacks
Priority ListenersSequential (sorted)Same frame, synchronousContinue to nextOrdered processing
Conditional ListenersSequential (filtered)Same frame, synchronousSkip if false, continueState-dependent logic
Persistent ListenersSequentialSame frame, synchronousContinue to nextCross-scene systems
Trigger EventsParallelSame frame, independentOthers unaffectedSide effects, notifications
Chain EventsSequentialMulti-frame, blockingChain stopsCutscenes, sequences

Key Differences Explained​

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

πŸ’‘ Best Practices​

1. Listener Management​

Always Unsubscribe​

Memory leaks are the #1 issue with event systems. Always clean up listeners.

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
}

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.

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
}
}

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.

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);
}
}

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​

// 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!

5. Conditional Listeners​

Effective Condition Design​

Keep conditions simple and fast.

// 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;
}
);

⚠️ 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.

// Multiple listeners for related operations
healthEvent.AddListener(UpdateHealthBar);
healthEvent.AddListener(UpdateHealthText);
healthEvent.AddListener(UpdateHealthIcon);
healthEvent.AddListener(UpdateHealthColor);

2. Avoid Heavy Operations in Listeners​

Keep listeners lightweight. Move heavy work to coroutines/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
}

3. Cache Delegate Allocations​

Avoid creating new delegate allocations every frame.

void OnEnable()
{
// Creates new delegate allocation every time
updateEvent.AddListener(() => UpdateHealth());
}

πŸ“Š 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