Spring Sale: 30% off bundles with SPRINGBUNDLE or 15% off individual products with SPRING15 — ends Apr 15

StraySparkStraySpark
ProductsFree AssetsDocsBlogGamesAbout
StraySparkStraySpark

Game Studio & UE5 Tool Developers. Building professional-grade tools for the Unreal Engine community.

Products

  • Complete Toolkit (Bundle)
  • Procedural Placement Tool
  • Cinematic Spline Tool
  • Blueprint Template Library
  • DetailForge
  • UltraWire
  • Unreal MCP Server
  • Blender MCP Server
  • Godot MCP Server

Resources

  • Free Assets
  • Documentation
  • Blog
  • Changelog
  • Roadmap
  • FAQ
  • Contact

Legal

  • Privacy Policy
  • Terms of Service

© 2026 StraySpark. All rights reserved.

Back to Blog
tutorial
StraySparkMarch 16, 20265 min read
10 UE5 C++ Patterns Every Game Developer Should Know 
Unreal EngineCppGame DevelopmentTutorialBest Practices

Why Patterns Matter in UE5 C++

UE5 C++ isn't standard C++. It's a framework with its own idioms, lifetime management, reflection system, and networking layer. Writing UE5 C++ like you'd write a desktop application leads to crashes, memory leaks, and architectural dead ends.

These ten patterns are the foundation of professional UE5 C++ development.

1. Subsystems: The Right Place for Singleton Logic

The Problem

You need a system that exists once per game/world/player — a quest manager, achievement tracker, or analytics service. The instinct is to make a singleton, but UE5 singletons fight the engine's lifecycle management.

The Pattern

Use Subsystems — UE5's built-in singleton-like classes that automatically manage their lifecycle:

UCLASS()
class UQuestSubsystem : public UGameInstanceSubsystem
{
    GENERATED_BODY()

public:
    virtual void Initialize(FSubsystemCollectionBase& Collection) override;
    virtual void Deinitialize() override;

    UFUNCTION(BlueprintCallable)
    void StartQuest(FName QuestId);

    UFUNCTION(BlueprintCallable)
    bool IsQuestComplete(FName QuestId) const;

private:
    TMap<FName, FQuestState> ActiveQuests;
};

Access from anywhere:

UQuestSubsystem* Quests = GetGameInstance()->GetSubsystem<UQuestSubsystem>();
Quests->StartQuest("MainQuest_01");

Subsystem Lifetimes

TypeLifetimeUse Case
UEngineSubsystemEditor lifetimeEditor tools, asset management
UGameInstanceSubsystemGame sessionSave systems, player progress
UWorldSubsystemPer-worldLevel-specific managers
ULocalPlayerSubsystemPer local playerInput, UI, camera

2. Delegates: Decoupled Communication

The Pattern

Delegates let objects communicate without hard references. The publisher doesn't know who's listening.

// Declaration
DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FOnHealthChanged, float, CurrentHealth, float, MaxHealth);

UCLASS()
class UHealthComponent : public UActorComponent
{
    GENERATED_BODY()

public:
    UPROPERTY(BlueprintAssignable, Category = "Health")
    FOnHealthChanged OnHealthChanged;

    void TakeDamage(float Damage)
    {
        CurrentHealth = FMath::Max(0.f, CurrentHealth - Damage);
        OnHealthChanged.Broadcast(CurrentHealth, MaxHealth);
    }
};
// Listener (in another class)
void AEnemyHealthBar::BeginPlay()
{
    UHealthComponent* Health = GetOwner()->FindComponentByClass<UHealthComponent>();
    Health->OnHealthChanged.AddDynamic(this, &AEnemyHealthBar::UpdateDisplay);
}

When to Use Which Delegate Type

  • Dynamic delegates (DECLARE_DYNAMIC_...): When Blueprints need to bind (slower, but Blueprint-accessible)
  • Native delegates (DECLARE_DELEGATE_...): C++ only listeners (faster, compile-time checked)
  • Multicast: Multiple listeners (Broadcast())
  • Single-cast: One listener (Execute())

Critical Rule

Always unbind delegates when the listener is destroyed:

void AEnemyHealthBar::EndPlay(EEndPlayReason::Type Reason)
{
    if (UHealthComponent* Health = GetOwner()->FindComponentByClass<UHealthComponent>())
    {
        Health->OnHealthChanged.RemoveDynamic(this, &AEnemyHealthBar::UpdateDisplay);
    }
    Super::EndPlay(Reason);
}

Failing to unbind causes crashes when the delegate fires and the listener no longer exists.

3. Async Asset Loading

The Problem

Loading assets synchronously blocks the game thread. Loading a 200MB mesh freezes the game for seconds.

The Pattern

Use FStreamableManager for async loading:

