Chapter 06

Object Lifecycle

The Class Default Object, the full initialization pipeline from constructor to BeginPlay, what a constructor may and may not do, how Blueprint differs, and why destroyed does not mean null.

In this chapter

  1. The Class Default Object
  2. The Initialization Pipeline
  3. What Belongs in a Constructor
  4. ConstructorHelpers
  5. C++ Constructors vs. Blueprint
  6. Garbage Collection
  7. Validity — IsValid vs. nullptr
1

The Class Default Object

For every UClass, Unreal creates exactly one Class Default Object — a real, allocated instance built once when the class first loads. It's the template holding default property values: when you spawn a real instance, the engine uses the CDO's values as the starting point, then applies any per-instance overrides on top.

Consequences worth internalizing

Your C++ constructor runs on the CDO first, at class-load time — before any gameplay instance exists, before the world exists, with no guarantee other actors are around. So the constructor is for setting defaults only.

Gameplay logic never goes in the constructor. It goes in BeginPlay. The constructor also runs per spawned instance, but conceptually its job is defaults, and it runs in that special no-world context.

Defaults set in the constructor land on the CDO:

AMyActor::AMyActor()
{
    Health = 100.f;
    bIsHostile = true;
}

Every future instance inherits these unless overridden.


2

The Initialization Pipeline

"Constructor for defaults, BeginPlay for logic" is the conclusion. The pipeline between them is what explains it — and there are several useful hooks in the gap.

// Spawning an instance of a Blueprint class derived from your C++ class

  1.  C++ constructor chain          // base -> derived. Native defaults,
                                     // CreateDefaultSubobject. NO world.
  2.  Serialized / Blueprint values  // designer-authored defaults applied
                                     // to the instance, over the top
  3.  PostInitProperties()           // first point those values are readable
  4.  Simple Construction Script     // components added in the BP Components
                                     // panel are created here
  5.  PostActorCreated()             // spawned; PostLoad() if loaded instead
  6.  OnConstruction()               // the Construction Script
  7.  Component registration         // -> InitializeComponent() on each
                                     // component that wants it
  8.  PostInitializeComponents()     // all components exist and are initialized
  9.  BeginPlay()                    // world is live, gameplay has started

The step that causes the most confusion

Step 1 happens before step 2. By the time your C++ constructor body runs, Blueprint-authored defaults and serialized values have not been applied to the instance. They land afterward, on top of whatever the constructor set.

So a designer setting Health = 250 in a Blueprint's Class Defaults is invisible to this:

AMyActor::AMyActor()
{
    Health = 100.f;
    MaxHealth = Health * 2.f;  // always 200. Never 500.
}

MaxHealth is computed from the native default, not the designer's value, because the designer's value hasn't arrived yet. PostInitProperties() is the first hook that can see it — that's what it's for.

What each hook is good for

HookState when it runsUse it for
ConstructorNo world, no BP values, no BP componentsNative defaults, default subobjects, tick settings
PostInitPropertiesProperty values fully applied; components not yet registeredDerived values computed from designer-set properties
OnConstructionProperties and components available; runs in-editor tooProcedural setup and editor preview driven by designer properties
InitializeComponentPer component, at registration, before the owner finishesA component initializing itself
PostInitializeComponentsEvery component exists and is initializedOwner-side wiring that needs components — caching pointers, binding component delegates
BeginPlayWorld live, gameplay started, other actors presentEverything else — spawning, timers, gameplay

Placed vs. spawned

The two diverge in the middle:

Both converge again at component registration, and BeginPlay fires for both when play begins.


3

What Belongs in a Constructor

Do
  • CreateDefaultSubobject<T>() for native components
  • SetRootComponent() and attaching those subobjects
  • Setting default property values
  • PrimaryActorTick.bCanEverTick / TickInterval
  • bReplicates = true
  • ConstructorHelpers lookups
Don't
  • Spawn actors
  • Call GetWorld() or rely on a world
  • Find or reference other actors
  • Read Blueprint-authored property values
  • Touch components added in the BP Components panel
  • Set timers or bind delegates to world objects
  • Anything random or non-deterministic

Why you can't spawn an actor in a constructor

The constructor runs on the CDO at class-load time. There is no world at that point — GetWorld() returns null. Spawning requires a world to spawn into, so the call has nothing to work with.

It's worth being precise about the failure, because it's easy to write a constructor that appears to work: the CDO is constructed once at load, so a spawn attempt there fails or asserts long before any gameplay path runs it. And even if a world existed, you'd be spawning an actor per CDO construction — which is not what anyone means to do.

The rule generalizes: a constructor may configure the object itself, and nothing beyond it. Anything that reaches outside — the world, other actors, the level — belongs later in the pipeline.

Why non-determinism is a real problem, not a style note

