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.
Expected Result: When BikeController takes damage, console shows
messages from HUD, Camera, AND Audio - all responding to the same event!
Implementation Step 5: Test Client
π File Structure Note - TESTING CODE
Create a new file: Assets/Scripts/Testing/TestClient.cs
Test client that simulates bike events with GUI buttons. β οΈ This is temporary testing code - you can remove it after testing your implementation.
using UnityEngine;
public classTestClient : MonoBehaviour
{
private BikeController _bikeController;
voidStart()
{
_bikeController = FindObjectOfType<BikeController>();
}
voidUpdate()
{
// Press D to take damageif (Input.GetKeyDown(KeyCode.D))
_bikeController.TakeDamage(25f);
// Press T to toggle turboif (Input.GetKeyDown(KeyCode.T))
{
if (_bikeController.isTurboActive)
_bikeController.DeactivateTurbo();
else
_bikeController.ActivateTurbo();
}
}
voidOnGUI()
{
GUILayout.Label("Press D: Take Damage");
GUILayout.Label("Press T: Toggle Turbo");
GUILayout.Label($"Health: {_bikeController.health:F0}");
GUILayout.Label($"Turbo: {(_bikeController.isTurboActive ? "ON" : "OFF")}");
}
}
Testing Your Observer System
TestClient's Role:
Simulates game events via keyboard so you can verify all observers respond correctly.
Input.GetKeyDown:
Returns true only on first frame key is pressed
Prevents repeated firing while held
Perfect for triggering discrete events
OnGUI for Quick Debug:
Built-in Unity debug display
No UI setup needed!
String Format :F0
{_bikeController.health:F0} F = Fixed-point (decimal number) 0 = Zero decimal places
Result: 75 instead of 75.00000
π‘ Alternative: When you have multiple test scripts with overlapping GUI buttons,
consider using TestPanel.cs to combine all test controls into one draggable window.
See the Event Bus lecture for the unified TestPanel implementation.
Updating TestPanel for Observer Pattern
π Evolving TestPanel.cs
Add Observer Pattern keyboard shortcuts and section to your TestPanel.cs.
// Add to TestPanel fieldsprivate bool _observerExpanded = true;
// Add to Update() - Observer Pattern shortcutsif (_bikeController != null)
{
// D = Take Damageif (Input.GetKeyDown(KeyCode.D))
_bikeController.TakeDamage(25f);
// T = Toggle Turboif (Input.GetKeyDown(KeyCode.T))
{
if (_bikeController.isTurboActive)
_bikeController.DeactivateTurbo();
else
_bikeController.ActivateTurbo();
}
}
Keymap Update: Don't forget to add Observer shortcuts to DrawKeymapWindow(): D = Take Damage, T = Toggle Turbo
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()
Create TestClient to trigger events via keyboard
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!