Skip to main content

02 / 16

Classes and lifecycle

Put the public API first, keep state private, and set up in the right lifecycle hook.

A header is read to find out what a class can do, so the answer belongs at the top. Where setup runs matters just as much, because a constructor and BeginPlay run in very different contexts.

Rules

  1. 01Keep each class focused enough that its purpose fits in one short sentence.
  2. 02Use protected functions for deliberate extension points. Keep the fields behind them private.
  3. 03It's ideal to use composition when unrelated actor types need the same behavior.
  4. 04Call the parent implementation for Unreal lifecycle overrides unless the engine contract explicitly says otherwise.
  5. 05CreateDefaultSubobject is valid only while the object is being constructed. Calling it later logs No object initializer found during construction at Fatal verbosity, which terminates the process.
  6. 06Pair BeginPlay setup with EndPlay teardown. EndPlay runs on explicit destruction, level transition, PIE ending, and level streaming, so gameplay cleanup belongs there.
  7. 07Do not release gameplay bindings in a UObject destructor. It runs during garbage collection, which can happen long after the object left play.
  8. 08Mark a class final unless it is designed for inheritance. Removing final later is a smaller change than untangling an unplanned hierarchy.
  9. 09Choose composition when the shared behavior carries its own state and lifecycle. Inheritance couples the entire type, while a component can be reused on an unrelated actor.
  10. 10A constructor also runs for the class default object, which has no world. Anything that calls GetWorld(), spawns, or reads gameplay state belongs in a later hook.
  11. 11Give a virtual function an override and let the compiler check it. A signature that drifts from the base silently becomes a new function that nothing calls.

Put the public API at the top of the class

List public functions first, followed by protected extension points and private state.

HealthComponent.hcpp
#pragma once

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

DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(
    FHealthChangedSignature,
    float, PreviousHealth,
    float, CurrentHealth);

UCLASS(ClassGroup = (Combat), meta = (BlueprintSpawnableComponent))
class ACTIONGAME_API UHealthComponent final : public UActorComponent
{
    GENERATED_BODY()

public:
    UHealthComponent();

    UFUNCTION(BlueprintPure, Category = "Health")
    float GetHealth() const { return Health; }

    UFUNCTION(BlueprintCallable, Category = "Health")
    bool ApplyDamage(float Damage);

    UPROPERTY(BlueprintAssignable, Category = "Health")
    FHealthChangedSignature OnHealthChanged;

protected:
    virtual void BeginPlay() override;

private:
    UPROPERTY(EditDefaultsOnly, Category = "Health", meta = (ClampMin = "1.0"))
    float MaxHealth = 100.0f;

    UPROPERTY(VisibleInstanceOnly, Category = "Health")
    float Health = 0.0f;
};
Why
Most developers open a header to find out what the class can do. They should not have to scroll past its internal storage first.
Costs
Inline accessors rebuild every dependent translation unit when changed. Keep only trivial accessors inline.
Instead
Give subclasses protected accessors when they need controlled access to state. Remove final only when the class has been designed for inheritance.

Initialization phases

Constructor

You are setting defaults or creating default subobjects. Applies the setup to the class default object and every instance.

CostsThere is no live world to use, and UObject constructors do not take runtime arguments.

PostInitProperties

One property depends on reflected values that have finished initializing. Runs after Unreal initializes reflected properties.

CostsAlso runs for objects outside gameplay, so check the object context.

OnRegister

A component needs setup after registration, including editor previews. Works for editor and runtime registration.

CostsIt can run more than once, so the setup has to tolerate re-registration.

BeginPlay

Gameplay setup needs a live actor, world, or another runtime instance. Provides a reliable runtime starting point for actors and components.

CostsDoes not run for an editor-only preview.

Keep runtime setup out of the constructor

Set defaults in the constructor. Initialize live gameplay state in BeginPlay.

HealthComponent.cppcpp
#include "Components/HealthComponent.h"

UHealthComponent::UHealthComponent()
{
    PrimaryComponentTick.bCanEverTick = false;
}

void UHealthComponent::BeginPlay()
{
    Super::BeginPlay();
    Health = MaxHealth;
}

bool UHealthComponent::ApplyDamage(const float Damage)
{
    if (Damage <= 0.0f || Health <= 0.0f)
    {
        return false;
    }

    const float PreviousHealth = Health;
    Health = FMath::Max(0.0f, Health - Damage);
    OnHealthChanged.Broadcast(PreviousHealth, Health);
    return true;
}
Why
UObject constructors also run for the class default object, where there is no live world to work with. BeginPlay runs when the actor or component has its runtime context.
Costs
Setup now lives in more than one function. Keep each phase small and use specific names for any extra setup functions.
Instead
Use PostInitProperties when a UObject needs work after its reflected properties initialize. Use OnRegister for component setup that must also run in the editor.

Use components for reusable gameplay behavior

The actor owns its health and inventory systems without implementing both of them itself.

CombatCharacter.cppcpp
ACombatCharacter::ACombatCharacter()
{
    HealthComponent = CreateDefaultSubobject<UHealthComponent>(TEXT("Health"));
    InventoryComponent = CreateDefaultSubobject<UInventoryComponent>(TEXT("Inventory"));
}

bool ACombatCharacter::CanEquip(const UItemDefinition& Item) const
{
    if (!HealthComponent->IsAlive())
    {
        return false;
    }

    return InventoryComponent->Contains(Item);
}
Why
You can reuse either component on another actor without copying behavior into another inheritance chain. Each component also has its own lifecycle.
Costs
Components add another UObject and another layer of indirection. Small behavior used by one class is often clearer as a private helper.
Instead
Use inheritance for a real is-a relationship with a stable virtual API. Use a plain F type when the logic does not need reflection, garbage collection, ticking, or editor exposure.

All 16 rules