Skip to content
C++
Domain Deep-Dive
Advanced

Unreal Engine C++ Architecture

"Unreal Engine C++ deep dive: UObject, UClass, reflection macros, Actors, Components, the Game Framework, and how to extend the engine safely."

TL;DR

Unreal Engine's C++ layer is a custom runtime on top of standard C++. UObject provides garbage collection, reflection, and serialization. You write gameplay in C++ classes decorated with UCLASS/UPROPERTY/UFUNCTION macros — these feed Unreal's Header Tool (UHT) which generates the reflection code. Blueprint visual scripting integrates via these same macros.

The UObject system

Everything in Unreal that participates in GC, serialization, or Blueprint exposure inherits from UObject.

cpp
// MyItem.h
#pragma once
#include "CoreMinimal.h"
#include "UObject/NoExportTypes.h"
#include "MyItem.generated.h"  // MUST be the last include

UCLASS(Blueprintable)
class MYGAME_API UMyItem : public UObject {
    GENERATED_BODY()  // injects reflection glue

public:
    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Item")
    FString Name;

    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Item")
    int32 Value = 0;

    UFUNCTION(BlueprintCallable, Category="Item")
    void Use(AActor* Target);
};
cpp
// MyItem.cpp
#include "MyItem.h"

void UMyItem::Use(AActor* Target) {
    // Implementation
    UE_LOG(LogTemp, Display, TEXT("Using %s on %s"), *Name, *Target->GetName());
}

Key points:

  • GENERATED_BODY() injects constructors and reflection data — never remove it.
  • MYGAME_API macro exports the class from the DLL (required for modular builds).
  • .generated.h is auto-generated by UHT — commit it but don't edit it.
  • FString is Unreal's heap-allocated string. Use TEXT("...") for literals.

Actor & Component hierarchy

cpp
AActor                    ← anything placeable in the world
├── APawn                 ← possessable by a Controller
│   └── ACharacter        ← humanoid with movement component built-in
├── AGameModeBase         ← game rules, per-level singleton
├── APlayerController     ← input translation + camera management
└── AHUD                  ← 2D overlay

UActorComponent           ← non-scene component (no transform)
└── USceneComponent       ← has transform, can be the root
    └── UPrimitiveComponent ← renderable / collidable
        ├── UStaticMeshComponent
        └── USkeletalMeshComponent

Writing an Actor

cpp
// AHealthPickup.h
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "AHealthPickup.generated.h"

UCLASS()
class MYGAME_API AHealthPickup : public AActor {
    GENERATED_BODY()

public:
    AHealthPickup();

protected:
    virtual void BeginPlay() override;

public:
    virtual void Tick(float DeltaTime) override;

private:
    UPROPERTY(VisibleAnywhere, Category="Components")
    UStaticMeshComponent* MeshComp;

    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Pickup",
              meta=(AllowPrivateAccess="true"))
    float HealAmount = 25.f;

    UFUNCTION()
    void OnOverlap(UPrimitiveComponent* OverlappedComp, AActor* Other,
                   UPrimitiveComponent* OtherComp, int32 OtherBodyIdx,
                   bool bFromSweep, const FHitResult& SweepResult);
};
cpp
// AHealthPickup.cpp
#include "AHealthPickup.h"
#include "Components/SphereComponent.h"
#include "GameFramework/Character.h"

AHealthPickup::AHealthPickup() {
    PrimaryActorTick.bCanEverTick = false;  // disable Tick if unused — saves CPU

    // Create components in constructor
    MeshComp = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("Mesh"));
    SetRootComponent(MeshComp);

    auto* Sphere = CreateDefaultSubobject<USphereComponent>(TEXT("Trigger"));
    Sphere->SetupAttachment(RootComponent);
    Sphere->SetSphereRadius(80.f);
    Sphere->OnComponentBeginOverlap.AddDynamic(this, &AHealthPickup::OnOverlap);
}

void AHealthPickup::BeginPlay() {
    Super::BeginPlay();
}

void AHealthPickup::Tick(float DeltaTime) {
    Super::Tick(DeltaTime);
}

