C++September 7, 202616 min read

UE5 C++ Best Practices: Modern Patterns for Game Development

Writing C++ for Unreal isn't like writing standard C++. Here's what you need to know to write robust, performant game code.

UE5 Is Not Standard C++

Unreal Engine has its own idioms, memory model, and coding standards. Code that's correct in standard C++ can be broken in UE. This guide covers the differences that matter.

Memory Management: The Garbage Collector

UObjects are garbage collected. This changes everything about memory management.

Rules for UObject Pointers

  • Always use UPROPERTY(): Raw UObject* without UPROPERTY can become dangling after GC
  • Check IsValid(): Before dereferencing any UObject pointer
  • TWeakObjectPtr: For references you don't want to prevent GC
  • TObjectPtr: The modern replacement for raw pointers in UE5
// BAD: Can become dangling
AActor* MyActor;

// GOOD: GC-aware
UPROPERTY()
TObjectPtr<AActor> MyActor;

// For optional references
TWeakObjectPtr<AActor> WeakActor;

Non-UObject Memory

For non-UObject types, standard C++ rules apply with some additions:

  • TSharedPtr/TWeakPtr: UE's smart pointers for non-UObject types
  • TUniquePtr: Exclusive ownership, no overhead
  • Avoid new/delete: Use MakeShared, MakeUnique, or placement new

UPROPERTY: More Than Just Reflection

UPROPERTY isn't optional decoration. It affects:

  • Garbage collection (prevents premature destruction)
  • Serialization (saving/loading)
  • Replication (networking)
  • Blueprint exposure
  • Editor visibility

Essential Specifiers

// Blueprint read/write
UPROPERTY(EditAnywhere, BlueprintReadWrite)
float Health;

// Blueprint read-only
UPROPERTY(VisibleAnywhere, BlueprintReadOnly)
UStaticMeshComponent* Mesh;

// Replicated
UPROPERTY(Replicated)
int32 Score;

// Replicated with notification
UPROPERTY(ReplicatedUsing=OnRep_Health)
float Health;

Categories and Display Names

UPROPERTY(EditAnywhere, Category="Combat|Damage")
float BaseDamage;

UPROPERTY(EditAnywhere, meta=(DisplayName="Attack Speed"))
float AttackRate;

UFUNCTION Patterns

Blueprint Integration

// Callable from Blueprint
UFUNCTION(BlueprintCallable, Category="Combat")
void TakeDamage(float Amount);

// Implementable in Blueprint
UFUNCTION(BlueprintImplementableEvent)
void OnDeath();

// C++ implementation, overridable in Blueprint
UFUNCTION(BlueprintNativeEvent)
void OnHit();
void OnHit_Implementation();

Networking

// Server only
UFUNCTION(Server, Reliable)
void ServerFire();

// All clients
UFUNCTION(NetMulticast, Unreliable)
void MulticastPlaySound();

// Owning client only
UFUNCTION(Client, Reliable)
void ClientShowUI();

Construction and Initialization

Constructor vs BeginPlay

The constructor runs when the CDO (Class Default Object) is created and for every instance. BeginPlay runs when the actor enters the world.

// Constructor: set defaults, create components
AMyActor::AMyActor()
{PrimaryActorTick.bCanEverTick = true; Mesh = CreateDefaultSubobject<UStaticMeshComponent>
    (TEXT("Mesh")); RootComponent = Mesh;}

// BeginPlay: initialize runtime state
void AMyActor::BeginPlay()
{Super::BeginPlay(); Health = MaxHealth; FindEnemies();}

Component Creation

  • CreateDefaultSubobject: In constructor only, creates component with CDO
  • NewObject: Runtime object creation
  • CreateComponent: Runtime component creation on actors

Performance Patterns

Avoid Tick When Possible

// In constructor
PrimaryActorTick.bCanEverTick = false;

// Or reduce frequency
PrimaryActorTick.TickInterval = 0.1f; // 10Hz

Prefer timers, events, or delegates over continuous polling in Tick.

Cache References

// BAD: Called every frame
void Tick(float DeltaTime)
{GetWorld()->GetFirstPlayerController()->...}

