# Movement System

> Give an enemy tactical positioning: create a movement profile, set the range it holds, point the AI Config at it.

Markdown version of https://rigbak.com/docs/soulslike-combat/movement-system, a page of the Soulslike Enemy Combat documentation. Link that page when citing it.

> An enemy that circles you, holds its range, backs off when crowded, and paths around corners. You author it as one data asset.

## Set it up (30 seconds)

1. **Create a Movement Behavior Profile**

   Right-click in the Content Browser, **SEC**, **AI**, **Movement Behavior Profile**.

   Create the asset: right-click in the Content Browser, choose **SEC > AI > Movement Behavior Profile**, and name it **DA_Move_Attacker**.

   *Right-click in the Content Browser, SEC, AI, Movement Behavior Profile.*
2. **Set the range it holds**

   Double-click the asset. Set **Desired Distance** to the range this enemy fights at, in centimeters. That one number is the whole first pass.

   In **DA_Move_Attacker**, under Distance, set:

   | Property | Value |
   | --- | --- |
   | Desired Distance | 250.0 (replaces 400.0) |
   | Distance Tolerance | 0.2 |
   | Crowd Tolerance | 0.45 |

   *Desired Distance is the range. The two tolerances widen the band around it.*
3. **Point the AI Config at it**

   Open the enemy's **Enemy AI Config**, and under **Movement Profiles** set **Default Movement Profile** to the asset. Leave **Manage Movement Profiles Automatically** ticked.

   In **EnemyConfig_Grunt**, under Movement Profiles, set:

   | Property | Value |
   | --- | --- |
   | Manage Movement Profiles Automatically | true |
   | Default Movement Profile | DA_Move_Attacker |

   *Default Movement Profile covers every role, so one entry is enough to start.*
4. **Play, and watch it work**

   Press Play and run `SEC.Debug.Movement 1` in the console. Each enemy gets a line above its head showing its movement layer, whether it is strafing and which way, and its heading.

> **Warning**
>
> An enemy only moves once it has something to fight. Movement reads the controller's focus actor, and nothing sets that until a combat target is registered. If the enemy stands still with everything above filled in, that is what is missing. See [Getting Started](https://rigbak.com/docs/soulslike-combat/getting-started) and [Targeting](https://rigbak.com/docs/soulslike-combat/targeting-system).

---

## Tune the range

An enemy holds a band around **Desired Distance** rather than one exact range, and the band is wider on the crowded side than the far side.

**Distance Comfort Zone** (MovementBehaviorProfile)

AI distance comfort zone, drawn as the plugin's own editor draws it

Settles here Eases back into place Too close or too far The enemy

*Comfort band = 2.2m to 4.8m, around a desired 4.0m with Crowd Tolerance 45% on the near side and Distance Tolerance 20% on the far side. Same rings, bands and colors the Movement Behavior Profile editor draws for these three fields, spaced out a little further here so the labels stay readable at this size.*

| Field | Default | What to do with it |
| --- | --- | --- |
| **Desired Distance** | 400 cm | The range this enemy fights at. 250 for a brawler, 600 for something that hangs back. |
| **Distance Tolerance** | 0.2 | The far half of the band, as a share of Desired Distance. Raise it and the enemy stops closing sooner. |
| **Crowd Tolerance** | 0.45 | The near half. Raise it for an enemy that stands its ground when you walk into it, lower it for one that insists on its space. |

Inside the band the enemy circles. Closer than the near edge it gives ground. Past the far edge it closes in. Set the two tolerances equal for a band the same width on both sides.

> **Warning**
>
> Keep **Desired Distance** under 700 cm. The enemy hands over to the pathfinding layer past 800 cm and takes direct input back only inside 700, so a range set between the two leaves it in the pathfinding layer, where it walks toward the target and never circles. **Distance Tolerance** stretches the far side of the band, so leave room for that too.

### Watch a live enemy while you tune

The profile editor has a **Watching** picker. Start Play In Editor, pick a running enemy, and the diagram draws that enemy's real distance and heading against the band you are editing. Change a value and the enemy answers on the next tick.

