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.
Developer's Note: "Never Panic Early"
โ ๏ธ What to Expect During Implementation
As we implement this pattern, we'll be creating multiple files in a specific order.
You will see errors in Unity and your IDE until all files are complete.
How to Handle Development Errors
Don't Ignore: Note the errors - they're telling you something
Don't Panic: These are expected until all files are created
Stay Calm: Follow the implementation order and errors will resolve
As Apollo 13's Fred Haise said: "Never Panic Early"
This is a critical skill for software development. Note problems, but stay focused on the implementation path.
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 classRaceManager {
public UIController ui;
public AudioManager audio;
public TimerController timer;
public ObstacleSpawner spawner;
public CameraController camera;
voidStartRace() {
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!
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
๐ File Structure Note
Create a new file: Assets/Scripts/Patterns/EventBus/RaceEventBus.cs
Both the RaceEventType enum and RaceEventBus class go in this ONE file.
Why? Simplicity for learning! In production, you'd typically separate them,
but keeping them together makes it easier to understand how they work together.
using UnityEngine;
using UnityEngine.Events;
using System.Collections.Generic;
// Enum defining all possible race eventspublic enumRaceEventType
{
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 singletonpublic static classRaceEventBus
{
// Dictionary mapping event types to UnityEventsprivate static readonly Dictionary<RaceEventType, UnityEvent>
_eventDictionary = new Dictionary<RaceEventType, UnityEvent>();
// Subscribe to an eventpublic static voidSubscribe(RaceEventType eventType, UnityAction listener)
{
// If event doesn't exist in dictionary, create itif (!_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 eventpublic static voidUnsubscribe(RaceEventType eventType, UnityAction listener)
{
// If event exists, remove the listenerif (_eventDictionary.ContainsKey(eventType))
{
_eventDictionary[eventType].RemoveListener(listener);
}
}
// Publish/broadcast an event to all subscriberspublic static voidPublish(RaceEventType eventType)
{
// If event exists, invoke all listenersif (_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
Understanding the Dictionary Pattern
The ContainsKey Pattern
All three methods (Subscribe, Unsubscribe, Publish) use the same defensive pattern:
if (_eventDictionary.ContainsKey(eventType))
{
// Safe to access the dictionary value
_eventDictionary[eventType].SomeAction();
}
Unsubscribe fails: Can't remove what doesn't exist
Publish fails: Can't invoke non-existent event
Pattern Use Cases:
โข Subscribe: Check if event exists, create if not
โข Unsubscribe: Check if event exists before removing listener
โข Publish: Check if event exists before invoking
Unity Coroutines & IEnumerator
What is a Coroutine?
A coroutine is a special method that can pause execution and resume later,
allowing time-based operations without blocking the main game loop.
โ Regular Method
Executes completely in one frame
Can't wait or pause
Blocks until finished
โ Coroutine
Can span multiple frames
Can pause and resume
Doesn't block execution
// Coroutine signature - returns IEnumeratorprivate IEnumerator Countdown()
{
// Do some work
Debug.Log("3...");
// Pause for 1 second (yield control back to Unity)yield return new WaitForSeconds(1.0f);
// Resume here after 1 second
Debug.Log("2...");
yield return new WaitForSeconds(1.0f);
Debug.Log("GO!");
}
// Start the coroutine from another methodvoidStart()
{
StartCoroutine(Countdown()); // Kicks off the coroutine
}
Key Points: Return type is IEnumerator, use yield return to pause,
must call StartCoroutine() to execute!
CountdownTimer Subscriber Implementation
๐ File Structure Note - PRODUCTION CODE
Create a new file: Assets/Scripts/UI/CountdownTimer.cs
This subscriber demonstrates how to use coroutines with the Event Bus pattern. โ ๏ธ This code goes into your Unity project for Blade Racer.
Update existing file: Assets/Scripts/Controllers/BikeController.cs
You already have this file from the State Pattern lecture. Add Event Bus subscriptions to it!
Highlighted comments show the code to add.
using UnityEngine;
public classBikeController : MonoBehaviour
{
// ... existing fields and Start() method stay the same ...// ADD: Subscribe to race events when component is enabledvoidOnEnable()
{
RaceEventBus.Subscribe(RaceEventType.START, StartBike);
RaceEventBus.Subscribe(RaceEventType.STOP, StopBike);
RaceEventBus.Subscribe(RaceEventType.RESTART, RestartBike);
}
// ADD: Unsubscribe when component is disabledvoidOnDisable()
{
RaceEventBus.Unsubscribe(RaceEventType.START, StartBike);
RaceEventBus.Unsubscribe(RaceEventType.STOP, StopBike);
RaceEventBus.Unsubscribe(RaceEventType.RESTART, RestartBike);
}
// Your existing StartBike() and StopBike() methods work as-is!// They already have the right signature for event handlers// ADD: New method for restart functionalitypublic voidRestartBike()
{
StopBike(); // Stop the bike first
transform.position = Vector3.zero; // Reset position
transform.rotation = Quaternion.identity; // Reset rotation
}
}
Key Point: Your existing StartBike() and StopBike() methods already exist!
We're just connecting them to the Event Bus. No need to rewrite working code!
HUDController Subscriber Implementation
๐ File Structure Note - PRODUCTION CODE
Create a new file: Assets/Scripts/UI/HUDController.cs
This subscriber shows how UI elements respond to Event Bus messages. โ ๏ธ This code goes into your Unity project for Blade Racer.
using UnityEngine;
public classHUDController : MonoBehaviour
{
voidOnEnable()
{
// 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);
}
voidOnDisable()
{
// Clean up subscriptions
RaceEventBus.Unsubscribe(RaceEventType.START, DisplayStartMessage);
RaceEventBus.Unsubscribe(RaceEventType.STOP, DisplayStopMessage);
RaceEventBus.Unsubscribe(RaceEventType.FINISH, DisplayFinishMessage);
RaceEventBus.Unsubscribe(RaceEventType.PAUSE, DisplayPauseMessage);
}
voidDisplayStartMessage()
{
Debug.Log("[HUD] GO! Race has started!");
// In real implementation: Update UI text, show animation
}
voidDisplayStopMessage()
{
Debug.Log("[HUD] Race stopped");
}
voidDisplayFinishMessage()
{
Debug.Log("[HUD] FINISH! You completed the race!");
}
voidDisplayPauseMessage()
{
Debug.Log("[HUD] PAUSED");
}
}
ClientEventBus Test Script
๐ File Structure Note - TESTING CODE
Create a new file: Assets/Scripts/Testing/ClientEventBus.cs
This is a temporary test script with GUI buttons to manually trigger events. โ ๏ธ For testing only - delete after verifying the Event Bus works!
using UnityEngine;
public classClientEventBus : MonoBehaviour
{
private bool _isButtonEnabled;
voidStart() { _isButtonEnabled = true; }
voidOnEnable()
{
RaceEventBus.Subscribe(RaceEventType.START, EnableButton);
RaceEventBus.Subscribe(RaceEventType.FINISH, EnableButton);
}
voidOnDisable()
{
RaceEventBus.Unsubscribe(RaceEventType.START, EnableButton);
RaceEventBus.Unsubscribe(RaceEventType.FINISH, EnableButton);
}
voidEnableButton() { _isButtonEnabled = true; }
voidOnGUI()
{
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);
}
}
}
How This Tests Everything
๐ฎ Button Click Flow:
User clicks โ Publish event โ All subscribers react
As you implement more patterns, you'll have multiple test scripts (ClientState, ClientEventBus,
GUITestClient, etc.) each creating GUI buttons. They all use default GUILayout
positioning, which means buttons overlap in the top-left corner!
๐ File Structure Note - OPTIONAL ENHANCEMENT
Create a new file: Assets/Scripts/Testing/TestPanel.cs
This consolidates all test controls into a single draggable window with collapsible sections. ๐ก This is optional - you can continue using individual test scripts if you prefer!
Alternative: Use separate GUILayout.Window()
calls with different window IDs for each pattern's test controls.
TestPanel Section Methods
// Continuing TestPanel class...voidDrawStatePatternSection()
{
if (_bikeController == null) return; // Only show if component exists
GUI.backgroundColor = Color.cyan;
_stateExpanded = GUILayout.Toggle(_stateExpanded, "โผ State Pattern", "button");
GUI.backgroundColor = Color.white;
if (_stateExpanded)
{
GUILayout.BeginVertical("box");
if (GUILayout.Button("Start Bike")) _bikeController.StartBike();
if (GUILayout.Button("Stop Bike")) _bikeController.StopBike();
if (GUILayout.Button("Turn Left")) _bikeController.Turn(Direction.Left);
if (GUILayout.Button("Turn Right")) _bikeController.Turn(Direction.Right);
GUILayout.EndVertical();
}
}
voidDrawEventBusSection()
{
GUI.backgroundColor = Color.yellow;
_eventBusExpanded = GUILayout.Toggle(_eventBusExpanded, "โผ Event Bus", "button");
GUI.backgroundColor = Color.white;
if (_eventBusExpanded)
{
GUILayout.BeginVertical("box");
if (GUILayout.Button("Countdown")) RaceEventBus.Publish(RaceEventType.COUNTDOWN);
if (GUILayout.Button("Stop")) RaceEventBus.Publish(RaceEventType.STOP);
if (GUILayout.Button("Restart")) RaceEventBus.Publish(RaceEventType.RESTART);
if (GUILayout.Button("Finish")) RaceEventBus.Publish(RaceEventType.FINISH);
if (GUILayout.Button("Pause")) RaceEventBus.Publish(RaceEventType.PAUSE);
GUILayout.EndVertical();
}
}
// Uncomment after Lecture 5 (Command Pattern) when Invoker class exists:/*
void DrawCommandSection()
{
if (_invoker == null) return;
GUI.backgroundColor = Color.green;
_commandExpanded = GUILayout.Toggle(_commandExpanded, "โผ Command Pattern", "button");
GUI.backgroundColor = Color.white;
if (_commandExpanded)
{
GUILayout.BeginVertical("box");
if (GUILayout.Button("Start Recording")) _invoker.StartRecording();
if (GUILayout.Button("Stop Recording")) _invoker.StopRecording();
if (GUILayout.Button("Play Replay")) _invoker.StartReplay();
GUILayout.EndVertical();
}
}
*/
Pattern: Each section checks if its required component exists before rendering.
Color-coded headers make sections easy to identify at a glance!
Testing the Event Bus in Blade Racer
In-Game Test Setup
Open your Blade Racer project
Find your bike GameObject (should already have BikeController)
Create empty GameObject "RaceManager"
Attach CountdownTimer and HUDController to RaceManager
Attach ClientEventBus script to RaceManager
Play the game and use GUI buttons to publish events
Watch console for subscriber responses!
Testing Strategy: Use GUI buttons to manually trigger race events.
Watch the console to verify CountdownTimer, BikeController, and HUDController all respond!
Expected Console Output: When you click "Start Countdown", you should see
messages from all three subscribers responding to the START event after the countdown!
Hands-On Implementation
35-Minute Implementation Challenge
Implement the complete Event Bus system:
Create RaceEventType enum (7 events)
Create RaceEventBus static class (Subscribe, Unsubscribe, Publish)
BBS Culture: The First Online Gaming Communities (1980s)
Before the internet as we know it, Bulletin Board Systems (BBS) connected gamers through dial-up modems. A player would call a BBS (literally dialing a phone number), connect at 300-2400 baud, and enter a text-based world. Games like Trade Wars 2002 and Legend of the Red Dragon were turn-based because only one player could connect at a time.
The BBS was event-driven: when you took your turn, the system published events - "Player attacked monster," "Player bought ship," "High score achieved." Other players would see these events when they dialed in later. Message boards let players communicate asynchronously. The SysOp (system operator) managed it all from their home computer!
Connection to Event Bus Pattern
BBS games pioneered event-driven architecture! The system published events (player actions, game state changes) to a central "board" that all players could read. Just like our Event Bus broadcasts RACE_START or COUNTDOWN_START to all subscribers, BBS games broadcast player events to anyone listening. This asynchronous, decoupled communication model became the foundation for modern online gaming!
voidOnEnable()
{
// Subscribe when component becomes active
RaceEventBus.Subscribe(RaceEventType.START, MyMethod);
}
voidOnDisable()
{
// 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
Using Outside Resources & Giving Credit
๐ต Freesound.org - Your Audio Resource
freesound.org is an excellent free resource for sound effects and audio clips.
Perfect for adding countdown beeps, race start sounds, and other audio to your projects!
Creating a Credits File
Always document outside resources you use!
Create a file named CREDITS.txt or CREDITS.md in your project root
List all outside resources (sounds, sprites, code, tutorials)
Include URLs and author names
Note the license type (CC0, CC-BY, etc.)
Professional Practice: Proper attribution is not just ethical - it's legally required
for many licenses and expected in the industry. Start this habit now!
Homework Assignment
Assignment: Event Bus Implementation
Due: Next class Submission: Unity project + video to D2L
Part 1: Core Event Bus (40 points)
Create RaceEventBus.cs file containing:
RaceEventType enum with all 7 events (COUNTDOWN, START, STOP, FINISH, RESTART, PAUSE, QUIT)
RaceEventBus static class (both in same file!)
Implement RaceEventBus class with:
Dictionary<RaceEventType, UnityEvent>
Subscribe(eventType, listener) method
Unsubscribe(eventType, listener) method
Publish(eventType) method
Create test scene with ClientEventBus GUI buttons
Homework Assignment (Continued)
Part 2: Subscribers (40 points)
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
All subscribers MUST use OnEnable/OnDisable pattern
Part 3: Keyboard Controls via TestPanel (20 points)
Add keyboard shortcuts to TestPanel.cs (not ClientEventBus):
C = Countdown, S = Stop, R = Restart
F = Finish, P = Pause, Q = Quit
Add a "Show Keymap" button to TestPanel that displays the shortcuts
See the next slide for the TestPanel keyboard implementation!
TestPanel Keyboard Controls
๐ Adding Keyboard Shortcuts to TestPanel.cs
Add these features to your TestPanel.cs class to centralize all keyboard shortcuts
and provide a helpful keymap popup for users.
// Add these fields to TestPanel classprivate bool _showKeymap = false;
private Rect _keymapRect = new Rect(250, 10, 200, 250);
voidUpdate()
{
// Event Bus keyboard shortcutsif (Input.GetKeyDown(KeyCode.C))
RaceEventBus.Publish(RaceEventType.COUNTDOWN);
if (Input.GetKeyDown(KeyCode.S))
RaceEventBus.Publish(RaceEventType.STOP);
if (Input.GetKeyDown(KeyCode.R))
RaceEventBus.Publish(RaceEventType.RESTART);
if (Input.GetKeyDown(KeyCode.F))
RaceEventBus.Publish(RaceEventType.FINISH);
if (Input.GetKeyDown(KeyCode.P))
RaceEventBus.Publish(RaceEventType.PAUSE);
if (Input.GetKeyDown(KeyCode.Q))
RaceEventBus.Publish(RaceEventType.QUIT);
// Toggle keymap with K keyif (Input.GetKeyDown(KeyCode.K))
_showKeymap = !_showKeymap;
}
voidOnGUI()
{
_windowRect = GUILayout.Window(0, _windowRect,
DrawWindow, "Test Panel");
// Show keymap window if enabledif (_showKeymap)
_keymapRect = GUILayout.Window(1, _keymapRect,
DrawKeymapWindow, "Keyboard Shortcuts");
}