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"); void HandleTurboStart() => Debug.Log("[HUD] Turbo activated!"); }

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. Verify TestPanel is in the scene with the Observer Pattern section
  4. 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 fields private bool _observerExpanded = true; // _bikeController already exists from Command Pattern! // No new Start() code needed. // Add to Update() - Observer Pattern shortcuts if (_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.

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 (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

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. Add Observer section to TestPanel (H/B keyboard shortcuts)
  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 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()
  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: H = hit (damage), B = boost (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 H several times (show damage event)
    • Show console logs from all observers
    • Show camera shake effect
    • Press B 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

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.

public class BikeController : MonoBehaviour { // Existing events... public event System.Action<float> OnDamage; public event System.Action OnTurboStart; public event System.Action OnTurboEnd; // NEW: Fourth event public event System.Action OnHealthCritical; public float health = 100f; public float maxHealth = 100f; private bool _criticalTriggered = false; public void TakeDamage(float amount) { health -= amount; health = Mathf.Max(health, 0); OnDamage?.Invoke(amount); // NEW: Check critical threshold if (health / maxHealth < 0.25f && !_criticalTriggered) { _criticalTriggered = true; OnHealthCritical?.Invoke(); } } }

Why a Separate Event?

OnDamage fires every hit. OnHealthCritical fires once when health drops below 25%. Different observers need different signals.

_criticalTriggered β€” Boolean flag prevents the event from firing repeatedly. Without it, every hit below 25% would trigger critical warnings.

Threshold check: health / maxHealth < 0.25f β€” dividing by maxHealth makes it percentage-based, so it works regardless of max health value.

Pattern: One subject can have many events β€” observers pick which ones matter to them!

BikeController in Unity Inspector showing health fields

Walkthrough Step 2: Observer Critical Responses

Each observer subscribes to OnHealthCritical and responds differently. Update all three observer files.

// === HUDController.cs === // Add to OnEnable(): _bikeController.OnHealthCritical += HandleHealthCritical; // Add to OnDisable(): _bikeController.OnHealthCritical -= HandleHealthCritical; void HandleHealthCritical() { Debug.Log("[HUD] CRITICAL! Flashing red!"); // Flash HUD red - visual warning } // === CameraController.cs === // Add to Start(): _bikeController.OnHealthCritical += HandleHealthCritical; // Add to OnDestroy(): _bikeController.OnHealthCritical -= HandleHealthCritical; void HandleHealthCritical() { Debug.Log("[Camera] Red tint activated!"); Camera.main.backgroundColor = Color.red; } // === AudioController.cs === // Add to Start(): _bikeController.OnHealthCritical += HandleHealthCritical; // Add to OnDestroy(): _bikeController.OnHealthCritical -= HandleHealthCritical; void HandleHealthCritical() { Debug.Log("[Audio] Warning alarm playing!"); // AudioSource.PlayOneShot(warningClip); }

Three Observers, One Event

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 void HandleDamage(float amount) { // Scale intensity by damage relative to 25 float intensity = shakeIntensity * (amount / 25f); // Stop any existing shake before starting new one if (_shakeCoroutine != null) StopCoroutine(_shakeCoroutine); _shakeCoroutine = StartCoroutine(ShakeCamera(intensity)); } private IEnumerator ShakeCamera(float intensity) { float elapsed = 0f; while (elapsed < shakeDuration) { // Decay intensity over time float 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!

Game view showing camera shake effect during damage

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 class BikeController : MonoBehaviour { // Existing fields... // NEW: Event counters for GUI display public int damageCount { get; private set; } public int turboCount { get; private set; } public int criticalCount { get; private set; } public void TakeDamage(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 void ActivateTurbo() { 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.

// Add to HUDController.cs void OnGUI() { if (_bikeController == null) return; // Position in top-right corner float x = Screen.width - 220; float y = 10; float w = 210; float h = 160; // Background box GUI.Box(new Rect(x, y, w, h), "Observer Status"); // Health display string healthColor = _bikeController.health / _bikeController.maxHealth < 0.25f ? "red" : "white"; GUI.Label(new Rect(x+10, y+25, w-20, 20), $"<color={healthColor}>Health: " + $"{_bikeController.health:F0} / " + $"{_bikeController.maxHealth:F0}</color>"); // Turbo status string turboStatus = _bikeController.isTurboActive ? "<color=cyan>TURBO: ON</color>" : "TURBO: OFF"; GUI.Label(new Rect(x+10, y+50, w-20, 20), turboStatus); // Event counters GUI.Label(new Rect(x+10, y+80, w-20, 20), "--- Event Counts ---"); GUI.Label(new Rect(x+10, y+100, w-20, 20), $"Damage: {_bikeController.damageCount}"); GUI.Label(new Rect(x+10, y+120, w-20, 20), $"Turbo: {_bikeController.turboCount}"); GUI.Label(new Rect(x+10, y+140, w-20, 20), $"Critical: {_bikeController.criticalCount}"); }

OnGUI Breakdown

OnGUI() β€” Called every frame for GUI rendering. Uses Unity's Immediate Mode GUI (IMGUI) system, same as TestPanel.

Rich text colors: <color=red> changes text color inline. Health turns red when critical!

Rect(x, y, w, h): Positions each label. We offset from top-right corner using Screen.width so it doesn't overlap TestPanel.

Live updates: Since OnGUI runs every frame, the counters and status update in real-time as events fire β€” no manual refresh needed!

Game view showing Observer Status GUI overlay with health, turbo, and event counters

Walkthrough Step 6: Scene Setup & Testing

Wiring It All Together in Unity

  1. Open your Blade Racer scene from previous lectures
  2. Select your Bike GameObject β€” verify BikeController has health fields (100/100)
  3. Select Main Camera β†’ Add Component β†’ CameraController
  4. Create empty GameObject "AudioManager" β†’ Add Component β†’ AudioController
  5. Verify HUDController is in the scene (from Event Bus lecture)
  6. Verify TestPanel is in the scene with Observer section added
  7. Press Play and test!
Unity Hierarchy showing Bike, Main Camera, AudioManager, HUD, and TestPanel GameObjects
Testing Checklist:
  • Press H repeatedly β€” console shows [HUD], [Camera], [Audio] logs + shake effect
  • Press B β€” console shows turbo on/off + GUI updates
  • Press H until health < 25% β€” all three critical handlers fire once
  • Check GUI: counters increment, health turns red at critical

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! πŸ’Ž