Skip to main content

๐ŸŽญ Lesson 3.2: Writing Actors in C++

Now the real craft begins. You'll build an actor from scratch โ€” assembling its components in the constructor, wiring a hierarchy, and hooking into each stage of its life. This is the pattern behind nearly everything you'll make in Unreal.

๐ŸŽฏ Learning Objectives

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

  • Build an actor's components with CreateDefaultSubobject and set a root
  • Assemble a component hierarchy via attachment
  • Explain the actor lifecycle and what belongs in each stage
  • Use the constructor, BeginPlay, Tick, and EndPlay correctly
  • Know why "constructor for setup, BeginPlay for gameplay" matters

Estimated Time: 60 minutes

Engine Version: Unreal Engine 5.8

In This Lesson

Components & the Root

You met CreateDefaultSubobject in Module 1. Here's the rule stated fully: in an actor's constructor, you create its default components with CreateDefaultSubobject<T>, and exactly one of them becomes the RootComponent โ€” the component that defines the actor's transform in the world.

// In the constructor:
USceneComponent* Root = CreateDefaultSubobject<USceneComponent>(TEXT("Root"));
RootComponent = Root;   // this actor's transform IS the root's transform

UStaticMeshComponent* Body = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("Body"));
Body->SetupAttachment(RootComponent);   // attach Body under the root

๐Ÿ“– The name string matters

The TEXT("...") name you pass to CreateDefaultSubobject is the component's internal name โ€” it must be unique within the actor and stable, because serialization uses it to match saved data to components. Don't rename these casually on an existing class; it can break instances that referenced the old name.

โš ๏ธ Root choices have consequences

An actor with no root component has no transform of its own. Often the root is a plain USceneComponent (an invisible transform anchor) with visible components attached beneath it โ€” this gives you a stable pivot independent of any one mesh. For a simple prop, the mesh itself can be the root.

Building a Hierarchy

Components attach into a tree. Moving a parent moves its children; the root moves everything. SetupAttachment (in the constructor) builds this tree.

graph TD Root["Root (USceneComponent)"] --> Mesh["Body (StaticMesh)"] Root --> Spring["SpringArm"] Spring --> Cam["Camera"] Mesh --> Muzzle["MuzzleFlash (SceneComponent)"]
Figure 1: A typical component hierarchy. The camera follows the spring arm, which follows the root; the muzzle point rides on the body mesh. Transform changes flow down the tree.
// Attach relationships form the tree above:
RootComponent = CreateDefaultSubobject<USceneComponent>(TEXT("Root"));

Body = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("Body"));
Body->SetupAttachment(RootComponent);

SpringArm = CreateDefaultSubobject<USpringArmComponent>(TEXT("SpringArm"));
SpringArm->SetupAttachment(RootComponent);

Camera = CreateDefaultSubobject<UCameraComponent>(TEXT("Camera"));
Camera->SetupAttachment(SpringArm);   // camera rides the spring arm

โœ… SetupAttachment vs AttachToComponent

Use SetupAttachment in the constructor for default components. Use AttachToComponent at runtime (in BeginPlay or later) when attaching dynamically-spawned components or actors. Mixing them up โ€” calling SetupAttachment at runtime โ€” silently fails to attach.

The Actor Lifecycle

An actor passes through a defined sequence. Each stage has an intended purpose, and putting code in the wrong stage is a top source of bugs.

graph TD A["Constructor
create components, set defaults"] --> B["OnConstruction
(construction script, editor)"] B --> C["BeginPlay
gameplay starts โ€” world is ready"] C --> D["Tick(DeltaTime)
every frame while alive"] D --> E["EndPlay
leaving play / being destroyed"] E --> F["Destroyed / GC"]
Figure 2: The actor lifecycle. The constructor runs very early (even in the editor, on the class default object); BeginPlay runs when the actor truly enters a live game.
StageRuns whenPut here
ConstructorObject created (incl. editor/CDO)Create components, set default values
BeginPlayPlay starts / actor spawned into live worldGameplay init that needs the world, other actors, or systems
TickEvery frame while alive (if enabled)Per-frame logic โ€” used sparingly
EndPlayRemoved from play / destroyedCleanup, unbind delegates, stop timers

Constructor vs BeginPlay

This is the distinction that separates working actors from mysteriously broken ones. The constructor runs in contexts where the game world does not exist yet โ€” including on the Class Default Object the engine creates just to know your class's defaults, and every time you open the editor.

โš ๏ธ Never do gameplay in the constructor

In the constructor you must NOT: look for other actors, spawn things, access the world (GetWorld() may be null), play sounds, or start timers. None of that exists yet. Doing so crashes or behaves erratically in the editor. The constructor is only for creating components and setting default property values.

AItemChest::AItemChest()
{
    PrimaryActorTick.bCanEverTick = false;   // this chest doesn't need Tick

    // โœ… OK in constructor: create components, set defaults.
    Mesh = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("Mesh"));
    RootComponent = Mesh;
    GoldAmount = 100;

    // โŒ WRONG in constructor โ€” the world isn't ready:
    // TArray<AActor*> Found;
    // UGameplayStatics::GetAllActorsOfClass(GetWorld(), ..., Found); // crash risk
}

