A component that implements a free-fly spectator camera. It takes input to rotate and move the Scene.Camera when NZGame.IsSpectator is true, preserving the camera's initial angles on activation and supporting faster movement, vertical motion, and clamping pitch.
using Sandbox;
namespace NZombies;
/// <summary>
/// Free-fly camera for Spectator mode.
///
/// ⚠️ SOMETHING HAS TO DRIVE THE VIEW ONCE THE BODY IS GONE. PlayerController
/// is what positions the camera normally, and disabling the player stops it —
/// so without this, entering spectator freezes the view wherever the player was
/// last standing and nothing moves it again.
///
/// No collision on purpose: a spectator that can be stopped by a wall is worse
/// than useless for checking a map you are building.
/// </summary>
public sealed class SpectatorCamera : Component
{
public static SpectatorCamera Instance { get; private set; }
[Property] public float Speed { get; set; } = 400f;
/// <summary>Hold Run to move faster. The map is big and 400u/s is a walk.</summary>
[Property] public float FastMultiplier { get; set; } = 3f;
Angles _look;
bool _wasActive;
protected override void OnAwake() => Instance = this;
protected override void OnDestroy() { if ( Instance == this ) Instance = null; }
protected override void OnUpdate()
{
var cam = Scene.Camera;
if ( !cam.IsValid() ) return;
if ( !NZGame.IsSpectator )
{
_wasActive = false;
return;
}
// ⚠️ Adopt the camera's CURRENT angles on the frame spectating starts,
// not a zeroed default. Snapping the view to face world-forward the
// instant you enter spectator is disorienting and makes it feel broken.
if ( !_wasActive )
{
_wasActive = true;
_look = cam.WorldRotation.Angles();
}
_look += Input.AnalogLook;
_look.pitch = _look.pitch.Clamp( -89f, 89f );
_look.roll = 0f;
cam.WorldRotation = _look.ToRotation();
var speed = Speed * (Input.Down( "Run" ) ? FastMultiplier : 1f);
var move = Input.AnalogMove;
// AnalogMove is forward/left in LOCAL space — rotate it by where we are
// looking, or the controls only make sense facing one direction.
var wish = cam.WorldRotation * new Vector3( move.x, move.y, 0f );
// Up and down on jump/duck, since a free camera without vertical is
// stuck at whatever height it inherited.
if ( Input.Down( "Jump" ) ) wish += Vector3.Up;
if ( Input.Down( "Duck" ) ) wish += Vector3.Down;
if ( !wish.IsNearlyZero() )
cam.WorldPosition += wish.Normal * speed * Time.Delta;
}
}