The observer pattern
Publisher/subscriber decouples systems so an achievement engine, a sound manager, and a UI can all react to game events without the player or enemy knowing they exist.
- Explain the observer pattern as publisher/subscriber without direct coupling
- Identify where games benefit from it — achievement systems, UI updates, audio triggers
- Contrast observer with direct method calls and articulate the trade-off
As a game grows, systems that were once independent start needing to react to
each other. Killing an enemy should: update the score, trigger a sound, check
achievement progress, and spawn a particle explosion. Writing all four calls
directly inside the enemy's die() method creates a tangled web of
dependencies — the Enemy class now imports SoundManager, AchievementSystem,
ParticleEmitter, and ScoreTracker. Changing any one of those systems means
touching Enemy.
The observer pattern untangles this by introducing an intermediary.
How it works
There are two roles:
Publishers (also called subjects or observables) raise events when something noteworthy happens. They do not know who is listening.
Subscribers (also called observers or listeners) register interest in a particular event and are called whenever it is raised.
A minimal event bus:
from collections import defaultdict
class EventBus:
def __init__(self):
self._subscribers = defaultdict(list)
def subscribe(self, event_type, callback):
self._subscribers[event_type].append(callback)
def publish(self, event_type, **data):
for callback in self._subscribers[event_type]:
callback(**data)
bus = EventBus()The enemy raises an event when it dies:
class Enemy:
def die(self):
self.alive = False
bus.publish("ENEMY_KILLED", pos=self.pos, enemy_type=self.kind)The achievement system, sound manager, and particle emitter all subscribe independently:
def on_enemy_killed(pos, enemy_type):
achievements.check("FIRST_KILL")
def on_enemy_killed_sound(pos, enemy_type):
sounds.play("enemy_die")
bus.subscribe("ENEMY_KILLED", on_enemy_killed)
bus.subscribe("ENEMY_KILLED", on_enemy_killed_sound)The Enemy class does not import achievements, sounds, or anything else.
It fires one event and its responsibility ends.
Where games use it
Achievement systems — subscribe to a wide range of events (ENEMY_KILLED,
LEVEL_COMPLETE, ITEM_COLLECTED) and check progress conditions internally.
Adding a new achievement never touches any other system.
UI / HUD updates — the score display subscribes to SCORE_CHANGED rather
than being polled every frame. The event carries the new value.
Audio triggers — a sound manager subscribes to gameplay events
(PLAYER_JUMP, PROJECTILE_FIRED, BOSS_PHASE_CHANGE) and plays the
appropriate clip. Tuning sound without touching gameplay code.
Analytics / telemetry — subscribe to events in a debug build to log player behaviour without any conditional code in the game systems.
Comparison with direct calls
| Direct call | Observer | |
|---|---|---|
| Coupling | Enemy imports every listener | Enemy imports only the bus |
| Adding a listener | Modify Enemy.die() | bus.subscribe(...) anywhere |
| Order of calls | Explicit | Determined by subscription order |
| Debugging | Follow call stack normally | Trace via the bus |
Direct calls are simpler for small games. The observer shines when more than two systems need to react to the same event, or when you want to add listeners from editor plugins or mods.
Keep event data plain and serialisable — tuples, dicts, and scalars rather than live object references. This makes events easy to log, replay, and inspect, and prevents subscribers from holding references that outlive the published object.
Where to go next
Next: component architecture — replacing deep inheritance hierarchies with a composition-based entity model that scales to complex games.
Lab: Visual effects
Add particle explosions on enemy death, screen shake on player hit, hit flash on taking damage, and a fade transition to the complete game.
Component architecture
Deep inheritance breaks down when entities need multiple capabilities. Composition — attaching small components to a plain entity object — scales cleanly to complex games.