public partial class MovementController : Node { [ExportGroup("Movement")] [Export] public float Acceleration { get; set; } = 4.0f; [Export] public float MaxSpeed { get; set; } = 2.1f; [Export] public float FrictionFloor { get; set; } = 12.0f; [Export] public float FrictionAir { get; set; } = 2.0f; public float Gravity { get; } = (float)ProjectSettings.GetSetting("physics/3d/default_gravity"); [ExportGroup("Jumping")] [Export] public float JumpVelocity { get; set; } = 4.0f; /// Time (in seconds) after pressing the jump button a jump may occur late. [Export] public float JumpEarlyTime { get; set; } = 0.0f; /// Time (in seconds) after leaving a jumpable surface when a jump may still occur. [Export] public float JumpCoyoteTime { get; set; } = 0.0f; public bool IsSprinting { get; private set; } /// The raw input movement vector with a maximum length of 1. [Export] public Vector2 InputVector { get; private set; } public bool IsMoving => LocalMoveVector.Length() > 0.01f; /// The actual amount the player is moving, relative to the world. Y is always 0. public Vector3 GlobalMoveVector { get; private set; } /// The actual amount the player is moving, relative to the player's viewpoint. Y is always 0. public Vector3 LocalMoveVector { get; private set; } /// public float LocalMoveAngle { get; private set; } public float TimeSinceJumpPressed { get; private set; } = float.PositiveInfinity; public float TimeSinceOnFloor { get; private set; } = float.PositiveInfinity; Player _player; public override void _Ready() => _player = GetParent(); public override void _UnhandledInput(InputEvent @event) { if (!_player.IsLocal) return; // TODO: Sprinting. if (@event.IsActionPressed("move_jump")) { TimeSinceJumpPressed = 0.0f; GetViewport().SetInputAsHandled(); } } public override void _PhysicsProcess(double delta) { var velocity = _player.Velocity; velocity.Y -= Gravity * (float)delta; // Get the (normalized) movement vector from the current input. InputVector = _player.IsLocal ? Input.GetVector("move_strafe_left", "move_strafe_right", "move_forward", "move_back") : Vector2.Zero; var horVelocity = velocity with { Y = 0 }; var basis = _player.Basis.Rotated(Vector3.Up, _player.Camera.CurrentYaw); var target = basis * new Vector3(InputVector.X, 0, InputVector.Y) * MaxSpeed; var isMoving = target.Dot(horVelocity) > 0.0f; var isOnFloor = _player.IsOnFloor(); var accel = isMoving ? Acceleration : isOnFloor ? FrictionFloor : FrictionAir; if (IsSprinting) { target *= 5; accel *= 5; } horVelocity = horVelocity.Lerp(target, accel * (float)delta); velocity.X = horVelocity.X; velocity.Z = horVelocity.Z; if (isOnFloor) TimeSinceOnFloor = 0.0f; else TimeSinceOnFloor += (float)delta; if ((TimeSinceJumpPressed <= JumpEarlyTime) && (TimeSinceOnFloor <= JumpCoyoteTime)) { TimeSinceJumpPressed = TimeSinceOnFloor = float.PositiveInfinity; velocity.Y = JumpVelocity; } else TimeSinceJumpPressed += (float)delta; _player.Velocity = velocity; _player.MoveAndSlide(); // TODO: Very simplified, but works for now. GlobalMoveVector = _player.Velocity with { Y = 0 }; LocalMoveVector = _player.Basis.Inverse() * GlobalMoveVector; LocalMoveAngle = Vector3.Forward.SignedAngleTo(LocalMoveVector, Vector3.Up); } }