---

## The rest of the profile

**Category: Distance**

| Property | Value |
| --- | --- |
| Desired Distance | 400 |
| Distance Tolerance | 0.2 |
| Crowd Tolerance | 0.45 |

**Category: Rest Behavior**

| Property | Value |
| --- | --- |
| Enable Strafe Rest | true |
| Strafe Rest Time Limit | 10 |
| Strafe Rest Duration | 1.5 |

**Category: Threat Response**

| Property | Value |
| --- | --- |
| Swap Strafe on High Threat | false |
| Adjust Distance by Threat | false |
| Threat Distance Scale | 1 (greyed out) |

**Category: Strafe**

| Property | Value |
| --- | --- |
| Enable Strafing | true |
| Strafe Preference Weight | 1 |
| Auto Strafe Swap | true |
| Strafe Swap Threshold | 3 |
| Strafe Swap Cooldown | 2 |

**Category: Strafe Time Limits**

| Property | Value |
| --- | --- |
| Enable Strafe Time Limit | true |
| Strafe Time Limit | 5 |
| Strafe Time Limit Fluctuation | 2.5 |
| Rest After Strafe Swap | false |
| Strafe Swap Rest Time | 0.5 (greyed out) |
| Strafe Swap Rest Fluctuation | 0.25 (greyed out) |

**Category: Positioning Rules**

| Property | Value |
| --- | --- |
| Positioning Rules | 0 Array elements |

**Rest Behavior** stops an enemy circling forever: after **Strafe Rest Time Limit** seconds of continuous strafing it pauses for **Strafe Rest Duration**. Both are jittered per enemy so a pack does not rest in lockstep.

