What Is It?
The Observer pattern defines a one-to-many relationship: a subject keeps a list of observers and notifies all of them when something interesting happens. The subject never references concrete observer types — it just raises an event.
In C#, this maps naturally onto event Action<T> or UnityEvent. Observers subscribe with += and unsubscribe with -=; the subject calls Invoke and moves on.
A classic Unity example is player health: when PlayerHealth changes, the health bar animates, the audio system plays a hurt sound and the game-over screen waits for Died — yet PlayerHealth has zero dependencies on any of them.
When Is It Used?
Use it whenever one change should trigger reactions in several unrelated systems: UI updates, achievements, analytics, sound, VFX, quest progress.
It shines when you want to add or remove reactions without touching the source. A new achievement system can subscribe to Died without editing the player at all.
Avoid it for tight, ordered sequences where one step must happen before the next — event chains are hard to follow and their call order is not something you should rely on.
Interactive Demo
Click the controls and watch the objects collaborate. The console mirrors what the C# code below would log.
Code
using UnityEngine;
namespace Patterns.Behavioral.Observer
{
/// <summary>
/// Drives the subject for testing. Note that it only talks to
/// PlayerHealth; the UI and audio react on their own.
/// </summary>
public class DamageTester : MonoBehaviour
{
[SerializeField] private PlayerHealth player;
private void Update()
{
if (Input.GetKeyDown(KeyCode.Space))
player.TakeDamage(10);
if (Input.GetKeyDown(KeyCode.H))
player.Heal(10);
}
}
}using System;
using UnityEngine;
namespace Patterns.Behavioral.Observer
{
/// <summary>
/// The Subject. It owns the health value and broadcasts changes,
/// but has no idea who is listening.
/// </summary>
public class PlayerHealth : MonoBehaviour
{
[SerializeField] private int maxHealth = 100;
public int Current { get; private set; }
public int Max => maxHealth;
// Observers subscribe to these events.
public event Action<int, int> HealthChanged; // (current, max)
public event Action Died;
private void Awake() => Current = maxHealth;
public void TakeDamage(int amount)
{
if (Current <= 0) return;
Current = Mathf.Max(0, Current - amount);
HealthChanged?.Invoke(Current, maxHealth);
if (Current == 0)
Died?.Invoke();
}
public void Heal(int amount)
{
if (Current <= 0) return;
Current = Mathf.Min(maxHealth, Current + amount);
HealthChanged?.Invoke(Current, maxHealth);
}
}
}using UnityEngine;
using UnityEngine.UI;
namespace Patterns.Behavioral.Observer
{
/// <summary>Observer: keeps the health bar in sync with the player.</summary>
public class HealthUI : MonoBehaviour
{
[SerializeField] private PlayerHealth player;
[SerializeField] private Slider bar;
private void OnEnable()
{
player.HealthChanged += OnHealthChanged;
OnHealthChanged(player.Current, player.Max);
}
private void OnDisable()
{
player.HealthChanged -= OnHealthChanged;
}
private void OnHealthChanged(int current, int max)
{
bar.value = (float)current / max;
}
}
}using UnityEngine;
namespace Patterns.Behavioral.Observer
{
/// <summary>Observer: plays feedback sounds when health changes.</summary>
public class AudioManager : MonoBehaviour
{
[SerializeField] private PlayerHealth player;
[SerializeField] private AudioSource source;
[SerializeField] private AudioClip hurtClip;
[SerializeField] private AudioClip deathClip;
private int lastHealth;
private void OnEnable()
{
lastHealth = player.Current;
player.HealthChanged += OnHealthChanged;
player.Died += OnDied;
}
private void OnDisable()
{
player.HealthChanged -= OnHealthChanged;
player.Died -= OnDied;
}
private void OnHealthChanged(int current, int max)
{
if (current < lastHealth)
source.PlayOneShot(hurtClip);
lastHealth = current;
}
private void OnDied() => source.PlayOneShot(deathClip);
}
}Advantages & Disadvantages
+ Advantages
- Subjects and observers are loosely coupled; either side can change independently.
- New behaviour is added by subscribing, not by modifying existing code (Open/Closed).
- Maps directly onto built-in C#
events, so there is almost no boilerplate.
− Disadvantages
- Forgetting to unsubscribe causes leaks and calls on destroyed objects (
MissingReferenceException). - Control flow becomes implicit — it is harder to trace “who reacts to this?” in a debugger.
- Notification order is not guaranteed; observers that depend on each other become fragile.
Tips
- 01Pair
+=inOnEnablewith-=inOnDisable. It is the single most important habit with events in Unity. - 02Prefer
event Action<T>for code-only wiring andUnityEventwhen designers need to hook things up in the Inspector. - 03Use the
?.Invoke()null-conditional so raising an event with no listeners is safe. - 04Pass data in the event (
HealthChanged(current, max)) so observers do not need to query back into the subject. - 05For game-wide events, consider a ScriptableObject event channel so scenes and prefabs can communicate without direct references.