📡 Lesson 3.5: Actor Communication Patterns
Games are actors talking: a bullet tells a target it was hit, a button tells a door to open, a pickup tells the player to heal. How you wire those conversations determines whether your codebase stays flexible or turns into a tangle. This lesson surveys the patterns and when to use each.
🎯 Learning Objectives
By the end of this lesson, you will be able to:
- Communicate via a direct reference and a
Cast - Reach another actor's component with
GetComponentByClass/FindComponentByClass - Rank communication patterns by how tightly they couple actors
- Recognize when a direct reference is fine and when to decouple
- Preview interfaces and delegates as the loosely-coupled options
Estimated Time: 45 minutes
Engine Version: Unreal Engine 5.8
In This Lesson
The Coupling Spectrum
Communication patterns trade simplicity against coupling — how much one actor must know about another. Tight coupling is quick to write but brittle; loose coupling takes more setup but scales.
(tightest)"] --> B["Component lookup"] B --> C["Interfaces
(Module 5.5)"] C --> D["Delegates / events
(Module 5.4, loosest)"]
📖 There's no single "right" pattern
A bullet that hits one specific known target can use a direct call. A button that should open any door without knowing which wants an interface or delegate. Good architecture is picking the loosest coupling that still does the job — not always the loosest possible.
Direct Reference & Cast
The simplest conversation: you have a pointer to the other actor, and you call its function. When you only have a base pointer, Cast (Lesson 2.2) narrows it to the type whose API you need.
// A projectile that got a hit result knows the actor it struck:
void AProjectile::OnHit(AActor* HitActor)
{
if (!IsValid(HitActor)) { return; }
// We need AEnemy-specific behavior, so cast:
if (AEnemy* Enemy = Cast<AEnemy>(HitActor))
{
Enemy->ReactToHit(Damage); // direct call into the enemy's API
}
}
⚠️ Cast couples you to a type
Every Cast<AEnemy> hard-wires this code to AEnemy. If barrels and turrets should also react to hits, you'd need a cast per type — a smell that says "reach for an interface instead" (Lesson 5.5). One or two known types: casting is fine. Many unrelated types sharing a behavior: decouple.
✅ Store references you'll reuse — as UPROPERTY
If an actor keeps a reference to another (a turret remembering its target), store it as a UPROPERTY() (Lesson 2.3/2.5) so the GC sees it, or TWeakObjectPtr if it may die. A raw pointer member to another actor is the dangling-pointer bug waiting to happen.
Talking to Components
Often you don't want the actor — you want a component on it, like the UHealthComponent from Lesson 3.3. Rather than casting the actor to a specific class, ask it for the component by type. This is looser: it works on any actor that has a health component, regardless of its class.
void AProjectile::OnHit(AActor* HitActor)
{
if (!IsValid(HitActor)) { return; }
// Works on ANY actor with a UHealthComponent — character, barrel, turret.
if (UHealthComponent* Health = HitActor->FindComponentByClass<UHealthComponent>())
{
const bool bKilled = Health->ApplyDamage(Damage);
if (bKilled) { /* award score, spawn effect... */ }
}
// No health component? Then this actor simply isn't damageable. No cast needed.
}
📖 Why this is better than casting the actor
Casting to AEnemy asks "are you this specific class?" Asking for a UHealthComponent asks "do you have this capability?" The second is exactly the composition mindset from Lesson 3.3 — and it means one projectile damages everything damageable, with zero per-type code. This is a favorite real-world pattern precisely because it scales.
Toward Loose Coupling
The two loosest patterns get full lessons in Module 5, but here's the preview so you know where the spectrum leads:
| Pattern | Question it answers | Use when |
|---|---|---|
| Direct + Cast | "Are you this exact class?" | One or two known types |
| Component lookup | "Do you have this capability?" | Many types share a component |
| Interface (5.5) | "Can you do this action?" | Many unrelated types implement a behavior (e.g. IInteractable) |
| Delegate / event (5.4) | "Tell whoever's listening that X happened" | One-to-many, sender shouldn't know receivers (e.g. "OnDeath") |
💡 The direction of knowledge: casting and component lookup are caller asks callee. Delegates flip it — the callee announces and callers subscribe, so the announcer needs to know nothing about who's listening. That inversion is what makes events so decoupled, and why aUHealthComponentfiring anOnDeathevent (Module 5.4) is cleaner than everyone pollingIsDead().
Choosing a Pattern
A practical decision guide you can apply on the spot:
specific known types?"} Q1 -->|Yes| Direct["Direct reference + Cast"] Q1 -->|No| Q2{"Do the targets share
a component/capability?"} Q2 -->|Yes| Comp["FindComponentByClass"] Q2 -->|No| Q3{"Is it 'tell whoever
cares that X happened'?"} Q3 -->|Yes| Del["Delegate / event"] Q3 -->|No| Iface["Interface"]
⚠️ The anti-pattern to avoid
A long chain of Cast<A> ... else Cast<B> ... else Cast<C> to handle "many things that all do X" is the classic sign you should have used an interface or a component. When you catch yourself writing the third cast for the same behavior, stop and decouple.
Hands-on Exercise & Quiz
🏋️ Exercise: Pick the pattern
Objective: Choose the loosest pattern that fits each scenario.
- A homing missile that must call
Explode()on its one specific target actor - An explosion that should damage every damageable thing in radius (characters, barrels, crates)
- A lever that should trigger "activate" on whatever it's linked to — doors, lifts, traps
- An enemy's death that a score manager, a spawner, and a quest tracker all need to know about
✅ Answers
- 1. Direct reference +
Cast— one known target, simplest is fine. - 2. Component lookup —
FindComponentByClass<UHealthComponent>()on each actor in radius; works regardless of class. - 3. Interface (e.g.
IActivatable) — many unrelated types share the "activate" action (Module 5.5). - 4. Delegate/event — the enemy fires
OnDeath; the three systems subscribe without the enemy knowing them (Module 5.4).
🎯 Quick Quiz
Question 1: Which asks "do you have this capability?" rather than "are you this class?"
Question 2: A sender that shouldn't know who receives its "X happened" message should use:
Question 3: Repeated Cast<A> else Cast<B> else Cast<C> for one shared behavior signals you should use:
Summary
🎉 Key Takeaways
- Communication patterns range from tight (direct reference +
Cast) to loose (interfaces, delegates) — pick the loosest that does the job. - Direct + Cast is fine for one or two known types; store reused references as
UPROPERTY/TWeakObjectPtr. FindComponentByClass<T>asks "do you have this capability?" — it works across unrelated actor classes and embodies composition.- Interfaces (Module 5.5) express "can you do X?"; delegates (Module 5.4) let a sender announce events without knowing listeners.
- Chained casts for one shared behavior is an anti-pattern — reach for an interface or component instead.
📚 Additional Resources
🚀 What's Next?
Module 3 is complete — you can build actors and components, spawn and destroy them, and wire them together. Module 4 puts a player in the driver's seat: Enhanced Input in C++, Pawns and Characters and movement, and the PlayerController.
🎉 Module 3 complete!
The gameplay framework is yours. Time to hand the controls to a player.