extends CharacterBody3D class_name Player # player settings @export_group("Movement") @export var walk_speed := 3.0 @export var jog_speed := 6.0 @export var charge_speed := 20 @export var air_speed := 3.0 @export var acceleration := 20.0 @export var rotation_speed := 10.0 @export var idle_timeout := 5.0 @export var hard_landing_limit := 10.0 @export_group("Physics") @export var push_force := 5.0 @export_group("Camera") @export_range(1.0, 10.0) var camera_distance := 2.0 @export_range(0.0, 1.0) var mouse_sensitivity := 0.15 @export_range(0.0, 1.0) var mouse_sensitivity_x := 1.0 @export_range(0.0, 1.0) var mouse_sensitivity_y := 0.5 @export_range(0.0, 10.0) var joystick_sensitivity_x := 4.0 @export_range(0.0, 10.0) var joystick_sensitivity_y := 2.0 @onready var _debug: CanvasLayer = %debug @onready var _camera_pivot: Node3D = %camera_pivot @onready var _camera: Camera3D = %camera @onready var _camera_spring: SpringArm3D = %spring @onready var _skin: AnimatedSkin = %skin @onready var _dust: GPUParticles3D = %dust var _last_movement_direction := rotation var _floor_normal := Vector3.ONE var _ground_slope_input := 0.0 var _camera_input_direction := Vector2.ZERO enum {CAMERA_MOUSE_INPUT, CAMERA_JOYSTICK_INPUT} var _camera_input_method := CAMERA_MOUSE_INPUT var _idle_time: float = 0.0 var _player_speed: float = 0 func _ready() -> void: _debug.draw.add_vector(self, "velocity", 1, 1, Color(0,1,0,1)) _debug.draw.add_vector(self, "_floor_normal", 1, 1, Color(0, 0, 1, 1)) _debug.draw.add_vector(self, "_last_movement_direction", 1, 1, Color(1,0,0,1)) _debug.stats.add_property(self, "velocity", "length") _debug.stats.add_property(self, "_idle_time", "round") _debug.stats.add_property(self, "_ground_slope_input", "round") _camera_spring.spring_length = camera_distance _player_speed = jog_speed _skin.set_grounded() func _physics_process(delta: float) -> void: _process_camera(delta) _process_player(delta) func _unhandled_input(event: InputEvent) -> void: # If user clicks on the window, capture the mouse and direct the camera with it if Input.get_mouse_mode() != Input.MOUSE_MODE_CAPTURED: return #_camera_input_direction *= mouse_sensitivity if event is InputEventMouseMotion: _camera_input_method = CAMERA_MOUSE_INPUT _camera_input_direction = event.screen_relative * mouse_sensitivity elif event is InputEventJoypadMotion: _camera_input_method = CAMERA_JOYSTICK_INPUT _camera_input_direction = Input.get_vector("camera-left", "camera-right", "camera-up", "camera-down") _camera_input_direction *= Vector2(joystick_sensitivity_x, -joystick_sensitivity_y) func _input(event: InputEvent): if event.is_action_pressed("player_run"): _player_speed = walk_speed elif event.is_action_released("player_run"): _player_speed = jog_speed if event.is_action_pressed("player_attack") and velocity.length() > jog_speed * .75: _player_speed = charge_speed elif event.is_action_released("player_attack"): _player_speed = jog_speed # Get the XZ input direction based on player's input relative to the camera func _get_player_move_direction() -> Vector3: var input_dir := Input.get_vector("player_left", "player_right", "player_forward", "player_backward") var forward := _camera.global_basis.z var right := _camera.global_basis.x var move_direction := (forward * input_dir.y + right * input_dir.x).normalized() move_direction.y = 0 return move_direction func _process_camera(delta: float) -> void: # vertical camera rotation _camera_pivot.rotation.x += _camera_input_direction.y * mouse_sensitivity_y * delta _camera_pivot.rotation.x = clamp(_camera_pivot.rotation.x, -PI / 6, PI / 3) # horizontal camera rotation _camera_pivot.rotation.y -= _camera_input_direction.x * mouse_sensitivity_x * delta # reset mouse movement vector if mouse input if _camera_input_method == CAMERA_MOUSE_INPUT: _camera_input_direction = Vector2.ZERO # change spring length depending on player speed _camera_spring.spring_length = lerp( _camera_spring.spring_length, camera_distance + velocity.length() / 4, delta ) func _process_player_on_floor(delta: float): var move_direction := _get_player_move_direction() # if we're not stuck, then it's okay to set the velocity _floor_normal = get_floor_normal() _ground_slope_input = (PI / 2) - velocity.angle_to(_floor_normal) velocity = velocity.move_toward( move_direction * (_player_speed + _ground_slope_input * _player_speed), acceleration * delta ) var movement_speed := Vector3(velocity.x, 0, velocity.z).length() # also, if we're moving, we're not idle if move_direction.length() < 0.2: if velocity == Vector3.ZERO: _idle_time += delta else: _last_movement_direction = move_direction _idle_time = 0.0 # if camera is unlocked, rotate whole skin to face movement direction var skin_target_angle := Vector3.BACK.signed_angle_to(_last_movement_direction, Vector3.UP) _skin.global_rotation.y = lerp_angle( _skin.global_rotation.y, skin_target_angle, rotation_speed * delta ) # lean into player momentum just a little bit _skin.rotation.z = lerp_angle( _skin.rotation.z, clamp(_last_movement_direction.signed_angle_to(velocity, Vector3.UP) * movement_speed * 0.08, -PI/4, PI/ 4), rotation_speed * delta * 0.25 ) # let skin know how fast player is moving along the ground _skin.set_grounded_speed(movement_speed) # timescale tweaking for fun effect! if _player_speed == charge_speed: _skin.set_timescale(2.0) else: _skin.set_timescale(1.0) func _process_player(delta: float) -> void: if is_on_floor(): _process_player_on_floor(delta) if _idle_time > idle_timeout: _skin.set_idle() else: _skin.set_grounded() _dust.emitting = velocity.length() > (0.75 * charge_speed) and is_on_floor() _dust.amount_ratio = velocity.length() else: _skin.set_falling() velocity += get_gravity() * air_speed * delta var prev_velocity := velocity move_and_slide() # handle collisions for i in get_slide_collision_count(): var c := get_slide_collision(i) if c.get_collider() is RigidBody3D: var col: RigidBody3D = c.get_collider() col.apply_central_impulse(-c.get_normal() * prev_velocity.length() * push_force)