extends Node
@export var player: CharacterBody3D
@export var camera: Camera3D
@export var anim_tree: AnimationTree
# remember previous on_floor state
var was_on_floor: bool = true
# for jump
var jump_pressed: bool = false
var jump_available: bool = true
# for walljump
var walljump_direction: Vector3 = Vector3.ZERO
var walljump_delay: float = -1
var wall_touched: bool = false
# used to compute step sounds
var distance_traveled: float = 0
var last_footstep_left: bool = true
# output values
var velocity: Vector3 = Vector3.ZERO
var is_moving: bool = false
var velocity_rate: float = 0
# accelerations
const WALK_ACCELERATION: float = 80
const WALK_DECELERATION: float = 50
const AIR_CONTROL_ACCELERATION: float = 15
const GRAVITY: float = 30
# velocities
const MAX_WALK_VELOCITY: float = 10
const JUMP_VELOCITY: float = 14
const MAX_AIR_CONTROL_ROTATION: float = PI/2 # radians per second
const MAX_RAILJUMP_IMPULSE: float = 20
# ref to the locomotion blend space 2d of the rigged model
const CHARACTER_LOCOMOTION: String = "parameters/LegsStateMachine/LegsRun/blend_position"
const CHARACTER_LOOKPITCH: String = "parameters/VerticalAim/blend_position"
const CHARACTER_AIRBORNE: String = "parameters/LegsStateMachine/conditions/airborne"
const CHARACTER_ONGROUND: String = "parameters/LegsStateMachine/conditions/onground"
# other
const JUMP_ANGLE: float = 35 * PI / 180
const STEP_LENGTH: float = 2.5
func _process(_delta: float) -> void:
if Input.is_action_just_pressed("jump"):
jump_pressed = true
if Input.is_action_just_released("jump"):
jump_available = true
jump_pressed = false
func _unhandled_input(event: InputEvent) -> void:
if not player.can_use_input(): return
if event is InputEventMouseMotion:
player.rotate_y(-event.relative.x * Global.mouse_sensitivity)
camera.rotate_x(-event.relative.y * Global.mouse_sensitivity)
camera.rotation.x = clamp(camera.rotation.x, -PI/2, PI/2)
func move_player(delta: float) -> void:
player.is_on_ground = player.is_on_floor()
velocity = player.velocity
if not player.is_on_ground:
distance_traveled = 0
# play the landing sound
if (not was_on_floor) and player.is_on_ground:
play_footstep.rpc(false, true)
was_on_floor = player.is_on_ground
# Get the input direction and handle the movement/deceleration.
velocity_rate = 0
var input_dir: Vector2 = Input.get_vector("left", "right", "up", "down")
var direction: Vector3 = (player.transform.basis * Vector3(input_dir.x, 0, input_dir.y)).normalized()
var horizontal_vel := Vector3(velocity.x, 0, velocity.z)
var is_on_wall: bool = player.is_on_wall_only()
is_moving = not direction.is_zero_approx()
if is_moving:
if player.is_on_ground:
var new_velocity: Vector3 = horizontal_vel + direction * WALK_ACCELERATION * delta
new_velocity = new_velocity.normalized() * min(new_velocity.length(), MAX_WALK_VELOCITY)
velocity.x = new_velocity.x
velocity.z = new_velocity.z
distance_traveled += velocity.length() * delta
if(distance_traveled >= STEP_LENGTH):
distance_traveled -= STEP_LENGTH
play_footstep.rpc(false, false)
elif direction != Vector3.ZERO:
# apply movement air control
var new_velocity: Vector3 = horizontal_vel + direction * AIR_CONTROL_ACCELERATION * delta
new_velocity = new_velocity.normalized() * min(new_velocity.length(), JUMP_VELOCITY)
horizontal_vel = Vector3(new_velocity.x, 0, new_velocity.z)
var target_angle: float = horizontal_vel.angle_to(direction)
if target_angle == 0:
velocity.x = horizontal_vel.x
velocity.z = horizontal_vel.z
else:
# apply rotation air control
var max_control: float = cos(min(target_angle, PI*0.5)) * MAX_AIR_CONTROL_ROTATION * delta
var turn_angle: float = min(target_angle, max_control)
var rotated_vel: Vector3 = horizontal_vel.slerp(direction, turn_angle/target_angle)
rotated_vel = rotated_vel.normalized() * horizontal_vel.length()
velocity.x = rotated_vel.x
velocity.z = rotated_vel.z
else:
if player.is_on_ground:
var brake_velocity: Vector3 = velocity.normalized() * WALK_DECELERATION * delta
if brake_velocity.length_squared() > velocity.length_squared():
velocity = Vector3.ZERO
else:
velocity -= brake_velocity
if distance_traveled > 0:
distance_traveled = 0
play_footstep.rpc(true, false)
# Handle Jump.
if jump_pressed and jump_available:
if player.is_on_ground:
jump_available = false
play_jump_sound.rpc()
play_footstep.rpc(false, false)
if is_moving:
var xz_velocity: Vector3 = direction.normalized()
velocity = Vector3(cos(JUMP_ANGLE) * xz_velocity.x,\
sin(JUMP_ANGLE),\
cos(JUMP_ANGLE) * xz_velocity.z) * JUMP_VELOCITY
else:
velocity.y = JUMP_VELOCITY
if is_on_wall and walljump_delay > 0:
jump_available = false
walljump_delay = -1
play_jump_sound.rpc()
play_footstep.rpc(false, false)
var xz_velocity: Vector3 = walljump_direction.normalized()
velocity = Vector3(cos(JUMP_ANGLE) * xz_velocity.x * JUMP_VELOCITY,\
velocity.y + sin(JUMP_ANGLE) * JUMP_VELOCITY,\
cos(JUMP_ANGLE) * xz_velocity.z * JUMP_VELOCITY)
# apply gravity and move
var velocity_before: Vector3 = velocity
velocity.y -= GRAVITY * delta
player.velocity = velocity
player.move_and_slide()
is_on_wall = player.is_on_wall_only()
velocity_rate = Vector2(player.velocity.x, player.velocity.z).length() / MAX_WALK_VELOCITY
# handle walljump
if Mutators.walljump:
horizontal_vel = Vector3(velocity_before.x, 0, velocity_before.z)
var horizontal_vel_norm: Vector3 = horizontal_vel.normalized()
var wall_normal: Vector3 = player.get_wall_normal()
if is_on_wall\
and not wall_touched\
and horizontal_vel.length() > 8\
and horizontal_vel_norm.dot(wall_normal) < -0.5:
walljump_direction = horizontal_vel_norm.bounce(wall_normal)
walljump_delay = 1 # delay in seconds to press jump
play_footstep.rpc(false, false)
if player.is_on_ground:
walljump_delay = -1
walljump_delay -= delta
wall_touched = is_on_wall
func process(_delta: float) -> void:
var cam_tr: Vector3 = -camera.global_transform.basis.z
var cam_tr2d := Vector2(cam_tr.x, cam_tr.z)
var angle_to: float = cam_tr2d.angle_to(Vector2(velocity.x, velocity.z))
var velocity_ground := Vector2(velocity.x, velocity.z)
var move_speed_factor: float = velocity_ground.length() / MAX_WALK_VELOCITY
var camera_pitch_sin: float = sin(camera.rotation.x)
anim_tree.set(CHARACTER_LOCOMOTION, Vector2(sin(angle_to), cos(angle_to)) * move_speed_factor)
anim_tree.set(CHARACTER_AIRBORNE, !player.is_on_ground)
anim_tree.set(CHARACTER_ONGROUND, player.is_on_ground)
anim_tree.set(CHARACTER_LOOKPITCH, camera_pitch_sin)
@rpc("call_local")
func play_footstep(optional: bool = false, both: bool = false) -> void:
if(optional and ($"../Feet/LeftFoot".playing or $"../Feet/RightFoot".playing)):
return
if last_footstep_left:
$"../Feet/RightFoot".play()
else:
$"../Feet/LeftFoot".play()
last_footstep_left = !last_footstep_left
if both:
$"../Feet/Timer".stop()
$"../Feet/Timer".start()
@rpc("call_local", "reliable")
func play_jump_sound() -> void:
$"../Feet/Jump".play()