Code/SubZero-Studios/Shared/CameraDolly.cs
using Sandbox;
using System;
namespace Sandbox.SubZero
{
/// <summary>
/// Camera dolly: moves the camera toward or away from a target in a straight line.
/// Good for push-in / pull-back shots. Add to a GameObject that has a CameraComponent.
/// </summary>
[Title( "Camera Dolly" )]
[Category( "Camera" )]
[Icon( "videocam" )]
public sealed class CameraDolly : Component
{
[Property, Group( "1. Target" )]
[Title( "Target" )]
[Description( "Object to move toward or away from" )]
public GameObject Target { get; set; }
[Property, Group( "2. Dolly" )]
[Title( "Speed (units/sec)" )]
[Description( "Positive = pull back (away from target). Negative = push in (toward target)." )]
[Range( -500f, 500f )]
public float Speed { get; set; } = 50f;
[Property, Group( "2. Dolly" )]
[Title( "Min Distance" )]
[Description( "Stop moving in when this close to target" )]
[Range( 10f, 5000f )]
public float MinDistance { get; set; } = 50f;
[Property, Group( "2. Dolly" )]
[Title( "Max Distance" )]
[Description( "Stop moving out when this far from target" )]
[Range( 50f, 10000f )]
public float MaxDistance { get; set; } = 2000f;
[Property, Group( "2. Dolly" )]
[Title( "Pause" )]
[Description( "Stop moving" )]
public bool Pause { get; set; } = false;
[Property, Group( "3. Video / Editor" )]
[Title( "Camera FOV" )]
[Description( "Field of view (0 = leave camera default)" )]
[Range( 0f, 120f )]
public float CameraFOV { get; set; } = 60f;
[Property, Group( "3. Video / Editor" )]
[Title( "Look At Target" )]
public bool LookAtTarget { get; set; } = true;
[Property, Group( "3. Video / Editor" )]
[Title( "Smooth Rotation" )]
public bool SmoothRotation { get; set; } = true;
[Property, Group( "3. Video / Editor" )]
[Title( "Rotation Smoothing" )]
[Range( 1f, 20f )]
[ShowIf( nameof( SmoothRotation ), true )]
public float RotationSmoothing { get; set; } = 8f;
private float _currentSpeed;
private Rotation _currentLookRotation;
private bool _lookRotationInitialized;
const float MaxDeltaTime = 0.1f;
Vector3 GetTargetPosition()
{
if ( Target != null && Target.IsValid )
return Target.WorldPosition;
return GameObject.WorldPosition;
}
protected override void OnStart()
{
_currentSpeed = Pause ? 0f : Speed;
var targetPos = GetTargetPosition();
if ( LookAtTarget )
{
_currentLookRotation = Rotation.LookAt( (targetPos - GameObject.WorldPosition).Normal, Vector3.Up );
_lookRotationInitialized = true;
}
if ( CameraFOV > 0f )
{
var cam = Components.Get<CameraComponent>();
if ( cam != null && cam.IsValid )
cam.FieldOfView = CameraFOV;
}
}
protected override void OnUpdate()
{
var dt = Math.Min( Time.Delta, MaxDeltaTime );
var targetPos = GetTargetPosition();
var toCamera = GameObject.WorldPosition - targetPos;
var distance = toCamera.Length;
if ( distance < 0.001f )
distance = 0.001f;
var direction = toCamera / distance;
// Smooth speed so pause/direction change don't jerk
var targetSpeed = Pause ? 0f : Speed;
var speedT = Math.Clamp( dt * 4f, 0f, 1f );
_currentSpeed = _currentSpeed + (targetSpeed - _currentSpeed) * speedT;
// Move along line toward/away from target
var newDistance = distance + _currentSpeed * dt;
newDistance = Math.Clamp( newDistance, MinDistance, MaxDistance );
GameObject.WorldPosition = targetPos + direction * newDistance;
if ( LookAtTarget )
{
var toTarget = (targetPos - GameObject.WorldPosition).Normal;
var targetRot = Rotation.LookAt( toTarget, Vector3.Up );
if ( !_lookRotationInitialized )
{
_currentLookRotation = targetRot;
_lookRotationInitialized = true;
}
else if ( SmoothRotation && RotationSmoothing > 0f )
{
var t = 1f - MathF.Exp( -RotationSmoothing * dt );
_currentLookRotation = Rotation.Lerp( _currentLookRotation, targetRot, Math.Clamp( t, 0f, 1f ) );
}
else
{
_currentLookRotation = targetRot;
}
GameObject.WorldRotation = _currentLookRotation;
}
}
[Button( "Push In" ), Group( "Dolly Control" )]
public void PushIn() { Pause = false; Speed = -Math.Abs( Speed ); }
[Button( "Pull Back" ), Group( "Dolly Control" )]
public void PullBack() { Pause = false; Speed = Math.Abs( Speed ); }
[Button( "Stop" ), Group( "Dolly Control" )]
public void Stop() => Pause = true;
}
}