Skip to content

Instantly share code, notes, and snippets.

@levidavidmurray
Last active June 6, 2024 19:43
Show Gist options
  • Save levidavidmurray/d1dd566cb0071e8661a2e354d4f4fe6d to your computer and use it in GitHub Desktop.
Save levidavidmurray/d1dd566cb0071e8661a2e354d4f4fe6d to your computer and use it in GitHub Desktop.
## Base class for all characters in the game.
class_name SC_Character
extends CharacterBody3D
##############################
## SIGNALS
##############################
signal died
signal spawned
##############################
## EXPORT VARIABLES
##############################
@export var base_move_speed: float = 7.0
@export var sprint_speed_multiplier: float = 1.7
@export var airborne_speed_multiplier: float = 0.6
@export var move_acceleration: float = 20.0
##############################
## ONREADY VARIABLES
##############################
@onready var state_chart: StateChart = %StateChart
@onready var health: HealthComponent = %HealthComponent
##############################
## VARIABLES
##############################
var target_velocity: Vector3
var actual_velocity: Vector3
var move_speed: float
var gravity: float
##############################
## LIFECYCLE METHODS
##############################
func _ready():
move_speed = base_move_speed
health.died.connect(_on_died)
health.changed.connect(_on_health_changed)
state_chart.set_expression_property("health", health.health)
##############################
## STATE CALLBACKS
##############################
# === ALIVE ===
func _on_alive_state_entered():
spawned.emit()
# === GROUNDED ===
func _on_grounded_state_physics_processing(delta: float):
if gravity < 0.0:
gravity = 0.0
_handle_physics_process(delta)
if not is_on_floor():
state_chart.send_event("airborne")
# === STANDING ===
func _on_standing_state_entered():
move_speed = base_move_speed
# === AIRBORNE ===
func _on_airborne_state_entered():
move_speed = base_move_speed * airborne_speed_multiplier
func _on_airborne_state_physics_processing(delta: float):
gravity -= 20.0 * delta
_handle_physics_process(delta)
if is_on_floor():
state_chart.send_event("grounded")
##############################
## SIGNAL CALLBACKS
##############################
func _on_health_changed(value: int):
state_chart.set_expression_property("health", value)
func _on_died():
state_chart.send_event("died")
died.emit()
##############################
## HELPER FUNCTIONS
##############################
func _handle_physics_process(delta: float):
target_velocity = transform.basis * target_velocity
actual_velocity = velocity.lerp(target_velocity, move_acceleration * delta)
actual_velocity.y = gravity
velocity = actual_velocity
move_and_slide()
class_name SC_Player
extends SC_Character
##############################
## EXPORT VARIABLES
##############################
@export_category("Movement")
@export var jump_speed: float = 6.0
@export var crouch_speed_multiplier := 0.4
@export var crouch_collider_height := 1.0
@export var ladder_speed := 4.0
@export_category("Camera")
@export var mouse_sensitivity := 700.0
@export var camera_responsiveness := 80.0
@export var vertical_angle_limit := 90.0
##############################
## ONREADY VARIABLES
##############################
@onready var hotbar: Hotbar = %Hotbar
@onready var tps_rig: TPSRig = %TPSRig
@onready var camera: Camera3D = %Camera
@onready var collision: CollisionShape3D = %CollisionShape3D
@onready var head_check_ray: RayCast3D = %HeadCheckRay
@onready var head: Marker3D = %Head
@onready var player_input: PlayerInput = %PlayerInput
@onready var spectate_handler: SpectateHandler = %SpectateHandler
@onready var interact_handler: InteractHandler = %InteractHandler
@onready var inventory: Inventory = %Inventory
##############################
## VARIABLES
##############################
var default_collider_height: float
var collider_target_height: float
var input_mouse: Vector2
var target_rotation: Vector3
var jump_request_time: float
var was_on_floor_last_frame: bool
var move_tween: Tween
# Ladder stuff
var ladder: Ladder
var ladder_enter_pos: Vector3
var ladder_exit_time: float
##############################
## GETTERS
##############################
var vertical_look_percent: float:
get:
return head.rotation_degrees.x / vertical_angle_limit
var jump_requested: bool:
get:
return G.get_time() - jump_request_time < 0.15
##############################
## LIFECYCLE METHODS
##############################
func _ready():
super()
state_chart.set_expression_property("is_crouching", false)
default_collider_height = (collision.shape as CapsuleShape3D).height
collider_target_height = default_collider_height
tps_rig.hide()
camera.make_current()
Input.mouse_mode = Input.MOUSE_MODE_CAPTURED
tps_rig.activate(hotbar)
interact_handler.activate(camera, inventory, hotbar)
await G.wait(0.1)
UIManager.game_ui.show()
func _physics_process(delta):
was_on_floor_last_frame = is_on_floor()
if collision.shape.height != collider_target_height:
var shape = collision.shape as CapsuleShape3D
var y_delta = signf(collider_target_height - shape.height) * delta * 8.0
var new_height = clampf(shape.height + y_delta, crouch_collider_height, default_collider_height)
y_delta = new_height - shape.height
tps_rig.position.y -= y_delta / 2.0
head_check_ray.position.y -= y_delta / 2.0
shape.height = new_height
##############################
## STATE CALLBACKS
##############################
# === ALIVE ===
func _on_alive_state_entered():
super()
tps_rig.set_ragdoll(false)
func _on_alive_state_processing(delta: float):
# Handle movement input
_set_target_velocity_from_input()
# Handle camera rotation
head.rotation.x = lerp_angle(
head.rotation.x, target_rotation.x, delta * camera_responsiveness
)
rotation.y = lerp_angle(rotation.y, target_rotation.y, delta * camera_responsiveness)
tps_rig.set_spine_blend_space(vertical_look_percent)
# Handle jump input buffer
if player_input.input_jump:
jump_request_time = G.get_time()
if player_input.input_interact:
interact_handler.interact()
player_input.input_jump = false
player_input.input_interact = false
func _on_alive_state_input(event):
if not event is InputEventMouseMotion:
return
if not Input.mouse_mode == Input.MOUSE_MODE_CAPTURED:
return
# Set camera's target rotation based on mouse input
input_mouse = event.relative / mouse_sensitivity
target_rotation.x -= input_mouse.y
target_rotation.y -= input_mouse.x
var angle_limit = deg_to_rad(vertical_angle_limit)
target_rotation.x = clamp(target_rotation.x, -angle_limit, angle_limit)
# === DEAD ===
func _on_dead_state_entered():
tps_rig.velocity = Vector3.ZERO
tps_rig.is_crouching = false
tps_rig.set_ragdoll(true)
# === GROUNDED ===
func _on_grounded_state_processing(delta: float):
var is_crouching = player_input.input_crouch
if not is_crouching:
is_crouching = head_check_ray.is_colliding()
state_chart.set_expression_property("is_crouching", is_crouching)
if jump_requested:
gravity = jump_speed
jump_request_time = 0.0
func _on_grounded_state_physics_processing(delta: float):
super(delta)
tps_rig.velocity = velocity
_process_collisions()
# === CROUCHING ===
func _on_crouching_state_entered():
move_speed = base_move_speed * crouch_speed_multiplier
collider_target_height = crouch_collider_height
tps_rig.is_crouching = true
func _on_crouching_state_physics_processing(delta: float):
pass
func _on_crouching_state_exited():
collider_target_height = default_collider_height
tps_rig.is_crouching = false
# === AIRBORNE ===
func _on_airborne_state_processing(delta: float):
pass
func _on_airborne_state_physics_processing(delta: float):
super(delta)
_process_collisions()
# === LADDER ===
func _on_ladder_state_entered():
move_speed = ladder_speed
func _on_ladder_state_physics_processing(delta: float):
if move_tween and move_tween.is_running():
return
# Move along the ladder in the direction of the camera
target_velocity = head.global_basis * target_velocity
actual_velocity = velocity.lerp(target_velocity, move_acceleration * delta)
velocity = actual_velocity
move_and_slide()
# Constrain planar movement to ladder
global_position.x = ladder_enter_pos.x
global_position.z = ladder_enter_pos.z
var dist_to_top = ladder.global_position.y - global_position.y
if dist_to_top < 0.70:
var top_pos = _to_collider_centered_pos(ladder.global_position)
move_tween = create_tween()
move_tween.tween_property(self, "global_position", top_pos, 0.25)
move_tween.finished.connect(func():
state_chart.send_event("ladder_exit")
move_tween = null
)
elif is_on_floor() and not was_on_floor_last_frame:
state_chart.send_event("ladder_exit")
return
if jump_requested:
gravity = jump_speed
jump_request_time = 0.0
state_chart.send_event("ladder_exit")
func _on_ladder_state_exited():
ladder = null
##############################
## HELPER FUNCTIONS
##############################
func _process_collisions():
for i in range(get_slide_collision_count()):
var col = get_slide_collision(i)
for k in range(col.get_collision_count()):
var body = col.get_collider(k)
if not body:
continue
if body is Ladder:
_collide_ladder(body)
func _collide_ladder(body: Ladder):
if vertical_look_percent < 0.2:
return
if global_position.y > body.global_position.y:
return
if G.get_time() - ladder_exit_time < 0.25:
return
ladder = body
ladder_enter_pos = global_position
state_chart.send_event("ladder_enter")
func _set_target_velocity_from_input():
# Handle movement input
var move_input = player_input.input_move
target_velocity = Vector3(move_input.x, 0.0, move_input.y).normalized() * move_speed
func _to_collider_centered_pos(pos: Vector3) -> Vector3:
return pos + Vector3(0.0, default_collider_height / 2.0, 0.0)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment