Movement System

Documentation Unreal Engine AI Movement

Tactical AI positioning with direction sampling, distance maintenance, strafing, navmesh awareness, and pawn avoidance.


Tactical positioning that considers distance, angle, navmesh walkability, pawn avoidance, and custom rules on every tick.

When to Use This

  • Any AI that needs tactical positioning around a target
  • Melee enemies that maintain attack distance
  • Ranged enemies that keep their distance
  • Group AI where pawns need to avoid each other
  • Boss fights with phase-based positioning changes
Works independently of the other plugin systems.

Core Concepts

Direction Sampling

Every tick, the system evaluates 16 directions around the AI and scores each one:

Interactive Direction Sampling

Move mouse to simulate Target
AI Pawn
Target
Ideal Range
Green arrows = High Score (Preferred Direction)
Each direction gets a score from:
  • Distance to target: how well the move serves the ideal range
  • Angle to target: whether it faces the target
  • Pawn avoidance: whether another AI sits in this direction
  • Navmesh walkability: whether it leads off a ledge, into a wall, or onto a hazard (see Navmesh-Aware Sampling)
  • Positioning Rules: custom modifiers you define (see below)
The highest-scoring direction wins.

Distance Maintenance

The AI holds a tolerance zone around its desired distance:
Too Close      Comfort Zone      Too Far
   |-----------|=============|-----------|
   0        -tolerance    distance    +tolerance     inf
          (approach)     (strafe)     (retreat)
Set DesiredDistance and DistanceTolerance (a percentage of it). DesiredDistance = 400 with DistanceTolerance = 0.2 gives a comfort band of 320-480 cm.

Strafing

The AI circles the target while holding distance:
  • AutoStrafeSwap: changes direction when the opposite side scores better
  • Strafe Fatigue: the AI rests after continuous strafing (broadcasts OnStrafeResting)
  • Threat-Triggered Swap: swaps direction when the player stares, if configured. Set per MovementBehaviorProfile (requires Threat Detection)

Movement Layers

Two layers switch automatically by distance:
LayerWhenHow
TacticalTarget is within HybridSwitchDistanceDirect movement input for responsive strafing and positioning. Still reads the navmesh per direction for walkability, ledges, and hazards (see Navmesh-Aware Sampling)
StrategicTarget is far away or path-blockedNavMesh pathfinding to navigate around obstacles
The switch happens at HybridSwitchDistance (default 800).

Detour Escalation (Around Corners)

A target that rounds a corner sits close in a straight line but a long walk away on the navmesh, so the tactical layer strafes into the wall trying to reach it. Detour Escalation hands the AI off to Strategic pathfinding to round the corner, then resumes strafing the moment it regains the angle. It reuses the distance switch's pathfinding, so the return to combat is seamless.
On by default. It acts only inside HybridSwitchDistance (beyond that, the distance switch already pathfinds), stays inert with no navmesh, and leaves the AI tactical when the target has no complete route (walled off, or on a separate navmesh island) instead of walking it to a dead end.
It escalates two ways:
  • Far detour (predictive): the route is much longer than the straight line. It engages when route / straight-line exceeds DetourEnterRatio (1.6) and route - straight-line exceeds DetourMinExcess (200 cm). The second gate lets the AI strafe past a thin pillar instead of pathfinding around it.
  • Tight corner (reactive): the target is just past a corner, so the route is barely longer than straight and the ratio can't see it, yet local steering still can't round the corner and grinds the wall. When the AI wants to close on a target it can't see but isn't gaining ground (closing slower than DetourStallSpeed, 60 cm/s), it escalates. A pillar never trips this, because the AI keeps closing past it.
