field-guide / writing-gameplay
04/10
04task lane

Writing gameplay

Components, movement, save/load, owner-simulated networking.

guides
Agent animation authoring: numbers for correctness, screenshots for the read, the owner for taste
The division of labor for authoring s&box animation with agents: the owner authors hero poses in the Animate studio, agents build the motion around them against a numeric definition of done, and the owner taste-gates frames, never live sessions. Covers the pose-to-pose synthesis pattern, numeric acceptance, and the sequence-time traps that break freeze-at-end logic.
Character-mounted assets and live-tuning their placement
The method for hanging custom models off the player rig -- gadgets, backpacks, held items -- with three mount styles off one skeleton, and a live slider panel that dumps C#-ready constants for paste-back.
Decals: projection, determinism, and performance in s&box
The practical patterns for Sandbox.Decal: orienting the projection box to a hit normal, keeping a decal byte-identical across multiplayer peers, making paint follow an animated character, and scaling to thousands of decals without a performance cliff, plus the pitfalls that catch each one.
Delta-log save for deterministic procedural worlds
When the world is deterministically generated from a spec, a save is {version, spec, edits[]}, never a geometry snapshot. Load = validate → regenerate → replay edits. Same format works for save, co-op edit sync, and late-join replay.
First-person viewmodel and the third-person/first-person camera split
The method for a CS/Rust-style two-view split off one player rig: third person shows the world model, first person hides it and shows a camera-attached viewmodel (arms + held item). Covers hysteresis, tag-based per-view exclusion, the ShadowRenderType correction, and viewmodel composition.
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.
P2P peer-hosted servers: from working co-op code to a friend actually joining
The operational recipe for shipping player-hosted (P2P) multiplayer in s&box (lobby mechanics, invite codes, the join handshake liveness contract, replication traps, and the three-rung testing ladder), covering the layer the official docs don't document and where live multi-peer sessions actually break.
Parkour movement: the trace-mover traversal kit
The method for feel-first traversal on a kinematic trace-based character: coyote-time jumps, double jump, mantle, slide, wall run, node climbing, and rope swinging, plus the ground-contact quality work that makes all of it read smooth.
Part-kit assets: manifest-driven multi-part assembly
The higher-order asset layer for kits of parts (vehicles, procedural buildings): the generator emits a manifest next to the meshes, code consumes ONLY the manifest, pivots sit at joints, collision comes from metadata, and assembly is transactional.
Proximity voice chat: the built-in Sandbox.Voice component
The complete method for wiring s&box's built-in Voice component into a networked game (push-to-talk, 3D positional playback, custom falloff curves, speaking indicators, and lip-sync), with no third-party voice SDK.
Ragdoll physics: scripted-rig NPC crumple without authored collapse clips
The method for making a procedurally-rigged NPC crumple via engine physics: author a physics skeleton in the vmdl, toggle Sandbox.ModelPhysics at runtime, and hand bone control back for stand-up. No authored collapse clips needed.
Runtime terrain meshing: chunked greedy voxel/heightfield terrain
The method for large runtime-generated terrain in s&box: a persistent cell grid as the single source of truth, chunked greedy meshing (tops + skirts), collision decoupled from render grain, palette-atlas UVs, and dirty-chunk remesh for a live terrain brush.
Vehicle audio -- engine loops, RPM crossfade, slip SFX
The method for vehicle engine audio that doesn't screech: two-layer RPM crossfade with narrow pitch bands, code-side looping, slip-driven skid feedback, and the persistent ConVar trap that ate two listen tests.
Vehicle physics: the slip-curve raycast-wheel stack
The proven architecture for driving games in s&box: raycast wheels on a single chassis Rigidbody, substepped slip-ratio/slip-angle tire physics with peaked curves, a torque-curve drivetrain, layered assists, and arcade dials on top of a sim core.
World scale and coordinate limits: units, the 20k folklore, and float precision from origin
What "a 20k world" actually means (units vs meters vs Source-1 map folklore, and they are ~1550x apart), why s&box has no VBSP-style coordinate wall, and the real modern ceiling: fp32 position precision that coarsens with distance from origin. Measured numbers, the lore separated from the measurements, and the reproduction method.
26.07.22
"[Sync] component created after NetworkSpawn silently never replicates"

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).

26.07.15a
"A -joinlocal dev-host client CAN read the host's Networking.SetData lobby metadata"

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.

