# Multiplayer Enemy AI

> Enemy AI for co-op and multiplayer games in Unreal Engine 5: the server decides every attack, reaction and move, on a listen server or a dedicated server, and every client receives the health, action, role and weapon state a HUD draws from.

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

> The server makes every AI decision. Clients receive the pawn state a health bar, a telegraph and a role icon need, and the animations arrive on their own through the ability system.

## What runs where

Decisions sit on the AI controller and stay on the server. Output sits on the pawn and replicates.

**Server side.** Nothing here decides anything on a client. The controller components exist on the server alone, and the two pawn components ride along on a client and stay inert there.

| Piece | Lives on | Decides |
| --- | --- | --- |
| **SEC Combat Controller** | AI controller | Resolves the AI Config, syncs sets on a role change, orchestrates death |
| **SEC Brain** | AI controller | Runs the StateTree or the native loop |
| **Action Evaluation** | AI controller | Scores actions and starts the winner |
| **Reaction Evaluation** | AI controller | Picks the answer to an incoming hit |
| **Movement Evaluator** | AI controller | Steers the pawn |
| **SEC Awareness** | AI controller | Perception memory, sight and hearing |
| **Threat Detection** | AI controller | How long the player has been staring |
| **SEC Territory** | Enemy pawn | Patrol posts, the wander area, the leash |
| **SEC Token Budget Component** | The actor being attacked | How many enemies may swing at once |

**Reaches every client.** Bind your UI to these.

| Piece | Lives on | Clients read |
| --- | --- | --- |
| **SEC Vitals** | Enemy pawn | Every vital's value, including a smooth regenerating one |
| **SEC Action Set Component** | Enemy pawn | The running action, the active set, cooldowns |
| **SEC Reaction Set Component** | Enemy pawn | The running reaction, the active set, cooldowns |
| **SEC Combat Role** | Enemy pawn | The assigned role tag |
| **SEC Equipment Component** | Enemy pawn | Which actor the enemy holds as its weapon |
| **SEC Team** | Enemy pawn | Team Id and Free for All |
| The ability system | Enemy pawn | Gameplay cues, replicated tags, montage playback |

> **Warning**
>
> An AI controller reaches no client. Unreal marks a controller relevant only to its own player, and an AI enemy has no player, so on a client **Get Controller** on that enemy returns nothing and every controller component in the first table is out of reach. Client UI binds to the pawn components in the second table. Blueprint that reads the controller works in a single-process Play In Editor session and returns null the moment you run a real client.

---

## Point the AI at every player

Enemies fight the actors registered as combat targets, one entry per player. An enemy whose AI Config sets an **Awareness Config** registers each hostile player it detects, so with awareness set up a player joins the pool once an enemy spots them. With no Awareness Config, or with **Always Aware** ticked, register each player yourself: call **Register Combat Target** from the player pawn's **Event BeginPlay**, behind a **Has Authority** branch, so each player registers itself as it spawns.

Blueprint graph, nodes in reading order:

- **Comment box: Player pawn · Event BeginPlay**

  - **AICombatRoleSubsystem** (subsystem getter)
  - **Register Combat Target** (function; Target is AICombat Role Subsystem); inputs: Target, Target Actor = Self, Auto Assign Unassigned = true, Reevaluate Existing = false