The constructor's results land on the CDO, and the CDO gets serialized. Randomizing a value there means the saved class defaults differ run to run, which produces nondeterministic cooks and spurious diffs on assets nobody edited. Per-instance randomization belongs in BeginPlay, or in OnConstruction with a seeded stream if it genuinely needs to be visible in the editor.

Where to do it instead

You want to…Do it in
Compute a value from a designer-set propertyPostInitProperties
Cache a pointer to a component, or bind to onePostInitializeComponents
Spawn an actor, start a timer, query the worldBeginPlay
Build geometry a designer should see while editingOnConstruction (the Construction Script)
Randomize per instanceBeginPlay

Adding Components in the Constructor

Attaching native components is the standard, recommended pattern, and it's the clearest illustration of "configure the object itself":

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

    RootMesh = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("RootMesh"));
    RootComponent = RootMesh;

    Collider = CreateDefaultSubobject<USphereComponent>(TEXT("Collider"));
    Collider->SetupAttachment(RootComponent);
}
Adding a component after construction — in BeginPlay or later — is a different, rarer tool: NewObject<T>() followed by RegisterComponent(). Reach for it only when the component shouldn't exist on every instance by default; it's more expensive than the constructor path.

4

ConstructorHelpers

ConstructorHelpers::FObjectFinder<T> and FClassFinder<T> grab an asset by path inside a constructor:

static ConstructorHelpers::FObjectFinder<UStaticMesh>
    MeshAsset(TEXT("/Game/Meshes/SM_Rock.SM_Rock"));

if (MeshAsset.Succeeded())
{
    MeshComponent->SetStaticMesh(MeshAsset.Object);
}

5

C++ Constructors vs. Blueprint

Blueprint has no constructor. Two things get mistaken for one, and they're different things.

The comparison that matters

C++ ConstructorConstruction Script
When it runsClass load (CDO) and object creation, before properties are appliedStep 6 — after properties and components exist
How oftenOnce per object creationRepeatedly — on spawn, and on every property edit or move in the editor
Sees Blueprint-set defaultsNoYes
Sees BP-added componentsNo — native default subobjects onlyYes
World accessNoneYes, but it's an editor world as often as a game one
Can spawn actorsNoTechnically yes — but don't, see below
PurposeNative defaults and default subobjectsProcedural setup and editor preview driven by designer properties

Components added in Blueprint don't exist in the C++ constructor

Dropping a component into a Blueprint's Components panel adds it to that class's Simple Construction Script, which runs at step 4 — after the native constructor chain has finished. A C++ constructor therefore cannot see, configure, or cache a pointer to it.

If your C++ needs a handle on a component a designer added, take it in PostInitializeComponents, where everything is guaranteed to exist.

Construction Script pitfalls

Which hooks Blueprint can actually reach

Blueprint exposes two points in the pipeline directly: the Construction Script and Event BeginPlay. The intermediate hooks — PostInitProperties, InitializeComponent, PostInitializeComponents — are C++-only.

That gap is a design surface, not just a limitation. If designers need to hook a stage Blueprint can't see, expose it yourself: override the C++ hook and call a BlueprintImplementableEvent from it (Chapter 3). You decide which moments in the lifecycle are worth handing over.

6

Garbage Collection

Unreal runs a mark-and-sweep garbage collector over UObjects. The key idea to internalize:

Destroyed does not mean null pointer.

Calling Destroy() doesn't wipe the object immediately — it sets an internal flag: Garbage in UE5, previously PendingKill in UE4. Memory and the pointer value persist until the GC's next pass reclaims them. In that window, a raw pointer is non-null but logically dead.

When does a reference become None?

If it's a UPROPERTY in C++ or an exposed Blueprint variable, the GC nulls it for you — but on the GC's schedule, not the frame Destroy() was called.

Raw C++ pointers not wrapped in UPROPERTY get no such courtesy. They dangle, and dereferencing one is a hard crash rather than a clean null.

UPROPERTY is also what makes a pointer visible to the GC in the first place — which is why Chapter 7 treats an unwrapped raw pointer as a bug rather than an optimization.

7

Validity — IsValid vs. nullptr

CheckCatchesMisses
!= nullptrNever-set or explicitly-nulled referencesDestroyed but not yet collected
IsValid()Both null and the Garbage / PendingKill flagNothing — this is the real "safe to use?" check

IsValid() is what the Blueprint Is Valid node calls under the hood, with its separate valid and not-valid exec pins.

Equality is identity, not liveness

== on object references compares pointers, not aliveness:

The variable does not become None the instant you destroy it.

Safe pattern. Never trust a cross-frame reference just because it's non-null. Run it through IsValid() before use, especially in Tick.

TWeakObjectPtr is the C++ tool for a reference that reports its own death without waiting on GC nulling — it checks liveness against the object's internal serial number.

← Chapter 5 ↑ Index Chapter 7 →