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.
// 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);
};// 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_APImacro exports the class from the DLL (required for modular builds)..generated.his auto-generated by UHT — commit it but don't edit it.FStringis Unreal's heap-allocated string. UseTEXT("...")for literals.
Actor & Component hierarchy
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
└── USkeletalMeshComponentWriting an Actor
// 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);
};// 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:
UPROPERTYpointers are tracked — GC won't collect them while live.- Raw
UObject*withoutUPROPERTYcan become dangling — GC may collect the object. - Use
TWeakObjectPtr<T>for non-owning references that survive GC safely. - Use
IsValid(Ptr)orPtr.IsValid()before dereferencing potentially-GC'd objects.
// 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
// 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:
// 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
// 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 frameCommon 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>():
// 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.