Event Bus Pattern - Managing Game Events

Managing Game Events

The Event Bus Pattern

CSCI 3213 - Game Programming

Spring '26 - Week 4, Class 1

Decoupling game systems with centralized events!

Today's Content

πŸ“š Based on Chapter 6

Implementing an Event Bus for Managing Race Events

From: Game Development Patterns with Unity 2021 (2nd Edition)
By David Baron

Available: Dulaney Browne Library or major book retailers. This chapter introduces centralized event management for race systems!

Today's Learning Objectives

What We'll Master

  • 🎯 Understand the Event Bus Pattern
  • 🎯 Identify tight coupling problems
  • 🎯 Implement a centralized event bus
  • 🎯 Use UnityEvent vs C# events
  • 🎯 Build race countdown system

Goal: Create a RaceEventBus to manage race countdown, start, stop, and completion events for Blade Racer.

The Problem: Tight Coupling

Scenario: Race Start Event

When a race starts, multiple systems need to respond:
- UI displays "GO!"
- Audio plays race music
- Timer starts counting
- Obstacles begin spawning
- Camera activates follow mode

❌ Without Event Bus (Tightly Coupled)

public class RaceManager { public UIController ui; public AudioManager audio; public TimerController timer; public ObstacleSpawner spawner; public CameraController camera; void StartRace() { ui.ShowStartText(); // Direct dependency! audio.PlayRaceMusic(); // Direct dependency! timer.StartTimer(); // Direct dependency! spawner.BeginSpawning(); // Direct dependency! camera.FollowPlayer(); // Direct dependency! } }

Problems: Hard to maintain, brittle, not scalable!

What is the Event Bus Pattern?

Event Bus Pattern

An architectural pattern that provides a centralized messaging system for decoupled communication between game components.

βœ… Key Concept

Publishers send events to a central bus.
Subscribers listen for events they care about.
No direct references between components!

In Simple Terms: Like a radio station - broadcasters send messages, listeners tune in to what they want to hear. Nobody needs to know each other!

How Event Bus Works

Publishers
β†’
Event Bus
β†’
Subscribers

Example: Race Start Event

Publisher:                    Event Bus:               Subscribers:
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”              β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”          β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ RaceManager β”‚              β”‚ RaceEventBusβ”‚          β”‚ HUDControllerβ”‚
β”‚             │──RaceStart──▢│             │─────────▢│ (shows GO!) β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜              β”‚  Dictionary β”‚          β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                             β”‚   of Events β”‚          β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
                             β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Άβ”‚ AudioManagerβ”‚
                                                      β”‚ (play music)β”‚
                                                      β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                                                      β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
                                                      β”‚TimerControl β”‚
                                                      β”‚(start timer)β”‚
                                                      β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                
One-to-Many: One publisher can notify multiple subscribers with zero coupling!

Event Bus: Benefits & Drawbacks

βœ… Benefits

  • Decoupling: Publishers don't know subscribers
  • Scalability: Easy to add new subscribers
  • Maintainability: Changes isolated to event bus
  • Flexibility: Subscribe/unsubscribe dynamically
  • Clean Code: No dependency chains

⚠️ Drawbacks

  • Debugging: Hard to trace event flow
  • Memory Leaks: Forgetting to unsubscribe
  • Performance: Slight overhead for lookups
  • Type Safety: Can lose compile-time checks
  • Overuse: Can become "event soup"
⚠️ Critical: ALWAYS unsubscribe in OnDisable or OnDestroy to prevent memory leaks!

When to Use Event Bus

βœ… Perfect For

  • Game state changes (Start, Pause, GameOver)
  • UI updates (Score changed, Health changed)
  • Race events (Countdown, Start, Finish)
  • Achievement unlocks
  • Player death/respawn
  • Level completion

❌ Not Suitable For

