"Idempotent world rebuild needs DestroyImmediate, not Destroy"
▸ SYMPTOM
A joining client in a multiplayer game with a code-built deterministic world sees doubled objects — colliders, props, and scene elements appear at 2x the expected count for at least one frame. A live audit (e.g. GetAllComponents / tag scan) confirms checked = 2 * expected. Physics, traces, and network snapshots may run against the duplicated world during that frame.
▸ CAUSE
When the joining client rebuilds the world (a common pattern: the client runs the same deterministic builder the host ran), GameObject.Destroy() is deferred to frame-end in both edit and play mode. If the rebuild tears down the old world with Destroy() and immediately builds the new one, both worlds coexist for the remainder of the frame.
This is a race condition — it triggers on roughly 1 in 3 join attempts, not every time.
▸ FIX
Use DestroyImmediate for the prior-world teardown, and pair it with a recipe hash as a complementary guard:
void RebuildWorld( int seed )
{
// Nest all authored content under one root
if ( worldRoot != null && worldRoot.IsValid() )
worldRoot.DestroyImmediate(); // not Destroy()
worldRoot = new GameObject( true, "World" );
// Deterministic build from seed...
BuildTerrain( seed );
BuildProps( seed );
// Recipe hash from static build lists (cleared + repopulated each build)
localHash = ComputeRecipeHash();
}Belt and suspenders — the recipe hash:
- Compute the MP-sync hash from per-build static lists (cleared and repopulated each build), not from a live scene scan.
- This makes the hash survive even a double-build: if the client double-builds, the recipe hash is recomputed from scratch each time and matches the host's, so the join guard passes regardless.
- The idempotent
DestroyImmediaterebuild guarantees the settled live scene is single; the recipe hash guarantees the join verification succeeds even mid-defect.
Important: snapshot-networked props that use [Sync] + [Rpc.Host] without NetworkSpawn are built locally by each peer (state replication only), so doubled props come entirely from the double-build, not from host→client replication. The idempotent rebuild fully resolves this.
▸ WHY IT WORKS
DestroyImmediate removes the old world's GameObjects from the scene graph synchronously, so the rebuild starts with a clean slate — no overlap frame. The recipe hash is a separate safety net: because it's computed from build-time lists rather than a live scene scan, it's deterministic and immune to transient duplication. Together, they ensure both the live scene and the join verification are correct, even under the race condition.