A Player Component that implements a creative-mode noclip (fly-through-walls) for map authoring. It toggles via V or console, disables physics/input/collider while active, moves the player transform directly based on eye angles and input, and restores the saved state when turned off or destroyed.
using Sandbox;
using System;
using System.Linq;
namespace NZombies;
/// <summary>
/// NOCLIP — fly through the map while building it. V toggles it, creative only.
///
/// Authoring a map means getting to places the map does not let a player go: the
/// top of a doorway to click a wall's height, the far side of a barrier you just
/// sealed, a rooftop you are fencing off. Walking there is often impossible by
/// construction — you are building the very thing that stops you.
///
/// ⛔ CREATIVE ONLY, AND FORCED OFF ON THE WAY OUT. A player who flies in
/// Survival is not playing nZombies, and leaving the mode with the collider
/// still disabled would hand them a body that falls through the world.
/// </summary>
public sealed class Noclip : Component
{
/// <summary>Units per second. 600 is a brisk walk-through-walls; the sprint
/// key multiplies it.</summary>
[Property] public float Speed { get; set; } = 600f;
/// <summary>How much faster the run key makes it.</summary>
[Property] public float SprintMultiplier { get; set; } = 3f;
/// <summary>Flying right now?</summary>
public bool Flying { get; private set; }
// ⚠️ WHAT THE CONTROLLER LOOKED LIKE BEFORE WE TOUCHED IT. Restoring
// hardcoded values instead would quietly overwrite anything else that had
// configured the body — and "noclip once, then gravity is subtly wrong
// forever" is a horrible bug to track down later.
bool _savedGravity, _savedMotion, _savedInput, _savedCollider, _saved;
/// <summary>The player's noclip, or null. For the console command.</summary>
public static Noclip Find()
{
var p = NZPlayer.Local;
return p?.Components.GetOrCreate<Noclip>();
}
protected override void OnUpdate()
{
// ⛔ CHECKED EVERY FRAME, not hooked onto the mode change. NZGame.Mode is
// static and survives a play restart, and PlayerPresence moves the body
// on its own — so there is no single event that reliably means "creative
// ended". Asking every frame cannot be missed.
if ( !NZGame.IsCreative )
{
if ( Flying ) SetFlying( false );
return;
}
// ⚠️ Not while a menu is up. The Q menu and the lobby both have text
// fields, and typing a config name containing a "v" would otherwise
// toggle noclip under the player. Mouse visibility is the one signal both
// menus already set, so this needs no new seam into the razor types.
if ( Input.Keyboard.Pressed( "V" ) && Mouse.Visibility != MouseVisibility.Visible )
SetFlying( !Flying );
if ( Flying ) Fly();
}
/// <summary>
/// Drive the transform directly.
///
/// ⚠️ NOT via WishVelocity. The controller's movement is ground-based — it
/// would still refuse to go up, still stick to surfaces, and still be the
/// thing deciding where we end up. Writing the position is the whole point:
/// nothing gets a vote.
/// </summary>
void Fly()
{
var c = Components.Get<PlayerController>();
if ( !c.IsValid() ) return;
// ⚠️ From the EYE ANGLES, not the body's rotation. The body yaws to
// follow the camera but never pitches, so using it would give a noclip
// that cannot fly up or down at the one moment you want to — lining up
// the top of a doorway.
// ⛔ NOT WHILE A CURSOR MENU IS UP. `Input.AnalogMove` holds its last value while the cursor
// is visible, so a key released with a panel open stays held as far as this is concerned —
// and this writes the position directly every frame, so the player keeps flying in that
// direction until noclip is switched off. Reported exactly that way.
//
// ⚠️ THE SAME TEST THE V KEY ABOVE ALREADY USES, and the same one `NZPlayer` uses to keep
// the scroll wheel out of the wallbuy picker: one check covers DevMenu, LobbyMenu, the
// tuner panels and the next menu for free. ⚠️ `Auto` is NOT `Visible` — that is the in-game
// state menus restore on close, so testing for it would disable noclip permanently.
if ( Mouse.Visibility == MouseVisibility.Visible ) return;
var rot = c.EyeAngles.ToRotation();
var move = Input.AnalogMove;
var dir = (rot.Forward * move.x) + (rot.Left * move.y);
if ( Input.Down( "Jump" ) ) dir += Vector3.Up;
if ( Input.Down( "Duck" ) ) dir += Vector3.Down;
// ⛔ NOTHING ELSE MAY CARRY THE PLAYER WHILE FLYING, AND SAYING SO EVERY FRAME IS THE FIX.
// `Detach` zeroes the velocity and disables motion ONCE, at the moment noclip starts — so
// anything that writes velocity afterwards (a boss's pull, a shove, a residual integration
// on a body whose `MotionEnabled` did not take) is carried for as long as noclip lasts and
// stops the instant it ends, because `Reattach` zeroes again. That is exactly the reported
// shape: "I keep moving in that direction until I drop noclip".
//
// ⚠️ CHEAP AND UNCONDITIONAL. Two writes a frame while a dev tool is on, against a class of
// bug that needs the one component that moved you to be identified first.
var body = c.Body;
if ( body.IsValid() )
{
body.Velocity = Vector3.Zero;
body.AngularVelocity = Vector3.Zero;
}
c.WishVelocity = Vector3.Zero;
if ( dir.IsNearZeroLength ) return;
float speed = Speed * (Input.Down( "Run" ) ? SprintMultiplier : 1f);
WorldPosition += dir.Normal * speed * Time.Delta;
}
/// <summary>Turn it on or off. Safe to call with it already in that state.</summary>
public void SetFlying( bool on )
{
if ( Flying == on ) return;
var c = Components.Get<PlayerController>();
if ( !c.IsValid() )
{
Log.Warning( "[nz] noclip: no PlayerController on this player" );
return;
}
if ( on ) Detach( c );
else Reattach( c );
Flying = on;
Log.Info( $"[nz] noclip {(on ? "ON — WASD + jump/duck, run to go faster" : "off")}" );
}
/// <summary>
/// Take the body out of the physics simulation.
///
/// Four separate things, because each one alone leaves a different half of
/// the problem: gravity still pulls, the controller still steers, physics
/// still owns the transform, and the collider still stops you at a wall.
/// </summary>
void Detach( PlayerController c )
{
var body = c.Body;
var col = c.ColliderObject;
_savedGravity = body.IsValid() && body.Gravity;
_savedMotion = body.IsValid() && body.MotionEnabled;
_savedInput = c.UseInputControls;
_savedCollider = col.IsValid() && col.Enabled;
_saved = true;
if ( body.IsValid() )
{
// Zeroed BEFORE motion is disabled — a body frozen mid-fall keeps its
// velocity and hands it straight back the moment noclip ends, so you
// would drop out of the sky at whatever speed you were falling when
// you turned it on.
body.Velocity = Vector3.Zero;
body.AngularVelocity = Vector3.Zero;
body.Gravity = false;
body.MotionEnabled = false;
}
c.UseInputControls = false;
// ⛔ AND THE CONTROLLER'S OWN WISH, WHICH THE BODY'S VELOCITY IS NOT.
// Zeroing body.Velocity stops the PHYSICS momentum; the controller keeps a
// separate WishVelocity — where it WANTS to go — and `UseInputControls =
// false` only stops that being refreshed from input. It does not clear it.
// So toggling noclip while walking froze the last wish in place and the
// controller kept applying it forever: "if i am moving the moment i click
// noclip, i maintain the movement permanently".
//
// ⚠️ Reported as a NOCLIP bug and it is really a handover bug — the same
// shape as the vault's transform ownership. Two systems both believe they
// are moving the body, and taking one out of the loop is not the same as
// telling it to stop.
c.WishVelocity = Vector3.Zero;
if ( col.IsValid() ) col.Enabled = false;
}
/// <summary>Put it back exactly as it was.</summary>
void Reattach( PlayerController c )
{
if ( !_saved ) return;
// ⚠️ Cleared on the way OUT as well. Flying moves the transform directly,
// so by the time control is handed back the old wish describes a direction
// from wherever noclip started — and the player would set off in it the
// instant input control returned.
c.WishVelocity = Vector3.Zero;
var body = c.Body;
var col = c.ColliderObject;
if ( col.IsValid() ) col.Enabled = _savedCollider;
if ( body.IsValid() )
{
body.Gravity = _savedGravity;
body.MotionEnabled = _savedMotion;
// Land, do not launch. Whatever velocity the body had before it was
// frozen is meaningless now that we have flown somewhere else.
body.Velocity = Vector3.Zero;
body.AngularVelocity = Vector3.Zero;
}
c.UseInputControls = _savedInput;
_saved = false;
}
// ⛔ BOTH OF THESE, OR A DISABLED NOCLIP LEAVES A BROKEN PLAYER. The state
// this component changes lives on OTHER components, so if it stops running
// while flying — hotload, component toggled off, player destroyed — nothing
// else will ever put gravity and the collider back, and the body falls
// through the world with no clue as to why.
protected override void OnDisabled() => SetFlying( false );
protected override void OnDestroy() => SetFlying( false );
// ── console ──────────────────────────────────────────────────────────────
/// <summary>
/// `nz_noclip_report` — every input and every carrier that could be moving you, right now.
/// </summary>
///
/// ⚠️ IT EXISTS BECAUSE TWO GUESSES AT THIS WERE WRONG. A drifting player can be the analog
/// stick latched by a visible cursor, a physics body that kept its velocity, a `WishVelocity`
/// nobody cleared, or the flags that were supposed to stop all three. They are indistinguishable
/// from the outside and each needs a different fix. Run it while drifting.
[ConCmd( "nz_noclip_report" )]
public static void ReportCmd()
{
var p = NZPlayer.Local;
if ( !p.IsValid() ) { Log.Warning( "[nz-noclip] no player" ); return; }
var n = p.Components.Get<Noclip>( FindMode.EverythingInSelfAndDescendants );
var c = p.Components.Get<PlayerController>( FindMode.EverythingInSelfAndDescendants );
var body = c.IsValid() ? c.Body : null;
Log.Info( $"[nz-noclip] flying {(n.IsValid() && n.Flying)}"
+ $" · cursor {Mouse.Visibility}"
+ $" · analog {Input.AnalogMove}" );
Log.Info( $"[nz-noclip] wish {(c.IsValid() ? c.WishVelocity.ToString() : "-")}"
+ $" · input {(c.IsValid() ? c.UseInputControls.ToString() : "-")}"
+ $" · body {(body.IsValid() ? $"vel {body.Velocity} motion {body.MotionEnabled} gravity {body.Gravity}" : "none")}" );
}
/// <summary>Toggle noclip, or set it: nz_noclip [0|1]. The V key equivalent.</summary>
[ConCmd( "nz_noclip" )]
public static void Toggle( int on = -1 )
{
var n = Find();
if ( n is null ) { Log.Warning( "[nz] no player" ); return; }
if ( !NZGame.IsCreative )
{
Log.Warning( "[nz] noclip is creative only — nz_mode creative first" );
return;
}
n.SetFlying( on < 0 ? !n.Flying : on != 0 );
}
/// <summary>How fast noclip flies: nz_noclip_speed <units/sec> [sprint].</summary>
[ConCmd( "nz_noclip_speed" )]
public static void SetSpeed( float speed = 600f, float sprint = 3f )
{
var n = Find();
if ( n is null ) { Log.Warning( "[nz] no player" ); return; }
n.Speed = MathF.Max( 1f, speed );
n.SprintMultiplier = MathF.Max( 1f, sprint );
Log.Info( $"[nz] noclip speed {n.Speed:0}/s, {n.SprintMultiplier:0.#}x sprinting" );
}
}