Skip to main content

Execution Order & Best Practices

GES Β· Runtime API

The exact order GameEvent runs listeners, triggers, and chains, followed by patterns and pitfalls for listener cleanup, schedule handling, priority, conditions, and performance.

For programmers building reliable, high-performance event flow who want to avoid the common failure modes.

Knowing how GameEvent executes callbacks and manages event flow is the key to 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 1024
πŸš€ healthEvent.Raise(15.0f)
β”‚
β”œβ”€β–Ί πŸ’Ύ SaveGame() ⏱️ 0.1ms
β”œβ”€β–Ί πŸ–₯️ UpdateUI() ⏱️ 0.3ms
└─► ⚠️ ShowWarning() ⏱️ 0.2ms
β”‚
πŸ“Š Total Cost: 0.6ms | ⚑ Status: Synchronous (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
{
[GameEventDropdown] public 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
{
[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.

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
{
[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.

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​

Even though the code has been highly optimized, there will still be some overhead for each listener. 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
  • 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