Observer Pattern - Decoupling Components

Decoupling Components

The Observer Pattern πŸ‘€

CSCI 3213 - Game Programming

One-to-many event notification - loosely coupled architecture!

Today's Content

πŸ“š Based on Chapter 9

Decoupling Components with the Observer Pattern

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 class BikeController : MonoBehaviour { public HUDController hud; public CameraController camera; public AudioController audio; void TakeDamage(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.

βœ… Key Concept

Broadcast events! The Subject doesn't know who's listening. Observers subscribe/unsubscribe independently. Fully decoupled!

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!

Observer Pattern UML Diagram

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚   BikeController        β”‚  ← Subject (Publisher)
│─────────────────────────│
β”‚ + OnDamage event        β”‚
β”‚ + OnTurboStart event    β”‚
β”‚ + TakeDamage()          β”‚
β”‚ + ActivateTurbo()       β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
         β”‚
         β”‚ notifies
         β–Ό
    πŸ“‘ EVENT BROADCAST
         β”‚
    β”Œβ”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
    β–Ό                          β–Ό                    β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”      β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ HUDControllerβ”‚      β”‚CameraFollowerβ”‚    β”‚AudioControllerβ”‚  ← Observers
│──────────────│      │──────────────│    │──────────────│
β”‚ + OnNotify() β”‚      β”‚ + OnNotify() β”‚    β”‚ + OnNotify() β”‚
β”‚ - UpdateHUD()β”‚      β”‚ - Shake()    β”‚    β”‚ - PlaySFX()  β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜      β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜    β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                
Key: Subject has events, Observers subscribe to events. When subject fires event, all observers are notified automatically!

Event Notification Flow

BIKE
TAKES DAMAGE
β†’
πŸ“‘

OnDamage Event

β†’
HUD Updates
Camera Shakes
Audio Plays

Sequence of Events

  1. BikeController.TakeDamage() is called
  2. BikeController fires OnDamage?.Invoke(damageAmount)
  3. All subscribed observers receive notification
  4. Each observer handles event independently
  5. BikeController continues execution (doesn't wait)

C# Events & Delegates Primer

Built-In Observer Pattern

C# has native support for the Observer Pattern through events and delegates!

// DELEGATE: Function signature (what the event looks like) public delegate void DamageHandler(float damage); // EVENT: Publisher declares event public event DamageHandler OnDamage; // INVOKE: Publisher fires event OnDamage?.Invoke(25f); // ? = null-safe // SUBSCRIBE: Observer registers handler bikeController.OnDamage += HandleDamage; // UNSUBSCRIBE: Observer removes handler bikeController.OnDamage -= HandleDamage;
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 class BikeController : 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 state public float health = 100f; public float maxHealth = 100f; public bool isTurboActive; // NEW: Method that triggers event public void TakeDamage(float amount) { health -= amount; health = Mathf.Max(health, 0); OnDamage?.Invoke(amount); // Fire! } public void ActivateTurbo() { if (!isTurboActive) { isTurboActive = true; OnTurboStart?.Invoke(); } } public void DeactivateTurbo() { 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.

public class HUDController : MonoBehaviour { private BikeController _bikeController; void OnEnable() { // Event Bus (from previous lecture) RaceEventBus.Subscribe(RaceEventType.START, DisplayStartMessage); // NEW: Direct Observer subscription _bikeController = FindObjectOfType<BikeController>(); _bikeController.OnDamage += HandleDamage; _bikeController.OnTurboStart += HandleTurboStart; } void OnDisable() { RaceEventBus.Unsubscribe(RaceEventType.START, DisplayStartMessage); if (_bikeController != null) { _bikeController.OnDamage -= HandleDamage; _bikeController.OnTurboStart -= HandleTurboStart; } } void HandleDamage(float amount) => Debug.Log($"[HUD] Health updated"); }

Understanding += and -=

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 class CameraController : MonoBehaviour { public float shakeIntensity = 0.3f; public float shakeDuration = 0.5f; private BikeController _bikeController; private Vector3 _originalPosition; void Start() { _bikeController = FindObjectOfType<BikeController>(); _originalPosition = transform.position; _bikeController.OnDamage += HandleDamage; } private void HandleDamage(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; } void OnDestroy() { 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.

using UnityEngine; public class AudioController : MonoBehaviour { private BikeController _bikeController; void Start() { _bikeController = FindObjectOfType<BikeController>(); // Subscribe to ALL bike events _bikeController.OnDamage += HandleDamage; _bikeController.OnTurboStart += HandleTurboStart; _bikeController.OnTurboEnd += HandleTurboEnd; } void HandleDamage(float amount) { Debug.Log("[Audio] Playing damage sound"); // AudioSource.PlayOneShot(damageClip); } void HandleTurboStart() => Debug.Log("[Audio] Turbo sound ON"); void HandleTurboEnd() => Debug.Log("[Audio] Turbo sound OFF"); void OnDestroy() { if (_bikeController != null) { _bikeController.OnDamage -= HandleDamage; _bikeController.OnTurboStart -= HandleTurboStart; _bikeController.OnTurboEnd -= HandleTurboEnd; } } }

Multiple Event Subscriptions

Audio needs ALL events:
Unlike Camera, AudioController subscribes to damage AND turbo events.

Handler Naming Convention:
  • Handle[EventName] pattern
  • Clear what each method responds to
  • Easy to match with += subscriptions
Unsubscribe Symmetry:
  • Every += needs a matching -=
  • Same method reference in both
  • Prevents memory leaks!

Pattern: One observer can listen to many events from one subject!

Testing the Observer Pattern

βœ… Use Your Existing Scene: Open one of your Blade Racer scenes from previous lectures. You already have BikeController and HUDController set up!

Add the New Components

  1. Select your existing Main Camera in Hierarchy β†’ Add Component β†’ CameraController
  2. Create empty GameObject "AudioManager" β†’ Add Component β†’ AudioController
  3. Create empty GameObject "TestClient" β†’ Add Component β†’ TestClient
  4. Run and press D for damage, T for turbo!
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 class TestClient : MonoBehaviour { private BikeController _bikeController; void Start() { _bikeController = FindObjectOfType<BikeController>(); } void Update() { // Press D to take damage if (Input.GetKeyDown(KeyCode.D)) _bikeController.TakeDamage(25f); // Press T to toggle turbo if (Input.GetKeyDown(KeyCode.T)) { if (_bikeController.isTurboActive) _bikeController.DeactivateTurbo(); else _bikeController.ActivateTurbo(); } } void OnGUI() { 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 fields private bool _observerExpanded = true; // Add to Update() - Observer Pattern shortcuts if (_bikeController != null) { // D = Take Damage if (Input.GetKeyDown(KeyCode.D)) _bikeController.TakeDamage(25f); // T = Toggle Turbo if (Input.GetKeyDown(KeyCode.T)) { if (_bikeController.isTurboActive) _bikeController.DeactivateTurbo(); else _bikeController.ActivateTurbo(); } }
// Add DrawObserverSection() method void DrawObserverSection() { 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 (D)")) _bikeController.TakeDamage(25f); if (GUILayout.Button("Toggle Turbo (T)")) { if (_bikeController.isTurboActive) _bikeController.DeactivateTurbo(); else _bikeController.ActivateTurbo(); } GUILayout.Label($"Health: {_bikeController.health:F0}"); GUILayout.Label($"Turbo: {(_bikeController.isTurboActive ? \"ON\" : \"OFF\")}"); GUILayout.EndVertical(); } }
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

void OnEnable() { bike.OnDamage += HandleDamage; } // Observer disabled/destroyed // bike still has reference! // MEMORY LEAK!

βœ… With Unsubscribe

void OnDisable() { 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 class BikeController : MonoBehaviour { // UnityEvent shows up in Inspector! public UnityEvent<float> OnDamage; public UnityEvent OnTurboStart; void Start() { // Initialize events OnDamage = new UnityEvent<float>(); OnTurboStart = new UnityEvent(); } public void TakeDamage(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:

  1. Add OnDamage and OnTurboStart events to BikeController
  2. Add observer subscriptions to HUDController (subscribes to both events)
  3. Create CameraController observer (subscribes to OnDamage)
  4. Create AudioController observer (subscribes to both events)
  5. Implement proper unsubscribe in OnDestroy()
  6. Create TestClient to trigger events via keyboard
  7. 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!

Learn More: Racing the Beam: The Atari VCS | Atari Age Magazine Archive

Observer Pattern vs Event Bus

Observer Pattern (Direct)

Observers know about the Subject

bike.OnDamage += HandleDamage;

Use when: 1-to-N relationship, observers know their subject

Event Bus (Indirect)

Everyone uses global message bus

EventBus.Subscribe("DAMAGE", HandleDamage); EventBus.Publish("DAMAGE", amount);

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)

  1. Add 3 events to your existing BikeController:
    • OnDamage (float damage)
    • OnTurboStart ()
    • OnTurboEnd ()
  2. Set up 3 observers: Update HUDController, create CameraController and AudioController
  3. Each observer subscribes to relevant events
  4. Implement proper OnDestroy() unsubscribe in all observers
  5. Add keyboard controls: D = damage, T = toggle turbo

Homework Assignment (Continued)

Part 2: Advanced Features (50 points)

  1. Add Fourth Event: OnHealthCritical (health < 25%)
    • HUD flashes red
    • Camera gets red tint (optional visual)
    • Audio plays warning sound
  2. Implement Real Camera Shake:
    • Use Coroutine to shake over time
    • Intensity scales with damage amount
  3. Add GUI Display:
    • Show current health
    • Show turbo status
    • Show event count (how many times each event fired)
  4. Test Memory Leaks:
    • Create/Destroy observers dynamically
    • Verify no console errors

Video Submission Guidelines

Recording Checklist (3-5 minutes)

  1. Show Code: BikeController events, all three observers
  2. Show Hierarchy: All GameObjects in scene
  3. Demonstrate Events:
    • Press D several times (show damage event)
    • Show console logs from all observers
    • Show camera shake effect
    • Press T to toggle turbo (show turbo events)
  4. Show Critical Health: Damage until health < 25%
  5. Show GUI: Health, turbo status, event counters
  6. Explain: How Observer Pattern decouples components

Grading Rubric (100 points)

Point Breakdown

25 pts: BikeController with 4 events correctly implemented (including OnHealthCritical)
25 pts: Three observers subscribe and respond to events
15 pts: Proper OnDestroy() unsubscribe in all observers
15 pts: Camera shake with Coroutine works correctly
10 pts: GUI shows health, turbo, event counters
10 pts: Video demonstrates all features with clear explanation

Additional Resources

πŸ“š Further Reading

πŸŽ₯ Unity Learn

Office Hours: Questions about delegates, events, or memory leaks? Come see me!

Questions & Discussion πŸ’¬

Open Floor

  • Difference between events and delegates?
  • When to use Observer vs Event Bus?
  • How do I prevent memory leaks?
  • UnityEvent vs C# events - which to use?
  • How many observers is too many?
  • Homework clarifications?

Observers Implemented! πŸ‘€

Today's Achievements:

Homework Due Next Class:

BikeController + 3 Observers + Critical Health Event

Video submission to D2L

Next: Visitor Pattern for power-up systems! πŸ’Ž