Camera-related parts of the Player class. Handles third person seated camera positioning and smoothing, seat detection, building bounds for contraptions, collecting weapons attached to the seat, and drawing seated weapon HUD elements.
using Sandbox.Movement;
public sealed partial class Player
{
[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<BaseCarryable> _seatedWeapons;
void PlayerController.IEvents.OnEyeAngles( ref Angles ang )
{
var angles = ang;
Local.IPlayerEvents.Post( x => x.OnCameraMove( ref angles ) );
ang = angles;
}
void PlayerController.IEvents.PostCameraSetup( CameraComponent camera )
{
camera.FovAxis = CameraComponent.Axis.Vertical;
camera.FieldOfView = Screen.CreateVerticalFieldOfView( Preferences.FieldOfView, 9.0f / 16.0f );
if ( Controller.ThirdPerson )
camera.RenderExcludeTags.Add( "firstperson" );
else
camera.RenderExcludeTags.Remove( "firstperson" );
Local.IPlayerEvents.Post( x => x.OnCameraSetup( camera ) );
UpdateSeatedWeapons();
ApplySeatedCameraSetup( camera );
DrawSeatedWeaponHud();
Local.IPlayerEvents.Post( x => x.OnCameraPostSetup( camera ) );
}
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( CameraComponent camera )
{
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 = camera.WorldRotation.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;
camera.WorldPosition = camPos;
camera.WorldRotation = 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<BaseCarryable>();
_seatedWeapons.Clear();
foreach ( var obj in builder.Objects )
{
foreach ( var weapon in obj.GetComponentsInChildren<BaseCarryable>() )
{
if ( !weapon.HasOwner )
_seatedWeapons.Add( weapon );
}
}
}
private void DrawSeatedWeaponHud()
{
if ( _seatedWeapons == null || _seatedWeapons.Count == 0 ) return;
if ( Scene.Camera is null ) return;
if ( Scene.Camera.RenderExcludeTags.Has( "ui" ) ) return;
var hud = Scene.Camera.Hud;
foreach ( var weapon in _seatedWeapons )
{
if ( !weapon.IsValid() ) continue;
if ( weapon is IPlayerControllable controllable && !controllable.CanControl( this ) ) continue;
Vector2 aimPos;
if ( weapon.IsTargetedAim )
{
aimPos = Screen.Size * 0.5f;
}
else
{
var muzzle = weapon.MuzzleTransform.WorldTransform;
var tr = Scene.Trace.Ray( muzzle.Position, muzzle.Position + muzzle.Rotation.Forward * 4096f )
.IgnoreGameObjectHierarchy( weapon.GameObject.Root )
.Run();
aimPos = Scene.Camera.PointToScreenPixels( tr.EndPosition );
}
weapon.DrawHud( hud, aimPos );
}
}
}