Skip to content

Intent Forge — Networking design (draft)

Status: Design draft. Replication is explicitly out of v1.0 scope. This document captures the design analysis so we don't carry the question around as a vague unknown — and so anyone using the plugin in a networked project knows what to do until first-class support lands.


The core question

The component holds three pieces of state:

  1. FWorldState — the agent's facts (bool bitset, scalar map, object map).
  2. FPlan — the immutable plan currently being executed (sequence of action handles + params).
  3. Dispatch state — which step is in flight, the active executor instance, action elapsed time.

For multiplayer, who owns each of these? Three viable models, ordered by cost-to-implement (cheapest first):

Model Where planning runs What replicates Pros Cons
A. Server-authoritative, observation-only Server Nothing — clients run no Intent Forge logic Zero protocol surface, no merge logic. Clients see effects via the existing game replication channels (animations, ability cues, etc.) Clients can't predict, can't draw debug overlays of the agent's mind
B. Server-authoritative with state replication Server FWorldState deltas + current action handle + current goal handle Clients can render the inspector / debug overlays; tooling parity in MP Replication cost scales with fact churn; need delta encoding
C. Server-authoritative with full plan replication Server Everything in B + the current FPlan step list Clients can do interpolation / lookahead UX (e.g. "agent is about to attack") Higher bandwidth; plan replan invalidates prior replication

Recommendation: ship Model A first, then opt-in Model B as a separate module (IntentForgeReplication) for projects that need debug parity. Model C is rarely worth it — most games don't need clients to know the future of an NPC's plan.


Why server-authoritative is the only sane default

  • Determinism. The A* planner is deterministic given the same world state. Running it on both client and server with non-deterministic inputs (variable timer firings, perception ticks) would split-brain.
  • Cheat surface. Goals like "flee at low health" or "attack on player detected" are direct windows into game logic. Client-side planning lets players see what the AI is about to do.
  • Existing UE patterns. AI Module already assumes server-side decision-making (BT/StateTree run on the server-controlled pawn). Intent Forge fits this pattern naturally.

The UIntentForgeComponent already supports server-only operation implicitly: just don't construct it on clients (GetNetMode() == NM_Client → skip InitializeComponent). This is the simplest path and what users should do today.


Model B sketch (when we eventually build it)

Per-component replication

UCLASS()
class UIntentForgeReplicatedComponent : public UIntentForgeComponent
{
    // Override SetReplicates(true), set bReplicates and bReplicateUsingRegisteredSubObjectList.
    UFUNCTION() void OnRep_ReplicatedSnapshot();

    UPROPERTY(ReplicatedUsing = OnRep_ReplicatedSnapshot)
    FIntentForgeReplicatedSnapshot LastSnapshot;
};

USTRUCT()
struct FIntentForgeReplicatedSnapshot
{
    // Bool facts as a delta-compressed bitset against the last snapshot.
    TArray<uint8> BoolDelta;
    // Scalar facts that changed since last snapshot.
    TArray<TPair<FName, float>> ScalarDelta;
    // Current goal + step.
    FGoalHandle CurrentGoal;
    FActionHandle CurrentAction;
    uint32 StateVersion;
};

Server side, build the snapshot every ~10 Hz (configurable) and only push when StateVersion advances. Clients run a read-only component that applies the snapshot to a local FWorldState mirror so the inspector works in PIE-as-client and live-server-attach debugging.

What clients can't / shouldn't do

  • Replan. Period. The local mirror is observation-only.
  • Mutate world state. SetFactBool on a client should be a no-op + warning.
  • Run sensors. Sensors live server-side; their outputs replicate in the fact deltas.

Bandwidth back-of-envelope

  • Bool deltas: ~8 bytes per ~64-fact schema per replication tick
  • Scalar deltas: average 2 changes/tick × 12 bytes = ~24 bytes
  • Handles: 16 bytes
  • Header: ~8 bytes

~56 bytes per agent per replication tick. At 10 Hz with 50 agents visible to a client, ~28 KB/s. Acceptable for AAA budgets, edge for casual networked games. We'd want a "high-priority agent" tier that replicates faster than "background NPCs" — same pattern as character movement.


What ships before Model B

The dispatcher abstraction's External mode is the escape hatch. A user who needs MP today can:

  1. Spawn the UIntentForgeComponent only on the server.
  2. Use the existing OnPlanGenerated / OnActionStarted delegates on the server to RPC anything they want clients to observe (e.g. an OnRep_ChosenGoal cosmetic FName).
  3. Use the existing UIntentForgeStatics::GetCurrentGoalName on the server's component instance to populate that replicated value.

This works without any plugin changes. It's how I'd ship a networked project today, and it's what Model B will optimize when we get there.


Open questions for v1.0

  • Should goals carry a bReplicateScore flag so only "interesting" goals push deltas? (Saves bandwidth for goals that exist only as preconditions.)
  • Should the dispatcher run client-side in Observation mode (a fifth EIntentDispatchMode) that consumes replicated handles but never spawns executors? Probably yes.
  • Replicating the live ActiveExecutor pointer is not possible (it's a transient C++ instance). For client-side progress bars, replicate the executor's class + the world time it entered, and let clients compute elapsed locally. Loses tick-perfect accuracy but matches what every other live-game progress UI does.

What this design intentionally doesn't tackle

  • Rollback / prediction. Intent Forge is server-authoritative. We do not model "client predicted that the agent would attack, server said otherwise" — that's too narrow a feature for the cost.
  • Dedicated server vs listen server differences. The replication contract is identical; only the spawn condition differs.
  • Cross-server sharding. Out of any reasonable scope.

Decision

v1.0 ships server-only Intent Forge. Users wanting MP debug tooling either wait for the optional IntentForgeReplication module or use the delegate-driven workaround above.

This decision unblocks shipping the plugin to a public audience without the design weight of getting MP right. Networking is a feature you add, not a feature you build the architecture around.