Combat Roles
Documentation Unreal Engine AI Combat Roles
Multi-enemy coordination with role-based behavior, slot management, and fitness evaluation.
Coordinating multiple enemies so they don't all attack at once. Some flank, some wait, some support.
Player
Attacker
Waiter
Waiter
Flanker
Combat Slots
Attackers:1 / 1
(Others must wait)
Enemies dynamically swap roles based on slot availability and priority.
When to Use This
- Any game with multiple enemies that should not all attack simultaneously
- Soulslike encounters where enemies take turns
- Group tactics with specialized roles (tank, flanker, support)
- Dynamic difficulty by adjusting how many attackers are allowed
This system can be used independently from the other plugin systems.
Core Concepts
The Role System
Instead of all enemies rushing the player, each gets assigned a role:
| Role | Behavior | Typical Actions |
|---|---|---|
| Attacker | Close-range aggressive | Melee combos, pressure |
| Flanker | Circles wide, attacks from angles | Side attacks, backstabs |
| Waiter | Maintains distance, watches | Occasional pokes, ready to swap in |
| Supporter | Stays back, buffs/debuffs | Ranged attacks, healing allies |
| Elite | Priority attacker, ignores some limits | Boss-tier behavior |
Roles are dynamic. Enemies swap based on distance, fitness scores, and slot availability. Roles use Gameplay Tags (
SEC.Role.*), so you can define custom roles beyond the built-in ones via Project Settings > Gameplay Tags.Slot Management
The subsystem limits how many enemies can hold each role simultaneously. Slot limits are configured per-role using a
TMap<FGameplayTag, int32> called RoleLimits:// In Project Settings > Plugins > Soulslike Enemy Combat:
RoleLimits:
SEC.Role.Attacker → 2 // Only 2 enemies attack at a time
SEC.Role.Flanker → 1 // One flanker
// Unlisted roles are unlimited
When an Attacker finishes or dies, a Waiter can take its place. Roles not listed in
RoleLimits have no cap.Fitness Evaluation
"Who should be the next Attacker?" is decided by Role Evaluators (
URoleEvaluator subclasses). These are instanced UObjects that score each AI on a 0.0-1.0 scale:// Built-in evaluators:
UDistanceRoleEvaluator // Scores based on distance to target
UCooldownRoleEvaluator // Scores based on action cooldown state (fatigue system)Each evaluator has a
RoleWeights map that controls how strongly it influences each role. For example, a distance evaluator can strongly affect Attacker scoring while barely influencing Waiter scoring. Evaluators also support a ScoreMode toggle (HigherIsBetter or LowerIsBetter) to invert the scoring direction.The AI with the highest combined fitness score gets priority for open slots.
Role-Based ActionSets
Each role can have a different ActionSet, configured in the
EnemyAIConfig data asset:// In EnemyAIConfig:
DefaultActionSet: DA_ActionSet_Attacker // Fallback for unlisted roles
RoleActionSets:
SEC.Role.Waiter → DA_ActionSet_Patient
SEC.Role.Flanker → DA_ActionSet_Flanking
When an AI's role changes, the
SECCombatRoleSyncLibrary swaps its ActionSet and MovementProfile to match the new role.Quick Setup
1. Create an EnemyAIConfig
Right-click > Miscellaneous > Data Asset > EnemyAIConfig
2. Configure Role Registration
In the
RoleRegistrationParams section of the config:| Property | Value | Notes |
|---|---|---|
| AllowedRoles | (empty) | Leave empty for flexibility (any role). Add specific tags to restrict. |
| Priority | 0 | Higher = higher priority for slots. 0 = standard, 50 = elite, 100 = mini-boss. |
| PreferredRole | SEC.Role.Attacker | System tries this role first if a slot is available. |
3. Set Up Fitness Evaluators
Add evaluators to the
FitnessEvaluators array in RoleRegistrationParams:FitnessEvaluators:
- DistanceEvaluator
RoleWeights: { SEC.Role.Attacker: 1.0, SEC.Role.Flanker: 0.5 }
IdealDistance: 0 // Closer is better
EffectiveRange: 2000 // Score drops to 0 at 2000cm
ScoreMode: HigherIsBetter
- CooldownEvaluator
RoleWeights: { SEC.Role.Attacker: 1.0 }
CurrentRolePenaltyMultiplier: 0.5 // Penalize staying in Attacker when on cooldownIf no evaluators are configured, the subsystem falls back to simple distance-based scoring.
4. Map Roles to ActionSets
In the Action Sets section of the config:
bManageActionSetsAutomatically: true
DefaultActionSet: DA_ActionSet_Default
RoleActionSets:
SEC.Role.Attacker → DA_ActionSet_Aggressive
SEC.Role.Waiter → DA_ActionSet_Patient
SEC.Role.Flanker → DA_ActionSet_FlankingYou can also map roles to MovementProfiles in the Movement Profiles section:
bManageMovementProfilesAutomatically: true
DefaultMovementProfile: DA_MovementProfile_Default
RoleMovementProfiles:
SEC.Role.Attacker → DA_MovementProfile_Aggressive
SEC.Role.Waiter → DA_MovementProfile_Passive5. Assign to Enemy
In your Enemy Blueprint > AIConfig > Set to your EnemyAIConfig.
How Role Assignment Works
1. Enemy spawns, controller possesses pawn
2. SECCombatControllerComponent resolves AIConfig
3. If bAutoRegisterForCombatRoles is true, registers with AICombatRoleSubsystem
4. Subsystem evaluates all registered enemies for each target pool
5. Fitness scores calculated per-evaluator, weighted per-role, combined via average
6. Roles assigned based on:
- Slot availability (RoleLimits)
- Fitness scores
- Priority (tiebreaker)
- Preferred role
- Hysteresis (MinTimeInRole prevents rapid swapping)
7. ActionSet and MovementProfile synced via SECCombatRoleSyncLibrary
8. Re-evaluation happens on a timer (RoleReassignmentInterval)
Custom Fitness Evaluators
Create your own scoring logic by subclassing
URoleEvaluator:UCLASS(BlueprintType, meta = (DisplayName = "Visibility Evaluator"))
class UVisibilityRoleEvaluator : public URoleEvaluator
{
GENERATED_BODY()
public:
virtual float EvaluateFitness_Implementation(
const FRoleEvaluationContext& Context) const override
{
// Context provides: Controller, Role, CurrentRole, TimeInCurrentRole
// Access pawn via Context.Controller->GetPawn()
// Return 0.0 (unfit) to 1.0 (perfect fit)
if (!PlayerCanSeeMe(Context.Controller->GetPawn()))
return 1.0f; // Great flanker candidate
return 0.3f;
}
};The
FRoleEvaluationContext contains:Controller- The AI controller being evaluated (access pawn, components, etc. through this)Role- The role being consideredCurrentRole- Currently assigned roleTimeInCurrentRole- Seconds spent in current role (useful for stability bonuses)
Setting up your evaluator:
RoleWeights- TMap of role tag to influence weight. Roles not listed useInfluenceOnUnlistedRoles.ScoreMode-HigherIsBetter(default) orLowerIsBetterto invert the score.
Add it to your EnemyAIConfig's
RoleRegistrationParams > FitnessEvaluators array. You can also create evaluators entirely in Blueprint by making a Blueprint class that inherits from URoleEvaluator and overriding EvaluateFitness.Runtime Control & API
You can control the role system dynamically during gameplay using the AICombatRoleSubsystem. This is useful for boss fights, cutscenes, or changing difficulty on the fly.
Blueprint Functions
Blueprint
Open in blueprintue.com ↗Configuration Overrides:
- SetReassignmentInterval: Change how often the system re-evaluates roles.
- SetMinTimeInRole: Stop enemies from switching roles too quickly (hysteresis).
- SetRoleLimitOverride: Change the max number of actors allowed in a role.
- Example: Allow 3 Attackers instead of 1 for an "Enrage" phase.
- ClearRoleLimitOverride / ClearAllRoleLimitOverrides: Reset limits back to project settings.
- UpdateConfig: Swap the entire configuration struct at runtime (with optional
bLimitAttritionEnabledfor soft updates).
Manual Control:
- ForceAssignRole: Lock an AI into a specific role. Pass
bLockRole = trueto prevent the evaluation system from reassigning it.- Useful for: Scripted events where an enemy must wait or taunt.
- UnlockRole: Return the AI to the automatic evaluation pool.
- ForceReassignment: Trigger an immediate re-evaluation of all AI.
- PauseReassignment / ResumeReassignment: Freeze role changes during cutscenes or special sequences.
// Example: Enrage phase - Allow more attackers
UAICombatRoleSubsystem* RoleSys = GetWorld()->GetSubsystem<UAICombatRoleSubsystem>();
RoleSys->SetRoleLimitOverride(FGameplayTag::RequestGameplayTag("SEC.Role.Attacker"), 3);
RoleSys->ForceReassignment();Multi-Target API
The subsystem supports multiple combat targets. Each target maintains its own independent pool of combatants with separate role limits.
| Function | Purpose |
|---|---|
RegisterCombatTarget | Register a new combat target (player, objective, etc.) |
UnregisterCombatTarget | Remove a target and orphan its combatants |
AssignCombatantToTarget | Move an AI to a specific target's pool |
TransferCombatantsToTarget | Move all combatants from one target to another |
BalanceCombatantsAcrossTargets | Round-robin redistribute combatants evenly |
ReevaluateAllTargets | Re-run each AI's TargetSelector |
SetPrimaryTargetAndAssignAll | High-priority target override (e.g., boss VIP) |
GetCombatantsForTarget | Query which AI are assigned to a target |
Per-AI target selection is configured via the
TargetSelector property on EnemyAIConfig. Built-in selectors: FirstTargetSelector, ClosestTargetSelector, RandomTargetSelector, BalancedTargetSelector.Delegates
The subsystem fires several delegates for global listeners:
| Delegate | Parameters | Fires When |
|---|---|---|
OnCombatRoleChanged | Controller, NewRole, OldRole | Any combatant's role changes |
OnCombatRoleChangedForTarget | Controller, Target, NewRole, OldRole | Role changes with target info |
OnCombatantRegistered | Controller | New AI enters combat |
OnCombatantUnregistered | Controller | AI leaves combat or dies |
OnCombatTargetRegistered | NewTarget | New target registered |
OnCombatTargetUnregistered | OldTarget | Target removed |
OnCombatTargetChanged | NewTarget, OldTarget | Default target switches |
OnCombatantsOrphaned | LostTarget, OrphanedControllers | Combatants lose their target |
OnCombatantsOrphanedfires when a target unregisters. The orphaned combatants have their roles reset toSEC.Role.None. Bind to this delegate to reassign them to a new target.
Role Syncing
When an enemy's role changes (e.g., from Waiter to Attacker), their abilities and movement behavior need to update to match.
Automatic Syncing (Default)
If you use the included
StateTree_SEC_Core, role syncing happens automatically. The StateTree tasks query the current role and resolve the appropriate ActionSet and MovementProfile from the EnemyAIConfig.Manual Syncing (Custom Controllers)
If you are building a fully custom controller or StateTree, use the SECCombatRoleSyncLibrary. It handles the logic of finding the correct asset for the role from the AIConfig.
Functions:
- SyncAllForCombatRole: Updates both ActionSets and MovementProfiles.
- SyncActionSetForRole: Updates only the action set on the pawn's
SECActionSetComponent. - SyncMovementProfileForRole: Updates only the movement profile on the controller's
MovementEvaluatorComponent.
// Inside your Custom Controller: On Role Changed Event
USECCombatRoleSyncLibrary::SyncAllForCombatRole(
this, // Controller
GetPawn(), // Pawn
NewRoleTag, // The new role
MyEnemyAIConfig // The config asset
);Bosses and Non-Coordinated AI
For enemies that should not participate in the subsystem's slot coordination (bosses, ambient wildlife, scripted AI):
- Set
bAutoRegisterForCombatRolestofalseon the enemy'sEnemyAIConfig - The AI will not be registered with the subsystem and will not consume any role slots
- It can still use ActionSets and MovementProfiles, just not through role-based switching
- Use
ForceAssignRolewithbLockRole = trueif you still want to assign a role to a boss that is registered (e.g., for ActionSet/MovementProfile resolution), without the subsystem ever reassigning it
For enemies withbIgnoreTargetRedistribution = trueon their config, the subsystem will not re-run theirTargetSelectorduring periodic re-evaluation. This is useful for bosses or scripted AI that should always fight the same target.
Client-Side Role Visibility (SECCombatRoleComponent)
The role subsystem runs on the server. It lives on AI controllers, which don't exist on clients. To let clients know what role an enemy has (for UI, VFX, debug overlays), the plugin includes
USECCombatRoleComponent.This component sits on the enemy pawn and replicates the currently assigned role tag. The server writes to it whenever the role changes, and clients can read it or bind to its delegate.
Setup
EnemyCharacterBase adds this component by default. If you are using a custom character class, add it manually:// In your custom character constructor:
CombatRoleComponent = CreateDefaultSubobject<USECCombatRoleComponent>(TEXT("CombatRoleComponent"));Or add it in Blueprint via the Components panel. Search for "SEC Combat Role".
How It Works
- The server-side
AICombatRoleSubsystemassigns a new role to a combatant - The subsystem notifies the AI controller via the
OnCombatRoleChangeddelegate - The controller (or StateTree) writes the role to the pawn's
SECCombatRoleComponentviaSetCombatRole() - The role tag replicates to all clients
- Clients receive the
OnCombatRoleChangeddelegate on the component
Reading on Clients
// Get the component from any pawn reference
USECCombatRoleComponent* RoleComp = Pawn->FindComponentByClass<USECCombatRoleComponent>();
// Read the current role
FGameplayTag CurrentRole = RoleComp->GetCombatRole();
// React to changes
RoleComp->OnCombatRoleChanged.AddDynamic(this, &UMyWidget::OnRoleChanged);
void UMyWidget::OnRoleChanged(FGameplayTag NewRole, FGameplayTag OldRole)
{
// Update role indicator icon, change outline color, etc.
}This component is purely informational on the client side. Setting the role on clients is a no-op. Only the server can assign roles through the subsystem.
Integration Points
| System | How Combat Roles Uses It |
|---|---|
| Action System | Swaps ActionSet when role changes |
| Movement System | Different roles have different positioning profiles |
| Threat Detection | Threatened AI may be deprioritized for Attacker role |
| Targeting System | Team-aware target filtering prevents friendly assignments |
| Multiplayer | Roles replicate to clients via SECCombatRoleComponent |