【Unreal Engine】Implementing Event-Driven Design with Event Dispatcher

Created: 2025-12-12

Event Dispatcher enables loose coupling design by separating event sources from handlers. From Bind/Unbind basics to C++ Delegate implementation with practical examples.

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."

ElementRoleCharacteristic
Event Source (Caller)"Calls/Broadcasts" the Event DispatcherDoesn't know who handles it
Event Handler (Listener)"Binds" to the Event DispatcherDoesn't know the event source
Event DispatcherMediates between source and handlerBreaks 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 OnItemPickedUp that 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.

  1. Get a reference to BP_Item.
  2. Right-click on the defined Event Dispatcher (e.g., OnItemPickedUp) from that reference and create a "Bind Event to OnItemPickedUp" node.
  3. 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.

  1. Right-click on the Event Dispatcher (e.g., OnItemPickedUp) and create a "Call OnItemPickedUp" node.
  2. 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 corresponding RemoveDynamic. Especially since Widgets and UI elements are frequently created and destroyed, if you don't properly unbind in EndPlay or Destruct, 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

  1. 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.
  2. Always Unbind when no longer needed: Especially when widgets or temporary objects subscribe to events, use the Unbind Event node (or RemoveDynamic in C++) to release bindings before the object is destroyed, or it causes memory leaks and access violations (accessing already-destroyed objects).
  3. 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

  1. 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).
  2. 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.

ItemTight Coupling (Direct Call)Loose Coupling (Event Dispatcher)
DependenciesSource directly knows handlerSource and handler don't know each other
Ease of ChangeLow. Changes easily propagate to other classesHigh. Adding/removing listeners doesn't affect source
ExtensibilityLow. Source must be modified each time new processing is addedHigh. 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.