Unreal Engine 5 (Blueprint + C++)
Build and debug UE5 games with the grain of the engine: the Gameplay Framework for structure, reflection macros so C++ and Blueprint see the same types, Enhanced Input for controls, UMG for UI, Paper2D for 2D — and a clean Blueprint↔C++ boundary so either can call the other. Hands 3D-only concerns (shaders, physics, netcode, shipping) to the siblings below.
Version contract — read first
Target Unreal Engine 5.x (patterns valid 5.3+; current stable UE 5.8, the last UE5 line before UE6). Emit modern UE5 APIs only. Never emit these deprecated / legacy patterns:
| Never emit | Use instead (UE5) |
|---|---|
Legacy input: Project-Settings Action/Axis Mappings + InputComponent->BindAction/BindAxis(FName,…) |
Enhanced Input: UInputAction + UInputMappingContext + UEnhancedInputComponent->BindAction(…), context added via UEnhancedInputLocalPlayerSubsystem (default since 5.1; legacy deprecated 5.2) |
Raw UObject* / AActor* as a UPROPERTY member |
TObjectPtr<T> for UPROPERTY members (raw T* is fine for locals/params) |
GENERATED_UCLASS_BODY() |
GENERATED_BODY() |
TAssetPtr<T> / FStringAssetReference |
TSoftObjectPtr<T> / FSoftObjectPath |
ANY_PACKAGE in FindObject / StaticLoadObject |
explicit package or FTopLevelAssetPath |
Slate/Canvas DrawText/HUD for game UI |
UMG (UUserWidget) |
AGameMode as the default base for a solo game |
AGameModeBase (use AGameMode only when you need MatchState/multiplayer login flow) |
DECLARE_MULTICAST_DELEGATE for a Blueprint-bindable event |
DECLARE_DYNAMIC_MULTICAST_DELEGATE… + BlueprintAssignable |
bCanEverTick = true "just in case" |
leave tick off; drive logic from events/overlaps/timers (FTimerManager) |
| Hard-referencing / loading heavy assets at runtime by path | soft refs + FStreamableManager async load (hard refs only via ConstructorHelpers in the constructor) |
Blueprint vs C++ — and the hybrid. Blueprint: designer-facing tweaks, per-level scripting,
UI wiring, rapid iteration, one-off actors. C++: core systems, performance-critical/tick-heavy
code, base classes, math, anything you want unit-tested or diffable in git. Default to the
hybrid: write the base class in C++ (UCLASS(Blueprintable)), expose tunables with
UPROPERTY(EditAnywhere, BlueprintReadWrite) and hooks with BlueprintImplementableEvent /
BlueprintNativeEvent, then create a Blueprint subclass for designers to set defaults, wire
assets, and script the specifics — C++ speed and testability with Blueprint iteration.
Gameplay Framework — who owns what
| Class | Responsibility | Lifetime / scope |
|---|---|---|
AActor |
Anything placeable/spawnable in a level; holds components. | Per instance in a level/world |
UActorComponent / USceneComponent |
Reusable behavior/data on an Actor; USceneComponent adds a transform. |
Owned by its Actor |
APawn |
An Actor that can be possessed and driven by a controller. | Per instance |
ACharacter |
A Pawn with a CapsuleComponent, SkeletalMesh, and CharacterMovementComponent (walk/jump/crouch, networked). |
Per instance |
AController / APlayerController / AAIController |
The "brain" that possesses a Pawn; APlayerController maps a human player to input, camera, and UI. |
One per player/AI |
APlayerState |
Per-player replicated state that must survive respawn (name, score, team). | One per player |
AGameModeBase |
The rules of the match; spawns players, defines default Pawn/Controller/HUD classes. Server-only — never exists on clients. | One per level |
AGameStateBase |
Game-wide replicated state all clients need (match phase, shared score, player array). | One, replicated |
AHUD / UUserWidget |
On-screen UI; prefer UMG UUserWidget over AHUD canvas drawing. |
Per PlayerController |
Rule of thumb: transient input/camera → PlayerController; persistent per-player data →
PlayerState; match rules → GameMode (server); shared world state → GameState. Full
responsibilities and a spawn-order diagram → references/gameplay-framework.md.
Actor lifecycle, components & attachment
Order for a spawned/placed Actor: constructor (set defaults, create subobjects — never
gameplay logic) → PostInitializeComponents → BeginPlay (world is live; safe to start logic)
→ Tick(DeltaSeconds) each frame (only if enabled) → EndPlay / Destroyed (release).
AMyActor::AMyActor()
{
PrimaryActorTick.bCanEverTick = false; // opt in only if you truly tick
RootComponent = CreateDefaultSubobject<USceneComponent>(TEXT("Root"));
Mesh = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("Mesh"));
Mesh->SetupAttachment(RootComponent); // attach in the CONSTRUCTOR
}
void AMyActor::BeginPlay() { Super::BeginPlay(); /* start logic here, not in ctor */ }
- Create components with
CreateDefaultSubobject<T>(TEXT("Name"))in the constructor; store them in aUPROPERTY() TObjectPtr<T>so they're kept alive and visible. - Attach in the constructor with
SetupAttachment; at runtime useAttachToComponent(Parent, FAttachmentTransformRules::SnapToTargetNotIncludingScale). - Prefer timers over tick for periodic work:
GetWorldTimerManager().SetTimer(Handle, this, &AMyActor::Fn, 1.f, true);
Reflection macros & garbage collection
Macros register types with Unreal Header Tool (UHT) so they get GC, serialization, editor, and
Blueprint support. Put GENERATED_BODY() first inside the class.
UENUM(BlueprintType)
enum class ETeam : uint8 { Red, Blue };
USTRUCT(BlueprintType) // value type, visible to BP; no GC of itself
struct FLoadout { GENERATED_BODY()
UPROPERTY(EditAnywhere, BlueprintReadWrite) int32 Ammo = 30;
};
UCLASS(Blueprintable) // Blueprintable = can be subclassed in BP
class MYGAME_API AWeapon : public AActor {
GENERATED_BODY()
public:
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Combat") float Damage = 10.f;
UPROPERTY(VisibleAnywhere) TObjectPtr<UStaticMeshComponent> Mesh; // GC-tracked ref
UFUNCTION(BlueprintCallable, Category="Combat") void Fire();
};
Common UPROPERTY specifiers: EditAnywhere (edit on instances + defaults),
EditDefaultsOnly (class defaults only), VisibleAnywhere (read-only in editor);
BlueprintReadWrite (get + set in BP) vs BlueprintReadOnly (get only); always give a
Category.
Garbage collection — the #1 gotcha. The GC frees any UObject that no reachable UPROPERTY
points to. A UObject reference stored without UPROPERTY() is invisible to GC: it will be
collected out from under you and the pointer dangles/crashes. So:
- Store every long-lived
UObject/AActorreference in aUPROPERTY() TObjectPtr<T>. - For a non-owning "might already be dead" reference (don't keep it alive, must null-check), use
TWeakObjectPtr<T>and.IsValid(). - Never
new/deleteaUObject— spawn Actors withSpawnActor<T>()and create other UObjects withNewObject<T>(); the GC owns their lifetime.new/deleteand smart pointers apply only to non-UObjectC++ types.
Delegates & events
// Dynamic multicast = Blueprint-assignable + serializable. Params need a typed macro variant.
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnHealthChanged, float, NewHealth);
UPROPERTY(BlueprintAssignable, Category="Events") // shows as a red event node in BP
FOnHealthChanged OnHealthChanged;
// C++ side: bind and fire.
OnHealthChanged.AddDynamic(this, &AMyActor::HandleHealth); // dynamic → AddDynamic + UFUNCTION
OnHealthChanged.Broadcast(NewHealth);
Use a dynamic multicast delegate whenever Blueprint must bind to the event (BlueprintAssignable);
its handler must be a UFUNCTION. Use a plain DECLARE_MULTICAST_DELEGATE… (bind with
AddUObject/AddLambda) only for C++-to-C++ events that never touch Blueprint. Overlap/hit
events (OnComponentBeginOverlap, OnActorHit) are already dynamic multicast — bind with
AddDynamic.
Blueprint ↔ C++ parity
Same graph, two languages. Left = the Blueprint node; right = the C++ that does the same thing.
| Blueprint node | C++ |
|---|---|
| Event BeginPlay | virtual void BeginPlay() override; (call Super::BeginPlay()) |
| Event Tick (Delta Seconds) | virtual void Tick(float DeltaSeconds) override; |
| Spawn Actor from Class | GetWorld()->SpawnActor<AWeapon>(Class, Loc, Rot); |
Cast To X |
if (AX* P = Cast<AX>(Obj)) { … } (always null-check) |
| Set Timer by Event | GetWorldTimerManager().SetTimer(H, this, &A::Fn, Rate, bLoop); |
| Print String | UE_LOG(LogTemp, Warning, TEXT("v=%d"), V); / GEngine->AddOnScreenDebugMessage(...) |
| Get Player Controller | UGameplayStatics::GetPlayerController(this, 0); |
Bind Event to OnClicked |
Button->OnClicked.AddDynamic(this, &U::Fn); |
Expose C++ → Blueprint:
UFUNCTION(BlueprintCallable, Category="X") void DoThing(); // callable node (has exec pins)
UFUNCTION(BlueprintPure, Category="X") int32 GetScore() const; // pure node (no exec pins)
UPROPERTY(EditAnywhere, BlueprintReadWrite) float Speed = 600.f; // exposed variable
Call Blueprint ← C++ (the hybrid hooks — declare in C++, implement/override in the BP subclass):
// No C++ body — Blueprint provides the whole implementation. C++ just calls OnScored().
UFUNCTION(BlueprintImplementableEvent) void OnScored(int32 Points);
// C++ gives a default; Blueprint may override. Implement the C++ default in OnDamaged_Implementation.
UFUNCTION(BlueprintNativeEvent) void OnDamaged(float Amount);
void AMyActor::OnDamaged_Implementation(float Amount) { Health -= Amount; }
// Call the event from C++ by its plain name: OnScored(10); OnDamaged(5.f);
Mapping table (every specifier), full expose/override examples, and the C++-base-+-BP-subclass
workflow → references/blueprint-cpp-parity.md.
UMG (UI)
UI is a UUserWidget (design the visuals in a WBP asset; drive logic in C++ or its BP graph).
Bind C++ members to named widgets with meta=(BindWidget) — the C++ name must match the WBP
widget.
UCLASS() class UHealthWidget : public UUserWidget { GENERATED_BODY()
UPROPERTY(meta=(BindWidget)) TObjectPtr<UProgressBar> HealthBar; // name matches WBP
UFUNCTION(BlueprintCallable) void SetHealth(float Pct);
};
// Create + show from a PlayerController:
UHealthWidget* W = CreateWidget<UHealthWidget>(PC, WidgetClass);
W->AddToViewport();
Layout with panels (Canvas/V-H Box/Overlay/Grid); SetVisibility,
RemoveFromParent to hide/close; anchors + a design resolution for scaling.
Paper2D (2D)
Enable the Paper2D plugin. Core types:
- Sprite (
UPaperSpriteasset,UPaperSpriteComponent) — a single textured quad. - Flipbook (
UPaperFlipbookasset,UPaperFlipbookComponent) — frames + FPS = animation;SetFlipbook(...)to swap states (idle/run/jump). APaperCharacter— aACharacterwhose mesh is a flipbook; you getCharacterMovementComponent(walk/jump) for free, driven by Enhanced Input just like 3D.- Tile maps (
UPaperTileMap,UPaperTileMapComponent) for level geometry.
2D uses the same Gameplay Framework, Enhanced Input, and lifecycle as 3D — only the visual components differ. Constrain movement to a plane and use an orthographic camera.
Packaging & build config (brief)
- Build configurations:
Debug,DebugGame,Development(default for iteration — optimized engine, hot-reloadable game code),Shipping(fully optimized, logging/console stripped — ship this),Test. - Target files (
*.Target.cs) define Game/Editor/Client/Server targets; module rules (*.Build.cs) declare module dependencies (e.g."EnhancedInput","UMG","Paper2D"). - Package from Platforms ▸ ▸ Package Project, or automate with
RunUAT BuildCookRun(cook content → stage → package). Per-platform settings live inConfig/DefaultGame.ini/Default<Platform>Engine.ini.
This is the minimum. Deep shipping — cooking, pak/IoStore, certification, size/perf budgets, CI —
is owned by gamedev-shipping. Overview + a starter .Build.cs →
references/packaging.md.
Guardrails & gotchas
- UPROPERTY-or-GC'd: any UObject ref you keep must be a
UPROPERTY()or it will be garbage collected and crash. This is the most common Unreal C++ bug. - Enhanced Input only — no legacy Action/Axis mappings; add the
UInputMappingContextvia the local player subsystem inBeginPlay/on-possess, bind actions onUEnhancedInputComponent. - GameMode is server-only — never read it on a client; put client-visible state on GameState or PlayerState and replicate.
- No gameplay in the constructor — the world isn't ready. Do it in
BeginPlay. - Always null-check
Cast<T>()andSpawnActorresults; they can returnnullptr. - Don't
new/deleteUObjects —NewObject/SpawnActorand let GC free them. Header changes need a full editor recompile (Live Coding covers.cppbodies only). - Regenerate project files after adding classes/modules; add module deps in
*.Build.cs(missing"EnhancedInput"/"UMG"= linker errors). - Multiplayer: replicate state with
UPROPERTY(Replicated), gate server actions behindHasAuthority()→ seegamedev-multiplayer.
Related skills
cpp— plain C++/RAII/smart-pointer ownership for non-UObject code; this skill owns the Unreal reflection + GC model layered on top.gamedev-shipping— deep cooking/packaging/store submission.gamedev-multiplayer— replication, RPCs, authority.gamedev-physics— Chaos physics, collision, constraints.game-design— mechanics/loops before you build them.godot/unity— the other engines; route there when the project is Godot or Unity, not Unreal.
Checklist
- Correct base class chosen (Actor/Pawn/Character; GameModeBase unless match-state needed).
- No legacy input — Enhanced Input (
UInputAction+UInputMappingContext+ subsystem). - Every long-lived UObject/Actor ref is a
UPROPERTY() TObjectPtr<T>(GC-safe). - Components made in ctor via
CreateDefaultSubobject+SetupAttachment; no logic in ctor. - Tick left off unless required; periodic work on a timer/event.
- C++↔BP boundary correct:
BlueprintCallable/Pureto expose,BlueprintImplementableEvent/NativeEventfor BP-overridable hooks; BP-bound delegates are dynamic multicast. - UI is UMG (
UUserWidget+BindWidget), not HUD canvas drawing. - Server-only vs replicated state placed correctly (GameMode server-only).
- Module deps (
EnhancedInput/UMG/Paper2D) added in*.Build.cs; project files regenerated.