Deep Dives

Networking

How Intent Forge handles multiplayer in v1.0-alpha — server-authoritative-with-replicated-current-action, what crosses the wire, and the v2.0 client-prediction candidate.

[ Deep Dive ]

Server-authoritative AI with replicated outputs

How multiplayer works in v1.0-alpha: the server runs the planner, the dispatcher, the sensors, and the anti-flap stack. Clients receive the current action via UE replication and drive cosmetic state from there. This page is the design record for that choice and the contract it implies.

v1.0-alphaServer-authoritative

The design space

Multiplayer game AI has three places state can live:

  1. Server — authoritative simulation, where game state is "real"
  2. Client — local representation, where game state is "displayed"
  3. Hybrid — predicted on client, corrected by server

For a GOAP planner, the natural questions are:

  • Where does the planner run? Server, client, or both?
  • What replicates from server to client?
  • How does the dispatcher behave on each side?
  • Who can write facts?

The shipped design picks server-authoritative across the board: the simplest model, cheat-resistant, bandwidth-cheap, and a clean fit with all four dispatcher modes. The trade is ~100 ms perceived latency on animation transitions for remote clients — fine for most game shapes; not fine for fast-paced shooters (see v2.0 candidate at the bottom).

What replicates

FieldReplicatedDirectionWhy
ReplicatedAction.ActionIdYesServer → ClientClients play matching animations / VFX
ArchetypeYesServer → ClientLets clients resolve ActionIdUIntentAction* via GetActionByName
PlanGenerationYesServer → ClientClient-side dispatcher invalidation detection
Plan step indexNoCosmetics key on action identity, not position. Replicating it would double-fire OnReplicatedActionChanged on same-id-different-step transitions.
Full FPlanNoBandwidth-heavy; clients don't need it
FWorldStateNo (default)Server-only. Opt-in per-fact replication via separate UPROPERTY if needed
Sensor dataNoServer-only. Sensors don't run on clients
OnFactChanged eventsNoServer-side only

ReplicatedAction uses OnRep_ReplicatedAction to broadcast OnReplicatedActionChanged on the client; cosmetic listeners resolve the action via GetActionByName(NewAction.ActionId).

Late-joiner ordering

On a late-joiner, the component may receive ReplicatedAction before Archetype has arrived. The component buffers the broadcast and re-fires it from OnRep_Archetype once the archetype reference lands. Delegate handlers can assume GetActionByName returns a valid pointer for any non-None id.

Dispatcher behaviour per net-mode

Auto dispatcher (built-in tickable subsystem):

  • Server: registers all bReplicates=true components and ticks them as normal
  • Client: does NOT register components; clients observe via replication only
  • Implementation: the subsystem's component registry filters by GetOwnerRole() == ROLE_Authority

StateTree dispatcher:

  • Runs wherever the StateTree runs (typically server-side)
  • The IntentForge task within the StateTree must check authority before calling planner methods
  • Recommended: wrap the task in a Server Authority Only decorator at the StateTree level

Behavior Tree dispatcher:

  • BT typically runs on the server-side AIController by UE convention
  • No special handling needed beyond what AIController already does
  • For clarity: BTDecorator_NetworkAuthority documents intent

External dispatcher:

  • User-owned; the component's ReplicatedAction snapshot replicates regardless of dispatcher choice
  • External dispatchers must check HasAuthority() themselves

RPCs

UFUNCTION(Server, Unreliable, WithValidation) ServerRequestReplan()

The single RPC the component exposes. No parameters — the server's replan logic already runs goal selection from current world state, so a reason payload would just add bandwidth without changing the decision.

  • Unreliable: replans are idempotent (the server coalesces intra-frame requests via SetTimerForNextTick), and the channel should not back up if a client spams the request. The server-side handler also applies a cooldown via LastReplanTime.
  • WithValidation: the auto-generated _Validate returns true; there's nothing client-provided to validate. The hook exists for forward compatibility.

Blueprint callers should use RequestServerReplan() (the wrapper) instead of calling the RPC directly — the wrapper verifies the owning actor has a network connection before dispatch and logs a warning otherwise.

Client-side fact writes — restricted by default

