๐ญ 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
CreateDefaultSubobjectand set a root - Assemble a component hierarchy via attachment
- Explain the actor lifecycle and what belongs in each stage
- Use the constructor,
BeginPlay,Tick, andEndPlaycorrectly - 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.
// 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.
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"]
| Stage | Runs when | Put here |
|---|---|---|
| Constructor | Object created (incl. editor/CDO) | Create components, set default values |
BeginPlay | Play starts / actor spawned into live world | Gameplay init that needs the world, other actors, or systems |
Tick | Every frame while alive (if enabled) | Per-frame logic โ used sparingly |
EndPlay | Removed from play / destroyed | Cleanup, 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 asRootComponent. - Build the component tree with
SetupAttachment(constructor); useAttachToComponentfor 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 inTickframe-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.