Observer Pattern
Summary
The Observer pattern defines a one-to-many dependency between objects: when one object (the subject) changes state, all registered objects (observers) are notified automatically. Neither the subject nor the observers need direct references to each other beyond the shared event — they remain loosely coupled.
In Unity and C#, the pattern is implemented either with the C# event keyword (backed by delegates) or with Unity’s built-in UnityEvent system. The choice between them affects performance and designer accessibility (Unity Technologies, Level Up Your Code with Game Programming Patterns, see source-unity-game-programming-patterns).
Implementation
C# Events (recommended for code-to-code communication)
The subject declares a public event. Any observer can subscribe with += and unsubscribe with -=. Only the subject class can invoke the event.
// Subject — raises the event
public class Subject : MonoBehaviour
{
public event Action ThingHappened;
public void DoThing()
{
ThingHappened?.Invoke(); // safe invoke — null check + call
}
}
// Observer — responds to the event
public class Observer : MonoBehaviour
{
[SerializeField] private Subject subjectToObserve;
private void Awake()
{
if (subjectToObserve != null)
subjectToObserve.ThingHappened += OnThingHappened;
}
private void OnDestroy()
{
if (subjectToObserve != null)
subjectToObserve.ThingHappened -= OnThingHappened;
}
private void OnThingHappened()
{
Debug.Log("Observer: thing happened");
}
}(Unity Technologies, Level Up Your Code with Game Programming Patterns, see source-unity-game-programming-patterns)
Subscription lifecycle: subscribe in Awake or OnEnable; unsubscribe in OnDestroy or OnDisable. Unsubscribing in OnDisable (rather than OnDestroy) ensures disabled objects stop receiving events, which prevents processing on inactive GameObjects.
Events with data (typed Action)
Use Action<T> to pass information to observers at the time of notification.
public class HealthSystem : MonoBehaviour
{
public event Action<int> OnHealthChanged;
public event Action OnPlayerDied;
private int _health = 100;
public void TakeDamage(int amount)
{
_health -= amount;
OnHealthChanged?.Invoke(_health);
if (_health <= 0)
OnPlayerDied?.Invoke();
}
}Multiple independent observers can respond to the same event — a health bar UI, a death sound trigger, and a camera shake script can each subscribe to OnHealthChanged without knowing about one another.
UnityEvent (for designer-accessible wiring)
UnityEvent fields appear in the Inspector, letting designers connect callbacks without writing code. This suits UI button responses, cutscene triggers, and other designer-facing configurations.
using UnityEngine.Events;
public class Door : MonoBehaviour
{
public UnityEvent OnOpened;
public void Open()
{
// Play animation, etc.
OnOpened?.Invoke(); // designers wire responses in the Inspector
}
}UnityAction is Unity’s serialisable delegate type and is what UnityEvent stores internally.
Trade-offs
C# event | UnityEvent | |
|---|---|---|
| Performance | Fast — direct delegate call | Slower — uses reflection |
| Inspector wiring | Not visible | Visible and editable by designers |
| Type safety | Compile-time | Partially — argument types are visible |
| Serialisation | Not serialised | Serialised with the scene/prefab |
| Null safety | ?.Invoke() | Built-in null safety |
| Best for | Code-to-code | Designer-configured callbacks |
For performance-critical code (physics callbacks, per-frame events), prefer C# events. For UI interactions, cutscenes, or anything a designer needs to configure, UnityEvent is often the better choice (Unity Technologies, Level Up Your Code with Game Programming Patterns, see source-unity-game-programming-patterns).
Common pitfall: memory leaks
If an observer subscribes to an event and never unsubscribes, the publisher holds a reference to the observer’s method. The observer object cannot be garbage-collected even if it has been “destroyed” in the scene. This is one of the most common sources of memory leaks in Unity projects. Always unsubscribe:
private void OnDisable() => subject.OnThing -= HandleThing;Examples
- Health system:
HealthSystemraisesOnHealthChanged(int newHealth). Subscribers:HealthBarUI(updates slider),DeathHandler(triggers death sequence),AudioManager(plays damage sound). No direct references between any of them. - Level completion:
LevelManagerraisesOnLevelComplete.ScoreTracker,UnlockManager, andAchievementSystemeach respond independently. - Input events: An
InputReaderScriptableObject raises events for button presses; gameplay systems subscribe without pollingInputdirectly.
Related
- csharp-delegates — the language foundation: delegate types, Action, Func, event keyword
- unity-object-communication — broader guide to object communication strategies including ScriptableObject channels
- command-pattern — complements observer; captures actions rather than reacting to state changes
- unity-gamemanager-pattern — often uses events to broadcast global state changes
- source-unity-game-programming-patterns — primary source for Unity-specific implementation