From: Game Development Patterns with Unity 2021 (2nd Edition)
By David Baron
Note: This is one of the most important patterns for game architecture!
Also called Publish-Subscribe or Event-Driven programming.
Today's Learning Objectives
What We'll Master
π― Understand tight vs loose coupling
π― Learn Observer Pattern (Subject-Observer)
π― Implement BikeController as Subject
π― Create HUD and Camera as Observers
π― Use C# events and delegates
Goal: Build a bike system where the HUD, camera, and audio
respond to bike events WITHOUT the bike knowing they exist!
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: Tightly Coupled Code
β Without Observer Pattern
public classBikeController : MonoBehaviour
{
public HUDController hud;
public CameraController camera;
public AudioController audio;
voidTakeDamage(float amount)
{
health -= amount;
// TIGHT COUPLING - BikeController knows about everyone!
hud.UpdateHealthBar(health);
camera.ShakeCamera();
audio.PlayDamageSound();
// What if we add more systems? Have to modify this class!
}
}
Problems: Hard dependencies, can't add features without modifying BikeController,
difficult to test, breaks Open/Closed Principle
β οΈ Maintainability Nightmare: Adding a new feature (like particle effects on damage)
requires editing BikeController, creating merge conflicts and breaking existing code!
What is the Observer Pattern?
Observer Pattern
A behavioral pattern that defines a one-to-many dependency
between objects so that when one object changes state,
all its dependents are notified automatically.
In Simple Terms: Like a newsletter - the publisher (Subject) sends updates,
subscribers (Observers) receive them. The publisher doesn't need to know who subscribes!
Observer Pattern: Benefits & Drawbacks
β Benefits
Loose Coupling: Subject doesn't know observers
Extensibility: Add observers without modifying subject
Dynamic: Subscribe/unsubscribe at runtime
Open/Closed: Open for extension, closed for modification
Testability: Mock observers easily
β οΈ Drawbacks
Memory Leaks: Forgetting to unsubscribe
Unpredictable Order: Observer execution order undefined
Debugging: Hard to trace event flow
Performance: Many observers = overhead
Cascading Updates: Chain reactions possible
Golden Rule: Always unsubscribe in OnDestroy() to avoid memory leaks!
Remember: event = special delegate that only allows += and -= (no direct invocation from outside)
Implementation Step 1: BikeController (Subject)
β Modifying Existing Code: You already have a BikeController.cs
from previous lectures. We'll ADD Observer events to it!
π File Structure Note - PRODUCTION CODE
Modify existing file: Assets/Scripts/Controllers/BikeController.cs
Add event delegates for observers to subscribe to. β οΈ This code goes into your Unity project for Blade Racer.
public classBikeController : MonoBehaviour
{
// ... existing fields ...// NEW: Observer Events (Publishers)public event System.Action<float> OnDamage;
public event System.Action OnTurboStart;
public event System.Action OnTurboEnd;
// NEW: Health statepublic float health = 100f;
public float maxHealth = 100f;
public bool isTurboActive;
// NEW: Method that triggers eventpublic voidTakeDamage(float amount)
{
health -= amount;
health = Mathf.Max(health, 0);
OnDamage?.Invoke(amount); // Fire!
}
public voidActivateTurbo()
{
if (!isTurboActive)
{
isTurboActive = true;
OnTurboStart?.Invoke();
}
}
public voidDeactivateTurbo()
{
if (isTurboActive)
{
isTurboActive = false;
OnTurboEnd?.Invoke();
}
}
}
Declaring Events (Subject Side)
Event Declaration: public event System.Action<float> OnDamage;
Creates an event that passes a float parameter
No Parameters: public event System.Action OnTurboStart;
Event with no data - just a signal
The ?.Invoke() Pattern:
?. = null-safe operator
Only fires if someone is listening
Prevents null reference errors
Key insight: BikeController doesn't know WHO listens - it just broadcasts!
Implementation Step 2: HUD Observer
β Modifying Existing Code: You already have a HUDController.cs
from the Event Bus lecture. We'll ADD Observer subscriptions to it!
π File Structure Note - PRODUCTION CODE
Modify existing file: Assets/Scripts/UI/HUDController.cs
Subscribe to BikeController events to update UI displays. β οΈ This code goes into your Unity project for Blade Racer.
Event Bus (before): RaceEventBus.Subscribe(event, handler)
Uses a static class as middleman
Direct Observer (now): _bikeController.OnDamage += HandleDamage
Subscribe directly to the source object
How += works:
+= adds your method to the event's subscriber list
When event fires, your method gets called
-= removes your method from the list
Key difference: No middleman - observer talks directly to subject!
Implementation Step 3: Camera Observer
π File Structure Note - PRODUCTION CODE
Create a new file: Assets/Scripts/Controllers/CameraController.cs
Observer that adjusts camera based on bike speed events. β οΈ This code goes into your Unity project for Blade Racer.
using UnityEngine;
using System.Collections;
public classCameraController : MonoBehaviour
{
public float shakeIntensity = 0.3f;
public float shakeDuration = 0.5f;
private BikeController _bikeController;
private Vector3 _originalPosition;
voidStart()
{
_bikeController = FindObjectOfType<BikeController>();
_originalPosition = transform.position;
_bikeController.OnDamage += HandleDamage;
}
private voidHandleDamage(float amount)
{
float intensity = shakeIntensity * (amount / 25f);
StartCoroutine(ShakeCamera(intensity));
}
private IEnumerator ShakeCamera(float intensity)
{
float elapsed = 0f;
while (elapsed < shakeDuration)
{
float x = Random.Range(-1f, 1f) * intensity;
float y = Random.Range(-1f, 1f) * intensity;
transform.position = _originalPosition + new Vector3(x, y, 0);
elapsed += Time.deltaTime;
yield return null;
}
transform.position = _originalPosition;
}
voidOnDestroy() {
if (_bikeController != null)
_bikeController.OnDamage -= HandleDamage;
}
}
Selective Subscription
Subscribe only to what you need:
Camera only cares about OnDamage - ignores turbo events entirely!
Coroutine for Time-Based Effects:
IEnumerator = runs over multiple frames
yield return null = wait one frame
Perfect for shake, fade, lerp effects
New Script = Start/OnDestroy:
New scripts use Start() to subscribe
Use OnDestroy() to unsubscribe
Different from HUD's OnEnable/OnDisable!
Flexible: Each observer chooses which events matter to it!
Implementation Step 4: Audio Observer
π File Structure Note - PRODUCTION CODE
Create a new file: Assets/Scripts/Audio/AudioController.cs
Observer that adjusts audio based on bike speed events. β οΈ This code goes into your Unity project for Blade Racer.
Verify TestPanel is in the scene with the Observer Pattern section
Run and press H for damage (Hit), B for turbo (Boost)!
Expected Result: When BikeController takes damage, console shows
messages from HUD, Camera, AND Audio - all responding to the same event!
TestPanel: Observer Fields & Input
π Update TestPanel.cs
Add these fields and keyboard shortcuts to your existing TestPanel.cs.
// Add to TestPanel fieldsprivate bool _observerExpanded = true;
// _bikeController already exists from Command Pattern!// No new Start() code needed.// Add to Update() - Observer Pattern shortcutsif (_bikeController != null)
{
// H = Hit (Take Damage)if (Input.GetKeyDown(KeyCode.H))
_bikeController.TakeDamage(25f);
// B = Boost (Toggle Turbo)if (Input.GetKeyDown(KeyCode.B))
{
if (_bikeController.isTurboActive)
_bikeController.DeactivateTurbo();
else
_bikeController.ActivateTurbo();
}
}
Code Breakdown
Reusing _bikeController: The Command Pattern section already added this field and finds it in Start(). No duplicate code needed!
Input.GetKeyDown() - Returns true only on the first frame a key is pressed. Prevents repeated firing while held β perfect for discrete events.
H key (Hit): Deals 25 damage to the bike. All observers subscribed to OnDamage will respond (HUD, Camera, Audio).
B key (Boost): Toggles turbo on/off. All observers subscribed to OnTurboStart will respond.
Note: Null check on _bikeController prevents errors if BikeController isn't in the scene.
TestPanel: GUI Section & Keymap
Add DrawObserverSection() and call it from DrawWindow(). Update the keymap.
voidDrawObserverSection()
{
if (_bikeController == null) return;
GUI.backgroundColor = Color.magenta;
_observerExpanded = GUILayout.Toggle(
_observerExpanded, "βΌ Observer Pattern", "button");
GUI.backgroundColor = Color.white;
if (_observerExpanded)
{
GUILayout.BeginVertical("box");
if (GUILayout.Button("Take Damage (H)"))
_bikeController.TakeDamage(25f);
if (GUILayout.Button("Toggle Turbo (B)"))
{
if (_bikeController.isTurboActive)
_bikeController.DeactivateTurbo();
else
_bikeController.ActivateTurbo();
}
GUILayout.Label($"Health: {_bikeController.health:F0}");
GUILayout.Label($"Turbo: {(_bikeController.isTurboActive ? "ON" : "OFF")}");
GUILayout.EndVertical();
}
}
// In DrawWindow(), add:
DrawObserverSection();
// Add to DrawKeymapWindow():
GUILayout.Label("--- Observer Pattern ---");
GUILayout.Label("H = Hit (Take Damage)");
GUILayout.Label("B = Boost (Toggle Turbo)");
GUI Section Pattern
Collapsible header: Magenta toggle button expands/collapses the section. Each pattern gets a unique color (green = Command, cyan = Object Pool, magenta = Observer).
Live status display: Health and Turbo state update every frame so you can see observers responding in real-time.
:F0 - Format specifier: F = fixed-point, 0 = zero decimal places. Shows 75 instead of 75.00000.
TestPanel Evolution
Lecture 4: Event Bus section + keymap
Lecture 5: + Command Pattern section
Lecture 6: + Object Pool section
Lecture 7: + Observer Pattern section
Don't forget: Call DrawObserverSection() in DrawWindow()!
Critical: Preventing Memory Leaks
β οΈ The #1 Observer Pattern Bug
Forgetting to unsubscribe! When an observer is destroyed,
but doesn't unsubscribe, the subject keeps a reference to it - memory leak!
β Without Unsubscribe
voidOnEnable()
{
bike.OnDamage += HandleDamage;
}
// Observer disabled/destroyed// bike still has reference!// MEMORY LEAK!
β With Unsubscribe
voidOnDisable()
{
if (bike != null)
{
bike.OnDamage -= HandleDamage;
}
}
// Clean unsubscribe!// No memory leak!
Best Practice: Always pair += with -= in matching methods! OnEnable/OnDisable or Start/OnDestroy
Alternative: Unity Events
using UnityEngine;
using UnityEngine.Events;
public classBikeController : MonoBehaviour
{
// UnityEvent shows up in Inspector!public UnityEvent<float> OnDamage;
public UnityEvent OnTurboStart;
voidStart()
{
// Initialize events
OnDamage = new UnityEvent<float>();
OnTurboStart = new UnityEvent();
}
public voidTakeDamage(float amount)
{
health -= amount;
OnDamage?.Invoke(amount);
}
}
UnityEvent Benefits
β Visible in Unity Inspector
β Wire up listeners without code
β Great for designers
β οΈ Slower than C# events (reflection overhead)
Hands-On Implementation π»
40-Minute Implementation Challenge
Implement the Observer Pattern:
Add OnDamage and OnTurboStart events to BikeController
Add observer subscriptions to HUDController (subscribes to both events)
Create CameraController observer (subscribes to OnDamage)
Create AudioController observer (subscribes to both events)
Implement proper unsubscribe in OnDestroy()
Add Observer section to TestPanel (H/B keyboard shortcuts)
Test and verify all observers respond
Gaming History Moment πΉοΈ
Atari Era & Third-Party Developers (1979-1983)
In 1979, four frustrated Atari programmers quit to form Activision - the first third-party game developer. Until then, Atari controlled everything about their platform. When Activision announced they'd publish games for the Atari 2600 without permission, Atari sued. They lost. This opened the floodgates.
Suddenly, anyone could publish Atari games. Publishers had to watch the market constantly - observe what sold, what failed, what Atari was doing, what competitors released. Companies like Parker Brothers, Coleco, and Imagic all monitored Atari's moves and market trends, reacting to changes. This observation-driven development led to the 1983 crash when too many publishers flooded the market with low-quality games nobody was watching for.
Connection to Observer Pattern
The third-party developer ecosystem was an Observer Pattern in action! Publishers (observers) subscribed to market changes (subject). When Atari announced a price drop, competitors observed and reacted. When a game type sold well, observers noticed and created similar games. The pattern broke down during the crash because too many observers reacting to the same signals caused market saturation. In code, proper Observer Pattern prevents this chaos through controlled, decoupled notifications!
Use when: M-to-N relationship, full decoupling needed
Remember Chapter 6? Event Bus is a global version of Observer Pattern!
Observer = direct subscription, Event Bus = centralized broker.
Observer Pattern in Game Dev
Achievement Systems
Subscribe to player actions: kills, deaths, distance traveled
UI Updates
HUD, health bars, minimaps observe player/game state
Analytics
Track events for telemetry without modifying gameplay code
Audio
Sound effects respond to game events automatically
Save Systems
Auto-save on important events (level complete, boss defeated)
Multiplayer
Network layer observes local events, sends to server
Common Observer Pattern Mistakes
β Common Mistakes
Forgetting to unsubscribe (memory leaks)
Subscribing in OnEnable, forgetting OnDisable
Null reference from destroyed observers
Circular event dependencies
Too many observers (performance hit)
Modifying observer list during iteration
β Best Practices
Always unsubscribe in OnDestroy()
Null-check before unsubscribing
Use ?. (null-conditional) when invoking
Keep event handlers lightweight
Document what events are available
Consider event pooling for frequent events
Debug Tip: If your game slows down over time, you likely have
a memory leak from observers not unsubscribing!
Homework Assignment π
Assignment: BikeController Observer System
Due: Next class Submission: Unity project + video to D2L
Part 1: Core Implementation (50 points)
Add 3 events and the methods that fire them to your existing BikeController:
OnDamage event (float damage) β fired by TakeDamage()
OnTurboStart event β fired by ActivateTurbo()
OnTurboEnd event β fired by DeactivateTurbo()
Set up 3 observers: Update HUDController, create CameraController and AudioController
Each observer subscribes to relevant events
Implement proper OnDestroy() unsubscribe in all observers
Add keyboard controls: H = hit (damage), B = boost (toggle turbo)
Homework Assignment (Continued)
Part 2: Advanced Features (50 points)
Add Fourth Event: OnHealthCritical (health < 25%)
HUD flashes red
Camera gets red tint (optional visual)
Audio plays warning sound
Implement Real Camera Shake:
Use Coroutine to shake over time
Intensity scales with damage amount
Add GUI Display:
Show current health
Show turbo status
Show event count (how many times each event fired)
Test Memory Leaks:
Create/Destroy observers dynamically
Verify no console errors
Video Submission Guidelines
Recording Checklist (3-5 minutes)
Show Code: BikeController events, all three observers
Show Hierarchy: All GameObjects in scene
Demonstrate Events:
Press H several times (show damage event)
Show console logs from all observers
Show camera shake effect
Press B to toggle turbo (show turbo events)
Show Critical Health: Damage until health < 25%
Show GUI: Health, turbo status, event counters
Explain: How Observer Pattern decouples components
Walkthrough Step 1: OnHealthCritical Event
π File Structure Note - PRODUCTION CODE
Modify existing file: Assets/Scripts/Controllers/BikeController.cs
Add a fourth event that fires when health drops below 25%. β οΈ This code goes into your Unity project for Blade Racer.
HUD: Flashes the screen red to warn the player. Could use a coroutine to pulse the color on and off.
Camera: Tints the background red. Camera.main.backgroundColor changes the clear color β visible when there's no skybox.
Audio: Plays a warning alarm. The PlayOneShot call is commented out since we don't have audio clips yet.
Subscribe/Unsubscribe symmetry: Every += in OnEnable/Start gets a matching -= in OnDisable/OnDestroy. Same pattern as before!
Key insight: BikeController doesn't know these responses exist β pure decoupling!
Walkthrough Step 3: Real Camera Shake
π File Structure Note - PRODUCTION CODE
Enhance existing: Assets/Scripts/Controllers/CameraController.cs
Replace the basic shake with a smooth, damage-scaled coroutine.
public float shakeIntensity = 0.3f;
public float shakeDuration = 0.5f;
private Vector3 _originalPosition;
private Coroutine _shakeCoroutine;private voidHandleDamage(float amount)
{
// Scale intensity by damage relative to 25float intensity = shakeIntensity * (amount / 25f);// Stop any existing shake before starting new oneif (_shakeCoroutine != null)
StopCoroutine(_shakeCoroutine);
_shakeCoroutine = StartCoroutine(ShakeCamera(intensity));
}
private IEnumerator ShakeCamera(float intensity)
{
float elapsed = 0f;
while (elapsed < shakeDuration)
{
// Decay intensity over timefloat currentIntensity =
Mathf.Lerp(intensity, 0f, elapsed / shakeDuration);float x = Random.Range(-1f, 1f) * currentIntensity;
float y = Random.Range(-1f, 1f) * currentIntensity;
transform.position = _originalPosition
+ new Vector3(x, y, 0);
elapsed += Time.deltaTime;
yield return null;
}
transform.position = _originalPosition;
_shakeCoroutine = null;
}
Enhanced Shake Mechanics
Mathf.Lerp(a, b, t) β Linearly interpolates between a and b by t (0 to 1). Here it smoothly decays shake intensity from full to zero.
Intensity scaling:amount / 25f means 25 damage = normal shake, 50 damage = 2x shake, 10 damage = gentle shake.
Coroutine reference: Storing _shakeCoroutine lets us stop an in-progress shake before starting a new one β prevents overlapping shakes from stacking.
Reset position: After the loop completes, camera snaps back to _originalPosition β no drift over time.
Pro tip:yield return null waits one frame β the while loop runs once per frame for smooth animation!
Walkthrough Step 4: Event Counters
π File Structure Note - PRODUCTION CODE
Modify existing file: Assets/Scripts/Controllers/BikeController.cs
Add public counters so the GUI can display how many times each event has fired.
public classBikeController : MonoBehaviour
{
// Existing fields...// NEW: Event counters for GUI displaypublic int damageCount { get; private set; }
public int turboCount { get; private set; }
public int criticalCount { get; private set; }public voidTakeDamage(float amount)
{
health -= amount;
health = Mathf.Max(health, 0);
damageCount++;
OnDamage?.Invoke(amount);
if (health / maxHealth < 0.25f && !_criticalTriggered)
{
_criticalTriggered = true;
criticalCount++;
OnHealthCritical?.Invoke();
}
}
public voidActivateTurbo()
{
if (!isTurboActive)
{
isTurboActive = true;
turboCount++;
OnTurboStart?.Invoke();
}
}
}
Tracking Event History
{ get; private set; } β Auto-property with public read, private write. Other scripts can read the count but only BikeController can increment it.
Increment before Invoke: Counter updates before the event fires, so observers see the correct count if they check it during their handler.
Three counters:
damageCount β every hit
turboCount β every turbo activation
criticalCount β should only reach 1
Why public counters? The GUI needs to read these values every frame to display live stats!
Walkthrough Step 5: GUI Display
π File Structure Note - PRODUCTION CODE
Modify existing file: Assets/Scripts/UI/HUDController.cs
Add an OnGUI method to display health, turbo status, and event counters.