Every call gives each idle enemy another pass over the pool, so a player joining late is picked up with no extra wiring. Tick **Auto Assign Unassigned** to evaluate roles straight away instead of waiting for the reassignment timer. See [Targeting System](https://rigbak.com/docs/soulslike-combat/targeting-system).

> **Warning**
>
> Register the pawn, not the controller. Perception and awareness key off the pawn, and team reads go through the pawn's **SEC Team** component, so a registered controller carries neither.

---

## Draw an enemy's health on a client

Health lives on the pawn's **SEC Vitals** component, which the shipped enemy carries with one Health row at 100.

1. On the client, find **SEC Vitals** on the enemy actor.
2. Bind **On Vital Changed**. It carries the vital tag, the new value, the old value and the max, and fires on the server and on each client as the change replicates. Filter by tag when the pawn holds more than one pool.
3. Drive the bar's fill from **Get Vital Fraction**, polled each frame.

Regeneration's smooth rise raises no event. Each machine works the current value out from the last replicated anchor against server time, so a client reads a smooth bar with no extra call and no per-tick traffic. See [Vitals](https://rigbak.com/docs/soulslike-combat/vitals).

---

## Show what an enemy is doing

The pawn's action and reaction components carry the running move down to clients. Bind these on the enemy actor from client code.

| Delegate | On | Fires when |
| --- | --- | --- |
| **On Action Execution Started** | SEC Action Set Component | A move starts, carrying the action id |
| **On Action Execution Completed** | SEC Action Set Component | The move ends, carrying whether it succeeded |
| **On Action Cooldown Started** / **Expired** | SEC Action Set Component | A cooldown opens or closes |
| **On Reaction Execution Started** | SEC Reaction Set Component | A parry, block or dodge starts |
| **On Combat Role Changed** | SEC Combat Role | The role tag changes, carrying new and previous |

Polling works too: **Get Current Action Id**, **Is Action Executing**, **Get Active Action Set** and **Get Remaining Cooldown** all answer on a client. A client measures each cooldown against the server's clock and receives every one still running, including those that started before it saw the enemy. Those arrive before the enemy's Begin Play, so read them there with **Get All Active Cooldowns** or **Get Remaining Cooldown**. **On Action Cooldown Started** and **Expired** mark each start and end after that, and **Duration** on a start is the length the cooldown started with (after an interrupt refund, what the refund left); **Get Remaining Cooldown** gives the time left. A set swap or a reset ends the running cooldowns on every machine with **Expired**, and a cooldown of 0 s fires **Started** on the server alone.

**Details panel: SEC Action Set Component** (component, in AEnemyCharacterBase)

**Category: SEC > Action State**

| Property | Value |
| --- | --- |
| Current Action Id | None |
| Action Executing | false |
| Active Action Set | DA_EnemyActions |

**Category: SEC > ActionSet**

| Property | Value |
| --- | --- |
| Provided Action Set | DA_GreatswordActions |

*Replicated runtime state on the pawn. The server writes via NotifyActionStarted and role sync; clients read fields or bind OnActionExecutionStarted.*

**Details panel: SEC Combat Role** (component, in AEnemyCharacterBase)

**Category: AI > Combat Role**

| Property | Value |
| --- | --- |
| Combat Role | SEC.Role.Attacker |

*Replicated runtime state on the pawn. SECCombatControllerComponent calls SetCombatRole on the server; clients bind OnCombatRoleChanged.*

<details>
<summary>Reading a cooldown entry on a client (Advanced)</summary>

**Server End Time** on an entry from **Get All Active Cooldowns** is the server's clock. On a client, compare it with **Get Server World Time Seconds** on the game state, not **Get Game Time in Seconds**, since the client's own clock started later than the server's.

A custom controller can start a cooldown by hand with **Notify Cooldown Started** on the pawn's **SEC Action Set Component** or **SEC Reaction Set Component**. A role change leaves such a cooldown alone, and it reads as over once its **Server End Time** passes, but no event marks that moment. Call **Notify Cooldown Expired** when it ends; that ends it and fires the expired event on every machine.

</details>

---

## Damage lands on the server

Melee sweeps run on the machine holding authority, so one swing lands once. Clients see the attack through the montage the ability replicates.

- **The swing.** SEC's ability base starts abilities on the server and replicates them, so the montage plays everywhere with no wiring on your side.
- **Impact cues.** A landed hit runs the damage config's **Hit Effects**. **SEC Play Gameplay Cues** goes through the ability system and reaches every client. An effect aimed at the struck character runs on the authority alone, since that is the only machine able to reach another player.
- **Defense.** **Resolve Incoming Damage** charges the cost of a parry or a block, so run it on the server. To show a player what a hit would cost without taking it, use **Preview Incoming Damage**, which charges nothing.

> **Warning**
>
> Call **Run Hit Effects** on the authority. An effect aimed at the attacker runs on the machine that called it, so a server call and a predicting client's call both run it. Gate the call when you drive damage yourself.

See [Melee Trace](https://rigbak.com/docs/soulslike-combat/melee-trace) and [Defense System](https://rigbak.com/docs/soulslike-combat/defense-system).

<details>
<summary>Hit effects when several blows land at once (Advanced)</summary>

Unreal sends one character at most two gameplay cue calls per network update. When three or more hits land on one character inside a single update, as a crowd or a multi-hit swing can manage, a hit effect's cue past the two waits for the next update, a frame or two later, and is given up only after a quarter of a second without room, so clients keep the sparks and sounds. Two cases skip the wait and can still drop a cue, with the server's log warning `Attempted to fire NetMulticast_InvokeGameplayCueExecuted_WithParams when no more RPCs are allowed this net update`: a cue your own code plays inside its own `FScopedGameplayCueSendContext`, which is counted when that context sends, and a cue on an ability system that replicates through a replication proxy, whose count the wait does not read. For those, raise the limit in your project's `Config/DefaultEngine.ini`:

```ini
[ConsoleVariables]
net.MaxRPCPerNetUpdate=4
```

The setting covers every unreliable multicast call in the project, so raise it only as far as your largest pile-on needs.

</details>

---

## Dedicated server checklist

Three things behave differently with no rendering and no local player.

| Check | Why |
| --- | --- |
| **Mesh pose** | The shipped enemy sets its mesh to **Always Tick Pose and Refresh Bones** on a dedicated server at Begin Play. **SEC Melee Trace** keeps the meshes its sockets ride refreshing their bones for the length of each trace window wherever it sweeps, so a custom character's swings land without the setting. Set it the same way when other server code reads a bone or a socket outside a trace window, or that code reads the bind pose. |
| **World tags** | Tag writes are server-side. For client reads, add a **SEC World Tag Component** to your GameState; it mirrors the server's tags and fires **On Tags Changed**. A client read without it returns nothing and warns once. |
| **Encounters** | A **SEC Encounter Trigger** reacts to the locally controlled player pawn on that player's own machine, so each player gets their own encounter and nothing crosses the wire. A player who respawns inside the box, or arrives there from another map, counts once they take control of the new pawn. Enemy health reaches the bar through the enemies' own replication. **BP_SEC_BossArenaTrigger** ends each player's fight as Defeated when that player's machine sees the last listed enemy fall. A project ending the fight itself calls **End Triggered Encounter** from an event each machine hears, such as **On Vital Depleted** on the boss's **SEC Vitals** reporting its health vital; **On Death** on the controller fires on the server alone. |

---

## Debug it

The overlays read live component state on the machine running them. Role, Actions, Reactions, Vitals and Teams read replicated pawn state, so they draw on a client as well as the server. Decision, Movement, Awareness, Territory, Scoring and Tokens are worked out on the server and draw there alone, so run those on the server or the listen-server host.

```
SEC.Debug.All 1                  // Every overlay section at once
SEC.Debug.WatchPawn <name>       // Narrow the overlays to one enemy
SEC.Debug.LogActionExecution 1   // Action starts, ends and cooldowns
SEC.Debug.Melee.DrawTracing 1    // Sweep shapes, on the machine that traces
```

To reproduce a client-only bug in the editor, open the Play dropdown, set **Net Mode** to **Play As Client** and **Number of Players** to 2 under **Multiplayer Options**. That runs a real server, which is where a HUD reading the AI controller falls over.

```
NetEmulation.PktLag 150   // Add 150 ms of latency to every packet
NetEmulation.Off          // Clear it
```

<details>
<summary>A client shows nothing at all (Advanced)</summary>

In the order worth checking:

- **The binding went to the AI controller.** It exists on the server alone. Bind to the pawn components instead.
- **Nothing put that player in the target pool.** An enemy with an **Awareness Config** adds a player once it detects them; without one, or with **Always Aware** ticked, only **Register Combat Target** does. An enemy with no combat target scores nothing and starts nothing, so every replicated field stays at its idle value. This is the common one and it logs nothing.
- **The widget bound before the pawn arrived.** An enemy streaming in later reaches a client after the HUD built. Bind as each enemy actor appears rather than once at level start.
- **The delegate fires and the widget is stale.** Regeneration raises no event by design; poll **Get Vital Fraction** for a moving bar.

</details>

<details>
<summary>Sweeping for hits on a machine other than the server (Alternative)</summary>

**SEC Melee Trace** asks **Should Detect Hits** before every sweep, which answers with the owner's authority. Override it in a Blueprint subclass to detect on the owning client instead, for a project that predicts its own hits.

Two things change once you do:

- A team id carried on a controller reads as no team on a client, because a controller replicates only to its own player. Put the id on the pawn or its Player State, which is what the pawn's **SEC Team** component does.
- A gameplay cue fired off the authority and outside a prediction window plays nothing, so a client-detected hit needs its cues driven locally.

</details>

<details>
<summary>Speed changes that disagree between machines (Advanced)</summary>

Walk speed is not a replicated property, so each machine works its own copy out from the tags it can see. A **SEC Speed Modifier** driven by a tag added with **Add Loose Gameplay Tag** slows the owner on one machine and leaves it at full speed on the others.

Add the driving tag through a Gameplay Effect, or through **Add Replicated Loose Gameplay Tag**, and both sides agree.

</details>

<details>
<summary>Which machine runs an animation notify (Advanced)</summary>

SEC's notifies fire wherever the animation plays and send nothing over the wire:

| Notify | Runs |
| --- | --- |
| **SEC State Tag Window** | On each machine playing the montage. The server plays it too, which is what server-side checks read. |
| **SEC Melee Trace Window** | Everywhere, but the sweep inside it is gated on authority. |
| **SEC Play Anim Effect** / **SEC Anim Effect Window** | On each machine playing the animation, and not at all on a dedicated server. |
| **SEC Gameplay Event** | On each machine playing the montage, raised locally. |
| **SEC Approach Window** | On the server and on a player's own machine. A copy of someone else's character receives the motion already resolved. |

One caveat for player characters: on the server's copy of a character a connected client controls, Unreal 5.6 and 5.7 begin and end every montage notify state once per frame while the montage plays on. Anything opening on begin and closing on end is torn down and rebuilt every frame there. An **SEC Telegraph Window** still announces once per run of the window, so enemies hear one warning.

</details>

<details>
<summary>Steering an attack toward a target across the wire (Advanced)</summary>

**SEC Approach** stores its target on the machine the call runs from, which suits an attack played through an ability, since the ability graph runs on the server and on the player's own machine alike.

**Replicate** on **Set Approach Target**, **Set Approach Target to Actor** and **Set Approach Target to Location** is deprecated. It still has the server send the target to the player's own machine, and since the engine cannot mark one input of a node deprecated, the compile gives no warning; the node's tooltip says so instead. Leave it off in new work: an ability stores its target on both machines by running on both, and an AI enemy has no player's machine to send to.

The glide goes through the ability behind the window. It predicts on the owning client when that ability predicts and runs on the server, and other players' machines see it through the character's replicated movement, smoothed as its walk is. The warp goal is worked out wherever the warp runs.

</details>

<details>
<summary>Every field that replicates, component by component (Advanced)</summary>

| Component | Replicated |
| --- | --- |
| **SEC Action Set Component** | Current Action Id, Action Executing, Active Action Set, Provided Action Set, Last Action Succeeded, Completed Action Id, and every running cooldown, sent as one list |
| **SEC Reaction Set Component** | The same shape for reactions |
| **SEC Combat Role** | Combat Role |
| **SEC Vitals** | One anchor per vital, holding value, server timestamp and regen rate |
| **SEC Equipment Component** | Equipped Weapon |
| **SEC Team** | Team Id, Free for All |
| **SEC Approach** | Stored approach targets, to the owning player alone |
| **SEC World Tag Component** | Tags, when you add it to GameState |

The enemy's ability system runs in **Minimal** replication mode, which keeps effect bookkeeping on the server while the cues an effect fires still reach every client. A weapon actor replicates, and its attachment to the character mesh travels with it. **SEC Weapon Base** sends updates ten times a second, and at once when it is equipped or released; a weapon Blueprint whose own replicated variables change often can raise **Net Update Frequency** in its Class Defaults.

**SEC Token Budget Component** replicates nothing on purpose: token accounting decides which enemy may swing, and that decision belongs to the server.

</details>

---

Previous page: [Configuration Reference](https://rigbak.com/docs/soulslike-combat/configuration). Next page: [StateTree Integration](https://rigbak.com/docs/soulslike-combat/statetree)