26.08.05
"A -joinlocal peer joins whoever owns 127.0.0.1:55333, not whoever is in play mode"

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.

26.07.15
"A -joinlocal test peer presents an empty invite code, so a wire-verified code gate rejects it"

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.

26.08.05
"A baked navmesh does not follow a moving door"

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.

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

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.

26.08.05
"A component added at runtime updates after every authored one"

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.

26.07.22
"A component that reads Input in OnUpdate fires on every instance at once"

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.

26.07.22
"A console or editor text field with keyboard focus starves every Input read in game code"

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.

26.07.18
"A decal on a character paints bare skin but skips clothing: projection depth must span the shell"

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.

26.07.22
"A dedicated server never creates a Steam lobby, so lobby-based join code does nothing"

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.

26.07.22
"A give-up timer gated on a control signal never fires when the control pulses through its reset value"

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.

26.07.22
"A public Reset() on a Component silently shadows the engine's Component.Reset()"

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.

26.08.05
"A Scene.Trace only reports a model hitbox when it opts in with UseHitboxes(true)"

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.

26.07.15a
"A second s&box instance on the same Steam account can make P2P joins fail rendezvous"

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.

26.07.22
"A self-crossing track breaks nearest-waypoint lap position; drive it from a monotone cursor"

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.

26.07.15a
"A static Func/Action lambda orphaned by hotload throws NotImplementedException"

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.

26.07.15a
"Absolute biome altitude thresholds break when height amplitude changes"

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.

26.07.15a
"An accumulator guard that resets on a transient flicker never fires"

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.

26.08.05
"An automated input harness reads zero movement unless it runs first and injects on both steps"

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.

26.08.05
"An ICameraModifier writes to every camera that draws, including the editor viewport"

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.

26.07.08e
"Array.Clone() blocked by whitelist but dotnet build misses it"

System.Array.Clone() compiles clean in dotnet build but fails SB1000 in the editor: the headless build does not enforce the whitelist.

26.08.05
"Bind lazily to a component created in OnStart, not from your own OnStart"

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.

26.07.08e
"Camera focus target switch causes jerk: smooth the target, not just the follower"

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.

26.07.15a
"Character stacking: seat at the visual mesh head-top, not the physics capsule height"

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.

26.07.15a
"Chase camera using raw fixed-tick position causes model sawtooth jitter"

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.

26.07.15a
"Components.Get misses a SkinnedModelRenderer on a child object"

Components.Get<SkinnedModelRenderer>() only searches the same GameObject: a renderer on a child returns null, silently no-oping every Set() call.

26.07.22
"Compute a follow slot from the leader's heading, not the line between the pair"

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.

26.07.22
"Connection.Stats reads zero on a local two-instance session"

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.

26.07.08e
"CreateLobby is async: IsActive is still false on the same frame"

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.

26.07.15a
"Decal colour is lit surface albedo: pale tints wash out"

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.

26.07.15a
"Decimated heightfield cliffs render as sawtooth teeth: snap the above-gap cluster"

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.

26.07.08e
"Dedicated server dies at launch with 'You must install .NET to run this application'"

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.

26.07.08e
"Dedicated server with an unpublished package: clients can't join"

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).

26.07.08e
"Deep vertical terrain sink defeats lateral eject and mantle: escalate through hard-recover to a top-out"

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.

26.07.15a
"Derive procedural character geometry from the collision surface, not the render mesh"

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.

26.07.15a
"Dev-host streaming skips loose data files, breaking deterministic sync"

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.

26.07.22
"Disabled components vanish from GetAllComponents: hold spawn-time references"

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.

26.07.08e
"Double jump doesn't work with Jump(): set Velocity.z directly"

Jump() helpers clamp against rising velocity, eating the second impulse. Set Velocity.z directly for a reliable double jump.

26.07.08e
"Edit-mode Destroy() is deferred: scene queries return stale objects"

GameObject.Destroy() in edit mode is deferred; a query fired right after returns the previous build's objects.

26.07.08e
"Editor auto-exposure makes screenshot comparisons report false positives"

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.

26.08.05
"Every ICameraModifier runs against the drawing camera, not the one it was authored for"

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.

26.07.08e
"First-person hide: use the viewer tag, not RenderType"

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.

26.07.22
"FirstOrDefault() on SkinnedModelRenderer can grab clothing, not the body"

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.

