Last week we looked at Blueprint Interfaces and how they stop you hard-casting to everything. Interfaces fix the "what do I call" problem. But there's a second problem they don't solve: who do I even tell?
Your player takes damage. The health bar needs to know. So does the damage-flash material, the "low health" heartbeat sound, maybe an achievement tracker. Does your BP_Player really need a reference to all four? Today's fix says no.
What an Event Dispatcher actually is
An Event Dispatcher is a broadcast. One Blueprint announces "this thing happened" and anyone who cared to listen gets notified. The announcer has no idea who's listening — could be nobody, could be ten systems. In programming this is the Observer pattern (or publish-subscribe), and it's the cleanest way to do one-to-many communication.
Compare the three ways actors talk to each other:
- Direct call — you hold a reference to one specific actor and call its function. One-to-one, tightly coupled.
- Interface — you call a function on something, without caring what class it is. Still one-to-one (or you loop), still need a reference.
- Event Dispatcher — you broadcast. One-to-many. The sender needs zero references to the listeners.
That last line is the whole point.
A concrete example
Health bar. The naïve version: BP_Player holds a reference to the WBP_HUD widget, and whenever health changes it calls UpdateHealthBar(NewHealth) on it. Works — until you add the damage flash. Now the player also needs a reference to the post-process. Then the heartbeat sound. Then... your player Blueprint knows about every UI and audio system in the game. Change one and you risk breaking the player.
The dispatcher version: BP_Player gets an Event Dispatcher called OnHealthChanged with a float parameter. When health changes, it Calls OnHealthChanged(NewHealth) and moves on. It does not know or care who's listening.
The HUD Binds to OnHealthChanged and updates the bar. Later you add a damage flash? It binds too. A heartbeat sound? Binds too. You never touch BP_Player again. That's the win: new listeners cost zero changes to the sender.
The four things you can do with a dispatcher
Drag off a dispatcher and you'll see four verbs. Don't let the names blur together:
- Call — fire it. Everyone bound gets notified now. This is the sender's job.
- Bind Event — start listening. You give it a custom event to run when the dispatcher fires. This is the listener's job.
- Unbind / Unbind All — stop listening.
- Assign — a Blueprint shortcut that creates a custom event and binds it in one node. It's the red node you get when you drag a dispatcher into the graph. Handy — but it hides the Bind, so people forget the Unbind.
A dispatcher can carry parameters: give OnHealthChanged a float and every bound event receives it. Define the signature once; all listeners share it.
Dispatcher, interface, or direct call?
Quick decision:
- One known target, simple call → direct reference. Don't over-engineer.
- One target, but it could be different classes → interface.
- You're announcing something and don't know (or care) who reacts → dispatcher.
Health, deaths, "wave started", "door opened", "item picked up" — anything where the number of reactors is unknown or growing is a dispatcher. UI and game-state notifications are the textbook case.
The reference catch nobody mentions
Here's the part that trips up people who think dispatchers mean "no references at all": to bind, the listener needs a reference to the sender. The HUD has to get hold of the player to say "bind me to your OnHealthChanged".
So the decoupling is one-directional. The sender stays blissfully ignorant of its listeners — that's the valuable half. But the listener still has to reach the sender once, to subscribe. Usually that's fine (the HUD can get the player from the controller). Just don't expect dispatchers to magically remove every reference — they remove the ones that hurt most.
Plugin tip
A dispatcher gets the signal to your listener — but should the listener act on it right now? "Only while alive", "only the first time", "did this bool just flip?" — that's flow-control logic you end up rebuilding with bool variables and Branch nodes every time.
FoxBranch packs it into single nodes: gates you open and close, do-once guards, edge detection, multi-way routing. Optional, but on the receiving end of a busy dispatcher it kills a lot of spaghetti.Common pitfalls
- Forgetting to Unbind on destroy. This is the big one. If a listener is destroyed but stays bound, the dispatcher keeps firing at a dead object — Accessed None spam, and a slow buildup of stale bindings. Unbind in
End Play. - Target defaults to Self. When you Bind, the Target pin auto-fills to
Self. If you meant to bind to another actor's dispatcher, you have to plug that actor in — otherwise you're just listening to yourself. - Binding twice. Bind the same event in
BeginPlayand somewhere that runs again, and it fires twice per Call. Bind once, or Unbind first. - Calling before anyone bound. If the sender Calls during
BeginPlaybefore the HUD has bound, the HUD misses it. Order matters — bind early. - Parameter mismatch. Change the dispatcher's signature and every bound event silently breaks until you re-create it. Decide your parameters before you wire up ten listeners.
How to debug a dispatcher
When a listener "isn't reacting":
- Drop a
Print Stringas the first node of the bound event. No print = the bind never happened (wrong target, or bound too late). - Check you actually called
Bind, not just created the event. Print Stringon the Call side too. If the sender never calls, nothing downstream fires.
Nine times out of ten it's a Self-vs-other target mix-up or a timing problem.
The 30-second recap
- Event Dispatcher = broadcast. One sender, many listeners, sender needs zero references to them.
- Call to fire, Bind to listen, Unbind when done, Assign for the quick combo node.
- Use it when you don't know (or don't want to know) who reacts — UI, health, game state.
- The listener still needs a reference to the sender to bind. The decoupling is one-way.
- Always Unbind on End Play, watch the Self target, and don't bind twice.
Next Sunday, votes permitting: Spawning actors — the patterns that don't crash your game. Deferred spawn, transform gotchas, and why your spawned actor sometimes ignores its own BeginPlay.
— Marco
Comments
Leave a comment