gates-multiplayer/player/player.gd
2024-03-31 11:20:32 +04:00

191 lines
6.5 KiB
GDScript

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