
Advanced Unreal Engine C++: Patterns and Event Handling
Advanced C++ Architecture: Building Scalable Systems in Unreal
If you find yourself casting to AMyCharacter every time you need to apply damage or check an inventory, you’re building a house of cards. One change to your character class, and the whole project crumbles.
Professional Unreal development is about decoupling: designing systems that cooperate without knowing each other’s concrete types. This guide connects the dots between the three pillars of scalable gameplay architecture:
- Contracts (Interfaces): “What can this thing do?”
- Signals (Delegates): “Something happened.”
- Routing (Event Managers): “Connect producers to consumers without spaghetti references.”
OOP on Unreal Engine C++
Unreal adds reflection, interfaces, and a full delegate/event system on top of standard C++. The goal is still the same: reduce coupling and make your gameplay code resilient to change.
Abstract - Interface
In pure C++ you might write a pure virtual like virtual void MyFunc() = 0;. In Unreal you can also use PURE_VIRTUAL when you want an abstract function declaration but still satisfy the reflection toolchain:
virtual void MyPureFunction() PURE_VIRTUAL(AMyActor::MyPureFunction, );If the function must return a value, provide a default return expression:
virtual uint32 GetIndex() PURE_VIRTUAL(AMyActor::GetIndex, return 0;);That “contract first” mindset is exactly what Interfaces formalize.
Interfaces: The “Contract” Pattern
Interfaces allow different classes to work together without knowing anything about each other’s internals.
Use an interface when you want to request an action from something (Host, Join, RefreshList) without casting to a specific class.
MenuInterface.h
#pragma once
#include "CoreMinimal.h"
#include "UObject/Interface.h"
#include "MenuInterface.generated.h"
// This class does not need to be modified.
UINTERFACE(MinimalAPI)
class UMenuInterface : public UInterface
{
GENERATED_BODY()
};
/**
* Interface for Menu callbacks
*/
class YOURGAME_API IMenuInterface
{
GENERATED_BODY()
public:
virtual void Host() = 0;
virtual void Join(const FString& Address) = 0;
virtual void LoadMainMenu() = 0;
// Pure Virtual with Verify
virtual void RefreshServerList() PURE_VIRTUAL(IMenuInterface::RefreshServerList, );
};Event Handling
Interfaces are great for direct requests (“do X”). Event handling is for notifications (“X happened”).
In Unreal, Delegates (and related Event macros) are the standard tool: producers broadcast without knowing who is listening, and listeners can subscribe/unsubscribe safely.
Single Delegate
A single-cast delegate is a one-to-one callback (one listener). Use it when exactly one system should respond (e.g., “OnFinishedLoading”).
Multicast Delegate
A multicast delegate is one-to-many (multiple listeners). Use it for events like “OnBossDied” or “OnMatchStarted”.
Event
Unreal uses the word “event” in a few ways. In pure C++ you’ll see macros like DECLARE_DELEGATE, DECLARE_MULTICAST_DELEGATE, and DECLARE_EVENT. If you want Blueprints to bind, you’ll typically use dynamic delegates (e.g., DECLARE_DYNAMIC_MULTICAST_DELEGATE) and expose them as UPROPERTY(BlueprintAssignable).
Below is a minimal speaker/listener example using a C++ multicast delegate:
SpeakerActor.h
DECLARE_MULTICAST_DELEGATE_OneParam(FMessageDelegate, FString);
UCLASS()
class YOURGAME_API ASpeakerActor : public AActor
{
GENERATED_BODY()
public:
FMessageDelegate MessageDelegate;
// Sends a message to listener
void Speak();
};SpeakerActor.cpp
void ASpeakerActor::Speak()
{
// Broadcast will call back all bound listener functions
MessageDelegate.Broadcast(TEXT("Hello from Speaker"));
}ListenerActor.h
UCLASS()
class YOURGAME_API AListenerActor : public AActor
{
GENERATED_BODY()
private:
FDelegateHandle DelegateHandle;
ASpeakerActor* Speaker;
UFUNCTION()
void ReceiveMessage(FString Message);
protected:
virtual void BeginPlay() override;
virtual void EndPlay(const EEndPlayReason::Type EndPlayReason) override;
};ListenerActor.cpp
void AListenerActor::BeginPlay()
{
Super::BeginPlay();
// Find speaker and bind
TArray<AActor*> TaggedActors;
UGameplayStatics::GetAllActorsWithTag(GetWorld(), "Speaker", TaggedActors);
if(TaggedActors.Num() > 0){
Speaker = Cast<ASpeakerActor>(TaggedActors[0]);
// Bind raw C++ function or UFunction
DelegateHandle = Speaker->MessageDelegate.AddUObject(this, &AListenerActor::ReceiveMessage);
}
}
void AListenerActor::ReceiveMessage(FString Message){
UE_LOG(LogTemp, Warning, TEXT("Delegate Message: %s"), *Message);
}
void AListenerActor::EndPlay(const EEndPlayReason::Type EndPlayReason){
if(Speaker){
Speaker->MessageDelegate.Remove(DelegateHandle);
}
Super::EndPlay(EndPlayReason);
}Event Manager
As your project grows, you can accidentally recreate coupling by directly binding listeners to specific invokers everywhere.
An Event Manager gives you a single place to connect and disconnect relationships so:
- listeners don’t need to find speakers (and vice versa)
- bindings can be cleaned up consistently
- adding a new invoker doesn’t require touching every listener
DelegateDeclarations.h
#include "UObject/NoExportTypes.h"
DECLARE_MULTICAST_DELEGATE_OneParam(FMessageEvent, FString);
UCLASS()
class YOURGAME_API ADelegateDeclarations : public AActor
{
GENERATED_BODY()
};EventManagerActor.h
#include "YourSpeakerActor.h"
#include "YourListenerActor.h"
#include "Delegates/DelegateInstanceInterface.h"
UCLASS()
class YOURGAME_API AEventManagerActor : public AActor
{
GENERATED_BODY()
public:
void AddInvoker(AYourSpeakerActor* Invoker);
void RemoveInvoker(AYourSpeakerActor* Invoker);
void AddListener(AYourListenerActor* Listener);
void RemoveListener(AYourListenerActor* Listener);
private:
TArray<AYourSpeakerActor*> MessageEventInvokers;
// Map Listener -> (Map Speaker -> Handle)
TMap<AYourListenerActor*, TMap<AYourSpeakerActor*, FDelegateHandle>> MessageEventListeners;
};EventManagerActor.cpp
void AEventManagerActor::AddInvoker(AYourSpeakerActor* Invoker){
MessageEventInvokers.Add(Invoker);
// When a new speaker joins, connect existing listeners to it
for(auto &Element : MessageEventListeners)
{
AYourListenerActor* Listener = Element.Key;
FDelegateHandle Handle = Invoker->GetMessageEvent().AddUObject(Listener, &AYourListenerActor::ReceiveMessage);
Element.Value.Add(Invoker, Handle);
}
}
void AEventManagerActor::AddListener(AYourListenerActor* Listener){
// When a new listener joins, connect it to all existing speakers
for(AYourSpeakerActor* Invoker : MessageEventInvokers)
{
FDelegateHandle Handle = Invoker->GetMessageEvent().AddUObject(Listener, &AYourListenerActor::ReceiveMessage);
MessageEventListeners.FindOrAdd(Listener).Add(Invoker, Handle);
}
}Next Steps
- If you want a refresher on how
UFUNCTIONspecifiers become Blueprint nodes, see: C++ to Blueprint Node - If you’re applying these patterns to UI (menus, settings, session browsers), see: Blueprint & UI Patterns