Camera component for the player skateboard pawn. It follows a SkatePawn, handles bailout ragdoll focus, speed-dependent FOV, vertical/vert-mode camera blending, free-look orbiting, collision tracing to avoid clipping, and smooth rotation/position interpolation.
namespace Skateboard;
internal sealed class SkateCamera : Component
{
[Property] public CameraComponent Camera { get; set; }
[Property] public Skateboard.Player.SkatePawn Target { get; set; }
private Skateboard.Player.SkatePawn _cachedTarget;
private float orbitDistance = 200f;
private float orbitHeight = 80f;
private float vertCameraHeight = 400f;
private float vertCameraTall = 50f;
// Speed-based FOV: widen the view as the skater speeds up for a sense of flow.
private float baseFieldOfView = 90f;
private float maxFieldOfViewAdd = 13f;
private float fieldOfViewSpeedRef = 1400f;
private float fieldOfViewLerp = 4f;
private float currentFieldOfView = 90f;
private float lookDown;
private float minimumDistance = 0.2f;
private float rotationLerpSpeed = 3f;
private float speedLerpSpeed = 5f;
private float speedDistanceMultiplier = 0.05f;
private float currentSpeedDistance;
private float currentVertLerp;
private float vertLerpSpeed = 1f;
private float vertRotationSpeed = 10f;
private float targetTowardsOriginOffset = 1f;
// Free-look: hold the free-look button to orbit the camera with the mouse /
// right stick. The angles are world-anchored while looking, so the player
// turning or moving underneath doesn't drag the view around. Releasing eases
// back behind the skater.
private float freeLookYaw;
private float freeLookPitch;
private string freeLookAction = "View";
private float freeLookSensitivity = 1f;
private float freeLookPitchUp = -70f;
private float freeLookPitchDown = 50f;
private float freeLookRotationSpeed = 14f;
protected override void OnUpdate()
{
var camera = Camera ?? Components.GetOrCreate<CameraComponent>();
var pawn = Target ?? _cachedTarget;
if ( pawn is null || !pawn.IsValid() )
{
var localId = Connection.Local.Id;
pawn = Scene.GetAllComponents<Skateboard.Player.SkatePawn>()
.FirstOrDefault( x => x.GameObject.Network.IsOwner || x.OwningConnectionId == localId );
_cachedTarget = pawn;
}
if ( pawn is null )
return;
var ragdoll = pawn.Ragdoll;
if ( pawn.Bailed && ragdoll.IsValid() )
{
var bailFocusPosition = ragdoll.WorldPosition;
var renderer = ragdoll.Components.Get<SkinnedModelRenderer>( FindMode.EverythingInSelfAndDescendants );
if ( renderer is not null )
{
if ( renderer.TryGetBoneTransform( "pelvis", out var pelvis ) ||
renderer.TryGetBoneTransform( "Pelvis", out pelvis ) ||
renderer.TryGetBoneTransform( "hips", out pelvis ) ||
renderer.TryGetBoneTransform( "Hips", out pelvis ) )
{
bailFocusPosition = pelvis.Position;
}
}
WorldPosition = bailFocusPosition + Vector3.Up * vertCameraHeight;
WorldRotation = Rotation.LookAt( Vector3.Down, pawn.WorldRotation.Forward );
return;
}
var controller = pawn.Controller;
var planarSpeed = pawn.Velocity.WithZ( 0 ).Length;
var targetFov = baseFieldOfView + maxFieldOfViewAdd * (planarSpeed / fieldOfViewSpeedRef).Clamp( 0f, 1f );
currentFieldOfView = currentFieldOfView.LerpTo( targetFov, fieldOfViewLerp * Time.Delta );
camera.FieldOfView = currentFieldOfView;
var pawnForward = pawn.WorldRotation.Forward;
var focusPosition = pawn.WorldPosition;
var focusUp = pawn.WorldRotation.Up;
if ( planarSpeed > Skateboard.Player.SkateController.StoppedVelocity && (controller?.IsGrounded ?? false) == false )
pawnForward = pawn.Velocity.WithZ( 0 ).Normal;
var freeLooking = Input.Down( freeLookAction );
var look = Input.AnalogLook;
Rotation pawnLook;
if ( freeLooking )
{
// World-anchored orbit: only the look input moves the camera, so the
// player driving/turning underneath doesn't drag the view around.
freeLookYaw += look.yaw * freeLookSensitivity;
freeLookPitch = (freeLookPitch + look.pitch * freeLookSensitivity).Clamp( freeLookPitchUp, freeLookPitchDown );
pawnLook = Rotation.From( freeLookPitch, freeLookYaw, 0f );
}
else
{
pawnLook = Rotation.LookAt( pawnForward, Vector3.Up )
.RotateAroundAxis( Vector3.Left, lookDown );
// Keep the free-look angles tracking the follow camera so engaging
// free-look starts exactly where the view is now (no snap), and the
// view eases back to here on release.
var angles = pawnLook.Angles();
freeLookYaw = angles.yaw;
freeLookPitch = angles.pitch;
}
var center = focusPosition + focusUp * orbitHeight;
var targetPos = center + WorldRotation.Backward * orbitDistance;
var speedOffset = planarSpeed * speedDistanceMultiplier;
currentSpeedDistance = MathX.Lerp(
currentSpeedDistance,
speedOffset,
speedLerpSpeed * Time.Delta
);
targetPos += WorldRotation.Backward * currentSpeedDistance;
var vertCamPos =
focusPosition +
focusUp * vertCameraTall +
Vector3.Up * vertCameraHeight;
var vertCenter = focusPosition + Vector3.Up * vertCameraHeight;
currentVertLerp = MathX.Lerp(
currentVertLerp,
pawn.OnVert ? 1f : 0f,
(pawn.OnVert ? vertLerpSpeed : speedLerpSpeed) * Time.Delta
);
targetPos = Vector3.Lerp( targetPos, vertCamPos, currentVertLerp );
center = Vector3.Lerp( center, vertCenter, currentVertLerp );
var dir = (targetPos - center).Normal;
center += dir * targetTowardsOriginOffset;
var trace = Scene.Trace.Ray( center, targetPos )
.WithAnyTags( "solid" )
.IgnoreGameObjectHierarchy( pawn.GameObject )
.IgnoreGameObjectHierarchy( ragdoll )
.Radius( 8f )
.Run();
WorldPosition =
trace.Fraction <= minimumDistance
? Vector3.Lerp( center, targetPos, minimumDistance )
: trace.EndPosition;
if ( pawn.OnVert )
{
pawnLook = Rotation.LookAt(
(pawn.WorldPosition - WorldPosition + pawn.WorldRotation.Up * vertCameraTall).Normal,
pawn.VertNormal
);
WorldRotation = Rotation.Slerp(
WorldRotation,
pawnLook,
vertRotationSpeed * Time.Delta
);
}
else
{
WorldRotation = Rotation.Slerp(
WorldRotation,
pawnLook,
(freeLooking ? freeLookRotationSpeed : rotationLerpSpeed) * Time.Delta
);
}
}
}