Movement System
Documentation Unreal Engine AI Movement
Tactical AI positioning: direction sampling, distance keeping, strafing, navmesh awareness, and corner detours.
Tactical positioning that weighs distance, angle, other enemies, and the navmesh every tick, then moves the AI to its best spot.
The
MovementEvaluatorComponent samples directions around the AI, scores each one, and steers toward the winner. You tune the behavior with a profile per combat role, and the component handles the rest: strafing, keeping range, avoiding the pack, staying off ledges, and pathing around corners.It runs on its own. None of this needs the other plugin systems.
How it works
Direction sampling
Each tick the component samples
NumSamples directions (16 by default) around the AI and scores each one.Viewport
LVL_SEC_Showcase
AI Pawn
Target
Ideal Range
Green arrows = High Score (Preferred Direction)
A direction's score comes from how well it serves the target distance, whether it faces the target, whether another enemy sits in the way, whether it stays on walkable navmesh, and any positioning rules you added. The best direction wins, and the top few blend for a smooth result.
Distance and the comfort zone
The AI holds a band around its
DesiredDistance instead of a single exact range. DesiredDistance = 400 with DistanceTolerance = 0.2 gives a comfort band of 320 to 480 cm. Inside the band the AI strafes; too close, it retreats; too far, it approaches.Distance Comfort Zone
MovementBehaviorProfile
Comfort band = 320 to 480 cm (desired 400 ±20%). Inside it the AI strafes; too close it retreats, too far it approaches.
Strafing
The AI circles the target while holding distance. It swaps sides automatically when the other side scores better (
bAutoStrafeSwap), and it rests after strafing too long so the motion stays natural (bEnableStrafeRest, which fires OnStrafeResting). Under sustained player attention it can swap sides on threat, configured per profile (needs Threat Detection).Two movement layers
The component switches strategy by distance:
| Layer | When | How |
|---|---|---|
| Tactical | Target within HybridSwitchDistance (800) | Direct input for responsive strafing and positioning. Still reads the navmesh per direction. |
| Strategic | Target far or path-blocked | Navmesh pathfinding to route around obstacles. |
Rounding corners: detour escalation
When a target ducks behind a corner it sits close in a straight line but far along the navmesh, so plain strafing grinds into the wall. Detour escalation hands off to pathfinding to round the corner, then resumes strafing the instant the AI regains the angle. On by default, it only acts inside
HybridSwitchDistance, stays inert with no navmesh, and leaves the AI tactical when the target has no real route (walled off) rather than walking it into a dead end.AdvancedDetour tuning knobs
Detour escalates two ways:
- Far detour (predictive): 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): 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 once it rounds the corner and regains sight, or once pathfinding closes it back into range. Progress ends the detour, so a tight corner whose route ratio stays low still releases.
For a crowd, the component runs the route query at most once per
DetourRecheckInterval (0.5 s) and staggers it across the pack. bDetourUseLineOfSightPreGate (on) skips the query while the AI can see its target, so open-field fights cost one cheap trace. Turn the pre-gate off to add a query each interval and catch see-over-but-walk-blocking geometry like railings.Tuning: if it paths around minor bumps, raise
DetourEnterRatio or DetourMinExcess. If it grinds a tight corner, raise DetourStallSpeed. SEC.Debug.Movement.LogMovement 1 logs Detour escalation ENGAGED / CLEARED.Staying on the navmesh
In the tactical layer each sampled direction gets a soft walkability score, so enemies stop strafing off ledges and into walls mid-fight. It is a preference, not a veto, so a boxed-in AI still picks its safest direction instead of freezing, and a forward probe eases speed down at a rim. On by default, inert with no navmesh. It caches per navmesh poly, refreshes at 10 Hz, and staggers across enemies to stay in budget.
Hazard areas let enemies avoid a patch they can still walk on (lava, spikes):
- Create a Blueprint subclass of
NavArea(e.g.NavArea_Lava) with 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 component, add the area to Hazard Area Avoidance with a strength from 0 to 1:
0ignores it,1treats it like a wall,~0.75routes around it but crosses when cornered.
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 with no entry needed.AdvancedCover-break hook: detecting a walled-off target
The detour recheck already computes whether the AI can see its target and whether a full route exists. These server-only, Blueprint-pure getters on
MovementEvaluatorComponent expose it, so you can react when a target hides behind something solid:| Getter | Meaning |
|---|---|
IsApproachBlocked() | Sight blocked and no full route: the target is walled off. The cover-break gate. |
HasLineOfSightToTarget() | Clear view at the last detour recheck. Reports true when out of range or not rechecking. |
IsTargetReachable() | A full navmesh route exists. Only refreshed while sight is blocked, so read it through IsApproachBlocked, not alone. |
IsApproachStalled() | Tactical steering is grinding cover without gaining ground. Transient, tactical-range only. |
Delegates:
| Event | Fires |
|---|---|
OnTargetLineOfSightChanged | Sight flips, clear to blocked or back. Carries the new value. |
OnApproachBlocked | Rising edge only: once when the target becomes walled off. |
The signal refreshes on the detour cadence (
DetourRecheckInterval, 0.5 s), inside HybridSwitchDistance, while bEnableDetourEscalation is on.Cover-break pattern: bind
OnTargetLineOfSightChanged and start a timer when it reports false. On expiry, gate on IsApproachBlocked(); if still true, line-trace from the pawn to the player on your cover channel, and the first blocking hit is the obstacle to attack. Clear the timer when sight returns. ResetApproachBlockedState() clears the signal by hand; it also runs on target loss, detour disable, and owner death.AlternativeRoot-motion output (evaluate-only mode)
To drive locomotion with root motion instead of direct movement input, turn off Apply Movement Input. The component keeps scoring and publishing a direction each tick without driving the pawn. You forward that direction into your Anim Blueprint, which blends the locomotion animations, and their root motion moves the character. No C++ required.
Each tick, read one of these Blueprint-callable getters and feed it to your locomotion blendspace:
- Get Current Direction Angle: signed yaw in degrees (0 ahead, +90 right, -90 left, +/-180 behind). Good for a 1D turn/strafe blendspace.
- Get Current Direction Local: a 2D vector (X = forward, Y = right) for a standard 2D locomotion blendspace.
- Get Current Direction: the world-space direction, if you need it raw.
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.
Setup
Role-based (recommended)
The automated path: the combat role picks the profile.
- In your EnemyAIConfig, assign a
MovementBehaviorProfileto each combat role (e.g. Attacker gets an aggressive profile, Waiter gets a patient one). SECCombatControllerComponent::SyncStateForCombatRolecallsMovementEvaluatorComponent::SyncForCombatRolewhen the role changes, applying the matching profile.
No wiring needed on
EnemyControllerBase. See Combat Roles.Data asset profile
Create a
MovementBehaviorProfile (Right-click → Miscellaneous → Data Asset → MovementBehaviorProfile). A profile holds only the role-swappable fields:Distance
Desired Distance
400
Distance Tolerance
0.2
Rest Behavior
Enable Strafe Rest
Strafe Rest Time Limit
10
Strafe Rest Duration
1.5
Threat Response
Swap Strafe On High Threat
Adjust Distance By Threat
Threat Distance Scale
1
Positioning Rules
Positioning Rules
0 Array elements
Desired Distance is the ideal range in cm, Distance Tolerance is the comfort band as a fraction of it, the Rest Behavior fields govern strafe fatigue, and the Threat Response fields feed off Threat Detection. Positioning Rules are covered below. Everything else lives on the component and is the same for every role (next section).Adjust at runtime. These are Blueprint-callable nodes on the component: Apply Behavior Profile swaps the whole profile, Set Desired Distance changes range, Set Strafe Side and Swap Strafe Side force a direction, and Set Distance Multiplier scales range (threat detection drives this one).
C++Preset configs
FMovementBehaviorConfig factory methods fill the profile-level fields for common archetypes, applied with ApplyBehaviorConfig:MovementEvaluator->ApplyBehaviorConfig(FMovementBehaviorConfig::MakeAttacker());MakeDefault(): balanced (400 cm).MakeAttacker(): close, aggressive, minimal rest (300 cm).MakeWaiter(): far, patient, frequent rest (600 cm).MakeFlanker(): medium, quick repositioning pauses (400 cm).MakeSupporter(): medium-far, moderate rest (500 cm).MakeElite(): relentless pressure, very short rest (350 cm).
The
Make* helpers are C++ only. In Blueprint, use a MovementBehaviorProfile data asset instead, or build a MovementBehaviorConfig struct by hand and pass it to Apply Behavior Config.Tuning the component
Beyond the profile, the component exposes the full tuning surface: strafe feel, avoidance, navmesh sampling, hybrid switching, blending, and stuck detection. Set these on the component defaults; they apply to every role.
Movement Evaluator
Apply Movement Input
Num Samples
16
Distance Tolerance
0.2
Direction Smoothness
8
Direction Change Penalty
1
Speed Dead Zone
0.1
Target Position Smoothness
5
Use Pivot Smoothing
Strafe
Auto Strafe Swap
Strafe Swap Threshold
3
Strafe Swap Cooldown
2
Strafe Speed Penalty
0.8
Strafe Penalty Threshold
0.6
Strafe Preference Weight
1
Close Range Retreat Bias
0.5
Strafe Time Limits
Enable Strafe Time Limit
Strafe Time Limit
5
Strafe Time Limit Fluctuation
2.5
Rest After Strafe Swap
Strafe Swap Rest Time
0.5
Strafe Swap Rest Fluctuation
0.3
Strafe Rest
Enable Strafe Rest
Strafe Rest Time Limit
10
Strafe Rest Time Limit Fluctuation
4
Strafe Rest Duration
1.5
Strafe Rest Duration Fluctuation
0.5
Avoidance
Enable Avoidance
Avoidance Radius
100
Avoidance Weight
0.4
Avoidance Slip Bonus
0.8
Velocity Aware Avoidance
Momentum Bias
0.7
Nav Sampling
Enable Nav Aware Sampling
Nav Probe Distance
120
Nav Score Weight
1
Nav Blocked Floor
0.1
Guard Ledge Drops
Nav Edge Speed Floor
0.1
Stuck Detection
Enable Stuck Detection
Stuck Velocity Threshold
50
Stuck Time Threshold
0.5
Stuck Check Interval
0.2
Direction Blending
Direction Blend Count
3
Direction Blend Min Score Ratio
0.7
Hybrid Movement
Enable Hybrid Movement
Hybrid Switch Distance
800
Hybrid Switch Hysteresis
100
Detour
Enable Detour Escalation
Detour Enter Ratio
1.6
Detour Min Excess
200
Detour Recheck Interval
0.5
Detour Use Line Of Sight Pre Gate
Detour Line Of Sight Channel
Visibility
Detour Escalate On Stall
Detour Stall Speed
60
See the Configuration Reference for the profile data asset.
Positioning rules
Positioning rules nudge direction scores to shape where the AI wants to be. Add them to a profile's Positioning Rules array; each has a
Weight (0 to 1) and they stack additively.The built-in
UAnglePreferenceRule pushes the AI toward or away from an angle relative to the target's facing:PreferredAngle = 0: frontal.PreferredAngle = 90: flanking.PreferredAngle = 180: behind the target.bAvoidInstead = true: invert it to avoid that angle.AngleTolerance(30) is the green zone where the rule stops nudging;DirectionCommitment(45) stops the AI flip-flopping between orbit directions.
AdvancedWrite a custom rule
UPositioningRule is Blueprintable, so you can author a rule in Blueprint or C++.Blueprint: create a Blueprint class deriving from
PositioningRule, override Evaluate Direction, and return -1 (discourage) to +1 (encourage). The context gives you the sample direction, the controller, and the desired distance; use the controller's Get Pawn for the AI and Get Focus Actor for the target and its facing. Add an instance to the profile's Positioning Rules array.C++: subclass and override
EvaluateDirection_Implementation:UCLASS(Blueprintable)
class UMyFlankingRule : public UPositioningRule
{
GENERATED_BODY()
virtual float EvaluateDirection_Implementation(
const FDirectionEvaluationContext& Context) const override
{
// Context.SampleDirection, Context.Controller, Context.DesiredDistance
// Return -1.0 to +1.0
}
};Behavior Tree tasks
For movement outside the main evaluation loop:
| Task | Purpose |
|---|---|
| Move Until Distance | Pathfind toward a blackboard target until within a set distance. Supports timeout, max chase distance, and debug draw. |
| Make Distance | Retreat from a target with direct input until reaching a distance. Includes stuck detection and timeout. |
| Debug SEC Values | Logs the SEC_ blackboard values (target, distance, action ID) to log and screen. |
Debug
Console commands toggle drawing and logging at runtime:
SEC.Debug.Movement.DrawScoring 1 // Direction scores
SEC.Debug.Movement.DrawAvoidance 1 // Avoidance radii
SEC.Debug.Movement.DrawNav 1 // Navmesh validity and the ledge guard
SEC.Debug.Movement.LogMovement 1 // Discrete events (layer, suspend, pathfinding, stuck, detour)
SEC.Debug.Movement.LogTick 1 // Per-tick state dump (distance, error, speed)
SEC.Debug.Movement.LogScoring 1 // Direction scoring detail
SEC.Debug.Movement.LogAvoidance 1 // Avoidance detail
SEC.Debug.Movement.LogStrafeSwap 1 // Strafe swap decisions
SEC.Debug.Movement.LogStrafeState 1 // Strafe state changes
LogTick is separate on purpose: it fires every tick for every enemy, so it buries the discrete events. Turn on LogMovement to follow events, and add LogTick only when you need the raw stream. The tick stream writes to its own log category, so you can mute it while one enemy floods the console:Log LogMovementEvaluatorTick off
AdvancedDebug one enemy from the Details panel
The console commands are global. To isolate a single enemy, each one carries a matching checkbox on its
MovementEvaluatorComponent, under Movement Evaluator → Debug in the Details panel: bDebugDrawScoring, bDebugDrawAvoidance, bDebugDrawNav, bDebugLogMovement, bDebugLogTick, bDebugLogScoring, bDebugLogAvoidance, bDebugLogStrafeSwap, and bDebugLogStrafeState. They are Blueprint-readable and writable too, for toggling at runtime.AdvancedAll movement events
The component broadcasts these delegates, bindable in Blueprint or C++:
| Event | When |
|---|---|
OnStrafeStarted | Strafing begins (carries the side) |
OnStrafeEnded | Strafing stops |
OnStrafeSwapped | Direction changes (carries the new side) |
OnStrafeResting | Strafe fatigue rest begins |
OnMovementSuspended | Movement suspended (carries duration) |
OnMovementResumed | Suspension ends |
OnArrivedAtDesiredDistance | AI enters the comfort zone |
OnDepartedDesiredDistance | AI leaves the comfort zone |
OnMovementLayerChanged | Layer switches (carries new and previous) |
OnSwitchToPathfinding | Switched to Strategic |
OnSwitchToDirectInput | Switched to Tactical |
OnPathfindingFailed | Pathfinding blocked or unreachable |
OnAttackApproachComplete | Attack approach reached its distance |
OnAttackApproachFailed | Attack approach failed |
OnTargetLineOfSightChanged | Detour-recheck sight flips (carries the new value) |
OnApproachBlocked | Target becomes walled off (rising edge) |
Integration
| System | How movement uses it |
|---|---|
| Threat Detection | SECCombatControllerComponent auto-wires it. Threat response (bSwapStrafeOnHighThreat, bAdjustDistanceByThreat) is set per profile and switches with role. |
| Combat Roles | Each role applies a different MovementBehaviorProfile. |
| Action System | Actions can request an attack approach to close distance before executing. |