Observer Pattern - Decoupling Components

Decoupling Components

The Observer Pattern πŸ‘€

CSCI 3213 - Game Programming

Spring '26 - Week 7, Class 1

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!

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)

using UnityEngine; namespace Chapter.Observer { public class BikeController : MonoBehaviour { // Events (Publishers) public event System.Action<float> OnDamage; public event System.Action OnTurboStart; public event System.Action OnTurboEnd; // Bike state public float health = 100f; public float maxHealth = 100f; public bool isTurboActive; // Public method that triggers event public void TakeDamage(float amount) { health -= amount; health = Mathf.Max(health, 0); // Fire event - all observers notified! OnDamage?.Invoke(amount); Debug.Log($"[BikeController] Took {amount} damage. Health: {health}"); } public void ActivateTurbo() { if (!isTurboActive) { isTurboActive = true; OnTurboStart?.Invoke(); Debug.Log("[BikeController] Turbo activated!"); } } public void DeactivateTurbo() { if (isTurboActive) { isTurboActive = false; OnTurboEnd?.Invoke(); Debug.Log("[BikeController] Turbo deactivated"); } } } }

Implementation Step 2: HUD Observer

using UnityEngine; namespace Chapter.Observer { public class HUDController : MonoBehaviour { private BikeController _bikeController; void Start() { _bikeController = FindObjectOfType<BikeController>(); // SUBSCRIBE to bike events _bikeController.OnDamage += HandleDamage; _bikeController.OnTurboStart += HandleTurboStart; _bikeController.OnTurboEnd += HandleTurboEnd; } // Event handler for damage private void HandleDamage(float amount) { // Update health bar UI float healthPercent = _bikeController.health / _bikeController.maxHealth; Debug.Log($"[HUD] Health bar updated: {healthPercent:P0}"); // Flash damage indicator Debug.Log("[HUD] Flashing damage indicator"); } private void HandleTurboStart() { Debug.Log("[HUD] Showing TURBO ACTIVE indicator"); } private void HandleTurboEnd() { Debug.Log("[HUD] Hiding TURBO indicator"); } void OnDestroy() { // CRITICAL: Unsubscribe to prevent memory leaks! if (_bikeController != null) { _bikeController.OnDamage -= HandleDamage; _bikeController.OnTurboStart -= HandleTurboStart; _bikeController.OnTurboEnd -= HandleTurboEnd; } } } }

Implementation Step 3: Camera Observer

using UnityEngine; using System.Collections; namespace Chapter.Observer { 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; // Subscribe to damage event only _bikeController.OnDamage += HandleDamage; } private void HandleDamage(float amount) { // Shake intensity scales with damage 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; Debug.Log("[Camera] Shake complete"); } void OnDestroy() { if (_bikeController != null) _bikeController.OnDamage -= HandleDamage; } } }

Implementation Step 4: Audio Observer

using UnityEngine; namespace Chapter.Observer { 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; } private void HandleDamage(float amount) { Debug.Log("[Audio] Playing damage sound effect"); // AudioSource.PlayOneShot(damageClip); } private void HandleTurboStart() { Debug.Log("[Audio] Playing turbo boost sound"); // AudioSource.Play(turboClip); } private void HandleTurboEnd() { Debug.Log("[Audio] Stopping turbo sound"); // AudioSource.Stop(); } void OnDestroy() { if (_bikeController != null) { _bikeController.OnDamage -= HandleDamage; _bikeController.OnTurboStart -= HandleTurboStart; _bikeController.OnTurboEnd -= HandleTurboEnd; } } } }

Testing the Observer Pattern

Test Setup Steps

  1. Create new Unity scene
  2. Add Cube GameObject (the bike) with BikeController
  3. Add Camera with CameraController
  4. Add empty GameObject "HUD" with HUDController
  5. Add empty GameObject "Audio" with AudioController
  6. Create TestClient script to trigger events (next slide)
  7. Run and press keys to test!
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

using UnityEngine; namespace Chapter.Observer { 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\")}"); } } }

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 Start() { bike.OnDamage += HandleDamage; } // Observer destroyed // bike still has reference! // MEMORY LEAK!

βœ… With Unsubscribe

void OnDestroy() { if (bike != null) { bike.OnDamage -= HandleDamage; } } // Clean unsubscribe! // No memory leak!
Best Practice: Always pair += in Start() with -= in 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. Create BikeController with OnDamage and OnTurboStart events
  2. Create HUDController observer (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

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. Create BikeController with 3 events:
    • OnDamage (float damage)
    • OnTurboStart ()
    • OnTurboEnd ()
  2. Create 3 observers: HUDController, CameraController, 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

20 pts: BikeController with 3 events correctly implemented
15 pts: Three observers subscribe and respond to events
15 pts: Proper OnDestroy() unsubscribe in all observers
15 pts: OnHealthCritical event implemented and tested
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! πŸ’Ž