Targeting System
Documentation Unreal Engine AI Targeting Multi-Player
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.
Selector:
Target 1(2)
Target 2(3)
E1
E2
E3
E4
E5
Target Counts
Target 1: 2
Target 2: 3
Events●
Target Counts
Target 1: 2
Target 2: 3
Event Log●
No events yet
Click targets to eliminate. System periodically re-evaluates assignments based on strategy.
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 or die mid-combat
- Boss fights where specific AI should ignore player-switching
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 (at spawn, on target death, or during 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 combatants (with stickiness) | Fair distribution |
| RandomTargetSelector | Random distribution | Unpredictable encounters |
Selectors are data-driven and can be configured per-AI via
EnemyAIConfig or project-wide.Lazy Target Acquisition
AI spawning before targets exist? Not a 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. Use
RegisterCombatTarget(Player, /*bAutoAssignUnassigned=*/ true) to trigger immediate assignment of waiting AI.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 the project default. If neither is configured, 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 via OnDestroyed binding
2. Orphaned AI roles reset to SEC.Role.None
3. OnCombatantsOrphaned delegate broadcasts (for custom handling)
4. Subsystem re-runs each orphan's TargetSelector
5. AI auto-assigns to remaining targets
Handling Target Loss on the AI Controller
The
SECCombatControllerComponent exposes an OnCombatTargetLost delegate. Bind to it on your AI Controller:// In your AI Controller's BeginPlay:
USECCombatControllerComponent* CombatComp = FindComponentByClass<USECCombatControllerComponent>();
CombatComp->OnCombatTargetLost.AddDynamic(this, &AMyAIController::HandleTargetLost);
void AMyAIController::HandleTargetLost(AActor* LostTarget)
{
// Play confused animation, delay before accepting new target, etc.
}EnemyControllerBase also provides a Blueprint-overridable event K2_OnCombatTargetLost that fires from this delegate.Target Selector API
Selection Context
Custom selectors receive pre-computed 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 squared 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 in C++ or Blueprint: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 first target if no damage history
return HighestThreat ? HighestThreat :
(AvailableTargets.Num() > 0 ? AvailableTargets[0] : nullptr);
}
};SelectTargetis aBlueprintNativeEvent, meaning you can also create selectors entirely in Blueprint by subclassingUTargetSelectorand overriding the function. No C++ required.
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;
}[!TIP] Action-Specific Overrides: You can bypass the entire targeting system for a single action (e.g., "Heal Ally" or "Attack Specific Object") by using Action Context. See Contextual Execution.
Manual Control API
Force Target Assignment
// Assign specific combatant to specific target
Subsystem->AssignCombatantToTarget(Controller, Target, /*bTriggerRoleEvaluation=*/ true);Select Target Utility
Run the full target selection pipeline (config lookup, self-exclusion, team filtering) for a specific AI with an explicit candidate list:
// Run selector with custom candidate list
AActor* Best = Subsystem->SelectTargetFor(Controller, CandidateTargets);
if (Best)
{
Subsystem->AssignCombatantToTarget(Controller, Best);
}This resolves the AI's
TargetSelector from its EnemyAIConfig, falls back to the project default, removes the AI's own pawn from candidates, filters out friendly targets, and returns the selector's pick.Trigger Re-evaluation
// Re-run target selection for one AI
Subsystem->ReassignCombatantTarget(Controller);
// Re-run for ALL AI (respects bIgnoreTargetRedistribution)
Subsystem->ReevaluateAllTargets();Periodic Target Refresh
By default, the reassignment timer only re-evaluates roles, not targets. In multi-target scenarios where targets move around, you may want AI to periodically reconsider which target is best.
Enable periodic target re-evaluation:
// In Project Settings > Plugins > Soulslike Enemy Combat > Timing:
bReevaluateTargetsOnReassignment: true
When enabled, each reassignment cycle (default every 8 seconds) will:
- Re-run each AI's
TargetSelector(respectsbIgnoreTargetRedistribution) - Then evaluate roles against the updated target assignments
This only takes effect when multiple combat targets are registered. Single-target setups are unaffected.
Runtime control:
// Enable/disable dynamically
Subsystem->SetReevaluateTargetsOnReassignment(true);
// Or via UpdateConfig
FAICombatRoleConfig Config = Subsystem->GetConfig();
Config.bReevaluateTargetsOnReassignment = true;
Subsystem->UpdateConfig(Config);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,
FGameplayTag::RequestGameplayTag("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 should not auto-switch targets:
// In EnemyAIConfig:
bIgnoreTargetRedistribution = true;This AI will:
- Get initial target assignment via its TargetSelector
- React to its own target dying (OnCombatTargetLost delegate)
- Ignore
ReevaluateAllTargets()calls - Ignore new target registration reshuffles
Useful for boss encounters in co-op where the boss should focus one player until they are dead.
Delegates
Subscribe to global events for UI, analytics, or custom logic:
// When any combatant's role changes (includes target info)
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);
void UMyClass::OnNewTarget(AActor* NewTarget);Team-Aware Target Filtering
The subsystem automatically filters targets based on team affinity. Any actor implementing
ISECTeamInterface receives a team ID (0-254). The subsystem uses USECTeamHelpers::AreFriendly() to exclude friendly targets from every assignment path:- Target selection (custom selectors receive only hostile/neutral candidates)
- Auto-assignment (unassigned AI skip friendly targets in
RegisterCombatTarget) - Fallback assignment (no-selector path skips friendly targets)
SetPrimaryTargetAndAssignAll(skips friendly combatants)BalanceCombatantsAcrossTargets(per-combatant hostile-only target list)
Team relationship rules (
ESECTeamRelation):| Relationship | Same Team? | Filtered? |
|---|---|---|
| Same non-255 TeamId | Friendly | Yes (skipped) |
| Different TeamIds | Hostile | No (kept) |
| Either has TeamId 255 (NoTeam) | Neutral | No (kept) |
| No team interface | Neutral | No (kept) |
This is fully backward compatible. Existing setups without teams are unaffected since all actors without
ISECTeamInterface are treated as neutral.Implementing ISECTeamInterface
In Blueprint:
- Open your Actor/Controller Blueprint
- Class Settings > Interfaces > Add "SEC Team Interface"
- Implement "Get SEC Team ID" (return your team number: 0 for player, 1 for enemy, etc.)
- Optionally implement "Set SEC Team ID" if you need runtime changes
Common team ID constants are available in
SECTeamID namespace: Player = 0, Enemy = 1, NoTeam = 255.Damage Filtering
USECTeamHelpers also provides CanDamageWithFilter for team-aware damage filtering:// Before applying damage:
if (USECTeamHelpers::CanDamageWithFilter(DamageCauser, Target, ESECTeamFilter::EnemiesOnly))
{
// Apply damage
}Available filter modes (
ESECTeamFilter):NoFilter- Can hit anyoneEnemiesOnly- Only hit enemies and neutrals (no friendly fire)FriendliesOnly- Only hit friendlies (for healing/buffs)NeutralOnly- Only hit neutral actors
Multi-Faction Example
Two boss factions fighting the player and each other:
// Boss A (Team 2) and Boss B (Team 3) both register as targets
Subsystem->RegisterCombatTarget(BossA);
Subsystem->RegisterCombatTarget(BossB);
Subsystem->RegisterCombatTarget(Player); // Team 0
// Boss A's minions (Team 2) will only target Boss B and the Player
// Boss B's minions (Team 3) will only target Boss A and the Player
// Player's allies (Team 0) will only target Boss A and Boss BNo additional configuration is needed. Set TeamIds on your pawns/controllers via
ISECTeamInterface and the subsystem handles the rest.Backward Compatibility
The legacy single-target API still works:
Subsystem->SetCombatTarget(Player); // -> calls RegisterCombatTarget(Player, true)
Subsystem->GetCombatTarget(); // -> returns first registered targetThese log deprecation warnings but compile and run correctly.
Query 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(
FGameplayTag::RequestGameplayTag("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 |