void AEnemySpawner::RequestSpawn()
{
    FSoftObjectPath AssetPath = EnemyMeshAsset.ToSoftObjectPath();

    FStreamableManager& Streamable = UAssetManager::GetStreamableManager();
    Streamable.RequestAsyncLoad(AssetPath,
        FStreamableDelegate::CreateUObject(this, &AEnemySpawner::OnAssetLoaded));
}

void AEnemySpawner::OnAssetLoaded()
{
    UStaticMesh* LoadedMesh = EnemyMeshAsset.Get();
    if (LoadedMesh)
    {
        SpawnEnemyWithMesh(LoadedMesh);
    }
}

For multiple assets:

TArray<FSoftObjectPath> AssetsToLoad;
AssetsToLoad.Add(EnemyMesh.ToSoftObjectPath());
AssetsToLoad.Add(EnemyMaterial.ToSoftObjectPath());
AssetsToLoad.Add(EnemySound.ToSoftObjectPath());

Streamable.RequestAsyncLoad(AssetsToLoad,
    FStreamableDelegate::CreateUObject(this, &AEnemySpawner::OnAllAssetsLoaded));

4. Component Architecture

The Pattern

Favor composition (components) over inheritance (deep class hierarchies):

// Bad: Deep inheritance
class ABaseEnemy → class AMeleeEnemy → class AShieldedMeleeEnemy → class AEliteShieldedMeleeEnemy

// Good: Composition
AEnemy
├── UHealthComponent
├── UCombatComponent (melee or ranged)
├── UShieldComponent (optional)
├── ULootDropComponent
└── UEliteModifierComponent (optional)

Components are:

  • Reusable across different actor types
  • Independently testable
  • Mix-and-matchable for variety
  • Easier to add/remove features without breaking hierarchies
UCLASS()
class UShieldComponent : public UActorComponent
{
    GENERATED_BODY()

public:
    // Intercepts damage before it reaches health
    float AbsorbDamage(float IncomingDamage);

    bool IsShieldActive() const { return CurrentShield > 0.f; }

private:
    UPROPERTY(EditDefaultsOnly)
    float MaxShield = 50.f;

    UPROPERTY(Replicated)
    float CurrentShield;
};

An enemy becomes "shielded" by adding this component — no inheritance change needed.

5. Gameplay Tags for State Management

The Pattern

Use Gameplay Tags instead of enums or booleans for actor state:

// Bad: Boolean soup
bool bIsStunned;
bool bIsOnFire;
bool bIsInvulnerable;
bool bIsSprinting;
// What if stunned AND on fire AND invulnerable?

// Good: Tag-based state
UPROPERTY()
FGameplayTagContainer ActiveStates;

// Check state
bool bStunned = ActiveStates.HasTag(FGameplayTag::RequestGameplayTag("State.Stunned"));
bool bOnFire = ActiveStates.HasTag(FGameplayTag::RequestGameplayTag("State.Burning"));

// Check multiple
FGameplayTagContainer RequiredTags;
RequiredTags.AddTag(Tag_State_Alive);
RequiredTags.AddTag(Tag_State_Grounded);
bool bCanJump = ActiveStates.HasAll(RequiredTags);

Benefits:

  • Hierarchical: State.Debuff.Stun is a child of State.Debuff
  • Query "has any debuff?" with HasTag(State.Debuff) — matches all children
  • Data-driven: define tags in config files, not code
  • No enum conflicts when merging branches

6. TArray and TMap Best Practices

Common Patterns

// Reserve capacity for known sizes (avoids reallocation)
TArray<AActor*> Enemies;
Enemies.Reserve(ExpectedEnemyCount);

// FindByPredicate instead of manual loops
AActor* Target = Enemies.FindByPredicate([](const AActor* Enemy) {
    return Enemy->IsAlive() && Enemy->GetDistanceTo(this) < AttackRange;
});

// RemoveAllSwap for fast removal (doesn't preserve order)
Enemies.RemoveAllSwap([](const AActor* Enemy) {
    return !IsValid(Enemy);
});

// Sort with predicate
Enemies.Sort([this](const AActor& A, const AActor& B) {
    return A.GetDistanceTo(this) < B.GetDistanceTo(this);
});

TMap Performance

// TMap for O(1) lookup
TMap<FName, FItemData> ItemDatabase;

// Add/Find
ItemDatabase.Add("Sword_01", SwordData);
FItemData* Found = ItemDatabase.Find("Sword_01");

// Iterate
for (auto& [Key, Value] : ItemDatabase)
{
    // Process each item
}

7. UObject Lifecycle Awareness

The Pattern

UE5 objects have a unique lifecycle. Understand it to avoid crashes:

// NEVER use raw new/delete for UObjects
AActor* Enemy = new AEnemy(); // WRONG — will crash
AActor* Enemy = GetWorld()->SpawnActor<AEnemy>(EnemyClass); // Correct

// Always check validity before use
if (IsValid(TargetEnemy)) // Checks both null AND pending kill
{
    TargetEnemy->TakeDamage(Damage);
}

