Imagine a player dies. The game needs to play a sound, update the HUD, unlock an achievement, write telemetry, and perhaps tell an enemy director to cool down. The simplest implementation is also the most tempting: make the player object call all five systems.
The code works. The dependency graph doesn’t.
Direct calls are not inherently bad. If the player asks its inventory to consume an item, that relationship is concrete and useful. Trouble starts when the player becomes responsible for every consequence of being damaged, healed, killed, revived, or moved into a new zone.
void Player::die() {
health = 0;
hud.showDeathScreen();
audio.play("player_down");
achievements.check("first_death");
analytics.track("player_died");
director.onPlayerDied();
}Now the Player class knows about UI, audio, achievements, analytics, and encounter pacing. Each new reaction forces you to edit the player. A class that should model player state slowly becomes the switchboard for the whole game.
NOTECoupling is not merely “class A calls class B.” It is the fact that A must change when B is added, removed, or redesigned.
Observer turns a command into a fact
With an observer, the player announces one fact: player died. It does not decide which systems should care. Interested systems subscribe and handle their own reaction.
void Player::die() {
health = 0;
events.emit(PlayerDied{ id, position });
}
// Elsewhere, each system owns its reaction:
events.on<PlayerDied>(hud.showDeathScreen);
events.on<PlayerDied>(audio.playDeathCue);
events.on<PlayerDied>(achievements.checkDeathRules);The total amount of code may even increase. That is fine. The win is architectural:
- The source stays focused. Player logic changes for player reasons.
- Reactions become additive. A photo-mode listener can be added without opening the player class.
- One fact can fan out. UI, sound, achievements, and tools receive the same event without knowing about each other.
- Features become easier to isolate. A listener can be tested with a synthetic event instead of constructing the entire game world.
The dependency did not disappear
Observer moves knowledge out of the sender, but it also makes control flow less visible. You can no longer read Player::die() and see everything that happens next. Subscription lifetime, event order, re-entrancy, and debugging tools now matter.
This is why a global event bus for every interaction often becomes its own kind of mess. If everything is an event, the program starts to feel like a room full of people shouting and hoping the right person hears.
| Observer is strong when | Direct calls are strong when |
|---|---|
| Many independent systems react to something that already happened. | One object asks a known collaborator to do work and needs a result. |
| Listeners are optional and can be added independently. | The relationship is central to the operation being performed. |
A rule that survives real projects
Use a direct call for a command: “inventory, consume this potion and tell me whether it worked.” Use an event for a fact: “the potion was consumed.”
That distinction keeps the important path explicit while allowing optional consequences—sound, particles, tutorials, analytics—to stay loosely coupled. Observer is not a way to avoid designing relationships. It is a way to decide which relationships deserve to be permanent.