Writing gameplay
Components, movement, save/load, owner-simulated networking.
A component with [Sync] fields added in OnStart (after NetworkSpawn) never replicates: each peer creates its own local instance and the sync pair silently never matches. The law covers engine components too: a Rigidbody built post-spawn is exactly this bug (Velocity is [Sync]-backed).
A second editor instance launched with -joinlocal is not a real Steam lobby member, yet Networking.GetData still resolves the host's Networking.SetData values -- they arrive over the ServerDataMsg the host sends on connect, not via lobby membership. The message can lag scene load by a few frames, so read GetData in a short poll loop.
A -joinlocal second instance connects to whichever process holds the loopback port 127.0.0.1:55333, so the precondition to check is socket ownership, not which editor is in play mode.
A -joinlocal second instance never runs the in-game Join UI: it connects straight to the editor's loopback dev-host socket, so it presents an empty invite code. Any host that verifies the code on the wire will correctly reject it, breaking the local two-peer test even though every other hop is healthy. It's a harness artifact, not a product bug.
A baked navmesh is a cached derived copy of the world, so a door leaf that swings clear in physics leaves the doorway solid on the navmesh until you regenerate the tile.
A joining client can build a code-generated world twice -- once on local StartupScene load, once on the networked scene handoff. A non-idempotent world author that clears its tracking lists but never destroys prior GameObjects stacks a whole second world. Make the author idempotent, and compute any sync hash from the build recipe, not a live scene scan.
Update order is a property of the authored scene. A component added at runtime with Components.GetOrCreate goes on the end of the update list, no matter which GameObject it sits on. Per-frame input written from it lands too late for movement code that reads it the same frame. Drive the runtime component from an authored component's tick.
Input.Pressed and Input.Down read a global input state, not a per-entity one. A component that reads them in its own OnUpdate samples that global state once per instance. With one entity on screen this is correct. With many non-networked instances of the same component, one keypress fires the action on every instance at once. Gating only on IsProxy does not help, because IsProxy answers whether the entity is replicated, not whether this instance should own the keypress. Gate the input read on genuine local ownership instead.
When the editor console (or any text field) holds keyboard focus, every Input.* read in game code returns nothing, so a feature toggled from the console looks dead while the exact same feature works from its keybind. A debug camera in that state is pixel-identical to a broken one. Game code cannot take focus back; the fix is to make the silence loud.
A projected Sandbox.Decal seated on a character's analytic hit surface (e.g. a capsule hit position) with a shallow projection Depth paints the base skin mesh but ends before the clothing renderers' outer shell, so paint shows only where bare skin peeks out. Increase Depth to span the cloth shell without punching through thin limbs.
Networking.CreateLobbyAsync branches on Application.IsDedicatedServer straight into the dedicated-server path and never opens a lobby, so lobby-creation code written for peer-hosted play does nothing useful on a dedicated server.
A timeout accumulator gated on a control-signal sample (throttle > threshold) resets to zero on frames where the signal legitimately reads zero, so it never fills. A gear change drives the throttle magnitude through zero on a fixed cadence, and an unstick routine that alternates gears makes that happen forever, so a stuck vehicle is never declared wrecked. Drive give-up timers from ground-truth state, such as position progress since the last known-good, never from a control signal that pulses through the reset value by design.
Naming a Component method `public void Reset()` compiles clean and silently hides the engine's own `Component.Reset()`. The only signal is a CS0114 warning, which `dotnet build -v q` never prints. Green is not the same as warning-free: pick a different name (ResetState, Clear) unless you deliberately mean to hook the lifecycle method.
Scene.Trace leaves SceneTraceResult.Hitbox null unless the trace calls .UseHitboxes(true). A target that carries model hitboxes but no body collider is invisible to a plain trace: the ray reports no hit at all. Mixing a coarse collider with hitboxes lets the collider shadow them, so a head shot resolves as a body hit.
Running a second s&box process under the same Steam account (an editor or dev instance alongside the published client) correlates with P2P joins to a live remote host failing rendezvous inside the engine's fixed ~3.5s connect budget. Close every second same-account instance before drawing any conclusion from a P2P connectivity failure.
On a track that crosses itself (a figure-eight, or any lap that revisits the same ground), a per-frame nearest-waypoint lookup jumps about half a lap at the crossing, because two points that sit close together can be arclength-distant. Drive lap position from a monotone cursor that only advances along the committed branch, do lateral math in a left-of-travel frame, and keep the branch-discrimination cone tighter than the branches' angular separation.
A static Func<>/Action<> field that holds a lambda survives hotload still pointing at the replaced assembly. Invoking it throws System.NotImplementedException: 'Unable to find matching substitution for a lambda method' in the next Play session, with a green compile. Guard the read with try/catch and self-heal, and re-assign the seam at session start.
Absolute-altitude biome thresholds tuned at one reference amplitude break when the slider moves -- the climate stack squashes or vanishes. Scale every altitude threshold by realized relief.
A per-run accumulator guard (budget, cooldown, or streak counter) that resets on a transient state flicker never actually fires, because the reset zeroes it faster than it can fill. Reset only on a signal that truly ends the episode (sustained air, real forward progress), never on a per-tick proxy the episode itself trips constantly.
A component that injects movement input for automated testing has to sit first in the scene hierarchy and write on both OnUpdate and OnFixedUpdate, or every movement phase reads zero while looking still works. Input.AnalogMove is computed once per frame from the movement actions before components run, so setting the Forward/Back/Left/Right actions afterward moves nothing that frame. Write Input.AnalogMove directly for held movement instead. Components run in scene order, so a harness under the controlled character runs after the stock mover and its writes land one frame late, then get wiped. Movement is consumed on the fixed step while looking is consumed on the per-frame update, so a harness that injects only from OnUpdate passes every look phase and reads exactly zero on every movement phase.
An ICameraModifier runs against every camera that draws, including the editor viewport camera during play-in-editor, not just the camera its component belongs to. A modifier that writes only some view fields makes the viewport obey some instructions and ignore others. A first-person modifier that set view.Rotation but not view.Position froze the editor viewport at the player aim, so a camera tool had its position honoured and its rotation discarded. A transform readback cannot see this, because the override happens later during view composition. Filter every modifier callback to the game view with a check on IsMainCamera.
System.Array.Clone() compiles clean in dotnet build but fails SB1000 in the editor: the headless build does not enforce the whitelist.
A scene-authored component cannot resolve, inside its own OnStart, a component that another component creates inside that other component's OnStart. Authored beside is not authored before, so the binder looks up the target before it exists, gets null, and holds that null for the whole session behind one warning line. The whole feature does nothing while every part it depends on works. Bind lazily instead: retry the lookup on a short interval until the target resolves, and warn once if it never does.
A camera that switches focus target or follow distance on a state change jerks even when position is exponentially smoothed. Smooth the target point and distance separately: the position lerp can't hide a discontinuous input.
When one character stands on another that uses a collider-less trace mover, standing can't happen physically -- you synthesise it by pinning the rider's feet to the carrier's head each fixed step. Seat at the VISUAL mesh head-top, not the physics capsule height, or the rider sinks into the carrier's head and reads as two merged characters.
A chase camera that reads a raw fixed-tick position field per render frame makes the player model sawtooth side-to-side. Read WorldPosition (context-sensitive interpolated getter) instead.
Components.Get<SkinnedModelRenderer>() only searches the same GameObject: a renderer on a child returns null, silently no-oping every Set() call.
A side-by-side follow slot must sit perpendicular to the leader's own heading, never perpendicular to the line between the two characters. The line-between-the-pair definition is circular, because it is built from where the follower already is, so each correction chases the last one and the pair collapses to single file. Measured error was 89.6 degrees from the line rule versus 0.2 degrees from the heading rule. Derive the slot axis from the leader's forward vector rotated 90 degrees.
Networking.HostStats and Connection.Stats read zero on a local loopback two-instance session, including Ping and ConnectionQuality, so a bandwidth instrument built on those fields latches zero even while traffic flows. Count bytes at the application layer instead.
Networking.CreateLobby is async: Networking.IsActive is still false on the same frame, so any branch on IsActive takes the wrong path. Gate on your own synchronous mode enum instead.
A projected Sandbox.Decal renders its colour as lit surface albedo, not an unlit overlay: a pale/pastel tint washes out to a barely-there stain on a bright surface.
A decimated heightfield mesher renders carved cliffs as alternating sawtooth teeth; consistent diagonals and majority-vote corner snapping don't fix the crest. Sort the four corner samples, find the largest gap, and average only the cluster above it.
A freshly SteamCMD-installed sbox-server.exe is not self-contained -- it needs the matching .NET runtime on the box, or it dies before any game logic.
A headless sbox-server running an unpublished local .sbproj boots and creates a lobby, but every joiner fails with 'Package local.<ident> wasn't found!': publish the package first (Hidden visibility is sufficient).
A stationary vertical sink at a chunk-corner seam defeats eject, mantle, and even hard-recover (the anchor itself can be buried under an overhang). The terminal escape is a top-out to the chunk surface.
A voxel/heightfield world usually carries two surfaces: a fine render mesh and a coarser collision heightfield. Procedural character-facing geometry (climb grips, standoff nodes) must be sampled from the collision surface, or it floats off the real face the character actually touches.
Dev-host streaming sends assemblies and compiled assets but NOT loose data files read via FileSystem.Mounted: a joining client that loads a raw JSON manifest builds differently, silently.
GetAllComponents<T> (and every Scene/GameObject component query) skips disabled components, so a distance cull that toggles ModelRenderer.Enabled and then re-queries each frame silently loses exactly the objects it just disabled: a census under-reports and a re-enable pass can never find its own objects. Capture renderer references once at spawn instead. ClothingContainer.Apply adds child renderers a self-only capture misses.
Jump() helpers clamp against rising velocity, eating the second impulse. Set Velocity.z directly for a reliable double jump.
GameObject.Destroy() in edit mode is deferred; a query fired right after returns the previous build's objects.
Two screenshots of byte-identical geometry differ by tens of percent because editor auto-exposure adapts over wall-clock frames: settle the viewport and exposure-normalize before comparing.
Every ICameraModifier in the scene runs against whichever camera is currently drawing, in ascending CameraOrder. A second camera inherits every modifier written for the primary one, even though the inspector shows its own transform as correct. Claim the view with your own modifier at a higher CameraOrder than anything else.
RenderType.Off only kills the shadow, not the draw: use Tags.Set("viewer", true) on the visual root plus camera RenderExcludeTags for correct first-person body hiding.
On a dressed citizen, GetAllComponents<SkinnedModelRenderer>().FirstOrDefault() can return a bone-merged clothing renderer (a hat) instead of the body, silently, with no null. A sequence probe then reports every clip absent. Select the body renderer explicitly.
When a world model is FP-hidden and replaced by a camera-attached viewmodel, cosmetics that must remain visible in FP need a separate copy parented into the viewmodel. Never re-tag the world copy visible.
Turning a composite physics actor into static scenery by destroying its Rigidbody silently kills every child system that keyed its own liveness off that body. Set MotionEnabled = false instead: the body stays alive, dependents keep ticking, and a kinematic body reads zero delta-v for free.
A [Sync(SyncFlags.FromHost)] field on a runtime-created singleton never replicates: the object needs NetworkSpawn, not just NetworkMode.Snapshot.
A runtime-generated world root torn down with GameObject.Destroy() keeps rendering in edit mode because the deferred queue isn't flushed: use DestroyImmediate and sweep all matching roots.
GameObject.Destroy() is deferred to the end of the frame, so an object you just destroyed still exists, still answers, and still holds any exclusive claim it owns (IsMainCamera, a singleton slot, a registry entry) for the rest of that frame. Clear exclusive claims on the outgoing owner BEFORE calling Destroy, and never read a leak census in the same frame as the teardown it checks.
Gamepad triggers DO have a public smooth 0..1 analog read via Input.GetAnalog(InputAnalog.LeftTrigger/RightTrigger). Named Input.config actions bound to triggers remain digital-only (on/off): use the direct analog surface for proportional control.
When a lobby query drives a connect, validate the exact lobby candidate you will connect to (not results[0]), or a peer can connect to a lobby it never validated (build-skew, wrong-host connect that no gate catches).
Per-cell dither or contour-wander in a greedy voxel mesher turns cliff skirts into vertical stripes: detect walls and key strata on raw height bands with dither neutralised.
Lifting a flat overlay clear of the ground is only half the rule. Overlays also z-fight each other, and a full-lap overlay on a self-crossing path z-fights itself, which no single lift value can fix.
An idempotent ground-snap jitters on curved terrain because the slope curves away within each step: ease the downward snap at a bounded glue rate.
A grounded wish-speed servo (MoveTowards) silently clamps any externally applied velocity (mantle carry, knockback, launch) to the wish target within a few ticks of ground contact -- boundary measurements read green while the effect is imperceptible.
A per-frame visual-smoothing offset computed from raw fixed-tick state double-smooths against the engine's built-in FixedUpdateInterpolation, producing a 50 Hz sawtooth that reads as model flicker on stepped terrain: delete the manual smoother or compute against interpolated state.
GameTask.RunInThreadAsync for parse/math; main thread only for engine objects; Yield every N items for loading UI.
Deferred Destroy() leaves the old code-built world overlapping the fresh one for a frame: use DestroyImmediate for teardown before rebuild, and pair it with a recipe hash for belt-and-suspenders join verification.
An Input.config action bound to an editor/host-reserved key (F1-F3, F7, F8, Escape) silently never fires in Play. The capture map also differs between editor and published client -- bind game hotkeys to plain letters and verify in both.
Input.Keyboard.Pressed fires again on the OS key auto-repeat, not only on the physical down edge, so a toggle written on Pressed flips twice for any held key. Use a true down-edge with previous-frame state (Down && !wasDown).
Input.Pressed (edge-trigger) is frame-scoped -- reading it in OnFixedUpdate drops presses on frames with no tick, or fires them twice on frames with multiple ticks. Level reads (Input.Down) are fine in OnFixedUpdate. A systematic 100% input failure usually points elsewhere -- instrument each hop.
A static Instance claimed behind if (!IsProxy) in OnStart grabs the host's character on a joining client: the client camera follows the wrong player forever.
A joining client's static join state (invite code, mode, attempt ID) gets wiped by the networked scene handoff: the bootstrap's OnEnabled resets statics before the join handshake uses them.
Always handle tr.StartedSolid: ignore that frame so an overlapped body can walk free; slide-trace the wish onto the hit plane.
sbox.exe -joinlocal +instanceid 1 gives you a real second peer against an editor host with no publish, no second Steam account, and no lobby discovery. It has no host selector: it joins whichever local editor is in PLAY MODE, so no other editor on the machine may be playing.
Lowering the sea level in a priority-flood water pass doesn't drain interior lakes: they're perched at their own spill surface. Use a per-basin depth gate and a land floor for below-sea valleys instead.
A world-space Sandbox.Decal is fixed in world space, so a splat spawned on a character projects onto empty air the instant they move. Parent the decal under the character, store the hit in a bone's local frame, and re-pin its world transform to that bone every frame so it re-projects onto the moving skinned mesh.
Per-instance Tint on a flat-color vmat (white PNG + g_vColorTint) rotates hue or crushes random instances to black: use scale/yaw jitter instead.
Input.AnalogLook returns zero (camera never turns) unless the cursor is locked via Mouse.Visibility = MouseVisibility.Hidden -- the deprecated Mouse.Visible = false does NOT lock it.
OfType<IInteractable> on GetAllComponents<Component> returns nothing: GetAllComponents answers off a concrete-type index, so enumerate concrete types (or a shared abstract base) and union them.
Component.Active is a real inherited member: naming your own bool Active silently shadows the engine's enabled flag. It is a CS0108 warning, not an error, so it ships easily; treat CS0108 as an error in code review.
The navmesh voxeliser erodes walkable surfaces by ceil(agentRadius/cellSize) whole cells on each side, so an opening narrower than that budget has no navmesh through it.
A batch of networking API facts, verified from the installed build's source and XML, that overturn common assumptions when you plan multiplayer code before it touches the compiler. LobbyInformation is a struct, so a null guard does not compile. Networking.Connections is deprecated in favour of Connection.All and emits CS0618. Connection.MaxChunkSize is internal, so game code hard-codes the 131072 value instead of referencing the symbol. NetworkAccessor exposes Owner, OwnerTransfer, and OrphanedMode as get-only, with AssignOwnership, SetOwnerTransfer, and SetOrphanedMode called after NetworkSpawn. Networking.TryConnectSteamId exists and is public. SB1000 whitelisting is assembly-level, so the real constraint on a networking call is public versus internal accessibility, not the whitelist. A green dotnet build does not mean an editor-green result for networked types.
A newly created .razor.scss file is not picked up by a running editor session: the panel component works but is unstyled until the next restart.
Disabling a collider does nothing for a hand-integrated kinematic controller: noclip must be a movement state that skips traces, gravity, and ground-snap entirely.
A trace-swept NPC with 'wall ahead, hold position this frame' freezes permanently when the desired direction is constant -- the identical trace hits the identical wall every frame.
A cached Component/GameObject reference guarded with == null still throws NullReferenceException after the object is destroyed: only IsValid() catches destroyed objects.
A singleton claimed in OnEnabled and nulled in OnDisabled traps any re-adopt poll that gates on Instance.IsValid(): the entity is never re-driven after a disable.
Rotation.FromYaw alone leaves a decal flat on the floor: build a rotation that aligns the thin axis to the surface normal, then offset along it.
Any cross-peer consumer (UI, host validators, scorers, range checks) that reads an owner-only simulation field gets frozen state on network proxies. Anchor off the replicated transform or redirect the field's getter.
[Sync] owner→proxies, IsProxy early-out; FromHost for shared truth. Every object carrying a [Sync] field has to be NetworkSpawned, singletons included.
A per-cell white-noise hash for terrain shade choice reads as a 50/50 checkerboard. Use a smooth low-frequency noise field instead, confining the hash to threshold-edge dithering.
A Sandbox.Decal projects onto runtime-built chunk meshes (ModelRenderers) and conforms to stepped/curved faces, confirmed live on voxel terrain.
A waypoint-graph builder that prunes only isolated nodes still ships fully-connected but walled-off pockets, because graph kept is not graph reachable. In one build, sealed arena corners stranded 27 of 524 nodes as three internal pockets, and about 5% of a max crowd spawned where it could never leave. Filter destination and spawn candidates by connected component, using a flood fill from a known-good root, not by per-node validity checks.
A published-build client join reloads the game assembly, wiping all statics. The reconstruct-not-reset fix from the scene-handoff case has nothing to reconstruct from unless join intent is persisted to disk.
Networking.QueryLobbies appends a hidden:0 filter unless you pass a truthy hidden key, so a Hidden lobby is structurally excluded from every ordinary query. The bool overload param is includeServers, not hidden-inclusion. And editor hosts force Private privacy, which no filter overrides.
Quit-to-menu tears the game scene down inside Networking.DisconnectScope, so networking is still active during game-side teardown. A local quit is distinguishable from a host disband, and teardown code can still send graceful goodbyes.
A collision rule that keys its direction on the struck object's own velocity is correct for the object that caused the hit and silently backwards for the object that got hit. A car rolling forward at 2 m/s and rammed from behind reads as moving forward, so it dents its nose instead of its rear. Derive the direction from the negated impulse delta-v, which points from the struck face inward for both bodies.
GetVertices/GetIndices flatten a multi-material compiled model into one buffer. Rebuilding with Materials[0] paints everything that first material (often black). Split indices by per-submesh counts and build one Mesh per range.
GameObject.Tags inherit to all descendants, so a CameraComponent.RenderExcludeTags exclusion on a tagged object culls every child renderer too: cosmetics attached as children silently vanish.
A 'walk up the parent chain to the first ancestor tagged X' resolver silently breaks the moment any later pass tags a descendant with the same tag: the walk stops on the nearest match, not the intended root. Resolve identity against an authoritative roster instead, especially when the tag is also used for physics filtering.
Rigidbody API verified across multiple projects: Gravity, MassOverride, Velocity, ApplyForce, and the recipe for a dynamic pushable prop. AutoSleep lives on PhysicsBody (set-only as of 26.08.05), which is now populated on the same tick the component is created.
Rotation.FromYaw(+angle) is a LEFT (CCW) turn. Get the sign right or steering, AI, and autopilot spiral the wrong way.
An [Rpc.Broadcast] called on a component whose GameObject is not networked executes locally and silently never crosses the wire -- no warning at any log level.
FlatBox/Prop/Deco/Wire helpers plus Obstacle records as plain data beat physics queries for build validity.
OnAwake runs synchronously inside Components.Create: set singletons there; derive from [Property] in OnStart after spawn helpers assign.
Sandbox.Decal self-seeds with Random.Shared.Int(10000) on enable: a decal spawned identically on two peers renders differently by default.
Sandbox.Decal instances are lightweight scene objects with no hard engine cap, so a persistent-paint system can hold thousands. The failure mode is a gradual frame-time sag driven by fill-rate/overdraw, never a crash cliff. Bound it with a convar-backed ring buffer and bench for your target hardware.
DTOs + one spawn path + deterministic static world: guard restored defaults; keep enums append-only.
A component with Enabled = false is invisible to GetAllComponents: search returns null even though the component exists.
SceneTraceResult.EndPosition on a sphere-radius sweep is the sphere's CENTRE at contact, not the contact point. Treating it as where the feet landed is off by a full radius. Derive the real contact position as from + (to - from) * Fraction in one helper. A related trap: snapping the sphere exactly tangent to the floor makes the next horizontal sweep intermittently report StartedSolid.
A trace-based ground controller flickers Ground/Air on seams, re-firing landing VFX mid-run. Require minimum air time before a JustLanded counts.
A SkinnedModelRenderer has no live SceneObject until the renderer first goes live. A Flags.CastShadows write at spawn time lands before the SceneObject exists, so it silently no-ops with no error and no warning. NPCs spawned with a shadow toggle off keep casting shadows anyway. Do not trust the write: read back the state that actually landed and retry on a staggered sweep across the following frames until the read-back matches the value you asked for.
When a continuous-float render surface sits over a quantized-step collision mesh, every quantize boundary becomes an invisible vertical wall the player hits but cannot see.
On the host, spawning a joiner's character clobbers the host's own camera-target singleton because IsProxy is false at Components.Create time: re-resolve the claim at the first OnFixedUpdate.
A Steam lobby survives its host switching to a different s&box game with stale metadata: a joiner connects and loads the wrong game's content.
For a swept sphere/hull trace, HitPosition is the true surface contact point while EndPosition is the swept shape's centre at impact. Seat surface effects at HitPosition.
An untyped or expression-bodied lambda on TextEntry.OnTextEdited fails with CS8917 or CS0029. Use an explicit param type plus a block body.
The joiner's engine-side connect budget after entering a Steam lobby is a fixed ~3 seconds that game code cannot extend: any host-side delay exhausts it fast, so join recovery must be a fresh retry, never a longer wait.
static Instance set in OnAwake, cleared in OnDestroy: everything reads Foo.Instance with null-guards, no inspector wiring.
A trace-based kinematic character controller has no collider component, so trigger volumes and ITriggerListener never fire against it. Use distance polling instead.
A hand-integrated trace mover has no collider component: ITriggerListener and OnTriggerEnter never fire. Use distance-polling instead.
The C# layer of s&box never closes per-pair Steam P2P sessions, so a host crash or task-kill poisons the pairwise transport state between two SteamIDs until Steam-side expiry clears it, typically minutes.
With a visible cursor (MouseVisibility.Visible), mouse buttons that land on a pointer-events panel never reach Input.Down/Pressed, and Input.Keyboard.Down("mouse2") is not a bypass. Raw Mouse.Delta stays live, though, creating a separate trap.
Literal spaces adjacent to a tag or @-expression boundary vanish in Razor markup: use a single interpolated string or CSS margin instead.
A zero-radius Scene.Trace.Ray passes straight through coarse voxel ModelColliders and returns Hit=false. Sweep a thin sphere (.Radius(...)) and it hits.