Code/Freecam.cs
namespace Duccsoft;
/// <summary>
/// Enables the use of AnalogMove and AnalogLook to control the position and rotation
/// of the main camera of the scene.
/// </summary>
[Title( "Freecam" )]
[Category( "Camera" )]
[Icon( "control_camera" )]
public sealed partial class Freecam : Component
{
/// <summary>
/// How many units per second the camera will move at normal speed.
/// </summary>
[Property] public float Speed { get; set; } = 300f;
/// <summary>
/// A factor applied to movement speed whenever the crouch button is held.
/// </summary>
[Property] public float LowSpeedFactor { get; set; } = 0.25f;
/// <summary>
/// A factor applied to movement speed whenever the run button is held.
/// </summary>
[Property] public float HighSpeedFactor { get; set; } = 2.5f;
/// <summary>
/// If true, prevents the player from looking higher than directly up or lower
/// than directly down. Prevents the camera from going upside-down and doing loop-de-loops.
/// </summary>
[Property] public bool ClampPitch { get; set; } = true;
/// <summary>
/// If true, the freecam will use a <see cref="CharacterController"/> to handle collisions.
/// If none exists already, one will be created.
/// </summary>
[Property] public bool UseCollision { get; set; } = true;
/// <summary>
/// The main scene camera. Will be refreshed each update.
/// </summary>
private CameraComponent _camera;
/// <summary>
/// The current angle/rotation that we are looking at.
/// </summary>
private Angles _lookAngle;
/// <summary>
/// If <see cref="UseCollision"/> is true, this will be an instance of a <see cref="CharacterController"/>
/// on the same GameObject as this component.
/// </summary>
private CharacterController _controller;
protected override void OnEnabled()
{
OnFreecamStart?.Invoke( this );
}
protected override void OnDisabled()
{
OnFreecamEnd?.Invoke( this );
_controller?.Destroy();
_controller = null;
}
protected override void OnUpdate()
{
if ( _camera is null || !_camera.IsMainCamera )
{
_camera = Scene.Camera;
}
if ( !_camera.IsValid() )
return;
RotateMainCamera();
MoveMainCamera();
}
protected override void OnFixedUpdate()
{
UpdatePosition();
}
private void RotateMainCamera()
{
_lookAngle += Input.AnalogLook;
if ( ClampPitch )
{
_lookAngle.pitch = _lookAngle.pitch.Clamp( -89f, 89f );
}
_camera.Transform.Rotation = _lookAngle;
}
/// <summary>
/// Move the main scene camera to roughly the position of this GameObject.
/// </summary>
private void MoveMainCamera()
{
if ( UseCollision )
{
// Put the camera up in to the center of the CharacterController's collision cube.
_camera.Transform.Position = Transform.Position + Vector3.Up * 8f;
}
else
{
_camera.Transform.Position = Transform.Position;
}
}
/// <summary>
/// Use input to move this GameObject. If <see cref="UseCollision"/> is true, a <see cref="CharacterController"/>
/// will be used to ensure that this GameObject doesn't clip through anything it shouldn't.
/// </summary>
private void UpdatePosition()
{
EnsureCollision();
// Move using WASD or left thumbstick
var movement = Input.AnalogMove * Speed * GetSpeedFactor();
// Move relative to the direction the camera is facing.
movement *= _lookAngle;
if ( UseCollision )
{
_controller.Velocity = movement;
_controller.Move();
_controller.IsOnGround = false;
}
else
{
Transform.Position += movement * Time.Delta;
}
}
private void EnsureCollision()
{
if ( !UseCollision )
return;
_controller ??= Components.GetOrCreate<CharacterController>();
_controller.Radius = 8f;
_controller.Height = 16f;
}
private float GetSpeedFactor()
{
var speedFactor = 1f;
if ( Input.Down( CameraActions.MoveSlow ) )
{
speedFactor = LowSpeedFactor;
}
else if ( Input.Down( CameraActions.MoveFast ) )
{
speedFactor = HighSpeedFactor;
}
return speedFactor;
}
}