The Need for Event Dispatcher
When developing games with Unreal Engine (UE), have you experienced how initially simple Blueprint or C++ class relationships gradually become entangled like spaghetti code?
For example, consider a sequence where "when the player picks up an item, update inventory, notify UI, and play sound effects."
In a naive implementation, the item class directly references the "inventory class," "UI class," and "sound manager class," calling functions on each.
The problem with this design is tightly coupled dependencies.
- If the inventory update method changes, the item class needs modification too.
- When replacing the sound manager, all references in the item class must be changed.
- The item class knows details it shouldn't need to—"who" does "what."
Such tightly coupled systems are fragile to change, difficult to extend, and prone to bugs.
This article thoroughly explains Event Dispatcher, a powerful tool for fundamentally solving this problem and building loosely coupled systems that are change-resistant and highly reusable, targeting beginners to intermediate developers.
What is Event Dispatcher
Event Dispatcher is an implementation of Delegates in Unreal Engine, a mechanism for notifying all objects interested in a specific event when it occurs. It's an easy way to implement the Observer pattern (or Publish-Subscribe pattern) in programming.
The primary role of Event Dispatcher is to completely separate "event sources" from "event handlers."
| Element | Role | Characteristic |
|---|---|---|
| Event Source (Caller) | "Calls/Broadcasts" the Event Dispatcher | Doesn't know who handles it |
| Event Handler (Listener) | "Binds" to the Event Dispatcher | Doesn't know the event source |
| Event Dispatcher | Mediates between source and handler | Breaks dependency by standing between them |
This allows the source to simply notify "an event occurred," while handlers just need to register (bind) to "do something when that event occurs."
Implementing Event Dispatcher in Blueprint
Event Dispatcher is primarily defined and used within Blueprint classes.
Step 1: Define the Event Dispatcher
Open the Blueprint where you want to define the Event Dispatcher (e.g., BP_Item), and create a new dispatcher in the "Event Dispatchers" section of the "My Blueprint" panel.
- Name: Give it a descriptive name like
OnItemPickedUpthat indicates the event content. - Parameters: Set data you want to pass to listeners when the event occurs (e.g., reference to the player who picked it up, item ID, etc.).
Step 2: Bind the Event (Registration)
In the Blueprint that should handle the event (e.g., BP_InventoryManager), bind the event to the BP_Item instance.
- Get a reference to
BP_Item. - Right-click on the defined Event Dispatcher (e.g.,
OnItemPickedUp) from that reference and create a "Bind Event to OnItemPickedUp" node. - Connect the processing you want to execute when the event occurs (Custom Event) to this node's execution pin.
Blueprint Example (Listener side: BP_InventoryManager):
// Execute during initialization like BeginPlay event
Event BeginPlay
-> Get Item Reference (BP_Item)
-> Bind Event to OnItemPickedUp
-> New Event (Custom Event: HandleItemPickup)
// Custom Event definition
Custom Event HandleItemPickup (Item Ref: BP_Item, Player Ref: BP_Player)
-> Add Item to Inventory
-> Update UI
Step 3: Call the Event (Notification)
In the Blueprint that is the event source (e.g., BP_Item), call the Event Dispatcher when the event occurs.
- Right-click on the Event Dispatcher (e.g.,
OnItemPickedUp) and create a "Call OnItemPickedUp" node. - Pass appropriate values to the defined parameters and execute.
Blueprint Example (Source side: BP_Item):
// When player touches the item, etc.
Event OnComponentBeginOverlap
-> Call OnItemPickedUp (Self, Other Actor as BP_Player)
Now BP_Item can notify events without knowing who handles them, and BP_InventoryManager can subscribe to events without knowing the item class.
Implementing Event Dispatcher in C++
In C++, Event Dispatcher is implemented as multicast delegates. To make it accessible from Blueprint, specific macros are used.
Step 1: Declare the Delegate
In the header file (e.g., Item.h), declare the delegate using the DECLARE_DYNAMIC_MULTICAST_DELEGATE macro.
// Item.h
// Delegate with no parameters
DECLARE_DYNAMIC_MULTICAST_DELEGATE(FOnItemPickedUpSignature);
// Delegate with parameters (e.g., picked up item and player)
DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FOnItemPickedUpWithParamsSignature, AItem*, PickedUpItem, APlayerCharacter*, Instigator);
UCLASS()
class AItem : public AActor
{
GENERATED_BODY()
public:
// Expose as Event Dispatcher accessible from Blueprint
UPROPERTY(BlueprintAssignable, Category = "Events")
FOnItemPickedUpWithParamsSignature OnItemPickedUp;
// ...
};
Step 2: Call the Event (Notification)
In the source file (e.g., Item.cpp), call the Broadcast function when the event occurs.
// Item.cpp
void AItem::OnOverlapBegin(UPrimitiveComponent* OverlappedComp, AActor* OtherActor, ...)
{
// ... necessary checks ...
// Broadcast (notify) the event
OnItemPickedUp.Broadcast(this, Cast<APlayerCharacter>(OtherActor));
// ...
}
Step 3: Bind the Event (Registration)
On the Blueprint side, you can bind events to the C++-defined OnItemPickedUp Event Dispatcher using the same Blueprint procedure described earlier. To bind on the C++ side, use the AddDynamic function.
// InventoryManager.cpp
void AInventoryManager::BeginPlay()
{
Super::BeginPlay();
// Get Item reference (omitted here)
AItem* MyItem = GetItemReference();
if (MyItem)
{
// Bind C++ function to delegate
MyItem->OnItemPickedUp.AddDynamic(this, &AInventoryManager::HandleItemPickup);
}
}
void AInventoryManager::HandleItemPickup(AItem* PickedUpItem, APlayerCharacter* Instigator)
{
// Process adding item to inventory
}
// Important: Unbind when object is destroyed
void AInventoryManager::EndPlay(const EEndPlayReason::Type EndPlayReason)
{
Super::EndPlay(EndPlayReason);
AItem* MyItem = GetItemReference();
if (MyItem)
{
// Remove binding (prevent memory leak/crash)
MyItem->OnItemPickedUp.RemoveDynamic(this, &AInventoryManager::HandleItemPickup);
}
}
AddDynamic and RemoveDynamic Pairing
When binding with
AddDynamic, it's important to unbind with the correspondingRemoveDynamic. Especially since Widgets and UI elements are frequently created and destroyed, if you don't properly unbind inEndPlayorDestruct, references to already-destroyed objects remain, causing crashes.
Best Practices and Common Mistakes
Event Dispatcher is powerful, but misuse can make code more complex.
Best Practices
- Make event names and parameters clear: Event names (e.g.,
OnHealthChanged) should clearly indicate what happened, and parameters should include only the minimum necessary information about that event. - Always Unbind when no longer needed: Especially when widgets or temporary objects subscribe to events, use the
Unbind Eventnode (orRemoveDynamicin C++) to release bindings before the object is destroyed, or it causes memory leaks and access violations (accessing already-destroyed objects). - Event sources don't need to know handlers: The listener side (binding side) doesn't need to know the event source's class. It should receive necessary information through Event Dispatcher parameters.
Common Mistakes
- Overusing Event Dispatcher: Using Event Dispatcher for all communication makes code flow difficult to trace. Appropriately distinguish between direct function calls (tight coupling) and Event Dispatcher (loose coupling).
- When tight coupling is appropriate: When processing is limited to a single object and unlikely to change (e.g., function calls within your own components).
- When loose coupling is appropriate: When multiple different objects are interested in the same event (e.g., UI updates, sound effect playback, game state changes).
- Passing large amounts of data in parameters: Event Dispatcher parameters should contain only the minimum data needed to notify the event occurrence. If large amounts of data are needed, it's better to have the listener reference the source object and retrieve necessary properties, keeping the delegate signature simple.
Guidelines for Using Event Dispatcher
Event Dispatcher is a key feature for building loosely coupled systems in Unreal Engine.
| Item | Tight Coupling (Direct Call) | Loose Coupling (Event Dispatcher) |
|---|---|---|
| Dependencies | Source directly knows handler | Source and handler don't know each other |
| Ease of Change | Low. Changes easily propagate to other classes | High. Adding/removing listeners doesn't affect source |
| Extensibility | Low. Source must be modified each time new processing is added | High. Features can be added just by binding new listeners |
By properly utilizing Event Dispatcher, you can build robust game systems that are easy to maintain and highly extensible even in large projects. Start by introducing Event Dispatcher to small features and experience its benefits.