Networking methods — spec replication + host-authority patterns
Two proven architectures on top of the engine networking primitives: replicate the generator spec (not the geometry) for deterministic worlds, and retrofit local-only gameplay systems to host-authority without breaking single-player.
Two proven architectures on top of the engine primitives. Official docs cover
[Sync], SyncFlags.FromHost, [Rpc.Host|Owner|Broadcast], NetworkSpawn,
and Networking.* well — this guide does not re-explain them. What they don't
cover is:
- Part A — deterministic multiplayer: replicate the spec, not the geometry.
- Part B — host-authority retrofit: making a dozen local-only gameplay systems host-authoritative without breaking single-player.
Baseline owner-simulated pattern is in owner-simulated-networking.
Part A — replicate the spec, not the geometry
The idea
If world generation is fully deterministic (same spec → byte-identical world), the host never streams terrain. It replicates the tiny generator input — a few dozen numbers — and every client regenerates the identical world locally. Bandwidth for the world is constant and tiny regardless of world size.
Prerequisites (they ARE the networking contract, not hygiene):
- No
System.Randomin the gen path; hash noise only; pure functions of (seed, x, y). - One unit conversion at emission; no platform/float-order-sensitive topology decisions.
- Spec fields are APPEND-ONLY — old and new peers must deserialize each other's specs (unknown fields skip, missing fields default).
- A process-independent content hash over the generated data — this turns "deterministic" from a hope into a checkable gate.
The decisions
D1 — replicate the spec as its canonical JSON STRING, not a networked
record: [Sync(SyncFlags.FromHost)] public string NetSpecJson on a scene-owned
session component. A string is the most boring thing [Sync] handles; the JSON
round-trip is already battle-tested by save/load; append-only field law makes it
version-tolerant. Serialize with the engine Json.Serialize (invariant culture).
D2 — client regen = observed property change; host regen = direct call.
Clients watch NetSpecJson and on change: validate → regenerate → report hash.
The host NEVER round-trips through its own sync property (self-echo loop;
single-player would depend on net plumbing) — it regenerates directly and
publishes only after a successful local regen.
D3 — the hash handshake (desync becomes a loud, named failure). After a
client's regen completes it sends ONE [Rpc.Host] ReportWorldHash(...). Host
compares to its own: match → client marked world-ready; mismatch → log
DESYNC with the first differing sub-hash and kick the client to menu —
never let a desynced client walk a wrong world. No retry logic: a desync is
a bug to fix, not a condition to paper over.
D4/D5 — an explicit mode state machine + per-mode UI lockdown.
Authoring → Hosting / Joining → Joined as ONE enum; every UI element declares
which modes it exists in. In a frozen-world MVP the HOST loses editing too — a
host edit would silently desync every client's world. Enforce at the UI and
in the mutation entry points.
D6/D7 — characters ride the standard owner-simulated pattern: host
OnActive(Connection) → clone prefab at a validated spawn →
NetworkSpawn(connection); each player simulates their own character; cameras
strictly local. No server-authoritative movement for a friends-co-op MVP — that's
a deliberate trust-model decision.
Scope cuts that make it small
- Frozen-world MVP: the world locks at launch — no live edit replication.
- Late join is cheap by construction: spec + regen + handshake (and, post-MVP, replay of the edit log — the delta-log save format is deliberately the same format the wire protocol needs).
Invite codes / lobby discovery
Verified on recent engine builds:
Networking.CreateLobbywithHidden=true, Privacy=Public= "anyone with the code, nobody without" (Private fights the code path; FriendsOnly blocks friends-of-friends).Networking.QueryLobbies(Dictionary filters)→LobbyInformationrows; stamp a protocol-version key in lobby data and refuse mismatches client-side BEFORE connecting.- Caveat: a host's self-query for its own hidden lobby often returns nothing — Steam commonly excludes lobbies the caller is already in, so self-query CANNOT verify the code path; only a real second peer can.
Verification ladder
- Determinism suite on one machine: same spec regenerated N× byte-identical (see /guides/agent-test-harness).
-joinlocaltwo-peer on one machine — no publish, no second Steam account. Exchange the world hash at join and refuse on mismatch — dev-host streaming does NOT send loose data files, so a code-built world can silently diverge.- A real second peer/account (the only test that settles Steam-backend behaviors).
Part B — host-authority patterns: retrofitting local systems for co-op
The situation this solves: a game built single-player-first has a dozen mutable systems (gates, pickups, NPC AI, progress, day/night, scoreboard, win latch) that every client would simulate independently — double-consumes, contradictory NPC targets, multiple winners.
Load-bearing engine facts
GameObject.NetworkModedefaults toSnapshot— objects created identically on every peer by a deterministic bootstrap are networked as part of the scene snapshot:[Sync]/[Sync(SyncFlags.FromHost)]fields on them converge with noNetworkSpawn.NetworkSpawn/NetworkMode.Objectis only for per-owner objects (player bodies, transient projectiles).- The engine does NOT stop a non-host writing a FromHost field locally —
the write just gets steamrolled by the next snapshot. The manual guard is
therefore load-bearing:
snippet
if ( Networking.IsActive && !Networking.IsHost ) return; // host-authoritative singletons if ( IsProxy ) return; // per-owner objects - Instance
[Rpc.Host]methods on snapshot-built objects route to the host's matching instance.Rpc.Calleridentifies the requester;using (Rpc.FilterInclude(conn)) SomeBroadcast(...)narrows a broadcast to exactly one client — the confirm/deny-to-caller primitive. INetworkListener.OnConnected/OnDisconnectedfire host-only (for remote peers); a joiner learns its own connection completed by pollingNetworking.IsActive && !Networking.IsConnecting.
The per-system recipe
For each mutable system, apply mechanically:
- Authoritative fields →
[Sync(SyncFlags.FromHost)]; every mutation host-gated (fact 2). - Client intent →
[Rpc.Host] RequestX(...). The host validates against its own state — never against client-sent positions. - First-request-wins via an idempotent latch (
_opened,_consumed,HolderId != Empty): the second simultaneous request finds the latch set and no-ops. This one pattern kills double-consumption, simultaneous grab, and multiple winners. - Confirm/deny to the caller via
Rpc.FilterInclude(Rpc.Caller). - Presentation = a pure function of the synced fields, run per-client with a primed edge-detect, so late-joiners reconstruct state without replaying one-shot side effects.
- Single-player short-circuit:
Networking.IsActive == falsetakes the direct pre-networking code path — byte-identical behavior, no RPC hop. - Disconnect cleanup: host-side liveness poll of recorded
Connections each tick.
NPC AI — decompose, don't rewrite
The biggest anti-double-apply surface is locally-simulated NPC AI. The
structural fix is small and staged: (a) gate every NPC's OnFixedUpdate to
the host — this alone kills divergent decisions; (b) NetworkSpawn the NPCs +
transform sync + a FromHost presentation-state enum; (c) reactions fire
host-side; (d) any vision/targeting that reads a local singleton must scan ALL
players. Do not rewrite the mega-classes — add the host gate and sync at their
public seams.
Player locomotion stays owner-authoritative (deliberately)
Movement, climb, swing, charge/jump remain owner-predicted with [Sync]'d
outputs — prediction is accepted for feel; the host is authoritative over the
WORLD the player acts on, not the player's own body.
Predict-then-confirm rollback without a rollback API
A denied prediction needs NO callback plumbing if the sim already reconciles against the owning object's state every tick: the deny handler just clears the object's local flag; the player sim's existing "item gone" path unwinds the prediction next tick. Look for the existing per-tick reconcile before inventing a rollback seam.
Late-join checklist
Walk every host-authoritative bit and ask: does a mid-run joiner reconstruct it?
Open gates, moved NPCs, consumed pickups, progress meters, clock value, run/win
state. Most fall out of FromHost syncs + the pure-presentation rule; anything
driven by a one-shot event needs an explicit snapshot. Test by joining a session
where everything has already happened.
Riskiest interactions — name them in every test plan
Simultaneous grab · double consumption · multiple winners · double daze/stat-bump · per-client divergent AI targets · late-join divergence. Each maps to exactly one recipe step above.