using Godot; using System; public partial class Player : CharacterBody3D { public float MouseSensitivity { get; set; } = 0.2F; /// Time after pressing the jump button a jump may occur late. public TimeSpan JumpEarlyTime { get; set; } = TimeSpan.FromSeconds(0.2); /// Time after leaving a jumpable surface when a jump may still occur. public TimeSpan JumpCoyoteTime { get; set; } = TimeSpan.FromSeconds(0.2); public Vector3 Gravity { get; set; } = new(0, -12.0F, 0); public float JumpVelocity { get; set; } = 5.0F; public float MoveAccel { get; set; } = 6.0F; public float MaxMoveSpeed { get; set; } = 4.0F; public float FrictionFloor { get; set; } = 12.0F; public float FrictionAir { get; set; } = 2.0F; public enum MovementMode { Default, Flying, NoClip } public MovementMode Movement { get; set; } = MovementMode.Default; private Node3D _neckBone = null!; private Node3D _headBone = null!; private Camera3D _camera = null!; private DateTime? _jumpPressed = null; private DateTime? _lastOnFloor = null; public bool IsSprinting { get; private set; } public override void _Ready() { _neckBone = GetNode("Neck"); _headBone = GetNode("Neck/Head"); _camera = GetNode("Neck/Head/Camera"); } public override void _Input(InputEvent ev) { // Inputs that are valid when the game is focused. // =============================================== if (ev.IsAction("move_sprint")) { IsSprinting = ev.IsPressed(); GetViewport().SetInputAsHandled(); } if (ev.IsActionPressed("move_jump")) { _jumpPressed = DateTime.Now; GetViewport().SetInputAsHandled(); } // Cycle movement mode between default, flying and flying+noclip. if (ev.IsActionPressed("cycle_movement_mode")) { if (++Movement > MovementMode.NoClip) Movement = MovementMode.Default; GetViewport().SetInputAsHandled(); } // Inputs that are valid only when the mouse is captured. // ====================================================== if (Input.MouseMode == Input.MouseModeEnum.Captured) { } } public override void _UnhandledInput(InputEvent ev) { var isMouseCaptured = Input.MouseMode == Input.MouseModeEnum.Captured; // When pressing escape and mouse is currently captured, release it. if (ev.IsActionPressed("ui_cancel") && isMouseCaptured) Input.MouseMode = Input.MouseModeEnum.Visible; // Grab the mouse when pressing the primary mouse button. // TODO: Make "primary mouse button" configurable. if (ev is InputEventMouseButton button && button.ButtonIndex == MouseButton.Left) Input.MouseMode = Input.MouseModeEnum.Captured; if (ev is InputEventMouseMotion motion && isMouseCaptured) { _neckBone.RotateX(Mathf.DegToRad(motion.Relative.Y * -MouseSensitivity)); _headBone.RotateY(Mathf.DegToRad(motion.Relative.X * -MouseSensitivity)); var rotation = _neckBone.RotationDegrees; rotation.X = Mathf.Clamp(rotation.X, -80, 80); _neckBone.RotationDegrees = rotation; } } public override void _PhysicsProcess(double delta) { var movementVector = new Vector3( Input.GetActionStrength("move_strafe_right") - Input.GetActionStrength("move_strafe_left"), Input.GetActionStrength("move_upward") - Input.GetActionStrength("move_downward"), Input.GetActionStrength("move_backward") - Input.GetActionStrength("move_forward")); if (Movement == MovementMode.Default) { Velocity += Gravity * (float)delta; var dir = Vector3.Zero; var camTransform = _camera.GlobalTransform; dir += camTransform.Basis.Z.Normalized() * movementVector.Z; dir += camTransform.Basis.X.Normalized() * movementVector.X; dir.Y = 0; dir = dir.Normalized() * movementVector.Length(); var hvel = Velocity; hvel.Y = 0; var target = dir * MaxMoveSpeed; var friction = IsOnFloor() ? FrictionFloor : FrictionAir; var accel = (dir.Dot(hvel) > 0) ? MoveAccel : friction; if (IsSprinting) { target *= 5; accel *= 5; } hvel = hvel.Lerp(target, accel * (float)delta); Velocity = new(hvel.X, Velocity.Y, hvel.Z); // Sometimes, when pushing into a wall, jumping wasn't working. // Possibly due to `IsOnFloor` returning `false` for some reason. // The `JumpEarlyTime` feature seems to avoid this issue, thankfully. if (IsOnFloor()) _lastOnFloor = DateTime.Now; if (((DateTime.Now - _jumpPressed) <= JumpEarlyTime) && ((DateTime.Now - _lastOnFloor) <= JumpCoyoteTime)) { Velocity = new(Velocity.X, JumpVelocity, Velocity.Z); _jumpPressed = null; _lastOnFloor = null; } } else { Velocity *= 1 - FrictionAir * (float)delta; var cameraRot = _headBone.GlobalTransform.Basis.GetRotationQuaternion(); var dir = cameraRot * movementVector; var target = dir * MaxMoveSpeed; var accel = (dir.Dot(Velocity) > 0) ? MoveAccel : FrictionAir; target *= 4; accel *= 4; if (IsSprinting) { target *= 5; accel *= 5; } Velocity = Velocity.Lerp(target, accel * (float)delta); } if (Movement == MovementMode.NoClip) Translate(Velocity * (float)delta); else MoveAndSlide(); } }