void AItemChest::BeginPlay()
{
    Super::BeginPlay();

    // โœ… Now the world exists โ€” safe to interact with it.
    UE_LOG(LogTemp, Log, TEXT("Chest ready with %d gold"), GoldAmount);
    // find other actors, register with a subsystem, start a timer, etc.
}

๐Ÿ“– The rule in one line

Constructor = "what am I made of?" ยท BeginPlay = "the game has started, go." If code needs the world or other actors, it belongs in BeginPlay, not the constructor.

A Complete Actor

Putting it together โ€” a pickup actor that bobs, exposes tunables, and logs when the game starts. Header:

// FloatingPickup.h
#pragma once

#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "FloatingPickup.generated.h"

class UStaticMeshComponent;

UCLASS()
class MYPROJECT_API AFloatingPickup : public AActor
{
    GENERATED_BODY()

public:
    AFloatingPickup();

protected:
    virtual void BeginPlay() override;

public:
    virtual void Tick(float DeltaTime) override;

    UPROPERTY(VisibleAnywhere, Category = "Pickup")
    TObjectPtr<UStaticMeshComponent> Mesh;

    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Pickup")
    float BobHeight = 20.0f;

    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Pickup")
    float BobSpeed = 2.0f;

private:
    float RunningTime = 0.0f;
    FVector StartLocation;
};

Source:

// FloatingPickup.cpp
#include "FloatingPickup.h"
#include "Components/StaticMeshComponent.h"

AFloatingPickup::AFloatingPickup()
{
    PrimaryActorTick.bCanEverTick = true;

    Mesh = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("Mesh"));
    RootComponent = Mesh;
}

void AFloatingPickup::BeginPlay()
{
    Super::BeginPlay();

    // World exists now โ€” safe to read our placed transform.
    StartLocation = GetActorLocation();
    UE_LOG(LogTemp, Log, TEXT("%s spawned, bobbing enabled"), *GetName());
}

void AFloatingPickup::Tick(float DeltaTime)
{
    Super::Tick(DeltaTime);

    RunningTime += DeltaTime;
    // Oscillate around the starting height using a sine wave.
    const float NewZ = StartLocation.Z + FMath::Sin(RunningTime * BobSpeed) * BobHeight;
    SetActorLocation(FVector(StartLocation.X, StartLocation.Y, NewZ));
}

โœ… Notice the discipline

Components created in the constructor; StartLocation captured in BeginPlay (because the placed transform is only meaningful once in the world); per-frame motion in Tick, frame-rate independent via DeltaTime. This shape โ€” construct, begin, tick โ€” recurs in almost every actor you'll write.

Hands-on Exercise & Quiz

๐Ÿ‹๏ธ Exercise: Build a rotating beacon

Objective: Apply the construct/begin/tick pattern.

Create ABeacon : public AActor with: a scene-component root, a static mesh attached under it, a point-light component attached under the root, an EditAnywhere RotationSpeed, and a BeginPlay log. In Tick, rotate the mesh (not the whole actor) around Z. Decide what goes in the constructor vs BeginPlay.

๐Ÿ’ก Hint

Constructor: create root + mesh + light, attach them, set bCanEverTick = true. BeginPlay: just the log (and anything needing the world). Tick: Mesh->AddLocalRotation(FRotator(0, RotationSpeed * DeltaTime, 0)).

โœ… Constructor skeleton
ABeacon::ABeacon()
{
    PrimaryActorTick.bCanEverTick = true;

    RootComponent = CreateDefaultSubobject<USceneComponent>(TEXT("Root"));

    Mesh = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("Mesh"));
    Mesh->SetupAttachment(RootComponent);

    Light = CreateDefaultSubobject<UPointLightComponent>(TEXT("Light"));
    Light->SetupAttachment(RootComponent);
}

๐ŸŽฏ Quick Quiz

Question 1: Where do you create an actor's default components?

Question 2: Why must you NOT call GetWorld()-dependent code in the constructor?

Question 3: Which attaches a default component in the constructor?

Summary

๐ŸŽ‰ Key Takeaways

  • Create default components in the constructor with CreateDefaultSubobject<T>; assign one as RootComponent.
  • Build the component tree with SetupAttachment (constructor); use AttachToComponent for runtime attachment.
  • The lifecycle is constructor โ†’ BeginPlay โ†’ Tick โ†’ EndPlay; each stage has a purpose.
  • Constructor = "what am I made of" (components + defaults only); BeginPlay = "the game started" (world/actors are ready).
  • Capture world-dependent state (like placed location) in BeginPlay, and keep per-frame work in Tick frame-rate independent.

๐Ÿ“š Additional Resources

๐Ÿš€ What's Next?

You've been attaching engine components. Next you'll write your own โ€” custom Actor and Scene Components that package reusable behavior you can drop onto any actor, the composition pattern that keeps Unreal codebases clean.

๐ŸŽ‰ Lesson complete!

You can build actors from parts. Now let's craft the parts themselves.