It returns to strafing the moment it rounds the corner and regains sight, or once pathfinding has closed it back into strafe range. There is no ratio-based release: a tight corner keeps a low ratio the whole way, so progress, not geometry, ends the detour.
Built for crowds: the route query is throttled to DetourRecheckInterval (0.5s) and staggered across the pack. bDetourUseLineOfSightPreGate (on) skips the query while the AI can see its target, so open-field fights cost one cheap trace. Turning the pre-gate off adds a query each interval and catches see-over-but-walk-blocking geometry like railings.
Tuning: if it paths around minor bumps, raise DetourEnterRatio or DetourMinExcess; if it still grinds a tight corner, raise DetourStallSpeed so a slower grind counts as stalled; if it pathfinds during a normal slow approach, lower DetourStallSpeed. SEC.Debug.Movement.LogMovement 1 logs Detour escalation ENGAGED / CLEARED, noting (stalled at cover) when the reactive path fired. Works under the StateTree, a Behavior Tree, or standalone, with no extra wiring.
In the tactical layer, each sampled direction gets a soft navmesh walkability score, so enemies stop strafing, circling, or backing off ledges and into walls during a fight. It is a preference, not a veto, so a boxed-in AI still picks its safest direction instead of freezing. A forward probe also eases speed down at a rim, so momentum does not carry the AI over the edge.
On by default; inert with no navmesh, so existing setups are unchanged.
Built for crowds: the check caches per navmesh poly, refreshes at 10Hz, and staggers across enemies, so a pack stays in budget. Each validates against its own nav agent, so large-agent archetypes read the navmesh built for them.
Tuning tip: lower NavProbeDistance (try ~60) for tight arenas and corridors, where a long probe over-reports edges.

Hazard Areas

To make enemies avoid a patch they can still walk on (lava, spikes, a fire zone):
  1. Create a Blueprint subclass of NavArea (e.g. NavArea_Lava) and give it a distinct draw color.
  2. Drop a Nav Modifier Volume over the patch, set its Area Class to your hazard area, and rebuild navigation (press P to verify it tints).
  3. On the movement component, add the area to Hazard Areas with an avoidance strength from 0 to 1: 0 ignores it, 1 treats it like a wall, ~0.75 routes around it but crosses when cornered.
The patch stays walkable: the AI routes around it but crosses when it has no better option. Raise the area's Default Cost too, so the long-range pathfinder also avoids it.
A patch authored as NavArea_Null carves a hole in the navmesh, so the AI treats it as an impassable wall without a Hazard Areas entry. Use a walkable hazard area only when the AI should be able to cross it.

Evaluate-Only Mode (Root Motion)

Set bApplyMovementInput = false and the component publishes a movement direction each tick without driving the pawn. Feed it to a root-motion locomotion graph:
// Read the chosen direction for a locomotion blendspace:
FVector   Dir   = MovementEvaluator->GetCurrentDirection();
float     Angle = MovementEvaluator->GetCurrentDirectionAngle();   // signed yaw: 0 ahead, +90 right, +/-180 behind
FVector2D Local = MovementEvaluator->GetCurrentDirectionLocal();   // (forward, right)
In this mode the component stays tactical-only and issues no path-following moves. It still scores directions against the navmesh when nav-aware sampling is on.

Quick Setup

Option 1: Use a Preset

// In your AI Controller's BeginPlay:
MovementEvaluator->ApplyBehaviorConfig(FMovementBehaviorConfig::MakeAttacker());
Available presets:
  • MakeDefault(): Balanced defaults (400 cm, moderate rest)
  • MakeAttacker(): Close-range aggressive (300 cm, minimal rest)
  • MakeWaiter(): Far-range passive (600 cm, frequent rest)
  • MakeFlanker(): Medium distance repositioning (400 cm, quick pauses)
  • MakeSupporter(): Medium-far range (500 cm, moderate rest)
  • MakeElite(): Aggressive, relentless pressure (350 cm, very short rest)

Option 2: Use a Data Asset

  1. Create a MovementBehaviorProfile data asset (Right-click, Miscellaneous, Data Asset, MovementBehaviorProfile)
  2. Configure desired distance, rest times, threat response, and positioning rules
  3. Apply it:
MovementEvaluator->ApplyBehaviorProfile(MyProfile);

Option 3: Configure at Runtime

MovementEvaluator->SetDesiredDistance(350.f);
MovementEvaluator->SetStrafeSide(1);  // 1 = right, -1 = left
The most automated path: combat role picks the profile.
  1. Configure your EnemyAIConfig data asset.
  2. Assign profiles to roles (e.g., Attacker gets Aggressive, Waiter gets Passive).
  3. The SECCombatControllerComponent syncs automatically when the role changes.
// Called by SECCombatControllerComponent automatically when role changes:
MovementEvaluator->SyncForCombatRole(RoleTag, MyAIConfig);

