Events
Summary
An event is a restricted, publicly-observable notification built on top of a delegate: any code can subscribe to be notified when the event fires, but only the class that declared the event is allowed to actually fire it. This turns a raw multicast delegate — which any outside code could invoke or overwrite — into a safe broadcast mechanism, letting many independent systems react to something happening without any of them being directly wired together.
Why This Matters
A raw public delegate field can be invoked or reassigned by any code that
can see it — outside code could accidentally call health.OnDamaged(9999)
to fake damage, or wipe out every existing subscriber with health.OnDamaged = null. A health system that needs to notify a UI bar, an achievement
tracker, and a sound system when the player takes damage shouldn't have to
trust every one of those systems not to misuse that access. Events close
that gap: outside code can subscribe and unsubscribe, but only Health
itself can fire OnDamaged.
Mental Model
An event is a newsletter, not a group chat. Subscribing to a newsletter
lets you receive it whenever the publisher sends one, but subscribers can't
write the newsletter themselves or force the publisher to send it early —
only the publisher decides when and what goes out. A delegate field without
the event keyword is closer to a shared group chat where anyone in it can
post as if they were the publisher; the event keyword removes that
ability from everyone except the class that owns it.
Core Explanation
eventrestricts a delegate to subscribe/unsubscribe from the outside.public event Action<int> OnDamaged;allows external code to dohealth.OnDamaged += Handler;but nothealth.OnDamaged(10)orhealth.OnDamaged = null— only code insideHealthitself can invoke or reassign it.- Publisher and subscribers are decoupled.
Healthnever referencesHealthBar,AchievementTracker, orAudioManagerby name — each of those subscribes independently, andHealthstays unaware of how many subscribers exist or what they do. - An event fires synchronously, in subscription order. Every subscribed handler runs, one after another, on the same call stack as the code that raised the event — an event is not automatically deferred or run on a separate thread/frame.
- Subscribers must unsubscribe to avoid leaks. An object that subscribes to an event but is destroyed without unsubscribing leaves a dangling reference inside the publisher, the same failure mode as an unmanaged delegate (see [[CON-0023]]'s Common Mistakes).
Syntax / Theory
C# (Unity)
public class Health : MonoBehaviour
{
public event Action<int> OnDamaged; // 'event' — outside code can only += / -=.
public void TakeDamage(int amount)
{
currentHealth -= amount;
OnDamaged?.Invoke(amount); // Only Health itself can raise this.
}
}
public class HealthBar : MonoBehaviour
{
[SerializeField] private Health target;
void OnEnable() => target.OnDamaged += UpdateBar;
void OnDisable() => target.OnDamaged -= UpdateBar; // Prevents a leaked reference.
void UpdateBar(int amount) => fillImage.fillAmount = target.CurrentHealthRatio;
}
C++ (Unreal — multicast delegates)
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnDamaged, int32, Amount);
UCLASS()
class AHealthComponent : public UActorComponent
{
public:
UPROPERTY(BlueprintAssignable)
FOnDamaged OnDamaged;
void TakeDamage(int32 Amount)
{
CurrentHealth -= Amount;
OnDamaged.Broadcast(Amount);
}
};
Unreal Blueprint
UPROPERTY(BlueprintAssignable) exposes the event directly in the
Blueprint editor as a bindable output pin — designers can drag off "On
Damaged" and wire up a response without writing C++, exactly matching the
subscribe-only access a public event grants in C#.
Practical Example
A save system needs to know whenever anything in the game changes persistent state, without hard-coding a list of every system that might change it.
public static class SaveManager
{
public static event Action OnDirty;
public static void MarkDirty() => OnDirty?.Invoke();
}
public class Inventory
{
public void AddItem(Item item)
{
items.Add(item);
SaveManager.MarkDirty(); // Inventory doesn't know who's listening.
}
}
public class AutoSaveIndicator : MonoBehaviour
{
void OnEnable() => SaveManager.OnDirty += ShowUnsavedIcon;
}
Inventory never references AutoSaveIndicator, and any future system
that needs to react to a dirty save state just subscribes — SaveManager
and Inventory never change.
Common Mistakes
- Exposing a raw delegate field instead of an
event.public Action<int> OnDamaged;without theeventkeyword lets any outside code invoke it directly or wipe out all subscribers with= null— always useeventfor anything meant to be observed rather than controlled. - Not unsubscribing in
OnDisable/OnDestroy. A destroyedMonoBehaviourthat never called-=leaves the publisher holding a reference to a dead object — the nextInvokeeither throws or silently calls into a destroyed object's state. - Doing expensive work directly inside an event handler that fires every
frame. An event that fires on every
Update(e.g. "position changed") and triggers heavy work per-subscriber can silently tank performance; consider throttling or batching instead. - Assuming subscriber order is meaningful or guaranteed by design. Handlers run in subscription order, but code shouldn't depend on which handler runs first — that coupling defeats the purpose of decoupling publisher from subscribers.
Game Development Example
A boss fight needs multiple independent systems — music, camera, UI — to react the moment the boss's health crosses a phase threshold, without the boss script knowing any of them exist.
public class BossHealth : MonoBehaviour
{
public event Action<int> OnPhaseChanged;
private int currentPhase = 1;
public void TakeDamage(int amount)
{
currentHealth -= amount;
int newPhase = CalculatePhase(currentHealth);
if (newPhase != currentPhase)
{
currentPhase = newPhase;
OnPhaseChanged?.Invoke(currentPhase);
}
}
}
Music, camera-shake, and UI systems each subscribe to OnPhaseChanged
independently — adding a fourth reactive system later requires no change
to BossHealth at all.
Best Practices
- Always declare public notifications with
event, never a bare public delegate field — reserve the bare form for private, internal use only. - Pair every
+=with a-=in the matching lifecycle method (OnEnable/OnDisableorOnDestroy). - Keep event handlers small and fast; if a handler needs to do significant work, have it kick off that work rather than performing it inline during the broadcast.
- Name events for what happened, not what to do (
OnDamaged, notUpdateHealthBar) — the publisher shouldn't know or care what subscribers do in response.
Exercises
- Add a
public event Action OnDefeated;to aBossHealthclass, invoke it when health reaches zero, and subscribe two independent handlers (a log message and a hypothetical "play victory music" call). - Take the
HealthBarexample and deliberately remove theOnDisableunsubscription, then explain, in terms of whatHealthis holding a reference to, what goes wrong when theHealthBarGameObject is destroyed. - You're deciding between many small, specific events (
OnDamaged,OnHealed,OnDied) versus one generalOnHealthChanged(int oldValue, int newValue)event. Argue for one approach for a UI-heavy game and justify the trade-off. - Research the Observer design pattern and explain, in your own words, how
C# events are a language-level implementation of that pattern — and
name one thing a hand-rolled Observer implementation could do that a
C#
eventcannot.
Related Concepts
- Delegates — events are a restricted-access wrapper around a multicast
delegate; understanding delegates first is necessary to see what the
eventkeyword is restricting. - Access Modifiers — the
eventkeyword applies the same publish-vs-consume access split that access modifiers apply more generally to fields and methods. - Functions — event handlers are ordinary methods matching the event's delegate signature; nothing about subscribing to an event requires new function syntax.
Further Reading
KNOWLEDGE_SCHEMA.md— GDKB's concept maturity ladder and cross-linking rules, which this document follows.- Microsoft C# documentation — Events: https://learn.microsoft.com/dotnet/csharp/programming-guide/events/
- Unreal Engine C++ documentation — Multicast Delegates: https://dev.epicgames.com/documentation/en-us/unreal-engine/delegates-and-lambda-functions-in-unreal-engine