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:
FWorldState— the agent's facts (bool bitset, scalar map, object map).FPlan— the immutable plan currently being executed (sequence of action handles + params).- 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.
SetFactBoolon 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:
- Spawn the
UIntentForgeComponentonly on the server. - Use the existing
OnPlanGenerated/OnActionStarteddelegates on the server to RPC anything they want clients to observe (e.g. anOnRep_ChosenGoalcosmetic FName). - Use the existing
UIntentForgeStatics::GetCurrentGoalNameon 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
bReplicateScoreflag so only "interesting" goals push deltas? (Saves bandwidth for goals that exist only as preconditions.) - Should the dispatcher run client-side in
Observationmode (a fifthEIntentDispatchMode) that consumes replicated handles but never spawns executors? Probably yes. - Replicating the live
ActiveExecutorpointer 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.