Player/FlightCamera.cs
/// <summary>
/// Camera for the flight ship. Supports third-person (default) and first-person cockpit view.
/// Toggle with the "View" input action (C / Right Joystick Button).
/// Updated by the owner's PlayerPawn each frame — continues running while dead for the death cam.
/// </summary>
public sealed class FlightCamera
{
private float _fovOffset = 0f;
private bool _isFirstPerson = false;
// Death cam state
private bool _isDead = false;
private Vector3 _deathPosition;
public void OnDied( Vector3 deathWorldPos )
{
_isDead = true;
// Sit slightly above and behind where the ship exploded
_deathPosition = deathWorldPos;
}
public void Update( PlayerPawn player )
{
var camera = Game.ActiveScene?.Camera;
if ( camera == null ) return;
if ( !player.IsAlive )
{
UpdateDeathCam( camera, player );
return;
}
// Back to flight cam when alive again
_isDead = false;
if ( Input.Pressed( "view" ) )
_isFirstPerson = !_isFirstPerson;
if ( _isFirstPerson )
UpdateFirstPerson( camera, player );
else
UpdateThirdPerson( camera, player );
camera.ZFar = 20500f;
// Screen shake from speed — zero when not alive
player.ChargeEffect ??= new ScreenShake.Charge();
player.ChargeEffect.Size = player.Speed.LerpInverse( 0f, 500f, true ) * 12.5f;
ScreenShake.Apply();
}
private void UpdateDeathCam( CameraComponent camera, PlayerPawn player )
{
camera.ZFar = 20500f;
player.ChargeEffect ??= new ScreenShake.Charge();
player.ChargeEffect.Size = 0f;
ScreenShake.Apply();
var target = player.DeathCamTarget;
var lookAt = (target != null && target.IsValid() && target.IsAlive)
? target.WorldPosition
: _deathPosition + camera.WorldRotation.Forward * 500f; // no killer — keep looking forward
var toTarget = lookAt - _deathPosition;
var dist = toTarget.Length;
// Smoothly rotate toward the killer
var wantRot = dist > 1f
? Rotation.LookAt( toTarget.Normal, Vector3.Up )
: camera.WorldRotation;
camera.WorldRotation = Rotation.Lerp( camera.WorldRotation, wantRot, Time.Delta * 4f );
camera.WorldPosition = _deathPosition;
// Zoom in when the killer is far away: 90° FOV at ≤300 units, 30° at ≥3000 units
var t = ((dist - 300f) / 2700f).Clamp( 0f, 1f );
var zoomFov = 90f - t * 60f;
_fovOffset = _fovOffset.LerpTo( 0f, Time.Delta * 4f );
camera.FieldOfView = Screen.CreateVerticalFieldOfView( zoomFov );
}
private void UpdateThirdPerson( CameraComponent camera, PlayerPawn player )
{
var centre = player.WorldPosition;
var orbitPoint = centre + player.WorldRotation.Forward * 90f;
var offset = centre - orbitPoint + player.WorldRotation.Up * 20f;
camera.WorldRotation = Rotation.Lerp( camera.WorldRotation, player.WorldRotation, Time.Delta * 10f );
camera.WorldPosition = orbitPoint + offset * 2.5f;
var speed = (player.Speed / 50f).Clamp( 0f, 1f );
var forwardSpeed = player.Velocity.Normal.Dot( player.WorldRotation.Forward );
_fovOffset = _fovOffset.LerpTo( speed * 10f * MathF.Abs( forwardSpeed ), Time.Delta * 4f );
camera.FieldOfView = Screen.CreateVerticalFieldOfView( 120f ) + _fovOffset;
}
private void UpdateFirstPerson( CameraComponent camera, PlayerPawn player )
{
camera.WorldPosition = player.WorldPosition
+ player.WorldRotation.Forward * 40f
+ player.WorldRotation.Up * 8f;
camera.WorldRotation = player.WorldRotation;
var speed = (player.Speed / 50f).Clamp( 0f, 1f );
var forwardSpeed = player.Velocity.Normal.Dot( player.WorldRotation.Forward );
_fovOffset = _fovOffset.LerpTo( speed * 8f * MathF.Abs( forwardSpeed ), Time.Delta * 4f );
camera.FieldOfView = Screen.CreateVerticalFieldOfView( 90f ) + _fovOffset;
}
}