public partial class CameraController : Camera3D { [ExportCategory("Follow")] [Export] public float FollowDistance { get; set; } = 1.2f; [Export] public float FollowYOffset { get; set; } = 1.0f; [Export] public float FollowSmoothing { get; set; } = 12.0f; [ExportCategory("Rotation")] [Export] public Vector2 MouseSensitivity { get; set; } = new(0.2f, 0.2f); // Degrees per pixel. [Export] public float PitchMinimum { get; set; } = 20; [Export] public float PitchMaximum { get; set; } = 65; [Export] public float PitchSmoothing { get; set; } = 12.0f; // TODO: Gradually return to maximum spring length. // TODO: Turn player transparent as the camera moves closer. public static bool IsMouseCaptured => Input.MouseMode == Input.MouseModeEnum.Captured; Node3D _player; Vector3 _smoothPlayerPos; public override void _Ready() { _player = this.GetParentOrThrow(); _smoothPlayerPos = _player.GlobalPosition; Transform = _player.GlobalTransform.Translated(new(0.0f, FollowYOffset, 0.0f)); } public override void _Input(InputEvent ev) { if (IsMouseCaptured && ev.IsActionPressed("ui_cancel")) { // When escape is pressed, release the mouse. Input.MouseMode = Input.MouseModeEnum.Visible; GetViewport().SetInputAsHandled(); } } public override void _UnhandledInput(InputEvent ev) { switch (ev) { case InputEventMouseButton { ButtonIndex: MouseButton.Left, Pressed: true } when !IsMouseCaptured: // When left mouse button is pressed, capture the mouse. Input.MouseMode = Input.MouseModeEnum.Captured; GetViewport().SetInputAsHandled(); break; case InputEventMouseMotion motion when IsMouseCaptured: ApplyRotation(-motion.Relative * MouseSensitivity); break; } } void ApplyRotation(Vector2 delta) { var (pitch, yaw, _) = RotationDegrees; pitch += delta.Y; yaw += delta.X; RotationDegrees = new(pitch, yaw, 0); } public override void _PhysicsProcess(double delta) { _smoothPlayerPos = _smoothPlayerPos.Damp(_player.GlobalPosition, FollowSmoothing, delta); var target = _smoothPlayerPos + Basis.Z * FollowDistance + Vector3.Up * FollowYOffset; Position = OffsetRayIntersection(_smoothPlayerPos, target, 0.2f); } Vector3 OffsetRayIntersection(Vector3 from, Vector3 to, float margin) { const PhysicsLayer CollisionMask = PhysicsLayer.Terrain | PhysicsLayer.Objects; var query = PhysicsRayQueryParameters3D.Create(from, to, (uint)CollisionMask); var result = GetWorld3D().DirectSpaceState.IntersectRay(query); if (result.Count > 0) { var hit = (Vector3)result["position"]; var safeDistance = Max(0, from.DistanceTo(hit) - margin); return from + (to - from).Normalized() * safeDistance; } else { // No intersection occured, return to; } } public override void _Process(double delta) { var pitch = RotationDegrees.X; var (min, max) = (-PitchMaximum, -PitchMinimum); if (pitch < min) pitch = pitch.Damp(min, PitchSmoothing, delta); if (pitch > max) pitch = pitch.Damp(max, PitchSmoothing, delta); RotationDegrees = RotationDegrees with { X = pitch }; } }