Calling SetFactBool on a non-authoritative component drops the write (the component checks ComponentIsAuthoritative on every SetFact* entry point). The console variable IntentForge.bAllowClientFactWrites (default false) is the development hook for client-initiated fact mutations; flipping it to true in v1.0-alpha does not yet route through an RPC — the component logs and drops with an explanatory warning.

Recommended pattern: write facts from server-side gameplay code and let the component's replicated state push the current action to clients. Don't ship production multiplayer with the cvar enabled.

Authority gating

Every mutating entry point on UIntentForgeComponent checks authority via an internal ComponentIsAuthoritative helper and no-ops on clients. Standalone (no-owner) components are treated as authoritative so editor automation tests and pre-PIE components behave identically to a server.

Gated entry points:

  • SetArchetype
  • SetFactBool / SetFactScalar / SetFactObject / ClearFactObject
  • BatchMutate
  • RequestReplan and the internal EvaluateReplan it schedules
  • NotifyActionCompleted
  • Sensor sampling timers
  • The safety-net replan timer

The helper calls HasAuthority() on the owner rather than on the component itself. The component-level helper returns the right answer regardless of whether bReplicates is set on the component or the owning actor, and avoids the UE quirk where UActorComponent::GetIsReplicated() lies in some construction paths.

Initialization is deferred to BeginPlay

HasAuthority() is unreliable during InitializeComponent on replicated actors — the net role hasn't been assigned yet on placed actors, so authority-dependent init (sensor activation, safety-net timer start) runs in BeginPlay. Both initialization helpers (InitializeActiveSensors, StartSafetyNetTimer) are guarded with idempotency checks so a pre-BeginPlay SetArchetype call doesn't double-up when BeginPlay runs.

Anti-flap is server-only

The anti-flap toolkit operates on world state that lives on the server. Clients see the result — a stable CurrentAction that only changes when the server's stack has decided the change is real.

  • Agents that look stable on the server look stable on clients too (animations follow CurrentAction).
  • No flap-correction logic is needed client-side — it's pre-stabilised by the time clients see it.
  • Bandwidth benefit: a stable AI changes CurrentAction rarely, so replication traffic stays low.

Bandwidth budget

Expected per replicated agent:

  • CurrentAction (FName + int32): ~12 bytes per change
  • PlanGeneration (int32): 4 bytes per change
  • Changes per second: ~0.5–2 (agents commit for 1–5 seconds at a time)
  • Per-agent bandwidth: ~20–80 bytes/second, peaking briefly during multi-step plan transitions

For a 2000-agent simulation with 64 connected clients: 2000 × 50 B/s × 64 = 6.4 MB/sec total upper bound. Relevance culling typically cuts this 10–50× in practice — well within UE's standard replication budget for AAA games.

What ships, what doesn't

In v1.0-alpha:

  • Co-op games with shared AI (companion shooters, dungeon crawlers)
  • Versus games with AI opponents (FPS bots, MOBA NPCs)
  • Spectator support — passive observers see the same AI as active players
  • Server-only headless tests via server-only NetMode

Out of scope for v1.0:

  • Client-side prediction with rollback
  • Per-fact replication policy DSL — projects that want clients to observe specific facts (e.g. a guard's alert level for HUD) mirror those facts onto the actor as separate replicated UPROPERTYs in v1.0-alpha
  • Cross-platform deterministic planning — only relevant for client prediction

Client-side prediction (v2.0 candidate)

For high-latency or fast-paced action / FPS shapes, the v1.0 server-authoritative model trades ~100 ms perceived latency on animation transitions for simplicity and cheat-resistance. Projects that need sub-100ms responsiveness on remote clients (twitch shooters, arena bots) want client-side prediction with server correction — the client runs the planner locally, the server's authoritative choice wins on mismatch, the client interrupts its animation to catch up.

The engineering trade is significant: the planner must remain deterministic given identical world state, world state must replicate identically on every client, and a correction mechanism must handle mismatched predictions cleanly. That's 2–3× the v1.0 scope, and it breaks the cheat-resistance story because sensors must run client-side too. It's the right model for shooter projects and a strong v2.0 candidate; until then, the server-authoritative path keeps the contract clean.

◢ Next

Try this next

On this page