  • High-frequency events (every frame)
  • Direct component communication
  • Parent-child relationships
  • Events needing guaranteed order
  • Events requiring return values
  • Simple one-to-one communication
Rule of Thumb: Use Event Bus for infrequent, broadcast-style events where many systems need to react.

Defining Race Event Types

RaceEventType Enum

Our racing game needs these discrete events:

COUNTDOWN

Timer counting down before race start (3... 2... 1...)

START

Race has officially started - GO!

STOP

Race paused or stopped mid-race

FINISH

Player crossed finish line

RESTART

Reset race to beginning

PAUSE

Game paused (show pause menu)

QUIT

Exit to main menu

RaceEventBus Implementation Part 1

using UnityEngine; using UnityEngine.Events; using System.Collections.Generic; namespace Chapter.EventBus { // Enum defining all possible race events public enum RaceEventType { COUNTDOWN, // Pre-race countdown START, // Race begins STOP, // Race stopped FINISH, // Race completed RESTART, // Reset race PAUSE, // Game paused QUIT // Exit to menu } // Static event bus - globally accessible singleton public static class RaceEventBus { // Dictionary mapping event types to UnityEvents private static readonly Dictionary<RaceEventType, UnityEvent> _eventDictionary = new Dictionary<RaceEventType, UnityEvent>(); // Subscribe to an event public static void Subscribe(RaceEventType eventType, UnityAction listener) { // If event doesn't exist in dictionary, create it if (!_eventDictionary.ContainsKey(eventType)) { _eventDictionary[eventType] = new UnityEvent(); } // Add listener to the event _eventDictionary[eventType].AddListener(listener); }
Key Points: Static class (no instantiation), Dictionary stores UnityEvents, Subscribe auto-creates events as needed.

RaceEventBus Implementation Part 2

// Unsubscribe from an event public static void Unsubscribe(RaceEventType eventType, UnityAction listener) { // If event exists, remove the listener if (_eventDictionary.ContainsKey(eventType)) { _eventDictionary[eventType].RemoveListener(listener); } } // Publish/broadcast an event to all subscribers public static void Publish(RaceEventType eventType) { // If event exists, invoke all listeners if (_eventDictionary.ContainsKey(eventType)) { _eventDictionary[eventType].Invoke(); } } } }

Complete Event Bus API

Subscribe(event, method) - Register to listen
Unsubscribe(event, method) - Stop listening
Publish(event) - Broadcast to all listeners

CountdownTimer Subscriber Implementation

using UnityEngine; using System.Collections; namespace Chapter.EventBus { public class CountdownTimer : MonoBehaviour { private float _currentTime; private float duration = 3.0f; // 3 second countdown void OnEnable() { // Subscribe when component is enabled RaceEventBus.Subscribe(RaceEventType.COUNTDOWN, StartTimer); } void OnDisable() { // CRITICAL: Unsubscribe to prevent memory leaks! RaceEventBus.Unsubscribe(RaceEventType.COUNTDOWN, StartTimer); } void StartTimer() { StartCoroutine(Countdown()); } private IEnumerator Countdown() { _currentTime = duration; while (_currentTime > 0) { Debug.Log("Countdown: " + Mathf.Ceil(_currentTime)); yield return new WaitForSeconds(1.0f); _currentTime--; } Debug.Log("GO!"); // Publish START event when countdown completes RaceEventBus.Publish(RaceEventType.START); } } }

BikeController Subscriber Implementation

using UnityEngine; namespace Chapter.EventBus { public class BikeController : MonoBehaviour { private bool _isRunning; void OnEnable() { // Subscribe to race events RaceEventBus.Subscribe(RaceEventType.START, StartBike); RaceEventBus.Subscribe(RaceEventType.STOP, StopBike); RaceEventBus.Subscribe(RaceEventType.RESTART, RestartBike); } void OnDisable() { // Unsubscribe from all events RaceEventBus.Unsubscribe(RaceEventType.START, StartBike); RaceEventBus.Unsubscribe(RaceEventType.STOP, StopBike); RaceEventBus.Unsubscribe(RaceEventType.RESTART, RestartBike); } void StartBike() { _isRunning = true; Debug.Log("[BikeController] Bike started!"); } void StopBike() { _isRunning = false; Debug.Log("[BikeController] Bike stopped"); } void RestartBike() { transform.position = Vector3.zero; Debug.Log("[BikeController] Bike restarted at origin"); } } }

HUDController Subscriber Implementation

using UnityEngine; namespace Chapter.EventBus { public class HUDController : MonoBehaviour { void OnEnable() { // Subscribe to events that affect UI RaceEventBus.Subscribe(RaceEventType.START, DisplayStartMessage); RaceEventBus.Subscribe(RaceEventType.STOP, DisplayStopMessage); RaceEventBus.Subscribe(RaceEventType.FINISH, DisplayFinishMessage); RaceEventBus.Subscribe(RaceEventType.PAUSE, DisplayPauseMessage); } void OnDisable() { // Clean up subscriptions RaceEventBus.Unsubscribe(RaceEventType.START, DisplayStartMessage); RaceEventBus.Unsubscribe(RaceEventType.STOP, DisplayStopMessage); RaceEventBus.Unsubscribe(RaceEventType.FINISH, DisplayFinishMessage); RaceEventBus.Unsubscribe(RaceEventType.PAUSE, DisplayPauseMessage); } void DisplayStartMessage() { Debug.Log("[HUD] GO! Race has started!"); // In real implementation: Update UI text, show animation } void DisplayStopMessage() { Debug.Log("[HUD] Race stopped"); } void DisplayFinishMessage() { Debug.Log("[HUD] FINISH! You completed the race!"); } void DisplayPauseMessage() { Debug.Log("[HUD] PAUSED"); } } }

Testing the Event Bus

Test Setup Steps

  1. Create new Unity scene
  2. Create empty GameObject "RaceManager"
  3. Attach CountdownTimer, BikeController, HUDController scripts
  4. Create test client script (next slide)
  5. Play and use GUI buttons to publish events
  6. Watch console for subscriber responses!
Testing Strategy: Use GUI buttons to manually publish events, verify all subscribers respond correctly in the console.

ClientEventBus Test Script

using UnityEngine; namespace Chapter.EventBus { public class ClientEventBus : MonoBehaviour { private bool _isButtonEnabled; void Start() { _isButtonEnabled = true; } void OnEnable() { RaceEventBus.Subscribe(RaceEventType.FINISH, EnableButton); } void OnDisable() { RaceEventBus.Unsubscribe(RaceEventType.FINISH, EnableButton); } void EnableButton() { _isButtonEnabled = true; } void OnGUI() { // GUI buttons to manually trigger events if (GUILayout.Button("Start Countdown")) { _isButtonEnabled = false; RaceEventBus.Publish(RaceEventType.COUNTDOWN); } if (_isButtonEnabled) { if (GUILayout.Button("Stop Race")) RaceEventBus.Publish(RaceEventType.STOP); if (GUILayout.Button("Restart Race")) RaceEventBus.Publish(RaceEventType.RESTART); if (GUILayout.Button("Finish Race")) RaceEventBus.Publish(RaceEventType.FINISH); if (GUILayout.Button("Pause")) RaceEventBus.Publish(RaceEventType.PAUSE); } } } }

Hands-On Implementation

35-Minute Implementation Challenge

Implement the complete Event Bus system:

  1. Create RaceEventType enum (7 events)
  2. Create RaceEventBus static class (Subscribe, Unsubscribe, Publish)
  3. Create CountdownTimer subscriber
  4. Create BikeController subscriber
  5. Create HUDController subscriber
  6. Create ClientEventBus test script
  7. Test all events with GUI buttons
Remember: OnEnable = Subscribe, OnDisable = Unsubscribe!

Memory Leak Prevention

⚠️ THE MOST IMPORTANT RULE

ALWAYS unsubscribe in OnDisable() or OnDestroy()

The OnEnable/OnDisable Pattern

void OnEnable()
{
    // Subscribe when component becomes active
    RaceEventBus.Subscribe(RaceEventType.START, MyMethod);
}

void OnDisable()
{
    // CRITICAL: Unsubscribe when component is disabled
    RaceEventBus.Unsubscribe(RaceEventType.START, MyMethod);
}
                

❌ What Happens Without Unsubscribe

  • Event bus holds reference to destroyed objects
  • Memory is never released (memory leak)
  • Null reference exceptions when events fire
  • Performance degradation over time

Event Bus vs Observer Pattern

Event Bus

  • Centralized: Single static messaging hub
  • Decoupled: No direct object references
  • Global: Accessible anywhere
  • Anonymous: Publishers don't know subscribers
  • Use for: Game-wide events

Observer Pattern

  • Distributed: Each subject has own observers
  • Direct: Subjects hold observer references
  • Local: Subject-specific subscriptions
  • Known: Subjects manage their observers
  • Use for: Component-specific notifications

Next Week: We'll learn the Observer Pattern and see the differences in practice!

Event Bus vs Unity Events

Event Bus (This Lecture)

  • Code-based: Subscribe via scripts
  • Static: Globally accessible
  • Runtime: Dynamic subscription
  • Type-safe: Enum-based events
  • Best for: System-level events

Unity Events (Inspector)

  • Visual: Wire up in Inspector
  • Component: Per-object instances
  • Design-time: Set up before runtime
  • Serialized: Saved with scene
  • Best for: UI buttons, triggers
Our Implementation: RaceEventBus uses UnityEvent internally but manages subscriptions through code for flexibility!

When NOT to Use Event Bus

❌ High-Frequency Events

Don't use for Update()-like events or per-frame updates. The dictionary lookup overhead adds up. Use direct method calls instead.

❌ Events Requiring Return Values

Event Bus is fire-and-forget. If you need a return value or acknowledgment, use direct method calls or callbacks.

❌ Guaranteed Execution Order

You can't control the order subscribers are notified. If order matters, use a different pattern.

❌ Simple Parent-Child Communication

For direct component relationships, just use GetComponent() or public references. Don't over-engineer!

Alternative & Related Patterns

ScriptableObject Events

Unity-specific pattern using ScriptableObjects as event channels. Data persists between scenes, designer-friendly.

Use for: Cross-scene events, data-driven events

Message Queue

Events stored in queue and processed in order. Better performance control, can batch process or defer.

Use for: Network events, async operations

C# Events

Built-in C# event system with delegates. More type-safe, no Unity dependency, standard .NET pattern.

Use for: Pure C# systems, non-Unity code

Reactive Extensions (Rx)

Advanced event streaming with LINQ-like operators. Powerful but complex, requires UniRx package.

Use for: Complex event chains, async streams

Homework Assignment

Assignment: Event Bus Implementation

Due: Next class
Submission: Unity project + video to D2L

Part 1: Core Event Bus (40 points)

  1. Implement RaceEventType enum with all 7 events
  2. Implement static RaceEventBus class:
    • Dictionary<RaceEventType, UnityEvent>
    • Subscribe(eventType, listener) method
    • Unsubscribe(eventType, listener) method
    • Publish(eventType) method
  3. Create test scene with ClientEventBus GUI buttons

Homework Assignment (Continued)

Part 2: Subscribers (40 points)

  1. Implement 4 subscriber classes:
    • CountdownTimer: Listens for COUNTDOWN, uses coroutine, publishes START
    • BikeController: Listens for START, STOP, RESTART
    • HUDController: Listens for START, STOP, FINISH, PAUSE
    • AudioManager: (Create new!) Listens for START, FINISH, PAUSE
      • Logs appropriate sound messages
  2. All subscribers MUST use OnEnable/OnDisable pattern

Part 3: Keyboard Controls (20 points)

  • Add keyboard shortcuts to ClientEventBus:
    • C = Countdown, S = Stop, R = Restart
    • F = Finish, P = Pause, Q = Quit

Grading Rubric (100 points)

Point Breakdown

15 pts: RaceEventType enum correctly defined
25 pts: RaceEventBus static class with Subscribe/Unsubscribe/Publish
30 pts: 4 subscriber classes (CountdownTimer, BikeController, HUDController, AudioManager)
10 pts: OnEnable/OnDisable pattern used correctly in all subscribers
10 pts: Keyboard controls implemented and working
10 pts: Video demonstrates all events firing with console output

Additional Resources

Further Reading

Video Recording Requirements

  • Length: 3-5 minutes
  • Show: Code files + Unity Editor + Console output
  • Demonstrate: All 7 events working via keyboard
  • Narration: Brief explanation of your implementation

Questions & Discussion

Open Floor

  • Event Bus vs Observer - what's the difference?
  • When should I use Event Bus vs direct references?
  • How do I debug event flow?
  • What about event parameters/data?
  • Memory leak prevention questions?
  • Homework clarifications?
Office Hours: Need help with coroutines or UnityEvent? Come see me!

D2L Submission Instructions

How to Submit

  1. Log in to D2L course shell
  2. Navigate to Assignments β†’ Event Bus Pattern Implementation
  3. Upload your video file
  4. In the comments, include:
    • Your name
    • Any challenges you encountered
    • What you learned about decoupling
  5. Submit before deadline
File Size: If your video is too large for D2L, upload to YouTube (unlisted) or Google Drive and submit the link instead.
Pro Tip: Record your console output alongside your Unity editor to show all subscribers reacting to events!

Event Bus Pattern Mastered!

Today's Achievements:

Homework Due Next Class:

Event Bus + 4 Subscribers + Keyboard Controls

Video submission to D2L

Next: Command Pattern for replay systems!