📣 Lesson 5.4: Delegates & Events
A delegate is a "call whoever's listening" mechanism — the loosest coupling on the spectrum from Module 3.5. When a health component hits zero, it announces death; a score manager, a spawner, and a UI all hear it, and the component never needs to know they exist. This is how professional Unreal code stays decoupled.
🎯 Learning Objectives
By the end of this lesson, you will be able to:
- Explain what a delegate is and how multicast & dynamic variants differ
- Declare a dynamic multicast delegate with the
DECLARE_macros - Expose an event as a
UPROPERTY(BlueprintAssignable)andBroadcastit - Subscribe from C++ with
AddDynamicand unsubscribe withRemoveDynamic - Refactor the
UHealthComponentto fire anOnDeathevent
Estimated Time: 60 minutes
Engine Version: Unreal Engine 5.8
In This Lesson
What Is a Delegate?
A delegate is a variable that holds one or more function references and can call them all. The object that owns the delegate broadcasts; other objects subscribe. Crucially, the broadcaster doesn't know or care who's listening — it just fires.
(broadcaster)"] -->|"OnDeath.Broadcast()"| D["OnDeath delegate"] D --> S1["ScoreManager.AddKill()"] D --> S2["Spawner.OnEnemyDied()"] D --> S3["QuestTracker.Notify()"]
OnDeath; three unrelated systems react. The component has zero references to any of them — the inversion of control from Module 3.5.📖 The inversion that matters
Casting and component lookup are "caller reaches into callee." Delegates flip it: the callee announces, and callers subscribe. That means you can add a fourth listener (an achievement system) without touching the health component at all. This is why events are the go-to for "one thing happened, many care."
Delegate Flavors
Unreal has several delegate types. Two axes describe them:
| Axis | Options |
|---|---|
| How many listeners? | Single-cast (one) vs Multicast (many) |
| Blueprint-visible? | Dynamic (yes, by name, serializable) vs non-dynamic (C++ only, faster) |
✅ The one to default to
For gameplay events that Blueprint and multiple systems should hear, use a Dynamic Multicast Delegate — the type behind OnComponentBeginOverlap and every BlueprintAssignable event. It's slightly slower than a C++-only delegate but integrates with Blueprint and the editor, which is almost always worth it for gameplay. Reach for non-dynamic delegates only in hot, C++-only paths where the Blueprint integration isn't needed.
Declaring & Broadcasting
You declare a delegate type with a macro whose name encodes its parameter count, then make a UPROPERTY of that type. The macro goes above the class, in the header.
// HealthComponent.h — declare the delegate TYPE (one param: the component).
// The macro name encodes arity: ..._OneParam, _TwoParams, etc.
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(
FOnDeathSignature, UHealthComponent*, DeadComponent);
UCLASS(ClassGroup = (Custom), meta = (BlueprintSpawnableComponent))
class MYPROJECT_API UHealthComponent : public UActorComponent
{
GENERATED_BODY()
public:
// The event instance. BlueprintAssignable lets Blueprints bind to it too.
UPROPERTY(BlueprintAssignable, Category = "Health")
FOnDeathSignature OnDeath;
// ... existing MaxHealth, CurrentHealth, ApplyDamage ...
};
// HealthComponent.cpp — fire the event when health reaches zero.
bool UHealthComponent::ApplyDamage(float Amount)
{
if (IsDead() || Amount <= 0.0f) { return false; }
CurrentHealth = FMath::Clamp(CurrentHealth - Amount, 0.0f, MaxHealth);
if (IsDead())
{
// Announce to every listener. We pass 'this' so they know who died.
OnDeath.Broadcast(this);
return true;
}
return false;
}
⚠️ Match the macro arity to the parameters
Use DECLARE_DYNAMIC_MULTICAST_DELEGATE for zero params, ..._OneParam for one, ..._TwoParams for two, and so on. Each parameter needs a type and a name in the macro. Dynamic delegate parameters must be reflection-friendly types (UObject pointers, structs, ints, etc.). A mismatch between the macro arity and your Broadcast call is a compile error.
Subscribing & Unsubscribing
A listener binds a UFUNCTION() whose signature matches the delegate. Same AddDynamic you used for collision in Lesson 5.1 — because those collision events are dynamic multicast delegates.
// A ScoreManager listens for a specific enemy's death.
void AScoreManager::WatchEnemy(AEnemy* Enemy)
{
if (UHealthComponent* Health = Enemy->FindComponentByClass<UHealthComponent>())
{
// Subscribe: call HandleDeath when this component broadcasts OnDeath.
Health->OnDeath.AddDynamic(this, &AScoreManager::HandleDeath);
}
}
// The handler — signature MUST match the delegate (one UHealthComponent* param).
UFUNCTION()
void AScoreManager::HandleDeath(UHealthComponent* DeadComponent)
{
Score += 100;
UE_LOG(LogTemp, Log, TEXT("Kill! Score = %d"), Score);
}
// Later, stop listening (e.g. in EndPlay, Lesson 2.6 cleanup discipline):
void AScoreManager::EndPlay(const EEndPlayReason::Type Reason)
{
if (WatchedHealth) { WatchedHealth->OnDeath.RemoveDynamic(this, &AScoreManager::HandleDeath); }
Super::EndPlay(Reason);
}
⚠️ Unsubscribe when you go away
A dangling subscription — a destroyed listener still bound — can lead to a callback into invalid memory. For dynamic delegates, Unreal is fairly robust (it validates the target UObject), but the clean discipline is to RemoveDynamic in EndPlay, mirroring your setup. Symmetric bind/unbind is the same habit as symmetric timer set/clear (5.3) and component setup/teardown (2.6).
Refactoring HealthComponent
Look at what the OnDeath event buys us across the modules. Before, an enemy had to poll Health->IsDead() and everyone needed a reference to the enemy. Now:
| Without the event | With OnDeath |
|---|---|
Enemy checks IsDead() in Tick | Component broadcasts once, at the moment of death |
| Score manager needs a list of every enemy | Score manager just subscribes to each enemy's event |
| Adding a new reaction edits the enemy | New reaction = new subscriber, enemy untouched |
✅ And Blueprint gets it free
Because OnDeath is BlueprintAssignable, a designer can drag from the health component in a Blueprint and add "On Death" logic — spawn loot, play a sound — with no C++ change. The C++ owns when death happens; Blueprint decides what to do about it. That's the hybrid model (Lesson 1.1) expressed through events, and it's exactly what Module 6 formalizes next.
💡 When NOT to use an event: if exactly one object cares and it already has a direct reference, a plain function call is simpler. Events shine for one-to-many and for decoupling a sender from unknown receivers. Don't add a delegate where a direct call would do — match the tool to the relationship (Lesson 3.5).
Hands-on Exercise & Quiz
🏋️ Exercise: An OnHealthChanged event
Objective: Add a second event for UI.
- Declare
FOnHealthChangedSignatureas a dynamic multicast delegate with two params:float NewHealth,float MaxHealth. - Add a
BlueprintAssignableOnHealthChangedproperty. - Broadcast it at the end of
ApplyDamageandHeal(any time health changes). - Describe how a health-bar widget would use it without the component knowing about the widget.
✅ Declaration & usage
DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(
FOnHealthChangedSignature, float, NewHealth, float, MaxHealth);
UPROPERTY(BlueprintAssignable, Category = "Health")
FOnHealthChangedSignature OnHealthChanged;
// After any change:
OnHealthChanged.Broadcast(CurrentHealth, MaxHealth);
The widget subscribes to OnHealthChanged (in C++ or Blueprint) and updates its bar when it fires. The component broadcasts blindly; it never references the widget — so the same component drives a HUD bar, a boss health bar, or nothing at all.
🎯 Quick Quiz
Question 1: What makes a delegate multicast?
Question 2: Which specifier lets Blueprints bind to a C++ event?
Question 3: The big advantage of firing OnDeath vs everyone polling IsDead()?
Summary
🎉 Key Takeaways
- A delegate holds function references; the owner broadcasts, others subscribe — the broadcaster stays ignorant of listeners.
- For gameplay events, default to a Dynamic Multicast Delegate (Blueprint-friendly, many listeners).
- Declare the type with
DECLARE_DYNAMIC_MULTICAST_DELEGATE[_NParams]; expose aUPROPERTY(BlueprintAssignable); fire with.Broadcast(...). - Subscribe with
AddDynamic(handler must beUFUNCTION(), signature matching);RemoveDynamicinEndPlay. - Events are for one-to-many and decoupling; use a direct call when a single known object cares.
📚 Additional Resources
🚀 What's Next?
Delegates broadcast "something happened." The last interaction pattern answers "can you do X?" across unrelated types — interfaces. We'll build IInteractable and finally close the coupling spectrum from Module 3.5.
🎉 Lesson complete!
Your objects can announce. Last stop: making them share abilities.