Event Dispatchers¶
At a glance
Lives in: Blueprint API / Blueprint Communication - Returns: nothing; dispatchers can pass inputs to bound events, but the call has no output values - Fails by: doing nothing when no events are bound, while execution continues - Main inputs: the dispatcher owner, the event list, and any dispatcher parameters - Official docs: Event Dispatchers, Binding and Unbinding Events, Creating Dispatcher Events, and Calling Event Dispatchers
The one-minute version¶
- An Event Dispatcher is a broadcast list owned by one object. Other events can bind to that list, and the owner can call the dispatcher later.
- The golden rule is direction: the broadcaster owns the dispatcher; listeners bind to the broadcaster.
Callmeans "fire every event currently bound to this dispatcher." If nothing is bound, nothing happens.Bind Eventadds a matching event to the list.Assignis the editor shortcut that creates a matching event and a bind node for you.Unbindremoves one bound event.Unbind Allclears the list for the target scope, which can be wider than beginners expect.- Dispatchers do not find listeners, remember past calls, return values, or replicate by themselves.
What it actually does¶
An Event Dispatcher is Unreal's Blueprint-facing version of a multicast delegate: a named event list with a fixed signature. The signature is the inputs you define on the dispatcher.
For a health example:
BP_PlayerCharacter owns:
Event Dispatcher OnHealthChanged
Inputs:
CurrentHealth (float)
MaxHealth (float)
WBP_HealthBar binds:
OnHealthChanged -> UpdateProgressBar
BP_PlayerCharacter calls:
OnHealthChanged(CurrentHealth, MaxHealth)
At runtime, the call is closer to this:
pseudocode of the engine behavior - not engine source
function CallOnHealthChanged(Character, CurrentHealth, MaxHealth):
for each bound event in Character.OnHealthChanged:
run bound event with CurrentHealth and MaxHealth
return nothing
The character does not know whether the listener is a widget, sound manager, door, quest tracker, or nothing at all. It only owns the dispatcher and decides when to broadcast.
That is the point of the tool. A dispatcher lets the object with the fact say "this happened" without hard-coding every object that might care.
The owner/listener rule¶
Put the dispatcher on the object whose state changes.
| Event | Dispatcher belongs on | Listener examples |
|---|---|---|
| Health changed | Character, enemy, or health component | Health bar, audio, damage vignette |
| Door finished opening | Door actor | Quest trigger, sound actor, checkpoint |
| Boss died | Boss actor or boss encounter controller | Door, reward spawner, music manager |
| Inventory changed | Inventory component or controller-owned inventory object | HUD, save manager, tutorial prompt |
Do not put OnHealthChanged on the widget just because the widget is the
thing you want to update. The widget is a listener. The character or health
component is the broadcaster because it owns the health state.
flowchart LR
Damage[Apply damage] --> Character[BP_PlayerCharacter]
Character --> Call[Call OnHealthChanged]
Call --> Widget[WBP_HealthBar bound event]
Call --> Audio[Low-health audio bound event]
Call --> Quest[Quest tracker bound event]
This direction keeps the character independent. It can keep working in a test map with no HUD, and the HUD can be recreated without rewriting damage logic.
Bind, Assign, Call, Unbind¶
These nodes all operate on the dispatcher's event list.
| Node | What it does | Beginner trap |
|---|---|---|
| Bind Event to X | Adds a compatible event to the dispatcher's list. | The event does not run now; it runs later when Call X happens. |
| Assign X | Creates a matching event node and a bind node already wired together. | It is not a special "replace every listener" command. Treat it as a convenience bind. |
| Call X | Runs the events currently bound to the dispatcher. | If the list is empty, nothing visible happens. |
| Unbind Event from X | Removes that matching event from the list. | The event signature and target must match the bound event you mean to remove. |
| Unbind All Events from X | Clears every event bound to that dispatcher for the target scope. | It can remove other listeners, including Level Blueprint bindings, so use it deliberately. |
Bind Event and Assign both need a target: the object whose dispatcher list
you are changing. Inside the broadcaster Blueprint, that target is usually
Self. In another Blueprint, the target must be a real reference to the
broadcaster instance.
Create Widget WBP_HealthBar
-> Get Player Character
-> Cast To BP_PlayerCharacter
-> Bind Event to OnHealthChanged
The event you bind must match the dispatcher's signature. If the dispatcher has
CurrentHealth and MaxHealth inputs, the bound event receives those same
values.
Where to bind and when¶
Bind once, at the moment both sides exist.
Common places:
| Listener | Bind when | Usually unbind when |
|---|---|---|
| Level Blueprint | BeginPlay, after getting a placed actor reference. |
Rarely, unless changing targets. |
| Actor listening to another actor | BeginPlay, after assignment, or right after spawn. |
EndPlay or before switching broadcaster targets. |
| Widget listening to a character | After the widget is created and it has the character reference. | Destruct, Remove from Parent cleanup, or before rebinding to a new character. |
| Spawn manager listening to spawned actor | Immediately after Spawn Actor from Class returns a valid actor. |
When the spawned actor is no longer relevant. |
Do not bind from Tick. Do not bind every time health changes. Do not bind inside a function that might run repeatedly unless the function first proves the binding is not already established.
One more lifecycle rule: dispatchers do not replay history. If the widget binds
after the character already lost health, the widget will not automatically
receive the old OnHealthChanged call. Bind, then do an initial manual refresh:
Bind Event to OnHealthChanged
-> UpdateHealthBar(CurrentHealth, MaxHealth)
When it fails (and what failure does)¶
There is no Cast Failed pin on a dispatcher call. The usual failure is
silence.
Common causes:
- No events are bound. Calling a dispatcher with an empty list has no effect.
- You bound on the wrong instance. Binding to
Enemy_1.OnDieddoes not listen toEnemy_2.OnDied. - You never executed the bind path. A dispatcher event node sitting in the
graph is not enough. It has to be connected through
Bind Event,Assign, or a special auto-bound Level Blueprint event. - The broadcaster reference is
None. Binding or calling through an empty object reference is still an object-reference problem; debug it like Accessed None. - The listener went away or the target changed. Recreated widgets, respawned pawns, and replaced actors need fresh bindings to the current broadcaster.
- You expected a return value. A multicast dispatcher tells listeners something happened. It does not ask them for answers.
- You expected a network message. Dispatcher calls are local Blueprint execution. Use the normal server/client remote procedure call path when multiplayer authority matters.
When debugging, put a breakpoint on the bind node and the call node. If the bind never runs, the call has nobody to notify. If the call never runs, the listener cannot fire.
Why it fired more than once¶
When a dispatcher seems to fire N times, first count the bindings.
Important nuance: Epic's docs state that the same event can be bound only once
to a dispatcher. Running the same Bind Event node again for the same event is
not usually the duplicate-binds bug beginners imagine.
The real repeat cases are usually:
- several different listener events are bound to the same dispatcher,
- several listener instances are alive, such as old widgets you never removed,
- each respawn created a new character, widget, or manager and you are listening to more than one of them,
- you bound both in a Blueprint Class and in the Level Blueprint,
- you call the dispatcher from more than one gameplay path.
For the respawn trap, print the broadcaster and listener names when binding:
Print String "Bind " + ToString(self) + " to " + ToString(Character)
If you see a new widget binding every death while the old widget still exists, the fix is lifecycle cleanup, not a different dispatcher node.
What Event Dispatchers do not do¶
- They do not find listeners. Listeners need a reference to the broadcaster and must bind.
- They do not store current state or replay missed events.
- They do not return values from listeners.
- They do not guarantee listener execution order. Do not build gameplay rules that depend on which listener runs first.
- They do not automatically mean "server to client" or "client to server."
- They do not replace interfaces for one target command calls.
- They do not make hard references disappear if your dispatcher signature uses concrete Blueprint classes as parameter types.
The pattern everyone actually uses¶
For a health bar:
BP_PlayerCharacter:
Variable Health
Variable MaxHealth
Event Dispatcher OnHealthChanged(CurrentHealth, MaxHealth)
Function ApplyHealthDelta(Delta):
Health = Clamp(Health + Delta, 0, MaxHealth)
Call OnHealthChanged(Health, MaxHealth)
PlayerController or HUD setup:
Create Widget WBP_HealthBar
Get controlled character
Cast To BP_PlayerCharacter
Bind Event to OnHealthChanged
-> WBP_HealthBar.UpdateHealth(CurrentHealth, MaxHealth)
WBP_HealthBar.UpdateHealth(Health, MaxHealth)
Add to Viewport
For a spawned boss:
Spawn Actor from Class (BP_Boss)
-> Is Valid Return Value
-> Bind Event to OnDied on that returned boss reference
-> OpenExitDoor
-> SpawnReward
Bind right after spawn because that is the moment you have the specific boss
instance. Calling OnDied on one boss does not notify bindings on another
boss.
The C++ twin, for the curious (our own example code):
DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(
FOnHealthChanged,
float, CurrentHealth,
float, MaxHealth);
UPROPERTY(BlueprintAssignable)
FOnHealthChanged OnHealthChanged;
void ATrainingCharacter::ApplyHealthDelta(float Delta)
{
Health = FMath::Clamp(Health + Delta, 0.0f, MaxHealth);
OnHealthChanged.Broadcast(Health, MaxHealth);
}
Lookalikes - which one do I want?¶
| Tool | Question it answers | Use when |
|---|---|---|
| Event Dispatcher | "Whoever is listening, this happened." | One broadcaster has zero, one, or many listeners. |
| Custom Event | "Start this named flow." | Something calls it directly, or a timer/delegate binding uses it as an entry point. |
| Blueprint Interface | "Can this target answer this contract?" | You have a target object and several possible classes should respond. |
| Cast To X | "Is this object this specific class family?" | You need members from one known class. |
| Direct function call | "Tell this known object to do this." | You already hold a typed reference to the exact target. |
| Timer by Event | "Run this event later or repeatedly." | Time scheduling is the point; it is not a listener list by itself. |
Rule of thumb: use a dispatcher when the broadcaster should not know its listeners. Use an interface when the caller has one target and knows the contract. Use a cast when the caller is truly coupled to one class.
Going deeper¶
- Blueprint Interfaces - the contract-based call when you have one target object.
- Cast To X - the class-specific option.
- The Blueprint survival kit - functions, custom events, and macros before cross-Blueprint communication.
- Breakpoints and watch values - how to prove whether the bind path and call path both executed.
- Spawn Actor from Class - where spawned actor references come from before binding to their dispatchers.
- Official docs: Blueprint Communication Usage, Event Dispatchers, Binding and Unbinding Events, Creating Dispatcher Events, Calling Event Dispatchers, and TMulticastDelegate.
- Engine source (requires engine access - we do not reproduce it here):
Blueprint delegate nodes such as
UK2Node_AddDelegate,UK2Node_AssignDelegate,UK2Node_RemoveDelegate,UK2Node_ClearDelegate, andUK2Node_CallDelegatelive underEngine/Source/Editor/BlueprintGraph/Classes/with shared expansion logic inEngine/Source/Editor/BlueprintGraph/Private/K2Node_MCDelegate.cpp; multicast delegate property metadata lives inFMulticastDelegatePropertyatEngine/Source/Runtime/CoreUObject/Public/UObject/UnrealType.h; script multicast delegate storage and invocation helpers live inEngine/Source/Runtime/CoreUObject/Public/UObject/ScriptDelegates.h.