Unreal Engine Notification Systems: Delegates, Events, and Broadcaster Implementation
In modern game development within Unreal Engine (UE), components must communicate without tight coupling. Three primary patterns facilitate this interaction: multicast delegates, events, and custom broadcaster classes. Each approach enables the observer design pattern but offers different trade-offs regarding flexibility, performance, and lifecycle management.
Multicast Delegates
Multicast delegates are a core UE framework feature designed for high-performance signaling between actors. Registration is performed using the DECLARE_MULTICAST_DELEGATE macro in headers. Subscribers attach callbacks to these instances using AddUObject, ensuring type safety with UObjects. Removal is handled via Remove. When data changes or an action completes, the owner invokes the Broadcast function to notify all active listeners simultaneously.
// GameNotification.h
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "GameNotification.generated.h"
declare_multicast_delegate_void_0(FLevelStatusSignal);
uclass(ANYCLASS_API AGameStateTracker : public AActor)
generated_body()
{
public:
AGameStateTracker();
void UpdateWorldState();
FLevelStatusSignal OnStateUpdated;
};
Subscribing actors register themselves during initialization. In C++ execution flow, when the state changes, every subscribed object receives the signal regardless of ownership hierarchy.
// GameStateTracker.cpp
#include "GameNotification.h"
AGameStateTracker::AGameStateTracker()
{
PrimaryActorTick.bCanEverTick = false;
}
void AGameStateTracker::UpdateWorldState()
{
UE_LOG(LogTemp, Warning, TEXT("Global environment status updated."));
OnStateUpdated.Broadcast();
}
// SubscriberLogic.cpp
#include "GameNotification.h"
vvoid APlayerMonitor::BeginPlay()
{
super::BeginPlay();
FGameModeBase* Mode = GetWorld()->GetAuthGameMode();
if (Mode)
{
AGameStateTracker* Tracker = Cast<AGameStateTracker>(Mode); // Simplified reference
if (Tracker)
{
Tracker->OnStateUpdated.AddUObject(this, &APlayerMonitor::HandleSystemChange);
}
}
}
vvoid APlayerMonitor::HandleSystemChange()
{
UE_LOG(LogTemp, Warning, TEXT("Subscriber detected state refresh."));
}
Event Objects
Events (DECLARE_EVENT) resemble delegates but often encapsulate logic differently, sometimes allowing blueprint compatibility where delegates might struggle. They behave similarly to multicast delegates regarding subscription methods (Add / Remove) and invocation (Broadcast). This mechanism is frequently utilized for system-wide notifications where strong typing is required across the editor and runtime.
// WorldEventDefinitions.h
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "WorldEventDefinitions.generated.h"
struct FConfigLoadResult;
DECLARE_EVENT(AConfigurationManager, FConfigLoadedEvent);
uclass(ANYCLASS_API AConfigurationManager : public AActor)
generated_body()
{
public:
AConfigurationManager();
void LoadSettings();
FConfigLoadedEvent OnConfigReady;
};
Implementation involves instantiating the event trigger within the owning component.
// ConfigurationManager.cpp
#include "WorldEventDefinitions.h"
AConfigurationManager::AConfigurationManager()
{
PrimaryActorTick.bCanEverTick = false;
}
void AConfigurationManager::LoadSettings()
{
UE_LOG(LogTemp, Warning, TEXT("Parsing configuration files..."));
// Simulated load process
OnConfigReady.Broadcast();
}
// ClientListener.cpp
#include "WorldEventDefinitions.h"
void AClientListener::BeginPlay()
{
super::BeginPlay();
AConfigurationManager* Manager = GetWorld()->SpawnActor<AConfigurationManager>();
if (Manager)
{
Manager->OnConfigReady.AddUObject(this, &AClientListener::ProcessStartupData);
}
}
vvoid AClientListener::ProcessStartupData()
{
UE_LOG(LogTemp, Warning, TEXT("Client listener ready after configuration load."));
}
Custom Broadcasters
For scenarios requiring granular control over notification filtering, validation, or asynchronous delivery, developers may implement a custom broadcaster class. This approach introduces manual management of a list of subscribers, usually defined by an interface. While more boilerplate-heavy, it allows for complex logic such as throttling events, checking listener validity dynamically before dispatch, or grouping subscribers by category.
// ITargetInterface.h
#pragma once
#include "CoreMinimal.h"
class ITargetInterface
{
public:
virtual void ProcessAlert() = 0;
};
// ManagerComponent.h
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "ManagerComponent.generated.h"
uclass(ANYCLASS_API ABroadcastHub : public AActor)
generated_body()
{
public:
ABroadcastHub();
void RegisterHandler(ITargetInterface* Handler);
void UnregisterHandler(ITargetInterface* Handler);
void SendNotice();
private:
TArray<ITargetInterface*> ActiveHandlers;
};
The broadcast logic iterates through the collection, ensuring robustness before invoking targets.
// BroadcastHub.cpp
#include "ManagerComponent.h"
ABroadcastHub::ABroadcastHub()
{
PrimaryActorTick.bCanEverTick = false;
}
void ABroadcastHub::RegisterHandler(ITargetInterface* Handler)
{
if (Handler && !ActiveHandlers.Contains(Handler))
{
ActiveHandlers.Add(Handler);
}
}
void ABroadcastHub::SendNotice()
{
for (ITargetInterface* Target : ActiveHandlers)
{
if (IsValid(Target))
{
Target->ProcessAlert();
}
}
}
// HandlerActor.cpp
#include "ManagerComponent.h"
uclass(ANYCLASS_API AHandlerActor : public AActor, public ITargetInterface)
generated_body()
{
public:
AHandlerActor();
void BeginPlay() override;
void ProcessAlert() override;
};
vvoid AHandlerActor::BeginPlay()
{
super::BeginPlay();
ABroadcastHub* Hub = GetWorld()->SpawnActor<ABroadcastHub>();
if (Hub)
{
Hub->RegisterHandler(this);
}
}
vvoid AHandlerActor::ProcessAlert()
{
UE_LOG(LogTemp, Warning, TEXT("Custom handler acknowledged alert signal."));
}
Choosing between these methods depends on project scale, Blueprint needs, and the required level of abstraction control over the communication layer.