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:
RoleBehaviorTypical Actions
AttackerClose-range aggressiveMelee combos, pressure
FlankerCircles wide, attacks from anglesSide attacks, backstabs
WaiterMaintains distance, watchesOccasional pokes, ready to swap in
SupporterStays back, buffs/debuffsRanged attacks, healing allies
ElitePriority attacker, ignores some limitsBoss-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:
PropertyValueNotes
AllowedRoles(empty)Leave empty for flexibility (any role). Add specific tags to restrict.
Priority0Higher = higher priority for slots. 0 = standard, 50 = elite, 100 = mini-boss.
PreferredRoleSEC.Role.AttackerSystem 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 cooldown
If 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_Flanking
You 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_Passive

5. 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 considered
  • CurrentRole - Currently assigned role
  • TimeInCurrentRole - Seconds spent in current role (useful for stability bonuses)
Setting up your evaluator:
  • RoleWeights - TMap of role tag to influence weight. Roles not listed use InfluenceOnUnlistedRoles.
  • ScoreMode - HigherIsBetter (default) or LowerIsBetter to 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

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 bLimitAttritionEnabled for soft updates).
Manual Control:
  • ForceAssignRole: Lock an AI into a specific role. Pass bLockRole = true to 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.
FunctionPurpose
RegisterCombatTargetRegister a new combat target (player, objective, etc.)
UnregisterCombatTargetRemove a target and orphan its combatants
AssignCombatantToTargetMove an AI to a specific target's pool
TransferCombatantsToTargetMove all combatants from one target to another
BalanceCombatantsAcrossTargetsRound-robin redistribute combatants evenly
ReevaluateAllTargetsRe-run each AI's TargetSelector
SetPrimaryTargetAndAssignAllHigh-priority target override (e.g., boss VIP)
GetCombatantsForTargetQuery 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:
DelegateParametersFires When
OnCombatRoleChangedController, NewRole, OldRoleAny combatant's role changes
OnCombatRoleChangedForTargetController, Target, NewRole, OldRoleRole changes with target info
OnCombatantRegisteredControllerNew AI enters combat
OnCombatantUnregisteredControllerAI leaves combat or dies
OnCombatTargetRegisteredNewTargetNew target registered
OnCombatTargetUnregisteredOldTargetTarget removed
OnCombatTargetChangedNewTarget, OldTargetDefault target switches
OnCombatantsOrphanedLostTarget, OrphanedControllersCombatants lose their target
OnCombatantsOrphaned fires when a target unregisters. The orphaned combatants have their roles reset to SEC.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):
  1. Set bAutoRegisterForCombatRoles to false on the enemy's EnemyAIConfig
  2. The AI will not be registered with the subsystem and will not consume any role slots
  3. It can still use ActionSets and MovementProfiles, just not through role-based switching
  4. Use ForceAssignRole with bLockRole = true if 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 with bIgnoreTargetRedistribution = true on their config, the subsystem will not re-run their TargetSelector during 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

  1. The server-side AICombatRoleSubsystem assigns a new role to a combatant
  2. The subsystem notifies the AI controller via the OnCombatRoleChanged delegate
  3. The controller (or StateTree) writes the role to the pawn's SECCombatRoleComponent via SetCombatRole()
  4. The role tag replicates to all clients
  5. Clients receive the OnCombatRoleChanged delegate 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

SystemHow Combat Roles Uses It
Action SystemSwaps ActionSet when role changes
Movement SystemDifferent roles have different positioning profiles
Threat DetectionThreatened AI may be deprioritized for Attacker role
Targeting SystemTeam-aware target filtering prevents friendly assignments
MultiplayerRoles replicate to clients via SECCombatRoleComponent