class_name Player extends CharacterBody3D @export var camera: Camera @export_group("Movement") @export var move_accel := 6.0 @export var move_max_speed := 4.0 @export var friction_floor := 12.0 @export var friction_air := 2.0 @export var gravity := -12.0 @export_group("Jumping") @export var jump_velocity := 5.0 @export var jump_early_time := 0.0 # Time (in seconds) after pressing the jump button a jump may occur late. @export var jump_coyote_time := 0.0 # Time (in seconds) after leaving a jumpable surface when a jump may still occur. enum MovementModeEnum { DEFAULT, FLYING, NO_CLIP } var movement_mode := MovementModeEnum.DEFAULT var is_moving := false var is_sprinting := false var time_since_jump_pressed := INF var time_since_on_floor := INF func _input(event: InputEvent) -> void: # Inputs that are valid when the game is focused. # =============================================== if event.is_action("move_sprint"): is_sprinting = event.is_pressed() get_viewport().set_input_as_handled() if event.is_action_pressed("move_jump"): time_since_jump_pressed = 0 get_viewport().set_input_as_handled() # Cycle movement mode between default, flying and noclip. if event.is_action_pressed("cycle_movement_mode"): if (+movement_mode > MovementModeEnum.NO_CLIP): movement_mode = MovementModeEnum.DEFAULT; get_viewport().set_input_as_handled() # Inputs that are valid only when the mouse is captured. # ====================================================== if Input.mouse_mode == Input.MOUSE_MODE_CAPTURED: pass func _physics_process(delta: float) -> void: match movement_mode: MovementModeEnum.DEFAULT: process_movement_default(delta) MovementModeEnum.FLYING: process_movement_flying(delta, false) MovementModeEnum.NO_CLIP: process_movement_flying(delta, true) func process_movement_default(delta: float) -> void: var input := Input.get_vector("move_strafe_left", "move_strafe_right", "move_forward", "move_backward") velocity.y += gravity * delta var hor_vel := Vector3(velocity.x, 0, velocity.z) var target := basis.rotated(Vector3.UP, camera.current_yaw) * Vector3(input.x, 0, input.y) * move_max_speed is_moving = target.dot(hor_vel) > 0 var accel: float if is_moving: accel = move_accel elif is_on_floor(): accel = friction_floor else: accel = friction_air if is_sprinting: target *= 5 accel *= 5 hor_vel = hor_vel.lerp(target, accel * delta) velocity = Vector3(hor_vel.x, velocity.y, hor_vel.z) # TODO: Check if this still applies. # 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 is_on_floor(): time_since_on_floor = 0 else: time_since_on_floor += delta if time_since_jump_pressed <= jump_early_time && time_since_on_floor <= jump_coyote_time: velocity.y = jump_velocity time_since_jump_pressed = INF time_since_on_floor = INF move_and_slide() func process_movement_flying(delta: float, no_clip: bool) -> void: var input := Vector3( Input.get_axis("move_strafe_left", "move_strafe_right"), Input.get_axis("move_downward", "move_upward"), Input.get_axis("move_forward", "move_backward")) velocity *= 1 - friction_air * delta; var target := camera.global_transform.basis.get_rotation_quaternion() * input * move_max_speed is_moving = target.dot(velocity) > 0 var accel: float if is_moving: accel = move_accel else: accel = friction_air target *= 4 accel *= 4 if is_sprinting: target *= 5 accel *= 5 velocity = velocity.lerp(target, accel * delta) if no_clip: translate(velocity * delta) else: move_and_slide()