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

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# eventUnityEvent
PerformanceFast — direct delegate callSlower — uses reflection
Inspector wiringNot visibleVisible and editable by designers
Type safetyCompile-timePartially — argument types are visible
SerialisationNot serialisedSerialised with the scene/prefab
Null safety?.Invoke()Built-in null safety
Best forCode-to-codeDesigner-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: HealthSystem raises OnHealthChanged(int newHealth). Subscribers: HealthBarUI (updates slider), DeathHandler (triggers death sequence), AudioManager (plays damage sound). No direct references between any of them.
  • Level completion: LevelManager raises OnLevelComplete. ScoreTracker, UnlockManager, and AchievementSystem each respond independently.
  • Input events: An InputReader ScriptableObject raises events for button presses; gameplay systems subscribe without polling Input directly.