Skip to main content

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

graph LR A["Direct reference + Cast
(tightest)"] --> B["Component lookup"] B --> C["Interfaces
(Module 5.5)"] C --> D["Delegates / events
(Module 5.4, loosest)"]
Figure 1: The coupling spectrum. Left = simplest but most entangled; right = most flexible but more machinery. Match the tool to the relationship.

📖 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:

PatternQuestion it answersUse 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 a UHealthComponent firing an OnDeath event (Module 5.4) is cleaner than everyone polling IsDead().

Choosing a Pattern

A practical decision guide you can apply on the spot:

graph TD Start["A needs to talk to B"] --> Q1{"Is it one/two
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"]
Figure 2: Routing a conversation. Notice how rarely the answer is "cast to a concrete class everywhere" — that's usually the pattern to grow out of.

⚠️ 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.

  1. A homing missile that must call Explode() on its one specific target actor
  2. An explosion that should damage every damageable thing in radius (characters, barrels, crates)
  3. A lever that should trigger "activate" on whatever it's linked to — doors, lifts, traps
  4. 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.