Topic 15 of 15 11 min intermediate

Events & Decoupling

Letting systems talk without knowing each other

The health bar needs to know when health changes. The easy fix is a reference from one to the other. Do that twenty times and every system knows every other system, and nothing can be deleted or tested alone.

Three ways to fire an event

MechanismWired up inBest for
C# eventCodeSystem-to-system. Fast, refactorable, type-safe.
UnityEventThe InspectorDesigner-facing hooks. Buttons, triggers.
ScriptableObject channelAn assetCross-scene, zero direct references.

C# events

The default choice. The publisher exposes an event; anyone can subscribe; the publisher never learns who is listening.

Health.cs
C#
using System;
using UnityEngine;

public class Health : MonoBehaviour
{
    [SerializeField] private int maxHealth = 100;

    private int current;

    // Anyone can listen. Health does not care who.
    public event Action<int, int> Changed;   // 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);
        Changed?.Invoke(current, maxHealth);

        if (current == 0) Died?.Invoke();
    }
}
The ?.Invoke() is not optional politeness. An event with no subscribers is null, and invoking it directly throws.

The leak that causes most crashes

A subscription is a reference. If a listener is destroyed without unsubscribing, the publisher still holds it and still calls into it — and Unity throws MissingReferenceException from a line that looks innocent.

C#
public class HealthBar : MonoBehaviour
{
    [SerializeField] private Health health;

    private void OnEnable()  => health.Changed += Redraw;
    private void OnDisable() => health.Changed -= Redraw;

    private void Redraw(int current, int max)
    {
        fill.fillAmount = (float)current / max;
    }
}
Subscribe in OnEnable, unsubscribe in OnDisable — not Start/OnDestroy. Objects get disabled and re-enabled far more often than they get created and destroyed, and only the OnEnable pair stays balanced through that.

UnityEvents

A UnityEvent is wired in the Inspector, which lets a designer connect a door to a button without opening a script. That is the whole reason to use one.

C#
using UnityEngine;
using UnityEngine.Events;

public class Interactable : MonoBehaviour
{
    // Appears in the Inspector with a + button
    public UnityEvent onInteract;

    public void Interact() => onInteract?.Invoke();
}
C# eventUnityEventNote
FasterSlowerUnityEvent uses reflection.
Survives refactorsBreaks silently on renameThe Inspector stores a method name string.
Invisible to designersVisible and editableThe real trade-off.
Compile-time checkedRuntime onlyA broken link is a silent no-op.
Rename a method that a UnityEvent points at and the link breaks with no compile error and no warning. The button simply stops doing anything. Search your scenes after renaming any method wired in the Inspector.

ScriptableObject channels

When two systems live in different scenes and cannot hold a reference at all, an asset in the middle solves it. Covered in ScriptableObjects; the short version is that both sides reference the asset, and neither references the other.

Do not over-decouple

Events have a cost too

Fully event-driven code is hard to follow. You can no longer answer "what happens when the player dies?" by reading one file — you have to find every subscriber.

A reasonable rule: use a direct reference when one object genuinely owns another (a weapon and its muzzle), and an event when unrelated systems need to react to the same thing (health, UI, audio, achievements).

What to take away

  • C# events for system-to-system, UnityEvents for designer wiring.
  • Always ?.Invoke(); an event with no listeners is null.
  • Subscribe in OnEnable, unsubscribe in OnDisable.
  • An unbalanced subscription is the usual cause of MissingReferenceException.
  • UnityEvent links break silently when you rename a method.
  • Use direct references for ownership; events for unrelated reactions.