Camera-related parts of the Player partial class. Implements ICameraModifier and camera event hooks, applies FOV, first/third person tag handling, view bob/roll, and a seated (third-person) camera that orbits a seat, traces for occlusion, and collects seat-linked weapons for HUD drawing.
using Sandbox.Movement;
public sealed partial class Player : ICameraModifier
{
[Property, Group( "Camera" )] public float SeatedCameraDistance { get; set; } = 200f;
[Property, Group( "Camera" )] public float SeatedCameraHeight { get; set; } = 40f;
[Property, Group( "Camera" )] public float SeatedCameraPositionSpeed { get; set; } = 3f;
[Property, Group( "Camera" )] public float SeatedCameraVelocityScale { get; set; } = 0.1f;
private ISitTarget _cachedSeat;
private bool _seatCameraInitialized;
private float _minCameraDistance;
private float _smoothedDistance;
private Angles _seatedAngles;
private Vector3 _lastSeatWorldPos;
private List<BaseSandboxWeapon> _seatedWeapons;
private float roll;
void PlayerController.IEvents.OnEyeAngles( ref Angles ang )
{
var angles = ang;
Local.IPlayerEvents.Post( x => x.OnCameraMove( ref angles ) );
ang = angles;
}
int ICameraModifier.CameraOrder => 50;
// Runs in the camera's modifier chain right after the engine PlayerController (order 0) - the
// game's view pass: preference FOV, first person tags, movement roll and the seated camera.
void ICameraModifier.ModifyCamera( CameraComponent camera, ref CameraView view )
{
if ( IsProxy || Scene.Camera != camera ) return;
if ( !Controller.IsValid() ) return;
// Keep the engine's camera effects scaled by the player's screenshake preference.
if ( CameraEffectSystem.Get( Scene ) is { } effects )
effects.Scale = GamePreferences.Screenshake;
camera.FovAxis = CameraComponent.Axis.Vertical;
view.FieldOfView = Screen.CreateVerticalFieldOfView( Preferences.FieldOfView, 9.0f / 16.0f );
if ( Controller.ThirdPerson )
camera.RenderExcludeTags.Add( "firstperson" );
else
camera.RenderExcludeTags.Remove( "firstperson" );
// Legacy game events still receive the camera - write the view out, let them run, fold back.
BridgeCameraEvent( camera, ref view, c => Local.IPlayerEvents.Post( x => x.OnCameraSetup( c ) ) );
ApplyMovementCameraEffects( ref view );
ApplySeatedCameraSetup( ref view );
BridgeCameraEvent( camera, ref view, c => Local.IPlayerEvents.Post( x => x.OnCameraPostSetup( c ) ) );
}
private static void BridgeCameraEvent( CameraComponent camera, ref CameraView view, Action<CameraComponent> post )
{
camera.WorldPosition = view.Position;
camera.WorldRotation = view.Rotation;
camera.FieldOfView = view.FieldOfView;
post( camera );
view.Position = camera.WorldPosition;
view.Rotation = camera.WorldRotation;
view.FieldOfView = camera.FieldOfView;
}
private void ApplyMovementCameraEffects( ref CameraView view )
{
if ( Controller.ThirdPerson ) return;
if ( !GamePreferences.ViewBobbing ) return;
var r = Controller.WishVelocity.Dot( EyeTransform.Left ) / -250.0f;
roll = MathX.Lerp( roll, r, Time.Delta * 10.0f, true );
view.Rotation *= new Angles( 0, 0, roll );
}
private void UpdateSeatedWeapons()
{
var seat = GetComponentInParent<ISitTarget>( false );
if ( seat is null )
{
_cachedSeat = null;
_seatedWeapons = null;
return;
}
if ( seat != _cachedSeat )
{
_cachedSeat = seat;
_seatCameraInitialized = false;
RebuildSeatedWeapons( (seat as Component).GameObject );
}
}
private void ApplySeatedCameraSetup( ref CameraView view )
{
if ( !Controller.ThirdPerson )
return;
if ( _cachedSeat is null )
return;
var seatGo = (_cachedSeat as Component).GameObject;
var seatPos = seatGo.WorldPosition + Vector3.Up * SeatedCameraHeight;
if ( !_seatCameraInitialized )
{
_seatCameraInitialized = true;
_minCameraDistance = MathF.Max( SeatedCameraDistance, RebuildContraptionBounds( seatGo ) );
_seatedAngles = view.Rotation.Angles();
_lastSeatWorldPos = seatPos;
_smoothedDistance = _minCameraDistance;
}
_seatedAngles.yaw += Input.AnalogLook.yaw;
_seatedAngles.pitch = (_seatedAngles.pitch + Input.AnalogLook.pitch).Clamp( -89, 89 );
// Derive velocity from position delta and add it to the target distance
var speed = (seatPos - _lastSeatWorldPos).Length / Time.Delta;
_lastSeatWorldPos = seatPos;
var targetDistance = _minCameraDistance + speed * SeatedCameraVelocityScale;
// Smooth orbit distance
_smoothedDistance = _smoothedDistance.LerpTo( targetDistance, Time.Delta * SeatedCameraPositionSpeed );
// Compose rotation: yaw around world up, then pitch around local right, no gimbal lock
var camRot = Rotation.FromYaw( _seatedAngles.yaw ) * Rotation.FromPitch( _seatedAngles.pitch );
var desiredPos = seatPos + camRot.Backward * _smoothedDistance;
var tr = Scene.Trace.FromTo( seatPos, desiredPos ).Radius( 8f ).WithTag( "world" ).IgnoreGameObjectHierarchy( GameObject.Root ).Run();
var camPos = tr.Hit ? tr.HitPosition + (seatPos - desiredPos).Normal * 4f : desiredPos;
view.Position = camPos;
view.Rotation = Rotation.LookAt( seatPos - camPos, Vector3.Up );
}
private float RebuildContraptionBounds( GameObject seatGo )
{
var builder = new LinkedGameObjectBuilder();
builder.AddConnected( seatGo );
var totalBounds = new BBox();
var initialized = false;
foreach ( var obj in builder.Objects )
{
if ( obj.Tags.Has( "player" ) ) continue;
var b = obj.GetBounds();
totalBounds = initialized ? totalBounds.AddBBox( b ) : b;
initialized = true;
}
return totalBounds.Size.Length;
}
private void RebuildSeatedWeapons( GameObject seatGo )
{
var builder = new LinkedGameObjectBuilder();
builder.AddConnected( seatGo );
_seatedWeapons ??= new List<BaseSandboxWeapon>();
_seatedWeapons.Clear();
foreach ( var obj in builder.Objects )
{
foreach ( var weapon in obj.GetComponentsInChildren<BaseSandboxWeapon>() )
{
if ( !weapon.HasOwner )
_seatedWeapons.Add( weapon );
}
}
}
private void DrawSeatedWeaponHud()
{
if ( _seatedWeapons == null || _seatedWeapons.Count == 0 ) return;
foreach ( var weapon in _seatedWeapons )
{
if ( !weapon.IsValid() ) continue;
if ( weapon is IPlayerControllable controllable && !controllable.CanControl( this ) ) continue;
// The engine projects the weapon's aim point onto the screen (targeted aim resolves to a
// camera ray, which projects to centre).
weapon.DrawHud( Scene.Camera );
}
}
}