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:
| Layer | When | How |
|---|---|---|
| Tactical | Target is within HybridSwitchDistance | Direct movement input for responsive strafing and positioning. Still reads the navmesh per direction for walkability, ledges, and hazards (see Navmesh-Aware Sampling) |
| Strategic | Target is far away or path-blocked | NavMesh 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-lineexceedsDetourEnterRatio(1.6) androute - straight-lineexceedsDetourMinExcess(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, raiseDetourEnterRatioorDetourMinExcess; if it still grinds a tight corner, raiseDetourStallSpeedso a slower grind counts as stalled; if it pathfinds during a normal slow approach, lowerDetourStallSpeed.SEC.Debug.Movement.LogMovement 1logsDetour 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.
Navmesh-Aware Sampling
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: lowerNavProbeDistance(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):
- Create a Blueprint subclass of
NavArea(e.g.NavArea_Lava) and give it a distinct draw color. - 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).
- On the movement component, add the area to Hazard Areas with an avoidance strength from 0 to 1:
0ignores it,1treats it like a wall,~0.75routes 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 asNavArea_Nullcarves 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
- Create a
MovementBehaviorProfiledata asset (Right-click, Miscellaneous, Data Asset, MovementBehaviorProfile) - Configure desired distance, rest times, threat response, and positioning rules
- Apply it:
MovementEvaluator->ApplyBehaviorProfile(MyProfile);Option 3: Configure at Runtime
MovementEvaluator->SetDesiredDistance(350.f);
MovementEvaluator->SetStrafeSide(1); // 1 = right, -1 = leftOption 4: Role-Based (Recommended)
The most automated path: combat role picks the profile.
- Configure your EnemyAIConfig data asset.
- Assign profiles to roles (e.g., Attacker gets Aggressive, Waiter gets Passive).
- The
SECCombatControllerComponentsyncs automatically when the role changes.
// Called by SECCombatControllerComponent automatically when role changes:
MovementEvaluator->SyncForCombatRole(RoleTag, MyAIConfig);Key Properties
| Property | Default | Purpose |
|---|---|---|
NumSamples | 16 | Directions to evaluate per tick |
DesiredDistance | 400 | Ideal distance to maintain (cm) |
DistanceTolerance | 0.2 | Comfort zone as percentage of desired distance |
bEnableAvoidance | true | Avoid other AI pawns |
AvoidanceRadius | 100 | How far to stay from other pawns (cm) |
bEnableHybridMovement | true | Switch between pathfinding and direct input |
HybridSwitchDistance | 800 | Distance to switch between Tactical and Strategic |
bEnableDetourEscalation | true | Pathfind around a corner or wall the AI can't reach the target through by steering |
DetourEnterRatio | 1.6 | Route-to-straight ratio that engages the far-detour (predictive) escalation |
DetourMinExcess | 200 | Extra walk over the straight line required alongside the ratio; ignores thin obstacles (cm) |
bDetourEscalateOnStall | true | Also escalate at a tight corner where the AI grinds the wall without gaining ground |
DetourStallSpeed | 60 | Closing speed below which a blocked approach counts as stalled, not progressing (cm/s) |
DetourRecheckInterval | 0.5 | How often the route is re-measured (seconds) |
bDetourUseLineOfSightPreGate | true | Skip the route query while the AI can see its target |
bAutoStrafeSwap | true | Automatically swap strafe direction |
StrafeSwapThreshold | 3.0 | Score ratio required to trigger auto-swap |
StrafeTimeLimit | 5.0 | Base seconds before forcing a strafe direction swap |
bEnableStrafeRest | true | Enable fatigue rest after continuous strafing |
StrafeRestTimeLimit | 10.0 | Seconds of strafing before forced rest |
StrafeRestDuration | 1.5 | Duration of forced rest (seconds) |
DirectionBlendCount | 3 | Top-scoring directions to blend (1 = pick best, 2-3 = smooth) |
bEnableStuckDetection | true | Detect when AI is stuck and cannot make progress |
bApplyMovementInput | true | Drive the pawn with direct input. Set false for Evaluate-Only (root motion) |
bEnableNavAwareSampling | true | Gate sample directions by the navmesh (ledges, walls, hazards) |
NavProbeDistance | 120 | How far ahead each direction is checked against the navmesh (cm) |
NavScoreWeight | 1.0 | Strength of the navmesh term (0 = off) |
NavBlockedFloor | 0.05 | Residual score for blocked directions, so a boxed-in AI never dead-stops |
bGuardLedgeDrops | true | Ease 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 assaultPreferredAngle = 90- flanking (side attacks)PreferredAngle = 180- backstab positioningbAvoidInstead = true- invert the rule to avoid the angle instead
Events
The component broadcasts delegates you can bind to in Blueprint or C++:
| Event | When |
|---|---|
OnStrafeStarted | AI begins strafing (includes strafe side) |
OnStrafeEnded | AI stops strafing |
OnStrafeSwapped | Direction changes (includes new side) |
OnStrafeResting | Strafe fatigue rest begins |
OnMovementSuspended | Movement suspended (includes duration) |
OnMovementResumed | Suspension ends |
OnArrivedAtDesiredDistance | AI enters the comfort zone |
OnDepartedDesiredDistance | AI leaves the comfort zone |
OnMovementLayerChanged | Layer switches (Tactical, Strategic) |
OnSwitchToPathfinding | Switched to Strategic layer |
OnSwitchToDirectInput | Switched to Tactical layer |
OnPathfindingFailed | Pathfinding blocked or unreachable |
Behavior Tree Tasks
BT tasks for movement outside the main evaluation loop:
| Task | Purpose |
|---|---|
| Move Until Distance | Pathfind toward a blackboard target until within a specified distance. Supports timeout, max chase distance, and debug visualization. |
| Make Distance | Retreat from a target using direct movement input until reaching a target distance. Includes stuck detection and timeout. |
| Debug SEC Values | Logs 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
| System | How Movement Uses It |
|---|---|
| Threat Detection | SECCombatControllerComponent auto-wires delegates; threat response behavior (bSwapStrafeOnHighThreat, bAdjustDistanceByThreat) is configured per MovementBehaviorProfile and switches automatically with combat role |
| Combat Roles | Different roles apply different MovementBehaviorProfiles |
| Action System | Actions can use Attack Approach to close distance before executing |