// GOOD: Cached in BeginPlay
UPROPERTY()
TObjectPtr<APlayerController> CachedPC;

void BeginPlay()
{CachedPC = GetWorld()->GetFirstPlayerController();}

Use FORCEINLINE Sparingly

FORCEINLINE is useful for trivial accessors. Don't use it for anything with significant logic—it increases code size and can hurt cache performance.

// Good use
FORCEINLINE float GetHealth() const { return Health; }

// Bad use - function is too complex
FORCEINLINE void ProcessDamage(...) { /* 50 lines */ }

Collections

TArray

The most common collection. Dynamic array similar to std::vector.

TArray<AActor*> Enemies;

// Add elements
Enemies.Add(Enemy);
Enemies.AddUnique(Enemy);

// Remove
Enemies.Remove(Enemy);
Enemies.RemoveAt(Index);

// Find
int32 Index = Enemies.Find(Enemy);
AActor* Found = *Enemies.FindByPredicate([](auto* E){
  return E->GetName() == "Boss";
});

TMap

Key-value pairs. O(1) lookup.

TMap<FName, int32> Inventory;

Inventory.Add(TEXT("Sword"), 1);
int32* Count = Inventory.Find(TEXT("Sword"));

for (auto& Pair : Inventory)
{UE_LOG(LogTemp, Log, TEXT("%s: %d"), *Pair.Key.ToString(), Pair.Value);}

TSet

Unique elements. O(1) membership check.

TSet<AActor*> ActiveActors;

ActiveActors.Add(Actor);
bool bIsActive = ActiveActors.Contains(Actor);

String Handling

UE has three string types:

  • FString: Mutable, general purpose (like std::string)
  • FName: Immutable, case-insensitive, fast comparison (for identifiers)
  • FText: Localization-ready (for UI text)
FString Str = TEXT("Hello");
FName Name = TEXT("ActorName");
FText Display = NSLOCTEXT("Game", "Greeting", "Hello!");

// Conversions
FString FromName = Name.ToString();
FName FromString = FName(*Str);

Error Handling

Check Macros

// Fatal in all builds
check(Pointer != nullptr);

// Fatal in debug/development only
checkSlow(ExpensiveValidation());

// Non-fatal, logs error
ensure(Pointer != nullptr);

// With message
checkf(Index < Array.Num(), TEXT("Index %d out of bounds"), Index);

Verify vs Check

verify evaluates the expression in all builds but only asserts in debug.check evaluates and asserts in debug, skips entirely in shipping.

// Side effect happens in all builds
verify(ImportantFunction());

// Side effect skipped in shipping
check(ImportantFunction()); // DON'T DO THIS

Common Pitfalls

1. Forgetting Super Calls

void AMyActor::BeginPlay()
{Super::BeginPlay(); // Don't forget! // Your code...}

2. Accessing World in Constructor

// BAD: World doesn't exist in constructor
AMyActor::AMyActor()
{GetWorld()->SpawnActor(...); // Crash!}

3. Modifying CDO

// BAD: Modifies all instances
AMyActor::AMyActor()
{GetClass()->GetDefaultObject()->Health = 100;}

4. Circular Dependencies

Use forward declarations in headers, include in .cpp files.

// In header
class AEnemy; // Forward declare

// In cpp
#include "Enemy.h"

5. Hot Reload Issues

Adding UPROPERTY to existing classes requires full rebuild. Hot reload can corrupt instances. When in doubt, restart editor.

Code Style

Follow Epic's coding standard for consistency:

  • PascalCase for types and functions
  • Prefix: A (Actor), U (UObject), F (struct), I (interface), T (template), E (enum)
  • Boolean prefix: b (bIsValid, bCanJump)
  • Tabs for indentation
  • Braces on new lines
class MYGAME_API AMyCharacter : public ACharacter
{GENERATED_BODY() public: AMyCharacter(); UPROPERTY(EditAnywhere, BlueprintReadWrite) float MaxHealth = 100.0f; UFUNCTION(BlueprintCallable) void TakeDamage(float Amount); private: UPROPERTY() float CurrentHealth;}