Skip to main content

🧩 Lesson 3.3: Custom Actor & Scene Components

Instead of cramming every feature into ever-bigger actor classes, Unreal encourages composition: package a behavior into a component and drop it onto any actor that needs it. Writing your own components is the single biggest lever for a clean, reusable codebase.

🎯 Learning Objectives

By the end of this lesson, you will be able to:

  • Explain composition over inheritance in Unreal terms
  • Choose between UActorComponent and USceneComponent
  • Write a custom UActorComponent with its own state and logic
  • Expose component functionality to its owning actor and to Blueprint
  • Get the owning actor safely from inside a component

Estimated Time: 60 minutes

Engine Version: Unreal Engine 5.8

In This Lesson

Composition over Inheritance

Imagine health. If you put health logic directly in your ACharacter, then your destructible ABarrel, your ATurret, and your ADoor each need their own copy — or an awkward shared base class that forces unrelated things to be siblings. Inheritance trees get tangled fast.

Composition flips it: write a UHealthComponent once, and attach it to any actor that should have health — character, barrel, turret, door. Each keeps its own base class; they just share a part.

graph TD HC["UHealthComponent
(one implementation)"] --> Char["ACharacter"] HC --> Barrel["ABarrel"] HC --> Turret["ATurret"] HC --> Door["ADoor"]
Figure 1: One component, many unrelated owners. This is how you avoid deep, brittle inheritance trees — the defining pattern of clean Unreal architecture.

📖 The mental shift

Ask not "what should this actor be?" (inheritance) but "what should this actor have?" (composition). Health, inventory, interaction, aiming — each becomes a component you mix onto whatever needs it. This mirrors how the engine itself is built: even ACharacter is "a Pawn that has a movement component."

ActorComponent vs SceneComponent

There are two component base classes, and the choice is simple once you know the rule:

BaseHas a transform?Use for
UActorComponentNo — pure logic/dataHealth, inventory, AI state, cooldowns — things without a position
USceneComponentYes — location/rotation/scale, can be attached in the hierarchyAnything that needs a place in the world: a spawn point, an attach socket, a camera boom

✅ The deciding question

"Does this thing need a position in the world?" Yes → USceneComponent (or a subclass like a mesh/light/camera). No → UActorComponent. Health has no position, so it's a UActorComponent. A "muzzle location" has a position, so it's a USceneComponent. Note every visible component (mesh, light) is ultimately a USceneComponent, because it must be somewhere.

Writing a Health Component

Health has no transform, so it's a UActorComponent. Create it via Tools → New C++ Class → Actor Component. Header:

// HealthComponent.h
#pragma once

#include "CoreMinimal.h"
#include "Components/ActorComponent.h"
#include "HealthComponent.generated.h"

// Meta specifier: makes this component addable in the editor's Add Component menu.
UCLASS(ClassGroup = (Custom), meta = (BlueprintSpawnableComponent))
class MYPROJECT_API UHealthComponent : public UActorComponent
{
    GENERATED_BODY()

public:
    UHealthComponent();

protected:
    virtual void BeginPlay() override;

    UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Health")
    float MaxHealth = 100.0f;

    UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Health")
    float CurrentHealth = 0.0f;

public:
    // Apply damage; returns true if this brought health to zero (a "kill").
    UFUNCTION(BlueprintCallable, Category = "Health")
    bool ApplyDamage(float Amount);

    UFUNCTION(BlueprintPure, Category = "Health")
    bool IsDead() const { return CurrentHealth <= 0.0f; }

    UFUNCTION(BlueprintPure, Category = "Health")
    float GetHealthPercent() const;
};

Source:

// HealthComponent.cpp
#include "HealthComponent.h"

UHealthComponent::UHealthComponent()
{
    // This component has no per-frame work, so disable Tick for performance.
    PrimaryComponentTick.bCanEverTick = false;
}

void UHealthComponent::BeginPlay()
{
    Super::BeginPlay();
    CurrentHealth = MaxHealth;   // start at full when the game begins
}

bool UHealthComponent::ApplyDamage(float Amount)
{
    if (IsDead() || Amount <= 0.0f)
    {
        return false;
    }

    CurrentHealth = FMath::Clamp(CurrentHealth - Amount, 0.0f, MaxHealth);
    UE_LOG(LogTemp, Log, TEXT("%s health: %.0f/%.0f"),
        *GetOwner()->GetName(), CurrentHealth, MaxHealth);

    return IsDead();   // true means this hit was fatal
}

float UHealthComponent::GetHealthPercent() const
{
    return (MaxHealth > 0.0f) ? (CurrentHealth / MaxHealth) : 0.0f;
}

⚠️ Don't forget BlueprintSpawnableComponent

Without meta = (BlueprintSpawnableComponent), your component won't appear in the editor's Add Component dropdown or be addable in Blueprints. It's the specifier that makes a custom component usable by designers — easy to omit and then wonder why the component is "missing."

Reaching the Owner

A component often needs to talk to the actor it's attached to — its owner. GetOwner() returns that actor as an AActor*; cast it when you need a specific type.

void UHealthComponent::BeginPlay()
{
    Super::BeginPlay();
    CurrentHealth = MaxHealth;

    // Generic owner access:
    AActor* Owner = GetOwner();
    if (!Owner) { return; }

    // Need a specific type? Cast safely (Lesson 2.2):
    if (ACharacter* OwningCharacter = Cast<ACharacter>(Owner))
    {
        // ... do character-specific setup, e.g. bind to its events ...
    }
}

📖 Keep components loosely coupled

A well-designed component tries not to assume a specific owner type. A UHealthComponent that works on any AActor is reusable on barrels and characters alike; one that hard-requires ACharacter is not. Cast only when you truly need the specialization, and handle the "wrong owner" case gracefully. This pays off when we broadcast events in Lesson 3.5 and 5.4.

Using the Component

Add the component to an actor in that actor's constructor, then use it:

// In some AEnemy constructor:
#include "HealthComponent.h"

AEnemy::AEnemy()
{
    // Components are created the same way as any subobject.
    Health = CreateDefaultSubobject<UHealthComponent>(TEXT("Health"));
}

// Later, when the enemy is hit:
void AEnemy::OnHit(float Damage)
{
    const bool bKilled = Health->ApplyDamage(Damage);
    if (bKilled)
    {
        // handle death — ragdoll, score, destroy, etc.
        Destroy();
    }
}

✅ The reuse payoff, realized

That exact same UHealthComponent now drops onto a barrel, a turret, or a destructible wall with zero changes — each just calls ApplyDamage. And because it's BlueprintSpawnableComponent, a designer can add it to a Blueprint actor with no code at all. One component, unlimited owners: that's the whole point.

Hands-on Exercise & Quiz

🏋️ Exercise: A cooldown component

Objective: Write a reusable UCooldownComponent (a UActorComponent — it has no transform).

Requirements: an EditAnywhere CooldownDuration; a BlueprintCallable bool TryUse() that returns true and starts a cooldown if ready, false if still cooling; a BlueprintPure bool IsReady() const. (Hint: you can track readiness with a timestamp using GetWorld()->GetTimeSeconds().)

💡 Hint — TryUse
bool UCooldownComponent::TryUse()
{
    if (!IsReady()) { return false; }
    LastUsedTime = GetWorld()->GetTimeSeconds();
    return true;
}

bool UCooldownComponent::IsReady() const
{
    return GetWorld()->GetTimeSeconds() - LastUsedTime >= CooldownDuration;
}
✅ Why a UActorComponent?

A cooldown is pure state and logic — it has no position in the world — so UActorComponent is correct. If you'd chosen USceneComponent, you'd be paying for a transform you never use.

🎯 Quick Quiz

Question 1: You need a component representing "current ammo" (no position). Which base?

Question 2: How does a component reference the actor it's attached to?

Question 3: Which meta makes a custom component addable in the editor's Add Component menu?

Summary

🎉 Key Takeaways

  • Composition over inheritance: package behavior in a component and attach it to any actor — ask "what does it have," not "what is it."
  • UActorComponent for logic/data with no position; USceneComponent when it needs a transform in the world.
  • Expose component API with UFUNCTION(BlueprintCallable/Pure); mark the class meta = (BlueprintSpawnableComponent) so it's addable.
  • Reach the owning actor with GetOwner(); Cast only when you need a specific type, and keep components loosely coupled.
  • Add components in the owner's constructor via CreateDefaultSubobject — the same pattern as any subobject.

📚 Additional Resources

🚀 What's Next?

You can build actors and the components that power them. Next: bringing actors into and out of existence at runtime — spawning, destroying, and iterating actors, the dynamic side of the world.

🎉 Lesson complete!

Composition unlocked. Let's make actors appear and vanish on command.