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
| Type | Lifetime | Use Case |
|---|---|---|
UEngineSubsystem | Editor lifetime | Editor tools, asset management |
UGameInstanceSubsystem | Game session | Save systems, player progress |
UWorldSubsystem | Per-world | Level-specific managers |
ULocalPlayerSubsystem | Per local player | Input, 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.Stunis a child ofState.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.