Key Properties

PropertyDefaultPurpose
NumSamples16Directions to evaluate per tick
DesiredDistance400Ideal distance to maintain (cm)
DistanceTolerance0.2Comfort zone as percentage of desired distance
bEnableAvoidancetrueAvoid other AI pawns
AvoidanceRadius100How far to stay from other pawns (cm)
bEnableHybridMovementtrueSwitch between pathfinding and direct input
HybridSwitchDistance800Distance to switch between Tactical and Strategic
bEnableDetourEscalationtruePathfind around a corner or wall the AI can't reach the target through by steering
DetourEnterRatio1.6Route-to-straight ratio that engages the far-detour (predictive) escalation
DetourMinExcess200Extra walk over the straight line required alongside the ratio; ignores thin obstacles (cm)
bDetourEscalateOnStalltrueAlso escalate at a tight corner where the AI grinds the wall without gaining ground
DetourStallSpeed60Closing speed below which a blocked approach counts as stalled, not progressing (cm/s)
DetourRecheckInterval0.5How often the route is re-measured (seconds)
bDetourUseLineOfSightPreGatetrueSkip the route query while the AI can see its target
bAutoStrafeSwaptrueAutomatically swap strafe direction
StrafeSwapThreshold3.0Score ratio required to trigger auto-swap
StrafeTimeLimit5.0Base seconds before forcing a strafe direction swap
bEnableStrafeResttrueEnable fatigue rest after continuous strafing
StrafeRestTimeLimit10.0Seconds of strafing before forced rest
StrafeRestDuration1.5Duration of forced rest (seconds)
DirectionBlendCount3Top-scoring directions to blend (1 = pick best, 2-3 = smooth)
bEnableStuckDetectiontrueDetect when AI is stuck and cannot make progress
bApplyMovementInputtrueDrive the pawn with direct input. Set false for Evaluate-Only (root motion)
bEnableNavAwareSamplingtrueGate sample directions by the navmesh (ledges, walls, hazards)
NavProbeDistance120How far ahead each direction is checked against the navmesh (cm)
NavScoreWeight1.0Strength of the navmesh term (0 = off)
NavBlockedFloor0.05Residual score for blocked directions, so a boxed-in AI never dead-stops
bGuardLedgeDropstrueEase speed down at a true ledge instead of crawling over it
HazardAreaAvoidance(empty)Map of NavArea classes the AI avoids stepping into (0-1 strength)
See Configuration Reference for the complete list.

Key Functions

// Main tick function - call from StateTree or Blueprint
EMovementEvaluatorResult EvaluateAndMove();
 
// Same as above, but with an explicit distance override
EMovementEvaluatorResult EvaluateAndMoveToDistance(float Distance);
 
// Apply a preset configuration
void ApplyBehaviorConfig(const FMovementBehaviorConfig& Config);
 
// Apply a data asset profile
void ApplyBehaviorProfile(const UMovementBehaviorProfile* Profile);
 
// Sync with AI Config (role-based switch)
bool SyncForCombatRole(const FGameplayTag& RoleTag, UEnemyAIConfig* AIConfig);
 
// Runtime adjustments
void SetDesiredDistance(float NewDistance);
void SetStrafeSide(int32 NewSide);  // 1 = right, -1 = left
void SwapStrafeSide();
void SetDistanceMultiplier(float Multiplier);  // E.g., from threat detection
 
// Movement suspension
void SuspendMovement(float Duration);
bool IsMovementSuspended();
 
 
// Queries
bool IsAtDesiredDistance();
bool IsStrafing();
float GetCurrentDistanceToTarget();
EMovementLayer GetCurrentMovementLayer();
 
// Root-motion direction output (Evaluate-Only mode)
FVector GetCurrentDirection();
float GetCurrentDirectionAngle(bool bRelativeToControlRotation = false);
FVector2D GetCurrentDirectionLocal();

Positioning Rules

Custom Positioning Rules are instanced UObjects that modify direction scores during evaluation.
UCLASS(Blueprintable)
class UMyFlankingRule : public UPositioningRule
{
    GENERATED_BODY()
 
