name: engine-unreal
description: Use when authoring or reviewing Unreal Engine 5 projects — C++ gameplay code, Blueprints, GameMode, GameState, PlayerController, Pawn/Character, PlayerState, Subsystems, replication, .uproject configuration. Triggers on edits to *.cpp/*.h inheriting from AActor/APawn/AGameModeBase/APlayerController/UGameInstance, *.uasset, *.umap, *.uproject. Also on natural-language prompts like "Unreal gameplay framework", "GameMode vs GameState", "PlayerController vs PlayerState", "Blueprint vs C++", "Unreal Subsystem", "Unreal replication", "UE5 architecture review", "Pawn possession". Grounded in Epic's official Unreal Engine 5 documentation (dev.epicgames.com).
engine-unreal
Opinionated rules for Unreal Engine 5 projects. Distilled from Epic's official gameplay framework documentation (dev.epicgames.com/documentation/en-us/unreal-engine/gameplay-framework-in-unreal-engine), Tom Looman's published guides, and a decade of Unreal community consensus.
The gameplay framework — the boundaries that matter most
Unreal's gameplay framework is opinionated. It works only if you respect the class boundaries. Misuse here causes the worst Unreal bugs — multiplayer desyncs, replication leaks, and "works in editor, breaks in build" failures.
| Class | Lives on | Owns | Don't put here |
|---|---|---|---|
AGameModeBase |
Server only (multiplayer); always (singleplayer) | Rules, win conditions, spawn logic | Player data; UI state; anything clients need to see |
AGameStateBase |
Replicated to all clients | Match state, score, time remaining | Per-player data; server-secret state |
APlayerController |
Client (with server stub) | Input handling, possession of pawn | Player data that persists past pawn death |
APawn / ACharacter |
World, possessed by controller | Movement, physics, animation, abilities | Per-player score, persistent stats |
APlayerState |
Replicated to all clients | Per-player score, ping, persistent data | Logic; this is a data class |
UGameInstance |
Client process | Cross-level state, save/load coordination | Anything per-level |
UGameInstanceSubsystem |
Client process | Cross-cutting infra (audio, save, telemetry) | Per-level state |
UWorldSubsystem |
Per world | Per-level shared services | Anything that needs to survive level transition |
Rules (the boundary errors that wreck Unreal projects)
- Never access
AGameMode*from a client. It doesn't exist there. Multiplayer-by-default thinking: ask "where does this code run?" before you write it. - Communication flows GameMode → GameState → clients. GameMode tells GameState; GameState replicates; clients read GameState. Never the reverse.
- Per-player data goes in
APlayerState, notAPlayerController, notAPawn. Because pawns die, controllers don't always exist on every client, butAPlayerStateis replicated and survives death. - Spawning lives in
AGameMode*.APlayerControllerpossesses an already-spawned pawn; it does not spawn pawns itself (in server-authoritative play). - Default to
AGameModeBase, notAGameMode.AGameModeis the full-fat class with match state machine;AGameModeBaseis leaner. Most games don't need match states. - Subsystems > custom GameInstance overrides.
UGameInstanceSubsystemis the modern equivalent of "where do I put global services" — auto-instantiated, no boilerplate.
Blueprint vs C++ — the rule
Unreal supports both. The shipping balance:
- C++ owns: gameplay framework classes, performance-critical loops, asset management, networking, anything that needs to be hard to override.
- Blueprint owns: designer-friendly variation, UI behavior, level scripting, rapid prototyping of mechanics, anything visual artists / designers will touch.
Rules
- C++ base, Blueprint subclass. Define
AMyEnemyin C++, deriveBP_Goblin,BP_Orcin Blueprint with assets and stats. UPROPERTYeverything Blueprint touches. No exposed property = no designer access.UFUNCTION(BlueprintCallable)for C++→BP API. Without it, Blueprints can't call your C++.UFUNCTION(BlueprintImplementableEvent)when C++ defines the call site but Blueprint provides the implementation. (Designer fills in animation cues, etc.)UFUNCTION(BlueprintNativeEvent)when C++ has a default implementation but Blueprint can override. The C++ side is named_Implementation.- No tick logic in Blueprint for entities that exist in large numbers. Blueprint tick is 5–10× slower than C++. Use C++ tick + BP events for state changes.
- Don't put network logic in Blueprint. Replication metadata yes; the actual replicated functions, no — easier to reason about in C++.
Replication — the rules
Multiplayer is where Unreal's class boundaries actually pay rent. Get them right.
Rules
UPROPERTY(Replicated)+GetLifetimeReplicatedPropsto replicate a property. Forgetting the lifetime function = silently nothing replicates.Server,Client,NetMulticastRPC specifiers. Pick deliberately:Server— client → server (the only path for client-initiated authority).Client— server → owning client (private to that player).NetMulticast— server → all clients (cosmetic FX broadcast).
ReliablevsUnreliable— reliable has cost, use sparingly. Unreliable for cosmetic effects.- Authority check before mutating state.
if (HasAuthority()) { ... }— server-only state changes happen only on the server. - Replication is async. Don't expect
SetReplicatedVar(true); CheckVar()to see the new value on clients immediately. UseRepNotifyfor value-change reactions. OnRep_Foonotify functions trigger on clients when Foo changes. Always implement them — never trust raw replicated state without a reaction.
Subsystems — the modern way to globals
UE5 introduced subsystems as the answer to "where do I put global services."
| Subsystem | Lifetime | Use for |
|---|---|---|
UEngineSubsystem |
Engine | Editor-time tooling |
UGameInstanceSubsystem |
Game process | Save system, telemetry, audio manager |
UWorldSubsystem |
Per world | Per-level shared services |
ULocalPlayerSubsystem |
Per local player | Input mapping, UI state |
Rules
- Subsystem > Singleton. Auto-instantiated, no static state, no header pollution.
- One subsystem, one concern. Don't write
UMyGameSubsystemthat does saves and audio and leaderboards. - Access via
GetSubsystem<>(). Cheap, type-safe. - No tick in subsystems unless required. They're services, not actors.
Performance — the Unreal-specific rules
Tickis opt-out by default for some classes, opt-in for others. CheckPrimaryActorTick.bCanEverTick. Most static actors don't need it.SetActorTickEnabled(false)for actors that don't need it right now.Componentticks separately fromActor. Same opt-in / opt-out story.- Garbage collector pause — UE has a GC. Frequency tuned in project settings. Many UObjects allocated per frame = stutter.
- Soft references over hard references for assets that aren't always needed.
TSoftObjectPtr<UTexture2D>doesn't force-load the texture. - Async asset loading via
FStreamableManagerfor content not needed immediately. - Stat commands.
stat fps,stat unit,stat scenerendering,stat game— the in-editor profiler is excellent. - No
Cast<T>in hot loops. Cache the cast.
Common Unreal pitfalls
UPROPERTYon a raw pointer is required for GC, otherwise the pointer is garbage-collected out from under you.UPROPERTY() AActor* MyActor;notAActor* MyActor;.GetWorld()returning null in constructors — actors don't have a world during construction. UseBeginPlay().- Tick order is undefined between unrelated actors. Use
AddTickPrerequisiteActorif you need ordering. - Hot-reload corrupts C++ class state. Use Live Coding (UE5+) instead of legacy hot-reload.
- Blueprint nativization is deprecated in UE5. Don't rely on it.
- Animation Blueprint thread-safety. Property accessors run on worker threads; only thread-safe data sources are valid.
- Replication graph bandwidth blows up with naïve
Multicast. Use replication conditions and relevancy.
Cross-engine concerns
- Game loop discipline. Unreal's tick uses variable-timestep by default; physics is sub-stepped. See [[game-architecture-patterns]] for theory.
- Camera. Unreal's
APlayerCameraManagerand Camera Modifier system implement many of the rules in [[game-navigation-camera]]. - Animation state machines — use them for animation; use plain C++ state machines for gameplay logic. Don't conflate.
- Object pooling — Unreal has no first-class pool; roll your own or use
FObjectPoolpatterns from community plugins.
Procedure (when reviewing an Unreal project)
- Stack detection. UE version (5.x), C++ / Blueprint mix, multiplayer or singleplayer, target platforms, Lumen / Nanite use.
- Map the gameplay framework hierarchy. Which GameMode? Which GameState? Which PlayerController/Pawn? Are class boundaries respected?
- Audit replication. Are
UPROPERTY(Replicated)properties paired withGetLifetimeReplicatedProps? Are RPC specifiers correct? - Check authority.
HasAuthority()guards before state mutation? - Audit subsystems. Are globals via subsystems, or via static state / Singletons?
- Profile.
stat fps,stat unit. Tick overhead? GC pauses? Asset load hitches? - Audit
Tickenablement. Are tickless actors actually tickless? - Blueprint hot-paths. Is anything Blueprint-ticking that should be C++?
- Output severity-ranked review.
Sources
- Unreal Engine 5 Gameplay Framework — https://dev.epicgames.com/documentation/en-us/unreal-engine/gameplay-framework-in-unreal-engine
- Tom Looman — Unreal Engine Gameplay Framework Guide for C++ (tomlooman.com/unreal-engine-gameplay-framework/).
- Epic Game Developers Library — replication and networking talks.
- Outscal — "Unreal Engine GameMode" architecture explainer.
- Unreal Engine Forums — official replication FAQ.
When this gets stale: re-check the gameplay framework page on dev.epicgames.com. Epic reorganizes their docs; class names stay stable.