A component that controls an orbital camera following a player or vehicle. It reads input to adjust pitch and yaw, traces scene geometry to avoid clipping, auto-zooms based on vehicle speed and grounded state, and provides a ShakeCamera coroutine to add roll shake.
using Sandbox;
using Sandbox.UI;
using System;
using System.Threading.Tasks;
public sealed class OrbitalCameraController : Component
{
[Property, Description("Camera to control")] CameraComponent MainCamera;
RotationControl RotationControl;
[Property, Description("Player to follow")] GameObject Player;
[Property] Rigidbody CarBody;
[Property, Description( "Starting distance" )] int TargetDistanceToPlayer = 200;
[Property, Description("Starting distance")] int DistanceToPlayer = 200;
[Property, Description( "Vertical Offset" )] int VerticalOffset = 170;
[Property] float HorizontalOffset = 80;
[Property, Description( "Minimal distance to player" )] int MinDistanceToPlayer = 200;
[Property, Description( "Maximal distance to player" )] int MaxDistanceToPlayer = 500;
[Property, Description( "Units one step zooms" )] int ZoomStrength = 0;
[Property, Description( "Units one step zooms" )] int AutoZoomStrength = 1;
float crosshairPitch = 0;
float Pitch = 30;
float Yaw = 180;
float Roll = 0;
bool IsShaking = false;
TimeSince SinceShake;
protected override void OnStart()
{
RotationControl = GameObject.GetComponent<RotationControl>();
Yaw = Rotation.LookAt(CarBody.WorldRotation.Backward, Vector3.Up).Yaw();
MainCamera.FieldOfView = 90;
}
protected override void OnUpdate()
{
// Capture mouse and add to pitch and yaw angles
Angles mouseMove = Input.AnalogLook;
// Pitch = (Pitch + mouseMove.pitch ).Clamp( 20, -20 );
crosshairPitch = (crosshairPitch + mouseMove.pitch ).Clamp( 50, -75 ); // Up&Down clamped in degrees
Pitch = (crosshairPitch / 1) + 10;
Yaw = Yaw + mouseMove.yaw;
Rotation rotation = Rotation.From( Pitch, Yaw, Roll );
SceneTraceResult checkingSightline = Scene.Trace
.Ray( Player.WorldPosition + Vector3.Up * VerticalOffset + Player.WorldRotation.Forward * HorizontalOffset, MainCamera.WorldPosition + MainCamera.WorldRotation.Forward * 10 )
.Radius(1)
.IgnoreGameObjectHierarchy(GameObject) // Ignores itself. Use tags depending on your setup
.WithoutTags("enemy", "dead")
.Run();
// DebugOverlay.Trace( checkingSightline );
// Checks for objects around cam to avoid stuttering
SceneTraceResult checkingBehind = Scene.Trace
.Sphere( AutoZoomStrength, MainCamera.WorldPosition, MainCamera.WorldPosition )
.IgnoreGameObjectHierarchy( GameObject ) // Ignores itself. Use tags depending on your setup
.Run();
// DebugOverlay.Trace( checkingBehind );
//Zoom
if ( Input.MouseWheel.y < 0 ) { TargetDistanceToPlayer = Math.Min( TargetDistanceToPlayer + ZoomStrength, MaxDistanceToPlayer ); }
if ( Input.MouseWheel.y > 0 ) { TargetDistanceToPlayer = Math.Max( TargetDistanceToPlayer - ZoomStrength, MinDistanceToPlayer); }
// Zoom out when GroundCheck fails
if ( !RotationControl.IsGrounded )
{
TargetDistanceToPlayer = (int)CarBody.Velocity.Length.Remap( 0, 4000, MinDistanceToPlayer, MaxDistanceToPlayer );
MainCamera.FieldOfView = 90 + (CarBody.Velocity.Length.Remap( 0, 4000, 0, 20 ));
VerticalOffset = Math.Min( VerticalOffset + AutoZoomStrength, 200 );
//TargetDistanceToPlayer = MaxDistanceToPlayer;
//VerticalOffset = Math.Max( VerticalOffset - AutoZoomStrength, 100 ); ;
}
// When on ground zoom depending on speed
else
{
TargetDistanceToPlayer = (int)CarBody.Velocity.Length.Remap( 0, 4000, MinDistanceToPlayer, MaxDistanceToPlayer );
MainCamera.FieldOfView = 90 + (CarBody.Velocity.Length.Remap( 0, 4000, 0, 20 ) );
VerticalOffset = Math.Min(VerticalOffset + AutoZoomStrength, 200);
}
switch ((TargetDistanceToPlayer, checkingSightline.Distance, checkingSightline.Hit ))
{
default: break;
// Nur wenn checkingBehind false soll distance mit autozoom größer werden
case( > 0, > 0, false ) when TargetDistanceToPlayer > checkingSightline.Distance + AutoZoomStrength:
DistanceToPlayer += AutoZoomStrength;
break;
case ( > 0, > 0, false) when TargetDistanceToPlayer < checkingSightline.Distance - AutoZoomStrength:
DistanceToPlayer -= AutoZoomStrength;
break;
}
if ( checkingSightline.Hit ) // If sightline is blocked change distance to be in front of hit collider
{
DistanceToPlayer = (int)(checkingSightline.HitPosition - (Player.WorldPosition + Vector3.Up * VerticalOffset + Player.WorldRotation.Forward * HorizontalOffset)).Length; // -AutoZoomStrength cheats it infront
}
// Apply Position and Rotation
MainCamera.WorldPosition = Player.WorldPosition + Vector3.Up * VerticalOffset + Player.WorldRotation.Forward * HorizontalOffset - rotation.Forward * DistanceToPlayer;
MainCamera.WorldRotation = rotation;
// 1000 is distance of crosshair to player, VerticalOffset/2
Rotation rot = rotation.Angles().WithPitch( crosshairPitch );
Vector3 crosshairDir = Player.WorldPosition + Vector3.Up * VerticalOffset/2 + new Vector3(rot.Forward.x, rot.Forward.y, rot.Forward.z ).Normal * 1500;
SceneTraceResult crosshairCheck = Scene.Trace
.Ray( Player.WorldPosition + Vector3.Up * VerticalOffset / 2, crosshairDir )
.IgnoreGameObjectHierarchy( GameObject )
.Run();
}
[Button]
public async Task ShakeCamera(float duration = 0.1f)
{
if ( IsShaking ) { return; }
SinceShake = 0;
IsShaking = true;
while ( SinceShake < duration )
{
Roll = (float)Math.Sin(Time.Now * 25f) * 10;
await Task.FrameEnd();
}
IsShaking = false;
Roll = 0;
}
}