**Threat Response** needs a [Threat Detection](https://rigbak.com/docs/soulslike-combat/threat-detection) component on the controller. Without one, both switches sit inert.

Untick **Enable Strafing** and this role stops circling entirely: the enemy still approaches, retreats, and steps around other enemies, every other **Strafe** and **Strafe Time Limits** field goes inert, and the side it was circling on is remembered for whenever strafing comes back on. With it ticked, **Strafe Preference Weight** balances circling against closing or opening distance: 1.0 is balanced, below 1.0 favors approach and retreat, above 1.0 favors circling. Tick **Auto Strafe Swap** and the enemy switches sides on its own once the far side scores enough better, by the ratio in **Strafe Swap Threshold**, no sooner than **Strafe Swap Cooldown** allows.

**Strafe Time Limits** forces a side swap after too long on one side, separately from any score-driven swap. With **Enable Strafe Time Limit** ticked, a swap fires after roughly **Strafe Time Limit** seconds, varied by **Strafe Time Limit Fluctuation** so a pack does not swap in lockstep. Tick **Rest After Strafe Swap** to pause briefly right after one of these forced swaps, for **Strafe Swap Rest Time** seconds, varied by **Strafe Swap Rest Fluctuation**.

**Positioning Rules** shape where the enemy wants to stand. Add an **Angle Preference Rule** and set **Preferred Angle** to 0 for frontal, 90 for flanking, 180 for behind the target. Tick **Avoid Instead** to invert it. Rules stack, each with its own **Weight**.

> **Note**
>
> A profile carries the fields above and nothing else. Sample count, avoidance, navmesh sampling, strafe speed, detour, and stuck detection all live on the **Movement Evaluator** component, so they are the same for every role on that controller.

### One profile per combat role

Add entries to **Role Movement Profiles** to give each [combat role](https://rigbak.com/docs/soulslike-combat/combat-roles) its own profile: an aggressive one for Attacker, a patient one for Waiter. Any role you do not list falls back to **Default Movement Profile**, and the swap happens on its own when the role changes.

> **Warning**
>
> Leave no entry with an empty **Movement Profile**. An entry that names a role but no asset applies nothing, so the enemy keeps whatever the previous role gave it.

---

## Tune the component

Everything that is the same for every role sits on the **Movement Evaluator** component, on the AI controller.

**Category: Movement Evaluator**

| Property | Value |
| --- | --- |
| Apply Movement Input | true |
| Num Samples | 16 |
| Distance Tolerance | 0.2 |
| Crowd Tolerance | 0.45 |
| Direction Smoothness | 8 |
| Direction Change Penalty | 1 |
| Speed Dead Zone | 0.1 |
| Target Position Smoothness | 5 |
| Use Pivot Smoothing | true |

**Category: Movement Evaluator > Strafe**

| Property | Value |
| --- | --- |
| Auto Strafe Swap | true |
| Strafe Swap Threshold | 3 |
| Strafe Swap Cooldown | 2 |
| Strafe Speed Penalty | 0.75 |
| Strafe Free Angle | 15 |
| Strafe Full Penalty Angle | 40 |
| Strafe Preference Weight | 1 |

**Category: Movement Evaluator > Retreat**

| Property | Value |
| --- | --- |
| Close Range Retreat Bias | 0.5 |
| Retreat Speed Scale | 0.45 |
| Yield Retreat to Closing Target | true |
| Retreat Yield Strength | 1 |

**Category: Movement Evaluator > Strafe Time Limits**

| Property | Value |
| --- | --- |
| Enable Strafe Time Limit | true |
| Strafe Time Limit | 5 |
| Strafe Time Limit Fluctuation | 2.5 |
| Rest After Strafe Swap | false |
| Strafe Swap Rest Time | 0.5 (greyed out) |
| Strafe Swap Rest Fluctuation | 0.25 (greyed out) |

**Category: Movement Evaluator > Strafe Rest**

| Property | Value |
| --- | --- |
| Enable Strafe Rest | true |
| Strafe Rest Time Limit | 10 |
| Strafe Rest Time Limit Fluctuation | 4 |
| Strafe Rest Duration | 1.5 |
| Strafe Rest Duration Fluctuation | 0.5 |

**Category: Movement Evaluator > Avoidance (collapsed until clicked)**

| Property | Value |
| --- | --- |
| Enable Avoidance | true |
| Avoidance Radius | 100 |
| Avoidance Weight | 0.4 |
| Avoidance Slip Bonus | 0.75 |
| Velocity Aware Avoidance | true |
| Momentum Bias | 0.7 |

**Category: Movement Evaluator > Nav Sampling (collapsed until clicked)**

| Property | Value |
| --- | --- |
| Enable Nav Aware Sampling | true |
| Nav Probe Distance | 120 |
| Nav Score Weight | 1 |
| Nav Blocked Floor | 0.05 |
| Guard Ledge Drops | true |
| Nav Edge Speed Floor | 0.15 |

**Category: Movement Evaluator > Stuck Detection (collapsed until clicked)**

| Property | Value |
| --- | --- |
| Enable Stuck Detection | true |
| Stuck Velocity Threshold | 50 |
| Stuck Time Threshold | 0.5 |
| Stuck Check Interval | 0.2 |

**Category: Movement Evaluator > Direction Blending (collapsed until clicked)**

| Property | Value |
| --- | --- |
| Direction Blend Count | 3 |
| Direction Blend Min Score Ratio | 0.7 |

**Category: Movement Evaluator > Hybrid Movement**

| Property | Value |
| --- | --- |
| Enable Hybrid Movement | true |
| Hybrid Switch Distance | 800 |
| Hybrid Switch Hysteresis | 100 |

**Category: Movement Evaluator > Hybrid Movement > Detour**

| Property | Value |
| --- | --- |
| Enable Detour Escalation | true |
| Detour Enter Ratio | 1.6 |
| Detour Min Excess | 200 |
| Detour Recheck Interval | 0.5 |
| Detour Use Line Of Sight Pre Gate | true |
| Detour Line Of Sight Channel | Visibility |
| Detour Escalate on Stall | true |
| Detour Stall Speed | 60 |
| Detour Release Ratio | 1.35 |
| Detour Release Min Excess | 100 |
| Detour Facing Mode | Use Default Facing |
| Detour Turn Rate | 0 |
| Strategic Facing Mode | Use Default Facing |
| Strategic Turn Rate | 0 |

---

## Debug it

```
SEC.Debug.Movement 1                  // Layer, strafe side and heading above each enemy
SEC.Debug.Movement.DrawScoring 1      // The 16 sampled directions, red low to green high
SEC.Debug.Movement.DrawAvoidance 1    // Avoidance radii and the pawns being avoided
SEC.Debug.Movement.DrawNav 1          // Navmesh validity and the ledge guard
SEC.Debug.Movement.LogMovement 1      // Layer switches, suspends, pathfinding, stuck, detour
```

`SEC.Debug.All 1` turns every overlay on at once, and `SEC.Debug.WatchPawn <name>` narrows them to one enemy.

Five more log switches exist for narrower work: `LogTick`, `LogScoring`, `LogAvoidance`, `LogStrafeSwap`, and `LogStrafeState`. `LogTick` fires every tick for every enemy and buries the rest, so turn it on last.

<details>
<summary>Debug one enemy from the Details panel (Advanced)</summary>

Every console switch above has a matching checkbox on that enemy's **Movement Evaluator** component, under **Movement Evaluator > Debug > Draw** and **Movement Evaluator > Debug > Log**. Ticking one there affects that enemy alone. They are readable and writable from Blueprint, so a debug menu can drive them at runtime.

The console switch and the checkbox add up: an enemy writes the per-tick stream while either one is on.

```
SEC.Debug.Movement.LogTick 1      // Per-tick stream on for every enemy
SEC.Debug.Movement.LogTick 0      // Undoes the line above and nothing more
```

When the stream floods the console after `LogTick 0`, an enemy has **Debug Log Tick** ticked. Untick it on that enemy.

</details>

<details>
<summary>It moves, but it never circles (Advanced)</summary>

Four causes, in the order worth checking:

- **Desired Distance is 700 or higher.** Circling only exists in the tactical layer. The component hands over to pathfinding past 800 cm and takes direct input back only inside 700, so a range between the two stays in pathfinding.
- **It is resting.** Strafe rest is on by default: roughly 10 seconds of circling, then a pause of about 1.5 seconds.
- **It is crowded.** Being pushed inside the near edge of the band favors giving ground over circling, scaled by **Close Range Retreat Bias** on the component.
- **It is still traveling.** While the distance error is large the enemy runs in or backs off, and circling only takes over once it reaches the band.

An enemy that stands still inside the band is doing that on purpose. With no neighbors to avoid, no positioning rules, and nothing to gain from moving, it holds its spot.

</details>

<details>
<summary>It does not move at all (Advanced)</summary>

- **No focus actor.** Nothing registered a combat target, so there is nothing to move relative to. This is the common one and it logs nothing.
- **The Movement Evaluator is on the pawn.** It belongs on the AI controller. On a pawn it warns once at BeginPlay and then does nothing.
- **A plain AAIController.** It needs a **SEC Combat Controller** component to resolve the config at possession, and a **SEC Brain** component to tick movement. `AEnemyControllerBase` ships with both.
- **No navmesh.** Past 800 cm the enemy asks for a path, the request fails, and the enemy does not close the gap.
- **A leftover `SEC.State.IsMovementBlocked` tag** on the pawn's ability system component freezes it outright.
- **Apply Movement Input is off** on the component, which is evaluate-only mode.

</details>

---

## How it steers

<details>
<summary>Direction sampling (Advanced)</summary>

Each tick the component samples **Num Samples** directions (16 by default) around the enemy and scores each one.

*Interactive diagram on the page: 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 on the profile. The best direction wins, and the top few blend for a smooth result.

</details>

<details>
<summary>Two layers, and rounding corners (Advanced)</summary>

The component switches strategy by distance. Inside **Hybrid Switch Distance** (800 cm) it drives the pawn directly, which is what makes strafing responsive. Beyond it, or when a path is blocked, it hands over to navmesh pathfinding, and it takes direct input back once the pawn is **Hybrid Switch Hysteresis** (100 cm) inside that distance.

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 notices and hands off to pathfinding to round the corner, then resumes strafing the instant the enemy regains the angle. It is on by default, acts only inside 800 cm, and stays inert with no navmesh.

Detour engages when the walking route runs much longer than the straight line, or when the enemy wants to close on a target it cannot see and is not gaining ground. It releases once the walk straightens out. A target walled off with no route at all leaves the enemy tactical rather than walking it into a dead end. Only the world hides a target from that check: other characters, and anything they hold, are seen through, and the target counts as seen when the middle, top or bottom of its body is in clear view.

Tuning lives on the component under **Detour**. Raise **Detour Enter Ratio** or **Detour Min Excess** if minor bumps trigger pathfinding, and raise **Detour Stall Speed** (60 cm/s) if a slow corner grind does not register. `SEC.Debug.Movement.LogMovement 1` prints each recheck.

A target that jumps onto a ledge off the navmesh leaves the long-range chase with no route. The enemy holds its place and asks for a route a few times a second, then sets off by itself once the target is back on walkable ground. While it waits, **Is Target Unreachable** on the component answers true and the movement result stays **Strategic**; **On Target Unreachable** fires once when the route goes. A chase you stop yourself, with **Stop Movement** on the controller for one, stays stopped.

</details>

<details>
<summary>Giving ground under pressure (Advanced)</summary>

An enemy pushed inside its near edge backs away at a fraction of its walk speed, set by **Retreat Speed Scale** (0.45) on the component.

With **Yield Retreat to Closing Target** on, an enemy whose target closes faster than it can retreat holds its footing and circles instead, so a player charging at a run does not walk it backward into a wall. The check weighs the target's speed against this enemy's retreat speed, so it needs no per-enemy tuning.

**Close Range Retreat Bias** (0.5) is how hard crowding favors giving ground over circling. Raise it toward 1 for a harder push out, drop it to 0 to leave the enemy circling at any range.

</details>

<details>
<summary>Staying off ledges, and avoiding hazards (Advanced)</summary>

Each sampled direction gets a walkability score, so enemies stop strafing off ledges and into walls mid-fight. It is a preference rather than a veto, so a boxed-in enemy still picks its safest direction instead of freezing. On by default, inert with no navmesh.

To make enemies avoid a patch they can still walk on (lava, spikes):

1. Create a Blueprint subclass of `NavArea` with a distinct draw color.
2. Drop a **Nav Modifier Volume** over the patch, set its **Area Class** to that area, and rebuild navigation. Press **P** to check it tints.
3. On the Movement Evaluator, add the area to **Hazard Area Avoidance** with a strength from 0 to 1. `0` ignores it, `1` treats it like a wall, `0.75` routes around it but crosses when cornered.

Raise the area's **Default Cost** as well, so long-range pathfinding avoids it too. A patch authored as `NavArea_Null` carves a hole in the navmesh, which the enemy treats as a wall with no setup at all.

</details>

<details>
<summary>Which way the pawn looks while it travels (Advanced)</summary>

By default an enemy keeps looking at its target however it moves, which reads well up close and looks wrong on a long walk or around a blind corner. Each pathfinding layer can face its direction of travel instead, through **Strategic Facing Mode** and **Detour Facing Mode** on the component. **Strategic Turn Rate** and **Detour Turn Rate** set how fast it turns, in degrees per second, and 0 keeps the controller's own turn speed.

Turning the pawn away from its target changes what every angle-based system sees, because they all read the pawn's real facing: reaction angle gates, action angle scoring, and the awareness sight cone. An enemy rounding cover with **Face Movement Direction** can lose sight of the player mid-detour, which is usually the point.

</details>

<details>
<summary>Strafe animations at full speed (Advanced)</summary>

Strafe animations read wrong at a full run, so the pawn slows as its heading turns away from the direction it faces. **Strafe Speed Penalty** (0.75) sets the cost, **Strafe Free Angle** (15 degrees) is the band around straight ahead that costs nothing, and **Strafe Full Penalty Angle** (40 degrees) is where the cost tops out.

Raise **Strafe Free Angle** if an enemy closing distance looks like it is jogging, since direction scoring angles the approach a few degrees while holding a strafe side. Set it to 0 to charge the penalty at any angle off forward. It applies to the tactical layer only.

</details>

<details>
<summary>Driving locomotion with root motion instead (Alternative)</summary>

To move the character with root motion rather than movement input, untick **Apply Movement Input**. The component keeps scoring and publishing a direction every tick without driving the pawn. Feed that direction into your Anim Blueprint, and the locomotion animations move the character.

Read one of these each tick and feed your blendspace:

- **Get Current Direction Angle**: signed yaw in degrees, 0 ahead, +90 right, -90 left. Good for a 1D turn or strafe blendspace.
- **Get Current Direction Local**: a 2D vector, X forward and Y right, for a standard 2D locomotion blendspace.
- **Get Current Direction**: the world-space direction, raw.

In this mode the component stays tactical-only and issues no path moves.

</details>

<details>
<summary>Reacting when a target hides behind something solid (Advanced)</summary>

The detour recheck already works out whether the enemy can see its target and whether a route exists, so a cover-break behavior can read it off the Movement Evaluator:

| Getter | Answers |
| --- | --- |
| **Is Approach Blocked** | Sight blocked and no route: the target is walled off. This is the cover-break trigger. |
| **Has Line Of Sight to Target** | Clear view at the last recheck. |
| **Is Target Reachable** | A full navmesh route existed at the last route measurement. Read it through **Is Approach Blocked** rather than alone. |
| **Is Approach Stalled** | Tactical steering is grinding cover without gaining ground. |

**On Target Line Of Sight Changed** fires whenever sight flips, and **On Approach Blocked** fires once when the target becomes walled off.

The pattern: bind **On Target Line Of Sight Changed**, start a timer when it reports false, and on expiry check **Is Approach Blocked**. If it still holds, line-trace from the pawn to the player and the first blocking hit is the obstacle to attack. Clear the timer when sight returns.

</details>

<details>
<summary>Every movement event (Advanced)</summary>

| Event | Fires when |
| --- | --- |
| `OnStrafeStarted` | Strafing begins, carrying the side |
| `OnStrafeEnded` | Strafing stops |
| `OnStrafeSwapped` | Direction changes, carrying the new side |
| `OnStrafeResting` | Strafe fatigue rest begins |
| `OnMovementSuspended` | Movement suspended, carrying the duration |
| `OnMovementResumed` | Suspension ends |
| `OnArrivedAtDesiredDistance` | The enemy reaches its band |
| `OnDepartedDesiredDistance` | The enemy leaves its band |
| `OnMovementLayerChanged` | The layer switches, carrying new and previous |
| `OnSwitchToPathfinding` | Switched to pathfinding |
| `OnSwitchToDirectInput` | Switched to direct steering |
| `OnPathfindingFailed` | A path move fails to start, or gets blocked on the way |
| `OnTargetUnreachable` | A long-range chase loses its route, once per loss |
| `OnAttackApproachComplete` | An attack approach reached its distance |
| `OnAttackApproachFailed` | An attack approach failed |
| `OnTargetLineOfSightChanged` | Sight to the target flips |
| `OnApproachBlocked` | The target becomes walled off |

</details>

<details>
<summary>Moving a pawn outside the loop (Alternative)</summary>

Three Behavior Tree tasks move the pawn without the evaluation loop, for use inside an action's tree:

| Task | Does |
| --- | --- |
| **Move Until Distance** | Paths toward the blackboard target until within a set distance. Takes a timeout and a max chase distance. |
| **Make Distance** | Backs away from a target until it reaches a distance. Includes stuck detection and a timeout. |
| **Debug SEC Values** | Shows the target and distance on screen and writes every `SEC_` blackboard value to the log at Verbose. Run `Log LogSECBrain Verbose` in the console to see the log lines. |

</details>

---

Previous page: [AI Agent Guide](https://rigbak.com/docs/soulslike-combat/agents). Next page: [Action System](https://rigbak.com/docs/soulslike-combat/action-system)
