the symptom, in your words

"A code-built world can run TWICE on a network join and stack a second world on the first"

✓ verified on 26.07.15a
lane Writing gameplayposted

▸ SYMPTOM

Your world is built in code (procedural placement of props, climb fields, ropes, etc.). On a network join you notice the joining client has roughly twice as much of some things as the host. Concretely, in one verified session: host reported rows=74 climbs=8 ropes=4 throwables=10; the joining client reported rows=96 climbs=16 ropes=8 throwables=20, exactly 2× the climb fields, ropes, and throwables, while boxes=14 and kit=38 stayed single.

A world-hash guard that compares peers then flags a mismatch, and depending on your collision setup the doubled geometry can be visible or subtly wrong.

▸ CAUSE

A network-joining client can run the scene's code-built world twice: once when the local StartupScene loads, and again on the networked-session scene handoff. If the world author is not idempotent (it clears its static tracking lists at the top of each build but never destroys the GameObjects a prior build created), the second build stacks a whole second world on top of the first.

The tell for which mechanism doubled a given count:

  • Counts sourced from a live scene scan (Scene.GetAllComponents<T>()) double, because there really are two sets of objects in the scene.
  • Counts sourced from a per-build static list (cleared at the top of each build) stay single, because the list only reflects the most recent build.

That split is the proof it was a second build, not network replication: a non-networked component (one with no [Sync]/NetworkSpawn, which physically cannot replicate) still doubled. Replication cannot explain a doubled non-replicated component; a second OnStart-driven build can.

▸ FIX

Two independent fixes, do both:

  1. Make the code-built world author idempotent. Before rebuilding, destroy the prior world root (or skip the redundant build entirely on a joining client). A second OnStart on join is real. Design for it:

    snippet
    protected override void OnStart()
    {
        WorldRoot?.Destroy();          // tear down any prior build
        WorldRoot = new GameObject("World");
        BuildWorld(WorldRoot);
    }
  2. Compute any determinism / MP-sync world hash from the build RECIPE, not a live scan. Hash the static placement lists (the inputs to the build), not Scene.GetAllComponents. A recipe hash cannot be perturbed by a second build or a replicated proxy, so a join-time world-hash guard compares each peer's own deterministic build apples-to-apples.

▸ WHY IT WORKS

The doubling comes from state that accumulates in the scene surviving across two builds. Destroying the prior root removes that accumulation, so a second build produces one world, not two. And hashing the recipe rather than the live scene decouples the integrity check from scene population entirely. The number of GameObjects currently alive (one build, two builds, or a set of replicated proxies) no longer changes the hash, so the guard measures the thing you actually care about: whether two peers agree on what to build.

Verified on engine 26.07.15a: seen in a real project.
s&box moves fast; an undated fix is a liability. Spot a stale detail?
changelog
  • Published

Want to know when new guides or fixes drop? Join the community to help build this out. Report gotchas, flag outdated fixes, or just lurk.

Join the Discord