Camera/CameraPanning.cs
using System;
using HC3.Terrain;
namespace HC3;
public sealed class CameraPanning : Component
{
public static CameraPanning Instance { get; private set; }
[Property] public CameraComponent Camera { get; set; }
[Property] float ZoomSpeed { get; set; } = 100.0f;
[Property] float MaxZoomDistance { get; set; } = 10000.0f;
[Property] float OriginHeight { get; set; } = 150.0f;
[Property] float PanSpeed { get; set; } = 25.0f;
[Property] float MoveSpeed { get; set; } = 500.0f;
[Property] Curve MoveSpeedZoomFactor { get; set; } = Curve.Linear;
[Property] float EdgeScrollBorder { get; set; } = 10.0f;
// We want to store the start position and angles. This way, other players
// will always have the camera default to starting where it should be. This
// saves us having to instantiate the camera at runtime.
[Property, Hide] private Vector3 _startPosition;
[Property, Hide] private Rotation _startAngles;
Vector3 _origin;
Vector3 _targetOrigin;
float _targetDistance = 1200;
float _distance = 1200;
float _yawVelocity = 0;
float _lastYaw = 0;
Vector3 _panStart = 0;
Vector2 _panSpeed;
bool _oribiting;
bool _panning;
bool _driving;
Vector3 _inputDirection;
Rotation _viewAngles;
float _terrainHeight;
bool _freeCamActive = false;
public GameObject FollowTarget => _followTarget;
GameObject _followTarget;
private TimeSince _timeSinceInput;
private const float IdleRotationDelay = 30f;
private const float IdleRotationSpeed = 5f;
private const float ManualOrbitSpeed = 60f;
[Property, Group( "Free Cam" )] public float FreeCamSpeed { get; set; } = 250f;
[Property, Group( "Free Cam" )] public float FreeCamAcceleration { get; set; } = 10f;
[Property, Group( "Free Cam" )] public float FreeCamDamping { get; set; } = 5f;
[Property, Group( "Free Cam" )] public float FreeCamLookSensitivity { get; set; } = 0.1f;
[Property, Group( "Free Cam" )] public float FreeCamMinPitch { get; set; } = -89f;
[Property, Group( "Free Cam" )] public float FreeCamMaxPitch { get; set; } = 89f;
Vector3 _freeCamVelocity = Vector3.Zero;
Angles _freeCamAngles;
protected override void OnEnabled()
{
base.OnEnabled();
Instance = this;
// If we're the host then store the start position of the camera.
if ( Networking.IsHost )
{
if ( Camera is not { } camera )
{
_startPosition = default;
_startAngles = Rotation.Identity;
}
else
{
// Figure out target origin and distance from initial camera transform
// TODO: this is still a bit off :(
var ray = new Ray( camera.WorldPosition, camera.WorldRotation.Forward );
var plane = new Plane( Vector3.Up, OriginHeight );
_startPosition = plane.Trace( ray, true ) ?? default;
_startAngles = camera.WorldRotation;
_targetDistance = _distance = (_startPosition - camera.WorldPosition).Length;
}
}
_origin = _startPosition;
_targetOrigin = _startPosition;
_viewAngles = _startAngles;
_freeCamAngles = Camera.WorldRotation.Angles();
}
private Vector2 _smoothedLookDelta;
private float _handheldTime;
private Angles _handheldRotationOffset;
private Angles _handheldRotationTarget;
private Vector3 _handheldPositionOffset;
private Vector3 _handheldPositionTarget;
private float ShakeAmplitude => 0.3f;
private float ShakeFrequency => 0.75f;
private float ShakePositionAmplitude => 0.01f;
private float ShakeSmoothness => 4.0f;
private void SimulateFreeCamera()
{
// Raw mouse look input
Vector2 lookInput = new( Input.AnalogLook.yaw * FreeCamLookSensitivity, Input.AnalogLook.pitch * FreeCamLookSensitivity );
// Smooth it (exponential moving average)
float smoothingSpeed = 10.0f; // Increase for faster responsiveness, decrease for more smoothing
_smoothedLookDelta = Vector2.Lerp( _smoothedLookDelta, lookInput, RealTime.Delta * smoothingSpeed );
_freeCamAngles.pitch = (_freeCamAngles.pitch + _smoothedLookDelta.y).Clamp( FreeCamMinPitch, FreeCamMaxPitch );
_freeCamAngles.yaw += _smoothedLookDelta.x;
_freeCamAngles.roll = 0;
var rotation = _freeCamAngles.ToRotation();
var moveInput = Input.AnalogMove.Normal;
var mult = 1f;
if ( Input.Keyboard.Down( "SHIFT" ) ) mult *= 4f;
if ( Input.Keyboard.Down( "CTRL" ) ) mult *= 0.25f;
var desiredVelocity = rotation * moveInput * FreeCamSpeed * mult;
_freeCamVelocity = Vector3.Lerp( _freeCamVelocity, desiredVelocity, RealTime.Delta * FreeCamAcceleration );
Camera.WorldPosition += _freeCamVelocity * RealTime.Delta;
Camera.WorldRotation = rotation;
_handheldTime += RealTime.Delta * ShakeFrequency;
// 😷😷
float yawNoise = MathF.Sin( _handheldTime * 0.9f + 12.5f ) + MathF.Sin( _handheldTime * 1.3f + 3.1f );
float pitchNoise = MathF.Sin( _handheldTime * 1.1f + 1.5f ) + MathF.Sin( _handheldTime * 0.7f + 6.2f );
float rollNoise = MathF.Sin( _handheldTime * 1.7f + 4.3f ) + MathF.Sin( _handheldTime * 1.0f + 8.1f );
_handheldRotationTarget = new Angles(
pitchNoise * ShakeAmplitude,
yawNoise * ShakeAmplitude,
rollNoise * ShakeAmplitude
);
float px = MathF.Sin( _handheldTime * 1.5f + 5.0f );
float py = MathF.Sin( _handheldTime * 1.2f + 1.0f );
float pz = MathF.Sin( _handheldTime * 1.8f + 3.0f );
_handheldPositionTarget = new Vector3(
px * ShakePositionAmplitude,
py * ShakePositionAmplitude,
pz * ShakePositionAmplitude * 0.5f
);
_handheldRotationOffset = Angles.Lerp( _handheldRotationOffset, _handheldRotationTarget, RealTime.Delta * ShakeSmoothness );
_handheldPositionOffset = Vector3.Lerp( _handheldPositionOffset, _handheldPositionTarget, RealTime.Delta * ShakeSmoothness );
Camera.WorldRotation *= _handheldRotationOffset.ToRotation();
Camera.WorldPosition += Camera.WorldRotation * _handheldPositionOffset;
SetMouseVisibility( MouseVisibility.Hidden );
var tiltShift = Scene.Get<TiltShiftEffect>();
if ( tiltShift.IsValid() )
{
tiltShift.Enabled = false;
}
var dof = Camera.GetComponent<DepthOfField>( true );
if ( dof.IsValid() )
{
dof.Enabled = true;
UpdateDepthOfField( dof );
}
var ui = Scene.Get<ScreenPanel>();
if ( ui.IsValid() )
{
Scene.Get<ScreenPanel>().Enabled = false;
}
//BuildingZone.Instance.Line.Enabled = false;
UpdateFieldOfView();
}
private void UpdateFieldOfView()
{
if ( Input.MouseWheel.y != 0 )
{
// Scale FOV change by vertical mouse movement
float delta = -Input.MouseWheel.y;
fov = fov.Clamp( 20, 120 );
fov += delta;
fov = fov.Clamp( 20, 120 );
}
Camera.FieldOfView = Camera.FieldOfView.LerpTo( Screen.CreateVerticalFieldOfView( fov, 9.0f / 16.0f ), Time.Delta * 2f );
}
private float fov = 50;
private Vector3 _focusPoint;
private void UpdateDepthOfField( DepthOfField dof )
{
if ( Input.Keyboard.Down( "MOUSE1" ) )
{
dof.BlurSize = Scene.Camera.FieldOfView.Remap( 20, 80, 50, 10 );
dof.FocusRange = 1024;
dof.FrontBlur = false;
var tr = Scene.Trace.Ray( Scene.Camera.Transform.World.ForwardRay, 5000 )
.Radius( 8 )
.WithTag( "ground" )
.Run();
_focusPoint = tr.EndPosition;
}
var target = Scene.Camera.WorldPosition.Distance( _focusPoint ) + 32;
dof.FocalDistance = dof.FocalDistance.LerpTo( target, Time.Delta * 10.0f );
}
protected override void OnUpdate()
{
if ( !Camera.IsValid() )
return;
// Toggle free camera
if ( DebugMode.Enabled && Input.Keyboard.Pressed( "J" ) )
{
_freeCamActive = !_freeCamActive;
// Reinitialize angles from current camera orientation
if ( _freeCamActive )
_freeCamAngles = Camera.WorldRotation.Angles();
}
UpdateTerrainHeight();
if ( _freeCamActive )
{
SimulateFreeCamera();
}
else
{
BuildInput();
Simulate();
}
if ( _panning )
{
Gizmo.Draw.Arrow( _origin - _panStart, _origin - _panStart + _inputDirection * 0.5f );
}
}
private void Simulate()
{
if ( _oribiting )
{
_yawVelocity = _viewAngles.Yaw() - _lastYaw;
_yawVelocity = _yawVelocity.Clamp( -10, 10 );
}
else
{
_yawVelocity = _yawVelocity.LerpTo( 0.0f, 5.0f * RealTime.Delta );
}
_lastYaw = _viewAngles.Yaw();
_targetOrigin += _inputDirection * RealTime.Delta;
_distance = _distance.LerpTo( _targetDistance, RealTime.Delta * 10.0f );
_origin = _origin.LerpTo( _targetOrigin, RealTime.Delta * 10.0f );
Camera.WorldPosition = _origin + _viewAngles.Backward * _distance;
Camera.WorldRotation = _viewAngles;
}
private void SetMouseVisibility( MouseVisibility mode )
{
if ( Mouse.Visibility == mode ) return;
Mouse.Visibility = mode;
}
private void BuildInput()
{
_oribiting = Input.Keyboard.Down( "MOUSE3" ) && !Input.Keyboard.Down( "ALT" );
_panning = Input.Keyboard.Down( "MOUSE3" ) && Input.Keyboard.Down( "ALT" );
_driving = _oribiting || _panning;
SetMouseVisibility( _driving ? MouseVisibility.Hidden : MouseVisibility.Auto );
var viewAngles = _viewAngles.Angles();
bool anyInput = Input.AnalogMove.Length > 0.01f
|| Input.MouseWheel.y != 0
|| Input.Keyboard.Down( "MOUSE3" )
|| Mouse.Delta.Length > 0.01f;
if ( anyInput )
{
_timeSinceInput = 0;
}
if ( Input.AnalogMove.Length > 0.01f && FollowTarget.IsValid() )
{
Follow( null );
}
var manualOrbit = 0;
if ( !BuildingPlacer.Instance.IsPlacing )
{
if ( Input.Keyboard.Down( "Q" ) ) manualOrbit = 1;
if ( Input.Keyboard.Down( "E" ) ) manualOrbit = -1;
}
if ( _oribiting && !_panning )
{
viewAngles.yaw += Input.AnalogLook.yaw;
viewAngles.pitch += Input.AnalogLook.pitch;
if ( Input.Keyboard.Pressed( "MOUSE3" ) )
{
_yawVelocity = 0;
}
}
else if ( manualOrbit != 0 )
{
viewAngles.yaw += manualOrbit * ManualOrbitSpeed * RealTime.Delta;
}
else if ( _timeSinceInput > IdleRotationDelay )
{
viewAngles.yaw += IdleRotationSpeed * RealTime.Delta;
}
else
{
viewAngles.yaw += _yawVelocity;
}
viewAngles.roll = 0;
viewAngles.pitch = viewAngles.pitch.Clamp( 10, 89 );
viewAngles = viewAngles.Normal;
_viewAngles = viewAngles.ToRotation();
var moveDir = Rotation.FromYaw( _viewAngles.Yaw() );
if ( _followTarget.IsValid() )
{
_inputDirection = new Vector3();
FrameOn( _followTarget.WorldPosition, false );
_origin = _targetOrigin;
}
else if ( _panning )
{
if ( _panStart == 0 )
{
var tr = Scene.Trace.Ray( Scene.Camera.ScreenPixelToRay( Mouse.Position ), 6000f )
.UsePhysicsWorld()
.WithTag( "ground" )
.Run();
var hitPos = tr.Hit ? tr.HitPosition : tr.EndPosition;
hitPos -= tr.Direction * 400f;
_panStart = _origin - hitPos;
}
_inputDirection = new Vector3( _panSpeed ) * moveDir * PanSpeed;
_panSpeed.x -= Input.AnalogLook.pitch;
_panSpeed.y += Input.AnalogLook.yaw;
_panSpeed.x = _panSpeed.x.Clamp( -PanSpeed, PanSpeed );
_panSpeed.y = _panSpeed.y.Clamp( -PanSpeed, PanSpeed );
}
else
{
float speed = MoveSpeed * MoveSpeedZoomFactor.Evaluate( _distance / MaxZoomDistance );
Vector3 move = Input.AnalogMove;
if ( !_driving ) move += GetEdgeScroll();
_inputDirection = move * moveDir * speed;
_panSpeed = 0;
_panStart = 0;
}
if ( !_followTarget.IsValid() )
{
_targetOrigin.z = OriginHeight + _terrainHeight;
}
if ( !Input.Down( "run" ) )
{
_targetDistance -= Input.MouseWheel.y * ZoomSpeed;
}
_targetDistance = _targetDistance.Clamp( 0, MaxZoomDistance );
}
private Vector3 GetEdgeScroll()
{
var isOnScreen = new Rect( 0, Screen.Size - 1 ).Shrink( 1 ).IsInside( Mouse.Position );
if ( !isOnScreen )
return Vector3.Zero;
var panMovement = Vector3.Zero;
PathMask move = 0;
if ( Mouse.Position.y < EdgeScrollBorder )
{
float factor = (EdgeScrollBorder - Mouse.Position.y) / EdgeScrollBorder;
panMovement += Vector3.Forward * factor;
move |= PathMask.Up;
}
else if ( Mouse.Position.y > Screen.Height - EdgeScrollBorder )
{
float factor = (Mouse.Position.y - (Screen.Height - EdgeScrollBorder)) / EdgeScrollBorder;
panMovement += Vector3.Backward * factor;
move |= PathMask.Down;
}
if ( Mouse.Position.x < EdgeScrollBorder )
{
float factor = (EdgeScrollBorder - Mouse.Position.x) / EdgeScrollBorder;
panMovement += Vector3.Left * factor;
move |= PathMask.Right;
}
else if ( Mouse.Position.x > Screen.Width - EdgeScrollBorder )
{
float factor = (Mouse.Position.x - (Screen.Width - EdgeScrollBorder)) / EdgeScrollBorder;
panMovement += Vector3.Right * factor;
move |= PathMask.Left;
}
string cursor = "arrow";
if ( move != 0 )
{
if ( move == PathMask.Up ) cursor = "dir_n";
else if ( move == PathMask.Down ) cursor = "dir_s";
else if ( move == PathMask.Left ) cursor = "dir_e";
else if ( move == PathMask.Right ) cursor = "dir_w";
else if ( move == (PathMask.Up | PathMask.Left) ) cursor = "dir_ne";
else if ( move == (PathMask.Up | PathMask.Right) ) cursor = "dir_nw";
else if ( move == (PathMask.Down | PathMask.Left) ) cursor = "dir_se";
else if ( move == (PathMask.Down | PathMask.Right) ) cursor = "dir_sw";
}
Mouse.CursorType = cursor;
return panMovement;
}
private void UpdateTerrainHeight()
{
ParkTerrain terrain = GridManager.Instance.Terrain;
if ( !terrain.IsValid() )
return;
var gridPos = GridManager.WorldToGridPosition( _targetOrigin );
_terrainHeight = terrain.GetHeight( gridPos ) * terrain.TileHeight;
}
public float GetZoomDistance()
{
return _distance;
}
public float GetMaxZoomDistance()
{
return MaxZoomDistance;
}
public void FrameOn( Vector3 position, bool setDistance = true ) => FrameOn( BBox.FromPositionAndSize( position, GridManager.GridSize ), setDistance );
public void FrameOn( BBox target, bool setDistance = true )
{
if ( setDistance )
{
_targetDistance = MathX.SphereCameraDistance( target.Size.Length, Camera.FieldOfView );
}
_targetOrigin = target.Center;
_targetOrigin -= (_viewAngles.Forward * OriginHeight * 2);
}
public void Follow( GameObject gameObject )
{
if ( !gameObject.IsValid() )
{
_followTarget = null;
return;
}
FrameOn( gameObject.WorldPosition );
_followTarget = gameObject;
}
public void ToggleFollow( GameObject gameObject )
{
if ( _followTarget != gameObject )
{
Follow( gameObject );
return;
}
Follow( null );
}
public void StopFollowing( GameObject gameObject )
{
if ( _followTarget != gameObject )
{
return;
}
Follow( null );
}
}