Targeting System
Multi-target combat support with intelligent target selection, orphan handling, and custom selectors.
Intelligent multi-target support for co-op, versus, and dynamic combat scenarios. AI automatically selects, switches, and redistributes targets.
When to Use This
- Co-op games where multiple players fight together
- Versus modes where AI needs to distribute attention across teams
- Dynamic encounters where targets spawn/die mid-combat
- Boss fights where specific AI should ignore player-switching
[!NOTE] Works seamlessly with single-player - zero configuration needed. AI auto-assigns to the first registered target.
Core Concepts
Multi-Target Architecture
Instead of a single "player" reference, the system tracks multiple combat targets:
// Register multiple targets (players, destructibles, objectives)
UAICombatRoleSubsystem* Subsystem = GetWorld()->GetSubsystem<UAICombatRoleSubsystem>();
Subsystem->RegisterCombatTarget(Player1);
Subsystem->RegisterCombatTarget(Player2);Each target maintains its own independent pool of combatants with separate role limits.
Target Selectors
When an AI needs a target (spawn, death, re-evaluation), it uses a Target Selector:
| Selector | Behavior | Best For |
|---|---|---|
| FirstTargetSelector | Picks first registered target | Single-player (default) |
| ClosestTargetSelector | Picks nearest target | Distance-based threat |
| BalancedTargetSelector | Picks target with fewest enemies | Fair distribution |
| RandomTargetSelector | Random distribution | Unpredictable encounters |
Selectors are data-driven and can be configured per-AI or project-wide.
Lazy Target Acquisition
AI spawning before targets exist? No problem:
1. Enemy spawns → No target available → Waits (unassigned)
2. Player registers → Subsystem notifies unassigned AI
3. Each AI runs its TargetSelector → Gets assigned
This solves the classic "AI spawns before player" race condition.
Quick Setup
1. Register Targets
In your GameMode or Player setup:
void AMyGameMode::HandlePlayerSpawn(APlayerController* PC)
{
if (APawn* Pawn = PC->GetPawn())
{
UAICombatRoleSubsystem* Subsystem = GetWorld()->GetSubsystem<UAICombatRoleSubsystem>();
// Auto-assign waiting AI to this target
Subsystem->RegisterCombatTarget(Pawn, /*bAutoAssignUnassigned=*/ true);
}
}2. Configure Per-AI Selectors (Optional)
In your EnemyAIConfig Data Asset:
// Pick closest player
TargetSelector: UClosestTargetSelector
// Bosses ignore global reshuffling
bIgnoreTargetRedistribution: true3. Project-Wide Default
Project Settings → Plugins → Soulslike Enemy Combat:
Default Target Selector: UBalancedTargetSelector
If no per-AI selector is set, AI uses this. If neither is set, it falls back to UFirstTargetSelector.
Target Destruction Handling
When a target dies or unregisters, the system automatically handles orphaned AI:
1. Target destroyed → Subsystem detects
2. Orphaned AI roles reset to "None"
3. Each AI receives OnCombatTargetLost() callback
4. Subsystem runs TargetSelector for each orphan
5. AI auto-assigns to new targets
6. OnCombatantsOrphaned delegate broadcasts (for custom handling)
Handling Target Loss Manually
Implement IAICombatRoleInterface on your AI Controller:
void AMyAIController::OnCombatTargetLost_Implementation(AActor* LostTarget)
{
// Play confused animation
PlayAnimMontage(ConfusedMontage);
// Wait before accepting new target
FTimerHandle Handle;
GetWorld()->GetTimerManager().SetTimer(Handle, [this]()
{
// TargetSelector will have already assigned a new target
// Resume normal behavior
}, 1.5f, false);
}Target Selector API
Selection Context
Custom selectors receive rich context for informed decisions:
USTRUCT(BlueprintType)
struct FTargetSelectionContext
{
// AI's current target (for stickiness decisions)
TWeakObjectPtr<AActor> CurrentTarget;
// How many AI assigned to each target
TMap<TObjectPtr<AActor>, int32> CombatantCountPerTarget;
// Pre-computed distances from AI to each target
TMap<TObjectPtr<AActor>, float> DistanceSquaredPerTarget;
// True if called due to target destruction
bool bIsTargetLossReselection;
};Creating Custom Selectors
Subclass UTargetSelector for custom logic:
UCLASS(DisplayName = "Threat-Based Target Selector")
class UMyThreatSelector : public UTargetSelector
{
GENERATED_BODY()
public:
virtual AActor* SelectTarget_Implementation(
AController* Controller,
const TArray<AActor*>& AvailableTargets,
const FTargetSelectionContext& Context) const override
{
// Example: Pick the target that has hurt this AI most
AActor* HighestThreat = nullptr;
float MaxDamage = 0.f;
for (AActor* Target : AvailableTargets)
{
float DamageTaken = GetDamageTakenFrom(Controller, Target);
if (DamageTaken > MaxDamage)
{
MaxDamage = DamageTaken;
HighestThreat = Target;
}
}
// Fall back to balanced if no damage history
if (!HighestThreat)
{
return AvailableTargets[0];
}
return HighestThreat;
}
};Skipping Auto-Reselection
Return nullptr from a selector during target loss to handle assignment manually:
if (Context.bIsTargetLossReselection)
{
// Don't auto-assign - let OnCombatTargetLost handle it
return nullptr;
}Manual Control API
Force Target Assignment
// Assign specific combatant to specific target
Subsystem->AssignCombatantToTarget(Controller, Target, /*bTriggerRoleEvaluation=*/ true);Trigger Re-evaluation
// Re-run target selection for one AI
Subsystem->ReassignCombatantTarget(Controller);
// Re-run for ALL AI (respects bIgnoreTargetRedistribution)
Subsystem->ReevaluateAllTargets();Balance Distribution
// Force even redistribution across all targets (ignores selectors)
Subsystem->BalanceCombatantsAcrossTargets();Transfer Combatants
// Player 1 dies, transfer their enemies to Player 2
Subsystem->TransferCombatantsToTarget(Player1, Player2, /*bForceReassignment=*/ true);Role Locking for Scripted Sequences
Lock AI into specific roles during boss phases or cinematics:
// Force this AI to be an Attacker, locked from evaluation
Subsystem->ForceAssignRole(Controller, SEC.Role.Attacker, /*bLockRole=*/ true);
// Later, unlock so normal evaluation can resume
Subsystem->UnlockRole(Controller);
// Check lock status
if (Subsystem->IsRoleLocked(Controller))
{
// This AI won't be affected by role re-evaluation
}Locked combatants are completely skipped during automatic role evaluation.
Configuration: bIgnoreTargetRedistribution
For bosses or special AI that shouldn't auto-switch targets:
// In EnemyAIConfig:
bIgnoreTargetRedistribution = true;This AI will:
- ✅ Get initial target assignment via TargetSelector
- ✅ React to its own target dying (OnCombatTargetLost)
- ❌ Ignore
ReevaluateAllTargets()calls - ❌ Ignore new target registration reshuffles
Perfect for boss encounters in co-op where the boss should focus one player until they're dead.
Delegates
Subscribe to global events for UI, analytics, or custom logic:
// When any combatant's target changes
Subsystem->OnCombatRoleChangedForTarget.AddDynamic(this, &UMyClass::OnRoleChanged);
void UMyClass::OnRoleChanged(AController* Controller, AActor* Target, FGameplayTag NewRole, FGameplayTag OldRole);
// When combatants lose their target
Subsystem->OnCombatantsOrphaned.AddDynamic(this, &UMyClass::OnOrphaned);
void UMyClass::OnOrphaned(AActor* LostTarget, const TArray<AController*>& OrphanedCombatants);
// When a new target registers
Subsystem->OnCombatTargetRegistered.AddDynamic(this, &UMyClass::OnNewTarget);Backward Compatibility
Existing single-player setups work with zero changes:
// Old API (deprecated but functional)
Subsystem->SetCombatTarget(Player); // → calls RegisterCombatTarget(Player, true)
Subsystem->GetCombatTarget(); // → returns first registered target
// These log deprecation warnings but compile and run correctlyQuery API
// Get all registered targets
TArray<AActor*> Targets = Subsystem->GetAllCombatTargets();
// Check if a target is registered
bool bIsTarget = Subsystem->HasCombatTarget(Player);
// Get which target an AI is assigned to
AActor* Target = Subsystem->GetCombatantTarget(Controller);
// Get all AI assigned to a specific target
TArray<AController*> Enemies = Subsystem->GetCombatantsForTarget(Player);
// Get role count for a specific target
int32 AttackerCount = Subsystem->GetRoleCountForTarget(SEC.Role.Attacker, Player);Integration Points
| System | How Targeting Uses It |
|---|---|
| Combat Roles | Each target has independent role pools |
| Action System | Actions can be target-aware |
| Movement System | Positioning relative to assigned target |