// Mark UPROPERTY on all UObject pointers (or GC will collect them)
UPROPERTY()
AActor* CachedTarget; // GC-safe

AActor* UnsafeTarget; // Will be garbage collected!

Weak Pointers for Non-Owning References

TWeakObjectPtr<AActor> WeakTarget;

// Set
WeakTarget = SomeActor;

// Check and use
if (WeakTarget.IsValid())
{
    WeakTarget->DoSomething();
}
// No crash if the actor was destroyed — just returns invalid

8. Interface-Driven Design

The Pattern

Use UInterfaces for polymorphic behavior without coupling:

UINTERFACE(MinimalAPI, BlueprintType)
class UDamageable : public UInterface { GENERATED_BODY() };

class IDamageable
{
    GENERATED_BODY()
public:
    UFUNCTION(BlueprintNativeEvent)
    float ApplyDamage(float Amount, AActor* Instigator, FGameplayTag DamageType);
};

Any actor can implement this interface:

class AEnemy : public ACharacter, public IDamageable
{
    virtual float ApplyDamage_Implementation(float Amount, AActor* Instigator, FGameplayTag DamageType) override;
};

class ADestructibleProp : public AActor, public IDamageable
{
    virtual float ApplyDamage_Implementation(float Amount, AActor* Instigator, FGameplayTag DamageType) override;
};

The damage system doesn't need to know about enemies or props:

void ApplyAreaDamage(FVector Center, float Radius, float Damage)
{
    TArray<AActor*> HitActors;
    // Sphere overlap...

    for (AActor* Actor : HitActors)
    {
        if (Actor->Implements<UDamageable>())
        {
            IDamageable::Execute_ApplyDamage(Actor, Damage, this, DamageTag);
        }
    }
}

9. Timer Management

The Pattern

Use FTimerManager instead of manual tick counters:

// One-shot delayed call
GetWorldTimerManager().SetTimer(RespawnTimer, this,
    &ASpawner::RespawnEnemy, 5.0f, false);

// Repeating timer
GetWorldTimerManager().SetTimer(HealthRegenTimer, this,
    &ACharacter::RegenerateHealth, 1.0f, true);

// Lambda timer
GetWorldTimerManager().SetTimer(DelayTimer,
    FTimerDelegate::CreateLambda([this]() {
        // Delayed logic here
    }), 2.0f, false);

// Clear timer
GetWorldTimerManager().ClearTimer(HealthRegenTimer);

// Pause/unpause
GetWorldTimerManager().PauseTimer(HealthRegenTimer);
GetWorldTimerManager().UnPauseTimer(HealthRegenTimer);

Why Not Tick?

Tick runs every frame (60+ times per second). Most game logic doesn't need that frequency. Health regeneration every second, AI checks every 0.5 seconds, and spawn checks every 3 seconds should use timers, not tick.

Fewer ticking actors = better performance.

10. Data-Driven Design with Data Assets

The Pattern

Separate data from code using UDataAsset:

UCLASS()
class UWeaponData : public UPrimaryDataAsset
{
    GENERATED_BODY()

public:
    UPROPERTY(EditDefaultsOnly)
    FText DisplayName;

    UPROPERTY(EditDefaultsOnly)
    float BaseDamage = 10.f;

    UPROPERTY(EditDefaultsOnly)
    float AttackSpeed = 1.0f;

    UPROPERTY(EditDefaultsOnly)
    TSoftObjectPtr<UStaticMesh> WeaponMesh;

    UPROPERTY(EditDefaultsOnly)
    TSoftObjectPtr<UAnimMontage> AttackMontage;

    UPROPERTY(EditDefaultsOnly)
    FGameplayTagContainer WeaponTags;
};

Create data assets in the Content Browser for each weapon. The weapon actor reads from the data asset:

void AWeapon::Initialize(UWeaponData* Data)
{
    WeaponData = Data;
    // Async load mesh
    // Set damage values
    // Configure attack speed
}

Benefits:

  • Designers edit data assets (no code changes needed)
  • Easy to create variants (duplicate and modify)
  • Soft references keep memory clean
  • Data can be loaded from tables or databases

These patterns aren't theoretical — they're the building blocks of every well-architected UE5 project. Master them, and you'll write code that's performant, maintainable, and plays well with the engine's systems.

For production-ready implementations of many common gameplay systems, check out the Blueprint Template Library — 15 systems built with these patterns, ready to integrate into your project.

Tags

Unreal EngineCppGame DevelopmentTutorialBest Practices

Continue Reading

tutorial

Getting Started with UE5 PCG Framework: Build Your First Procedural World

Read more
tutorial

Nanite Foliage in UE5: The Complete Guide to High-Performance Vegetation

Read more
tutorial

UE5 Lumen Optimization Guide: Achieving 60fps with Dynamic Global Illumination

Read more
All posts