26.07.15a
"FP cosmetics need a parallel viewmodel copy, not a re-tagged world child"

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.

26.07.22
"Freeze a physics actor by going kinematic, not by destroying its Rigidbody"

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.

26.07.08e
"FromHost field on a runtime singleton does not replicate without NetworkSpawn"

A [Sync(SyncFlags.FromHost)] field on a runtime-created singleton never replicates: the object needs NetworkSpawn, not just NetworkMode.Snapshot.

26.07.08e
"GameObject.Destroy keeps rendering in edit mode: deferred queue not processed between regenerations"

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.

26.07.22
"GameObject.Destroy() is deferred: a destroyed object still holds its exclusive claims for the rest of the frame"

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.

26.07.08e
"Gamepad triggers have a public analog read: Input.GetAnalog works"

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.

26.07.15a
"Gate the exact lobby you connect to, not query result [0]"

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).

26.07.08e
"Greedy voxel mesher produces vertical stripes on cliff faces instead of horizontal strata"

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.

26.07.22
"Ground overlays z-fight each other, and a full-lap overlay z-fights itself"

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.

26.07.08e
"Ground snap pops on rolling slopes: rate-limit the downward snap"

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.

26.07.15a
"Grounded wish-speed servo silently destroys applied velocity"

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.

26.07.08e
"Hand-rolled visual smoother fights engine FixedUpdateInterpolation: 50 Hz model flicker"

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.

26.07.08e
"Heavy work without frame hitches"

GameTask.RunInThreadAsync for parse/math; main thread only for engine objects; Yield every N items for loading UI.

26.07.15a
"Idempotent world rebuild needs DestroyImmediate, not Destroy"

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.

26.07.08e
"Input.config action on an editor-reserved key silently never fires in Play"

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.

26.08.05
"Input.Keyboard.Pressed refires on OS key auto-repeat: toggles flip twice"

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).

26.07.15a
"Input.Pressed edges drop or double-fire inside OnFixedUpdate"

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.

26.07.15a
"Joining client's singleton claim grabs the host's character"

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.

26.07.08e
"Joining client's static state wiped by networked scene handoff"

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.

26.07.08e
"Kinematic movement that doesn't get stuck"

Always handle tr.StartedSolid: ignore that frame so an overlapped body can walk free; slide-trace the wish onto the hit plane.

26.07.22
"Local two-peer multiplayer testing with -joinlocal"

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.

26.07.08e
"Lowering sea level doesn't drain interior lakes: depression lakes are perched"

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.

26.07.18
"Make a Sandbox.Decal follow an animated character by re-pinning it to a bone each frame"

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.

26.07.08e
"ModelRenderer.Tint on flat-color vmats causes purple/black corruption"

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.

26.07.08e
"Mouse-look camera reads zero from Input.AnalogLook unless cursor is locked"

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.

26.07.22
"My interface scan returns nothing at runtime"

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.

26.07.08e
"Naming a property 'Active' shadows Component.Active"

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.

26.08.05
"Navmesh openings erode by whole cells against agent radius"

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.

26.07.22
"Networking API facts to check against the installed build before you compile"

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.

26.07.08e
"New .razor.scss files are not applied until the editor restarts"

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.

26.07.08e
"Noclip in a trace-based kinematic controller is a state, not a collider toggle"

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.

26.07.15a
"NPC steer loop freezes forever against a wall"

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.

26.07.08e
"Null check on a destroyed GameObject still NREs: use IsValid()"

A cached Component/GameObject reference guarded with == null still throws NullReferenceException after the object is destroyed: only IsValid() catches destroyed objects.

26.07.15a
"OnDisabled nulls a singleton, blocking re-adoption forever"

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.

26.07.08e
"Orient a flat decal box to the hit normal"

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.

26.07.08e
"Owner-only simulation field reads frozen state on network proxies"

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.

26.07.08e
"Owner-simulated networking"

[Sync] owner→proxies, IsProxy early-out; FromHost for shared truth. Every object carrying a [Sync] field has to be NetworkSpawned, singletons included.

26.07.08e
"Per-cell white noise hash gives salt-and-pepper terrain instead of organic shade patches"

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.

26.07.18
"Projected decals conform to runtime model renderers"

A Sandbox.Decal projects onto runtime-built chunk meshes (ModelRenderers) and conforms to stepped/curved faces, confirmed live on voxel terrain.

