Player camera and visibility logic for a networked player pawn. Manages the first-person CameraComponent (position, rotation, FOV, near/far), handles special camera states (spectating, caught by monster, death/ragdoll), hides the local body in first person, and applies a spectator outline glow for other players.
using Sandbox;
namespace BrickJam;
public sealed partial class Player
{
/// <summary>
/// First-person camera. Lives on a child GameObject of the player prefab and is only
/// enabled for the owning (non-proxy) client. Replaces the legacy <c>FrameSimulate</c>
/// camera code which manipulated the global <c>Camera</c>.
/// </summary>
[Property] public CameraComponent Camera { get; set; }
protected override void OnStart()
{
SetLocal();
// Footstep anim events come from the body's animgraph; route them to our handler.
if ( Body.IsValid() )
Body.OnFootstepEvent = OnFootstepEvent;
// Only the owner drives/sees through their own camera.
if ( Camera.IsValid() )
Camera.GameObject.Enabled = !IsProxy;
// The owner loads its stored save and pushes it to the host.
if ( !IsProxy )
SendStoredSaveToServer();
}
private void UpdateCamera()
{
if ( !Camera.IsValid() )
return;
// While spectating (dead / late joiner) the connection's spectator pawn owns the view, so turn
// our own camera off and let it take over.
if ( Spectating )
{
Camera.Enabled = false;
return;
}
if ( !Camera.Enabled )
Camera.Enabled = true;
if ( CameraTarget.IsValid() )
{
// Caught: turn the camera to face the monster that grabbed us, looking at its head bone (legacy
// CameraTarget death cam). Falls back to the monster's head height if its model has no "head" bone.
var headPos = CameraTarget.Body.IsValid() && CameraTarget.Body.TryGetBoneTransform( "head", out var headTx )
? headTx.Position
: CameraTarget.WorldPosition + Vector3.Up * CameraTarget.CollisionHeight;
var lookRot = Rotation.LookAt( headPos - Camera.WorldPosition );
// Camera stays at our own eyes (same "eyes" attachment EyePosition uses), nudged back/down along
// the look direction so the near plane stays off our face (legacy parity).
var eyes = Body.IsValid() && Body.GetAttachment( "eyes" ) is { } a
? a.Position
: WorldPosition + Vector3.Up * (IsCrouching ? 28f : 64f);
Camera.WorldPosition = eyes - lookRot.Up * 2f - lookRot.Forward * 3f;
Camera.WorldRotation = lookRot;
}
else if ( !MovementLocked )
{
Camera.WorldRotation = InputRotation;
Camera.WorldPosition = EyePosition;
}
else
{
// Death/ragdoll cam - pull back.
var target = WorldPosition - Velocity.WithZ( 0 ).Normal * 30f + Vector3.Up * 50f;
Camera.WorldRotation = Rotation.LookAt( WorldPosition + Vector3.Up * 16f - Camera.WorldPosition );
Camera.WorldPosition = Vector3.Lerp( Camera.WorldPosition, target, Time.Delta * 10f );
}
Camera.ZNear = 2f;
Camera.ZFar = 4096f;
Camera.FieldOfView = Screen.CreateVerticalFieldOfView( 60f );
}
/// <summary>
/// Runs for EVERY pawn on EVERY client (see Player.OnUpdate): hide OUR OWN body in first person, and force
/// every other player's body visible. The force-visible is essential - the host's body RenderType is
/// replicated to clients on spawn, so a host who set ShadowsOnly locally would otherwise stay invisible to
/// clients; setting proxies to On every frame corrects that locally on each client.
/// </summary>
private void UpdateBodyVisibility()
{
// Hide our own body in first person (alive). ALSO hide a player we're currently SPECTATING in first
// person - the local spectator views out through their eyes, so their body+mask shouldn't be in shot.
// Everyone else stays visible (force-On overrides the spawn-replicated host RenderType, see above).
var spectatedByLocal = Spectator.Local.IsValid() && Spectator.Local.Following == this;
SetOwnBodyHidden( (!IsProxy && !MovementLocked) || spectatedByLocal );
}
/// <summary>
/// Set our body+mask render type. <c>RenderType</c> is non-synced, so this only affects what THIS client
/// renders. <c>ShadowsOnly</c> hides the model but keeps the shadow.
/// </summary>
private void SetOwnBodyHidden( bool hidden )
{
var renderType = hidden ? ModelRenderer.ShadowRenderType.ShadowsOnly : ModelRenderer.ShadowRenderType.On;
if ( Body.IsValid() )
Body.RenderType = renderType;
if ( Mask.IsValid() )
Mask.RenderType = renderType;
}
private HighlightOutline spectatorGlow;
/// <summary>
/// Runs on every client (see Player.OnUpdate): when the LOCAL client is spectating (has a Spectator pawn)
/// and isn't currently watching this player in first person, outline this living player in their slot
/// colour - visible through walls - so spectators can find everyone. Scene-System port of the legacy
/// per-frame <c>Glow</c> loop in MansionGame.
/// </summary>
private void UpdateSpectatorGlow()
{
var local = Spectator.Local;
var show = IsAlive && local.IsValid() && local.Following != this;
if ( !show )
{
if ( spectatorGlow.IsValid() )
spectatorGlow.Enabled = false;
return;
}
spectatorGlow ??= Components.GetOrCreate<HighlightOutline>();
spectatorGlow.Enabled = true;
spectatorGlow.Color = SlotColor;
spectatorGlow.ObscuredColor = SlotColor.WithAlpha( 0.6f ); // shown through walls
spectatorGlow.Width = 0.5f;
}
}