    virtual float EvaluateDirection_Implementation(
        const FDirectionEvaluationContext& Context) const override
    {
        // Return -1.0 to +1.0
        //  +1.0 = strongly encourage this direction
        //   0.0 = no preference
        //  -1.0 = strongly discourage this direction
    }
};
Add rules to your MovementBehaviorProfile's PositioningRules array. Each rule has a Weight (0-1) controlling its influence on the final score. Multiple rules stack additively.
Built-in rule: UAnglePreferenceRule pushes the AI toward (or away from) a specific angle relative to the target's facing direction:
  • PreferredAngle = 0 - frontal assault
  • PreferredAngle = 90 - flanking (side attacks)
  • PreferredAngle = 180 - backstab positioning
  • bAvoidInstead = true - invert the rule to avoid the angle instead

Events

The component broadcasts delegates you can bind to in Blueprint or C++:
EventWhen
OnStrafeStartedAI begins strafing (includes strafe side)
OnStrafeEndedAI stops strafing
OnStrafeSwappedDirection changes (includes new side)
OnStrafeRestingStrafe fatigue rest begins
OnMovementSuspendedMovement suspended (includes duration)
OnMovementResumedSuspension ends
OnArrivedAtDesiredDistanceAI enters the comfort zone
OnDepartedDesiredDistanceAI leaves the comfort zone
OnMovementLayerChangedLayer switches (Tactical, Strategic)
OnSwitchToPathfindingSwitched to Strategic layer
OnSwitchToDirectInputSwitched to Tactical layer
OnPathfindingFailedPathfinding blocked or unreachable

Behavior Tree Tasks

BT tasks for movement outside the main evaluation loop:
TaskPurpose
Move Until DistancePathfind toward a blackboard target until within a specified distance. Supports timeout, max chase distance, and debug visualization.
Make DistanceRetreat from a target using direct movement input until reaching a target distance. Includes stuck detection and timeout.
Debug SEC ValuesLogs all SEC_ blackboard values (target, distance, action ID) to output log and screen. Useful for debugging.

Debug Tools

Console Commands:
SEC.Debug.Movement.DrawScoring 1      // Visualize direction scores
SEC.Debug.Movement.DrawAvoidance 1    // Show avoidance radii
SEC.Debug.Movement.DrawNav 1          // Show navmesh sample validity and the ledge guard
SEC.Debug.Movement.LogMovement 1      // Log discrete movement events (layer change, suspend/resume, pathfinding, stuck, detour)
SEC.Debug.Movement.LogTick 1          // Log the per-tick state dump (distance, error, speed)
SEC.Debug.Movement.LogScoring 1       // Log direction scoring details
SEC.Debug.Movement.LogAvoidance 1     // Log avoidance detection and scoring
SEC.Debug.Movement.LogStrafeSwap 1    // Log strafe swap decisions
SEC.Debug.Movement.LogStrafeState 1   // Log strafe state changes
LogMovement and LogTick are separate on purpose. The per-tick state dump fires every evaluation tick for every enemy, so leaving it on with LogMovement buries the discrete events you want to watch. Turn on LogMovement to follow events, and add LogTick only when you need the raw distance/error/speed stream.
The tick stream also writes to its own log category, LogMovementEvaluatorTick. To watch events while one enemy floods the console, mute the category instead of the CVar:
Log LogMovementEvaluatorTick off
Per-Instance (Blueprint/C++):
MovementEvaluator->bDebugDrawScoring = true;
MovementEvaluator->bDebugDrawAvoidance = true;
MovementEvaluator->bDebugDrawNav = true;
MovementEvaluator->bDebugLogMovement = true;
MovementEvaluator->bDebugLogTick = true;
MovementEvaluator->bDebugLogScoring = true;
MovementEvaluator->bDebugLogAvoidance = true;
MovementEvaluator->bDebugLogStrafeSwap = true;
MovementEvaluator->bDebugLogStrafeState = true;

Integration Points

SystemHow Movement Uses It
Threat DetectionSECCombatControllerComponent auto-wires delegates; threat response behavior (bSwapStrafeOnHighThreat, bAdjustDistanceByThreat) is configured per MovementBehaviorProfile and switches automatically with combat role
Combat RolesDifferent roles apply different MovementBehaviorProfiles
Action SystemActions can use Attack Approach to close distance before executing