extends CharacterBody3D class_name Player ## Character maximum run speed on the ground. @export var move_speed := 8.0 ## Forward impulse after a melee attack. @export var attack_impulse := 10.0 ## Movement acceleration (how fast character achieve maximum speed) @export var acceleration := 6.0 ## Jump impulse @export var jump_initial_impulse := 12.0 ## Jump impulse when player keeps pressing jump @export var jump_additional_force := 4.5 ## Player model rotation speed @export var rotation_speed := 12.0 ## Minimum horizontal speed on the ground. This controls when the character's animation tree changes ## between the idle and running states. @export var stopping_speed := 1.0 ## Clamp sync delta for faster interpolation @export var sync_delta_max := 0.2 @onready var _rotation_root: Node3D = $CharacterRotationRoot @onready var _camera_controller: CameraController = $CameraController @onready var _ground_shapecast: ShapeCast3D = $GroundShapeCast @onready var _character_skin: CharacterSkin = $CharacterRotationRoot/CharacterSkin @onready var _synchronizer: MultiplayerSynchronizer = $MultiplayerSynchronizer @onready var _move_direction := Vector3.ZERO @onready var _last_strong_direction := Vector3.FORWARD @onready var _gravity: float = -30.0 @onready var _ground_height: float = 0.0 ## Sync properties @export var _position: Vector3 @export var _velocity: Vector3 @export var _direction: Vector3 = Vector3.ZERO @export var _strong_direction: Vector3 = Vector3.FORWARD var position_before_sync: Vector3 var last_sync_time_ms: int var sync_delta: float func _ready() -> void: if is_multiplayer_authority(): _camera_controller.setup(self) else: rotation_speed /= 1.5 _synchronizer.delta_synchronized.connect(on_synchronized) _synchronizer.synchronized.connect(on_synchronized) on_synchronized() func _physics_process(delta: float) -> void: if not is_multiplayer_authority(): interpolate_client(delta); return # Calculate ground height for camera controller if _ground_shapecast.get_collision_count() > 0: for collision_result in _ground_shapecast.collision_result: _ground_height = max(_ground_height, collision_result.point.y) else: _ground_height = global_position.y + _ground_shapecast.target_position.y if global_position.y < _ground_height: _ground_height = global_position.y # Get input and movement state var is_just_jumping := Input.is_action_just_pressed("jump") and is_on_floor() var is_air_boosting := Input.is_action_pressed("jump") and not is_on_floor() and velocity.y > 0.0 _move_direction = _get_camera_oriented_input() if EditMode.is_enabled: is_just_jumping = false is_air_boosting = false _move_direction = Vector3.ZERO # To not orient quickly to the last input, we save a last strong direction, # this also ensures a good normalized value for the rotation basis. if _move_direction.length() > 0.2: _last_strong_direction = _move_direction.normalized() _orient_character_to_direction(_last_strong_direction, delta) # We separate out the y velocity to not interpolate on the gravity var y_velocity := velocity.y velocity.y = 0.0 velocity = velocity.lerp(_move_direction * move_speed, acceleration * delta) if _move_direction.length() == 0 and velocity.length() < stopping_speed: velocity = Vector3.ZERO velocity.y = y_velocity # Update position velocity.y += _gravity * delta if is_just_jumping: velocity.y += jump_initial_impulse elif is_air_boosting: velocity.y += jump_additional_force * delta # Set character animation if is_just_jumping: _character_skin.jump.rpc() elif not is_on_floor() and velocity.y < 0: _character_skin.fall.rpc() elif is_on_floor(): var xz_velocity := Vector3(velocity.x, 0, velocity.z) if xz_velocity.length() > stopping_speed: _character_skin.set_moving.rpc(true) _character_skin.set_moving_speed.rpc(inverse_lerp(0.0, move_speed, xz_velocity.length())) else: _character_skin.set_moving.rpc(false) var position_before := global_position move_and_slide() var position_after := global_position # If velocity is not 0 but the difference of positions after move_and_slide is, # character might be stuck somewhere! var delta_position := position_after - position_before var epsilon := 0.001 if delta_position.length() < epsilon and velocity.length() > epsilon: global_position += get_wall_normal() * 0.1 set_sync_properties() func set_sync_properties() -> void: _position = position _velocity = velocity _direction = _move_direction _strong_direction = _last_strong_direction func on_synchronized() -> void: velocity = _velocity position_before_sync = position var sync_time_ms = Time.get_ticks_msec() sync_delta = clampf(float(sync_time_ms - last_sync_time_ms) / 1000, 0, sync_delta_max) last_sync_time_ms = sync_time_ms func interpolate_client(delta: float) -> void: _orient_character_to_direction(_strong_direction, delta) if _direction.length() == 0: # Don't interpolate to avoid small jitter when stopping if (_position - position).length() > 1.0 and _velocity.is_zero_approx(): position = _position # Fix misplacement else: # Interpolate between position_before_sync and _position # and add to ongoing movement to compensate misplacement var t = 1.0 if is_zero_approx(sync_delta) else delta / sync_delta sync_delta = clampf(sync_delta - delta, 0, sync_delta_max) var less_misplacement = position_before_sync.move_toward(_position, t) position += less_misplacement - position_before_sync position_before_sync = less_misplacement velocity.y += _gravity * delta move_and_slide() func _get_camera_oriented_input() -> Vector3: var raw_input := Input.get_vector("move_left", "move_right", "move_up", "move_down") var input := Vector3.ZERO # This is to ensure that diagonal input isn't stronger than axis aligned input input.x = -raw_input.x * sqrt(1.0 - raw_input.y * raw_input.y / 2.0) input.z = -raw_input.y * sqrt(1.0 - raw_input.x * raw_input.x / 2.0) input = _camera_controller.global_transform.basis * input input.y = 0.0 return input func _orient_character_to_direction(direction: Vector3, delta: float) -> void: var left_axis := Vector3.UP.cross(direction) var rotation_basis := Basis(left_axis, Vector3.UP, direction).get_rotation_quaternion() var model_scale := _rotation_root.transform.basis.get_scale() _rotation_root.transform.basis = Basis(_rotation_root.transform.basis.get_rotation_quaternion().slerp(rotation_basis, delta * rotation_speed)).scaled( model_scale ) @rpc("any_peer", "call_remote", "reliable") func respawn(spawn_position: Vector3) -> void: global_position = spawn_position velocity = Vector3.ZERO