void AHealthPickup::OnOverlap(UPrimitiveComponent*, AActor* Other,
                               UPrimitiveComponent*, int32, bool, const FHitResult&) {
    if (auto* Char = Cast<ACharacter>(Other)) {
        // Apply heal...
        Destroy();
    }
}

Garbage collection

Unreal uses a mark-and-sweep GC. Rules:

  • UPROPERTY pointers are tracked — GC won't collect them while live.
  • Raw UObject* without UPROPERTY can become dangling — GC may collect the object.
  • Use TWeakObjectPtr<T> for non-owning references that survive GC safely.
  • Use IsValid(Ptr) or Ptr.IsValid() before dereferencing potentially-GC'd objects.
cpp
// Safe pattern
UPROPERTY()
AActor* OwnedTarget;  // GC-tracked

TWeakObjectPtr<AActor> WeakRef;  // non-owning, safe to check

void UseRef() {
    if (IsValid(OwnedTarget)) {
        OwnedTarget->DoSomething();
    }
    if (AActor* Actor = WeakRef.Get()) {
        Actor->DoSomething();
    }
}

Delegates & events

cpp
// Declare a delegate type
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnHealthChanged, float, NewHealth);

UCLASS()
class AMyCharacter : public ACharacter {
    GENERATED_BODY()
public:
    // Blueprint-bindable event
    UPROPERTY(BlueprintAssignable, Category="Events")
    FOnHealthChanged OnHealthChanged;

    void TakeDamage(float Amount) {
        Health -= Amount;
        OnHealthChanged.Broadcast(Health);  // fires all bound listeners
    }

private:
    float Health = 100.f;
};

// Binding in C++
MyChar->OnHealthChanged.AddDynamic(this, &UMyComponent::HandleHealthChanged);

Subsystems — the modern singleton pattern

Subsystems replace old static singletons with lifecycle-managed instances:

cpp
// UMyGameSubsystem.h
#include "Subsystems/GameInstanceSubsystem.h"
#include "UMyGameSubsystem.generated.h"

UCLASS()
class UMyGameSubsystem : public UGameInstanceSubsystem {
    GENERATED_BODY()
public:
    virtual void Initialize(FSubsystemCollectionBase& Collection) override;
    virtual void Deinitialize() override;

    UFUNCTION(BlueprintCallable)
    void SaveProgress();
};

// Access from anywhere
UGameInstance* GI = GetGameInstance();
UMyGameSubsystem* Sub = GI->GetSubsystem<UMyGameSubsystem>();
Sub->SaveProgress();

Subsystem types: UWorldSubsystem, UGameInstanceSubsystem, UEngineSubsystem, ULocalPlayerSubsystem.

Performance guidelines

cpp
// Disable Tick when not needed
PrimaryActorTick.bCanEverTick = false;

// Use timers instead of Tick for periodic work
GetWorldTimerManager().SetTimer(
    TimerHandle, this, &AMyActor::OnTimer, 1.0f, /*bLoop=*/true);

// Async tasks for heavy work
AsyncTask(ENamedThreads::AnyBackgroundThreadNormalTask, [this]() {
    HeavyComputation();
    AsyncTask(ENamedThreads::GameThread, [this]() {
        ApplyResult();  // back on game thread
    });
});

// LOD — reduce component tick rates for distant actors
PrimaryActorTick.TickInterval = 0.1f;  // tick every 100ms instead of every frame

Common mistakes

Calling engine functions off the game thread — most UE functions are not thread-safe. Always marshal back to the game thread.

Not calling Super::BeginPlay() — BeginPlay initializes components; skipping it causes subtle bugs.

Creating UObjects with new — always use NewObject<T>() or CreateDefaultSubobject<T>():

cpp
// BAD — bypasses UObject system
auto* item = new UMyItem();

// OK
auto* item = NewObject<UMyItem>(this, UMyItem::StaticClass());

Storing non-UPROPERTY pointers to UObjects — GC can collect them silently.

Edit on GitHubUpdated 2026-05-01T00:00:00.000Z