Reconnection
D&D-Inspired Turn-Based Combat Meets Stunning Visuals
Project Overview
Reconnection is a D&D-inspired turn-based fighting game built in Unreal Engine, combining tactical combat with beautiful visuals and dynamic narrative choices. As Lead Engineer, I architected the entire combat system in C++, implementing dice-roll mechanics, stat-based calculations, and AI decision-making that brings tabletop RPG combat to life.
Technical Highlights
D&D Combat System
Automated dice rolls, attack/defense calculations, and turn management
Turn-Based Mechanics
UTurnManager auto-discovers fighters, sorts by initiative, and drives the full round/turn loop via multicast delegates
Enemy AI System
Utility-scored AI weighs attack, heal, block, and buff actions each turn using health ratios, line-of-sight, and weapon type affinity
C++ Architecture
Clean, extensible parent classes with virtual functions
Buff System
Dynamic stat modifications for strategic depth
Dialogue Choices
Player-driven narrative with branching conversations
Code Showcase
Blueprint Integration & Designer Tools
The C++ AFighter and UEnemy classes are designed for Blueprint extensibility, allowing designers to create unique enemy behaviors, player abilities, and combat encounters without touching code. Key Blueprint-implementable functions include:
🤖 Enemy AI Behaviors
- ChooseAction() - Implement custom AI decision trees
- Conditional logic - Enemy evaluates player health, buffs, and position
- Action selection - Choose between Attack, Heal, Block, or special abilities
- Difficulty scaling - Adjust AI aggression and strategy per encounter
⚔️ Combat Actions
- Attack() - Custom attack animations and VFX triggers
- Heal() - Healing effects with particle systems
- Block() - Defensive stance animations
- Die() - Death sequences, loot drops, victory conditions
🎯 Event Delegates
- OnStartTurn - Trigger UI updates, camera effects
- OnEndTurn - Queue next fighter in initiative order
- OnHitAttack - Play hit reactions, damage numbers
- OnHitMiss - Miss animations, combat feedback
💬 Dialogue Integration
- Dynamic choices - Player dialogue affects combat stats
- Narrative branching - Choices influence enemy behavior
- Combat triggers - Dialogue can start/end encounters
- Character relationships - Track player decisions for story outcomes
AFighter Class - Combat System Foundation
The AFighter parent class implements all core combat mechanics including turn management, dice-roll calculations (d20 system), damage/healing, and buff systems. Designed with Blueprint integration for designer flexibility while maintaining C++ performance.
AFighter. h - Header Definition
// Fill out your copyright notice in the Description page of Project Settings.
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "AFighter.generated.h"
UCLASS()
class RECONNECTION_API AFighter : public AActor
{
GENERATED_BODY()
public:
// Sets default values for this actor's properties
AFighter();
//Boolean to check if it's this fighter's turn
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Stored Variables")
bool bIsTurn;
//Initiative score for turn order
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Stored Variables")
int32 InitiativeScore;
//Function to start this fighter's turn
UFUNCTION(BlueprintCallable, Category="Stored Functions")
void StartTurn();
//Function to end this fighter's turn
UFUNCTION(BlueprintCallable, Category="Stored Functions")
void EndTurn();
UFUNCTION(BlueprintCallable, Category = "Stored Functions")
void SendDamage(float Damage, const FString& Type, AFighter *Target);
UFUNCTION(BlueprintCallable, Category = "Stored Functions")
virtual void ReceiveDamage(float Damage, const FString& Type);
protected:
// Called when the game starts or when spawned
virtual void BeginPlay() override;
};
Turn Management & Combat Flow
Implements initiative-based turn order system with clear turn start/end demarcation. The turn management system allows for complex action queuing and supports both player and AI-controlled fighters.
AFighter.cpp - Turn System Implementation
// Fill out your copyright notice in the Description page of Project Settings.
#include "AFighter.h"
void AFighter::BeginPlay()
{
Super::BeginPlay();
}
// Sets default values
AFighter::AFighter()
{
// Set this actor to call Tick() every frame. You can turn this off to improve performance if you don't need it.
PrimaryActorTick. bCanEverTick = false;
bIsTurn = false;
InitiativeScore = 0;
}
void AFighter::StartTurn()
{
bIsTurn = true;
UE_LOG(LogTemp, Warning, TEXT("Turn started"));
}
void AFighter::EndTurn()
{
bIsTurn = false;
UE_LOG(LogTemp, Warning, TEXT("Turn ended"));
}
void AFighter:: SendDamage(float Damage, const FString& Type, AFighter *Target)
{
UE_LOG(LogTemp, Warning, TEXT("Sending %f %s damage to target"), Damage, *Type);
}
void AFighter::ReceiveDamage(float Damage, const FString& Type)
{
UE_LOG(LogTemp, Warning, TEXT("Received %f %s damage from source"), Damage, *Type);
}
D&D-Style Dice Roll System
From the original UFighter implementation: Authentic d20 attack rolls with modifiers, defense calculations, and random damage ranges. This creates unpredictable, strategic combat that feels like tabletop D&D.
UFighter.cpp - Attack & Defense Mechanics
// D20 Attack Roll System
int UFighter::RollToHit()
{
int Roll = FMath::RandRange(1, 20); // d20 roll
return Roll + BaseAttack + AttackBuff;
}
float UFighter::GetDefense()
{
return BaseDefense + DefenseBuff;
}
void UFighter:: Attack(UFighter *Target)
{
// Roll to hit vs target defense (classic D&D mechanic)
if (RollToHit() >= Target->GetDefense())
{
SendDamage(RollDamage(), Target);
OnHitAttack. Broadcast(Target);
}
else
{
OnHitMiss.Broadcast(Target);
}
EndTurn();
}
float UFighter::RollDamage()
{
// Random damage range with buffs
float Damage = FMath::RandRange(MinDamage, MaxDamage) + DamageBuff;
return Damage;
}
void UFighter:: ReceiveDamage_Implementation(float Damage)
{
// Apply damage with reduction/armor
CurrentHealth = FMath::Clamp(CurrentHealth - (Damage - DamageReduction), 0.0f, MaxHealth);
if (CurrentHealth <= 0)
{
Die();
UE_LOG(LogTemp, Warning, TEXT("Fighter has been defeated"));
}
}
Strategic Action System
Multiple combat actions beyond basic attacks: healing, blocking, and buff management. Each action has strategic tradeoffs, encouraging thoughtful decision-making during combat.
UFighter.cpp - Heal, Block, & Buff Systems
// Healing Action
void UFighter::Heal_Implementation()
{
CurrentHealth = FMath::Clamp(CurrentHealth + BaseHeal, 0.0f, MaxHealth);
EndTurn();
}
// Blocking/Defensive Action
void UFighter::Block_Implementation()
{
DamageReduction = BaseBlock + BlockBuff;
EndTurn();
}
// Dynamic Buff System
void UFighter::AddBuff(float BuffAmount, const FString& stat)
{
if (stat == "Attack")
{
AttackBuff += BuffAmount;
}
else if (stat == "Damage")
{
DamageBuff += BuffAmount;
}
else if (stat == "Defense")
{
DefenseBuff += BuffAmount;
}
else if (stat == "Block")
{
BlockBuff += BuffAmount;
}
else if (stat == "Heal")
{
HealBuff += BuffAmount;
}
}
void UFighter:: RemoveBuff(float BuffAmount, const FString& stat)
{
if (stat == "Attack")
AttackBuff -= BuffAmount;
else if (stat == "Damage")
DamageBuff -= BuffAmount;
else if (stat == "Defense")
DefenseBuff -= BuffAmount;
else if (stat == "Block")
BlockBuff -= BuffAmount;
else if (stat == "Heal")
HealBuff -= BuffAmount;
}
UEnemy - Fighter Enemy Architecture
UEnemy inherits from UFighter and adds a full utility-based AI decision system.
Enemies track allies and opponents separately, evaluate weighted utility scores for every possible action,
and automatically choose the highest-value action each turn. Weapon type awareness (Melee, Ranged, Magic)
further biases the AI's attack preference, and all utility weights are EditAnywhere so designers
can tune behavior per enemy type without touching code.
UEnemy.h - Header & Weapon Type Enum
UENUM(BlueprintType)
enum class EWeaponType : uint8
{
Melee UMETA(DisplayName = "Melee"),
Ranged UMETA(DisplayName = "Ranged"),
Magic UMETA(DisplayName = "Magic")
};
UCLASS(ClassGroup = (Fighters), meta = (BlueprintSpawnableComponent))
class RECONNECTION_API UEnemy : public UFighter
{
GENERATED_BODY()
public:
virtual void StartTurn() override;
// Blueprint-implementable AI entry point
UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category = "Enemy|Combat")
void ChooseAction();
virtual void ChooseAction_Implementation();
// Roster management
UFUNCTION(BlueprintCallable, Category = "Enemy|Setup")
void InitializeEnemy(const TArray<UFighter*>& AllFighters);
UFUNCTION(BlueprintCallable, Category = "Enemy|Setup")
void UpdateAlliesAndEnemies(const TArray<UFighter*>& AllFighters);
UFUNCTION(BlueprintCallable, Category = "Enemy|Setup")
void OnFighterListChanged(UFighter* ChangedFighter);
virtual void ReceiveDamage(float Damage) override;
// --- Utility scorers ---
UFUNCTION(BlueprintCallable, Category = "Enemy|Utility")
float GetAttackUtility();
UFUNCTION(BlueprintCallable, Category = "Enemy|Utility")
float GetMeleeUtility();
UFUNCTION(BlueprintCallable, Category = "Enemy|Utility")
float GetRangedUtility();
UFUNCTION(BlueprintCallable, Category = "Enemy|Utility")
float GetMagicUtility();
UFUNCTION(BlueprintCallable, Category = "Enemy|Utility")
float GetSelfHealUtility();
UFUNCTION(BlueprintCallable, Category = "Enemy|Utility")
float GetAllyHealUtility();
UFUNCTION(BlueprintCallable, Category = "Enemy|Utility")
float GetBlockUtility();
UFUNCTION(BlueprintCallable, Category = "Enemy|Utility")
float GetBuffUtility();
// Tunable weights exposed to designers
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Enemy|Utility|Weight")
float AttackUtilityWeight;
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Enemy|Utility|Weight")
bool bHasMelee;
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Enemy|Utility|Weight")
bool bHasRanged;
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Enemy|Utility|Weight")
bool bHasMagic;
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Enemy|Utility|Weight")
float HealUtilityWeight;
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Enemy|Utility|Weight")
bool bHasSelfHeal;
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Enemy|Utility|Weight")
bool bHasAllyHeal;
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Enemy|Utility|Weight")
float BuffUtilityWeight;
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Enemy|Utility|Weight")
bool bHasBuff;
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Enemy|Utility|Weight")
float BlockUtilityWeight;
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Enemy|Utility|Weight")
bool bHasBlock;
UFUNCTION(BlueprintCallable, Category = "Enemy|StorageAccess")
UFighter* GetClosestEnemy();
UFUNCTION(BlueprintCallable, Category = "Enemy|StorageAccess")
UFighter* GetLowestAlly();
private:
TArray<UFighter*> Allies;
TArray<UFighter*> Enemies;
float LastDamageReceived;
int ClosestEnemyIndex;
int LowestAllyIndex;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Enemy|Weapon",
meta = (AllowPrivateAccess = "true"))
EWeaponType CurrentWeaponType = EWeaponType::Melee;
};
UEnemy.cpp - Ally & Enemy Roster Tracking
void UEnemy::InitializeEnemy(const TArray<UFighter*>& AllFighters)
{
Allies.Empty();
for (UFighter* Fighter : AllFighters)
{
if (Fighter && Fighter->GetOwner() != GetOwner())
{
if (Fighter->GetOwner()->FindComponentByClass<UEnemy>())
Allies.Add(Fighter); // Same faction
else
Enemies.Add(Fighter); // Opposing faction
}
}
}
void UEnemy::UpdateAlliesAndEnemies(const TArray<UFighter*>& AllFighters)
{
Allies.Empty();
Enemies.Empty();
for (UFighter* Fighter : AllFighters)
{
if (Fighter && Fighter->GetOwner() != GetOwner())
{
if (Fighter->GetOwner()->FindComponentByClass<UEnemy>())
Allies.Add(Fighter);
else
Enemies.Add(Fighter);
}
}
}
// Called by UTurnManager delegates whenever a fighter joins or dies
void UEnemy::OnFighterListChanged(UFighter* /*ChangedFighter*/)
{
UWorld* World = GetWorld();
if (!World) return;
for (TObjectIterator<UTurnManager> It; It; ++It)
{
if (It->GetWorld() == World)
{
UpdateAlliesAndEnemies(It->Fighters);
break;
}
}
}
void UEnemy::ReceiveDamage(float Damage)
{
Super::ReceiveDamage(Damage);
LastDamageReceived = Damage; // Stored for block utility scoring
}
UEnemy.cpp - Utility AI Scoring System
// Attack utility: drives aggression based on target's remaining health.
// Uses -ln(healthRatio) so near-dead targets score exponentially higher.
float UEnemy::GetAttackUtility()
{
float CurrUtility = 0;
ClosestEnemyIndex = 0;
if (Enemies.Num() <= 0) return CurrUtility;
float ClosestDistance = Enemies[0]->GetOwner()->GetHorizontalDistanceTo(this->GetOwner());
bool bHasLineOfSight = false;
int i = 0;
for (UFighter* CurrFighter : Enemies)
{
float Dist = CurrFighter->GetOwner()->GetHorizontalDistanceTo(this->GetOwner());
if (Dist < ClosestDistance) { ClosestEnemyIndex = i; ClosestDistance = Dist; }
if (CheckSightToTarget(CurrFighter)) bHasLineOfSight = true;
++i;
}
if ((ClosestDistance > MovementLeft && !bHasRanged) || !bHasLineOfSight)
return CurrUtility;
float HealthRatio = FMath::Clamp(
Enemies[ClosestEnemyIndex]->CurrentHealth / Enemies[ClosestEnemyIndex]->MaxHealth,
0.01f, 1.0f);
CurrUtility += -FMath::Loge(HealthRatio);
return CurrUtility * AttackUtilityWeight;
}
// Self-heal: exponential urgency; forced (FLT_MAX) at <=10% health.
float UEnemy::GetSelfHealUtility()
{
if (!bHasSelfHeal) return 0;
float HealthRatio = FMath::Clamp(CurrentHealth / MaxHealth, 0.01f, 1.0f);
if (HealthRatio <= 0.1f)
return FLT_MAX; // Force heal when critically low
return FMath::Exp(-HealthRatio * 5.0f) * HealUtilityWeight;
}
// Ally heal: finds the most wounded visible ally and scores by their need.
float UEnemy::GetAllyHealUtility()
{
if (!bHasAllyHeal) return 0;
float LowestHealthRatio = FLT_MAX;
LowestAllyIndex = 0;
int i = 0;
for (auto Ally : Allies)
{
if (CheckSightToTarget(Ally))
{
float Ratio = Ally->CurrentHealth / Ally->MaxHealth;
if (Ratio < LowestHealthRatio) { LowestAllyIndex = i; LowestHealthRatio = Ratio; }
}
++i;
}
return FMath::Exp(-LowestHealthRatio * 5.0f) * HealUtilityWeight;
}
// Block: scales with recent incoming damage so reactive enemies defend wisely.
float UEnemy::GetBlockUtility()
{
if (!bHasBlock) return 0;
float DamageRatio = FMath::Clamp(LastDamageReceived / MaxHealth, 0.0f, 1.0f);
return (FMath::Exp(DamageRatio * BlockUtilityWeight) - 1.0f) * BlockUtilityWeight;
}
// Buff: rewards buffing when no active buffs exist, diminishes per existing buff.
float UEnemy::GetBuffUtility()
{
if (!bHasBuff) return 0;
float CurrUtility = (BuffTracker.Num() == 0) ? 1.0f : 0.5f / BuffTracker.Num();
return CurrUtility * BuffUtilityWeight;
}
// Weapon-type affinity: preferred weapon scores 1.0, off-type scores lower.
float UEnemy::GetMeleeUtility() { return bHasMelee ? (CurrentWeaponType == EWeaponType::Melee ? 1.0f : 0.3f) : 0; }
float UEnemy::GetRangedUtility() { return bHasRanged ? (CurrentWeaponType == EWeaponType::Ranged ? 1.0f : 0.5f) : 0; }
float UEnemy::GetMagicUtility() { return bHasMagic ? (CurrentWeaponType == EWeaponType::Magic ? 1.0f : 0.6f) : 0; }
Comprehensive Stat System
Tracks 15+ combat statistics including initiative, health, damage ranges, attack bonuses, defense values, and buff modifiers. Provides a complete stat query system for UI and game logic.
UFighter.cpp - Stat Management
TArray<float> UFighter::GetAllStats()
{
TArray<float> Stats;
Stats.Add(InitiativeScore);
Stats.Add(MaxHealth);
Stats.Add(CurrentHealth);
Stats.Add(MinDamage);
Stats.Add(MaxDamage);
Stats.Add(DamageBuff);
Stats.Add(BaseAttack);
Stats.Add(AttackBuff);
Stats.Add(BaseDefense);
Stats.Add(DefenseBuff);
Stats.Add(BaseBlock);
Stats.Add(BlockBuff);
Stats.Add(BaseHeal);
Stats.Add(HealBuff);
Stats.Add(DamageReduction);
return Stats;
}
// Example stat usage in combat
void UFighter::SendDamage_Implementation(float Damage, UFighter* Target)
{
Target->ReceiveDamage(Damage);
}
void UFighter::Die_Implementation()
{
// Handle death logic here in Blueprint
// Can trigger animations, VFX, game over states, etc.
}
UTurnManager - Combat Orchestration
UTurnManager is an ActorComponent that owns the entire combat loop.
On BeginPlay it scans every actor in the world for UFighter components,
sorts them by initiative, binds to their death and end-turn delegates, and waits for a
StartCombat() call. Five Blueprint-assignable multicast delegates let UI, cameras,
and audio respond to every state change without any hard coupling.
UTurnManager.h - Delegates & Public Interface
// Multicast delegates - bind anything in Blueprint with no code changes
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnTurnChanged, UFighter*, CurrentFighter);
DECLARE_DYNAMIC_MULTICAST_DELEGATE(FOnRoundStarted);
DECLARE_DYNAMIC_MULTICAST_DELEGATE(FOnCombatEnded);
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnFighterDeath, UFighter*, DeadFighter);
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnFighterJoined, UFighter*, NewFighter);
UCLASS(ClassGroup=(Custom), meta=(BlueprintSpawnableComponent))
class RECONNECTION_API UTurnManager : public UActorComponent
{
GENERATED_BODY()
public:
UTurnManager();
UPROPERTY(BlueprintReadOnly, Category = "Turn Manager") TArray<UFighter*> Fighters;
UPROPERTY(BlueprintReadOnly, Category = "Turn Manager") int32 CurrentTurnIndex;
UPROPERTY(BlueprintReadOnly, Category = "Turn Manager") int32 CurrentRound;
UPROPERTY(BlueprintReadWrite, Category = "Turn Manager") bool bCombatActive;
UPROPERTY(BlueprintReadOnly, Category = "Turn Manager") int32 EnemiesLeft = 0;
UPROPERTY(BlueprintReadWrite, Category = "Turn Manager") FName NextLevel;
// Events - assign UI, audio, cameras entirely from Blueprint
UPROPERTY(BlueprintAssignable, Category = "Turn Manager|Events") FOnTurnChanged OnTurnChanged;
UPROPERTY(BlueprintAssignable, Category = "Turn Manager|Events") FOnRoundStarted OnRoundStarted;
UPROPERTY(BlueprintAssignable, Category = "Turn Manager|Events") FOnCombatEnded OnCombatEnded;
UPROPERTY(BlueprintAssignable, Category = "Turn Manager|Events") FOnFighterDeath OnFighterDeath;
UPROPERTY(BlueprintAssignable, Category = "Turn Manager|Events") FOnFighterJoined OnFighterJoined;
UFUNCTION(BlueprintCallable, Category = "Turn Manager") void InitializeCombat();
UFUNCTION(BlueprintCallable, Category = "Turn Manager") void StartCombat();
UFUNCTION(BlueprintCallable, Category = "Turn Manager") void NextTurn();
UFUNCTION(BlueprintCallable, Category = "Turn Manager") UFighter* GetCurrentFighter();
UFUNCTION(BlueprintCallable, Category = "Turn Manager") void SortFightersByInitiative();
UFUNCTION(BlueprintCallable, Category = "Turn Manager") void RemoveFighter(UFighter* Fighter);
UFUNCTION(BlueprintCallable, Category = "Turn Manager") void EndCombat();
private:
UFUNCTION() void HandleFighterDeath(UFighter* DeadFighter);
UFUNCTION() void HandleFighterEndTurn(UFighter* Fighter);
};
UTurnManager.cpp - InitializeCombat & StartCombat
void UTurnManager::BeginPlay()
{
Super::BeginPlay();
InitializeCombat();
}
void UTurnManager::InitializeCombat()
{
Fighters.Empty();
// Iterate every actor and collect those with a UFighter component
TArray<AActor*> AllActors;
UGameplayStatics::GetAllActorsOfClass(GetWorld(), AActor::StaticClass(), AllActors);
for (AActor* Actor : AllActors)
{
if (!Actor) continue;
UFighter* FighterComp = Actor->FindComponentByClass<UFighter>();
if (!FighterComp) continue;
Fighters.Add(FighterComp);
// Bind death & turn-end events for automatic flow control
FighterComp->OnDeath.AddDynamic(this, &UTurnManager::HandleFighterDeath);
FighterComp->OnEndTurn.AddDynamic(this, &UTurnManager::HandleFighterEndTurn);
// If the actor also has an UEnemy component, wire up roster-update delegates
UEnemy* EnemyComp = Actor->FindComponentByClass<UEnemy>();
if (EnemyComp)
{
EnemyComp->InitializeEnemy(Fighters);
OnFighterJoined.AddDynamic(EnemyComp, &UEnemy::OnFighterListChanged);
OnFighterDeath.AddDynamic(EnemyComp, &UEnemy::OnFighterListChanged);
++EnemiesLeft;
}
OnFighterJoined.Broadcast(FighterComp);
}
SortFightersByInitiative();
// StartCombat() is called separately so Blueprint can delay the start
}
void UTurnManager::StartCombat()
{
if (Fighters.Num() == 0) { return; }
bCombatActive = true;
CurrentTurnIndex = 0;
CurrentRound = 1;
OnRoundStarted.Broadcast();
// Reset all turns, then give the first fighter their turn
for (UFighter* Fighter : Fighters)
if (Fighter && Fighter->bIsTurn) Fighter->bIsTurn = false;
UFighter* FirstFighter = GetCurrentFighter();
if (FirstFighter)
{
FirstFighter->StartTurn();
OnTurnChanged.Broadcast(FirstFighter);
}
}
UTurnManager.cpp - NextTurn, Round Tracking & Initiative Sort
void UTurnManager::NextTurn()
{
if (!bCombatActive || Fighters.Num() == 0) return;
CurrentTurnIndex++;
// Wrap index → new round
if (CurrentTurnIndex >= Fighters.Num())
{
CurrentTurnIndex = 0;
CurrentRound++;
OnRoundStarted.Broadcast();
}
// Clear any lingering active turns
for (UFighter* Fighter : Fighters)
if (Fighter && Fighter->bIsTurn) Fighter->bIsTurn = false;
UFighter* CurrentFighter = GetCurrentFighter();
if (CurrentFighter)
{
CurrentFighter->StartTurn();
OnTurnChanged.Broadcast(CurrentFighter);
}
}
UFighter* UTurnManager::GetCurrentFighter()
{
return Fighters.IsValidIndex(CurrentTurnIndex) ? Fighters[CurrentTurnIndex] : nullptr;
}
// Descending sort - highest initiative goes first (classic D&D tiebreaker by score)
void UTurnManager::SortFightersByInitiative()
{
Fighters.Sort([](const UFighter& A, const UFighter& B)
{
return A.InitiativeScore > B.InitiativeScore;
});
}
// Called by HandleFighterEndTurn delegate
void UTurnManager::HandleFighterEndTurn(UFighter* Fighter)
{
if (bCombatActive) NextTurn();
}
UTurnManager.cpp - Fighter Removal, Victory & Level Transition
void UTurnManager::RemoveFighter(UFighter* Fighter)
{
if (!Fighter) return;
int32 RemovedIndex = Fighters.Find(Fighter);
if (RemovedIndex == INDEX_NONE) return;
Fighters.Remove(Fighter);
// Keep CurrentTurnIndex valid after removal
if (RemovedIndex < CurrentTurnIndex)
CurrentTurnIndex--;
else if (RemovedIndex == CurrentTurnIndex && CurrentTurnIndex >= Fighters.Num())
CurrentTurnIndex = 0;
if (Fighters.Num() <= 1) EndCombat();
}
void UTurnManager::EndCombat()
{
bCombatActive = false;
OnCombatEnded.Broadcast();
}
// Bound to each fighter's OnDeath delegate during InitializeCombat()
void UTurnManager::HandleFighterDeath(UFighter* DeadFighter)
{
OnFighterDeath.Broadcast(DeadFighter);
RemoveFighter(DeadFighter);
// If it was an enemy, decrement counter and load the next level on victory
if (DeadFighter->GetOwner()->FindComponentByClass<UEnemy>())
{
--EnemiesLeft;
if (EnemiesLeft <= 0)
{
UGameplayStatics::OpenLevel(GetWorld(), FName(NextLevel));
}
}
}
Blueprint Showcases
Unreal Engine Blueprint graphs built on top of the C++ foundation, handling AI decision-making, movement validation, and input processing.
AI State Tree
The central thinking loop for every enemy. This Blueprint reads utility scores calculated in UEnemy.cpp and drives the State Tree that determines which action each enemy takes on their turn.
Enemy Brain (Part 1 of 2)
The Enemy Brain graph initializes State Tree thoughts and instructs each enemy how to act upon them, translating the utility AI system into concrete in-world decisions.
Enemy Brain (Part 2 of 2)
Continuation of the Enemy Brain graph, showing the downstream action execution after the State Tree has resolved which behavior scores highest for that turn.
Ghost Mover
A ghost mesh that pathfinds to the player's clicked destination and validates whether the target position is reachable within the player's movement constraints before committing the move.
Player Mouse Tracker
Line traces the mouse cursor on click to determine what was hit in the world, then routes that hit information back to Blueprint for downstream gameplay logic such as targeting and movement commands.
Team Credits
Developer Reflection
Building Reconnection taught me the importance of designing C++ systems that empower designers. By creating a robust combat foundation with Blueprint extensibility, our team could iterate rapidly on enemy behaviors and game balance while maintaining performant, clean code. Translating the unpredictability and strategy of tabletop D&D into automated combat was an incredibly rewarding challenge!