26.07.22
"Prune a waypoint graph by connected component, not by isolated nodes"

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.

26.07.08e
"Published-build client join reloads the assembly, wiping all statics"

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.

26.07.15
"QueryLobbies never finds your Hidden lobby"

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.

26.07.15a
"Quit-to-menu teardown runs inside DisconnectScope: networking is still active"

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.

26.07.22
"Read collision direction from the impulse, not the struck object's velocity"

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.

26.07.15a
"Rebuilding a multi-material model loses submesh materials"

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.

26.07.15a
"RenderExcludeTags on a tagged parent culls all child renderers too"

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.

26.07.22
"Resolve identity by registry, not by walking the tag chain"

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.

26.08.05
"Rigidbody component API"

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.

26.07.08e
"Rotation.FromYaw is counter-clockwise"

Rotation.FromYaw(+angle) is a LEFT (CCW) turn. Get the sign right or steering, AI, and autopilot spiral the wrong way.

26.07.08e
"Rpc.Broadcast on a non-networked GameObject runs locally only -- no warning"

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.

26.07.08e
"Runtime world-building helpers"

FlatBox/Prop/Deco/Wire helpers plus Obstacle records as plain data beat physics queries for build validity.

26.07.08e
"s&box component lifecycle in practice"

OnAwake runs synchronously inside Components.Create: set singletons there; derive from [Property] in OnStart after spawn helpers assign.

26.07.15a
"Sandbox.Decal renders differently on each peer due to random self-seed"

Sandbox.Decal self-seeds with Random.Shared.Int(10000) on enable: a decal spawned identically on two peers renders differently by default.

26.07.18
"Sandbox.Decal scales to thousands: the bound is fill rate, not a hard cap"

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.

26.07.08e
"Save/load without drift"

DTOs + one spawn path + deterministic static world: guard restored defaults; keep enums append-only.

26.07.08e
"Scene.GetAllComponents skips disabled components"

A component with Enabled = false is invisible to GetAllComponents: search returns null even though the component exists.

26.07.22
"SceneTrace sphere sweep EndPosition is the sphere centre, not the feet"

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.

26.07.08e
"Single-tick ground-check flicker machine-guns the landing squash"

A trace-based ground controller flickers Ground/Air on seams, re-firing landing VFX mid-run. Require minimum air time before a JustLanded counts.

26.07.22
"SkinnedModelRenderer.SceneObject is null until the renderer goes live, so a spawn-time CastShadows write no-ops"

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.

26.07.15a
"Smooth render mesh over quantized collision creates invisible curbs"

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.

26.07.08e
"Spawning a joiner's character clobbers the host's camera target"

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.

26.07.15a
"Steam lobby metadata survives host game switch"

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.

26.07.15a
"Swept trace: HitPosition is the surface, EndPosition is the shape centre"

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.

26.07.15a
"TextEntry.OnTextEdited needs an explicit-type block lambda"

An untyped or expression-bodied lambda on TextEntry.OnTextEdited fails with CS8917 or CS0029. Use an explicit param type plus a block body.

26.07.15a
"The P2P join handshake has a fixed ~3-second connect budget"

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.

26.07.08e
"The singleton pattern that removes reference-wiring"

static Instance set in OnAwake, cleared in OnDestroy: everything reads Foo.Instance with null-guards, no inspector wiring.

26.07.08e
"Trace-based kinematic controllers don't fire trigger volumes"

A trace-based kinematic character controller has no collider component, so trigger volumes and ITriggerListener never fire against it. Use distance polling instead.

26.07.08e
"Trace-based kinematic controllers don't fire triggers"

A hand-integrated trace mover has no collider component: ITriggerListener and OnTriggerEnter never fire. Use distance-polling instead.

26.07.15a
"Unclean host exit poisons the Steam P2P transport between two peers"

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.

26.07.22
"Visible cursor blocks game mouse input: no raw bypass exists"

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.

26.07.08e
"Whitespace next to a Razor tag or expression boundary collapses to nothing"

Literal spaces adjacent to a tag or @-expression boundary vanish in Razor markup: use a single interpolated string or CSS margin instead.

26.07.15a
"Zero-radius Scene.Trace.Ray slips through coarse voxel ModelColliders"

A zero-radius Scene.Trace.Ray passes straight through coarse voxel ModelColliders and returns Hit=false. Sweep a thin sphere (.Radius(...)) and it hits.

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