Files
modbison/scripts/player_spawner.gd
T
2026-09-17 16:42:15 +00:00

98 lines
2.9 KiB
GDScript

class_name PlayerNode
extends CharacterBody3D
## Server-authoritative player entity.
##
## One per connected peer, spawned as a child of the Server node. Movement runs
## in _physics_process at the project's fixed physics rate (30 Hz, matching
## scripts/server.gd's snapshot rate and the client's prediction step).
## Movement constants -- mirror scripts/server.gd (the source of truth) and
## scripts/network_client.gd (the client's prediction).
const WALK_SPEED := 5.0
const JUMP_VELOCITY := 4.5
const GRAVITY := 9.8
var peer_id: int = -1
## Where the server spawned this player (set by server.gd). Used as the
## respawn point if the player somehow ends up below the world.
var spawn_position: Vector3 = Vector3(0, 1.5, 0)
## Highest input seq applied authoritatively so far (echoed in snapshots so
## the client can drop acked inputs and replay the rest).
var last_applied_seq: int = 0
## Replicated avatar loadout (base_body, slots, skin_tone).
var loadout: Dictionary = {}
## Queue of {seq:int, move_x:float, move_z:float, jump:bool, look_yaw:float}
## appended by rpc_request_input, drained each physics frame.
var _input_queue: Array[Dictionary] = []
var _look_yaw: float = 0.0
func _physics_process(delta: float) -> void:
if _input_queue.is_empty():
_integrate(0.0, 0.0, false, delta)
else:
# Reliable ENet preserves order; sort defensively anyway.
_input_queue.sort_custom(func(a, b): return a["seq"] < b["seq"])
for input in _input_queue:
_look_yaw = input.get("look_yaw", _look_yaw)
_integrate(input.get("move_x", 0.0), input.get("move_z", 0.0), input.get("jump", false), delta)
last_applied_seq = input["seq"]
_input_queue.clear()
rotation.y = _look_yaw
# Safety net: if a player ends up below the world (physics shove, tunnel
# through the floor), put them back on their spawn instead of falling
# forever.
if global_position.y < -10.0:
global_position = spawn_position
velocity = Vector3.ZERO
func _integrate(move_x: float, move_z: float, jump: bool, delta: float) -> void:
if not is_on_floor():
velocity.y -= GRAVITY * delta
elif jump:
velocity.y = JUMP_VELOCITY
var dir := Vector3(move_x, 0.0, move_z)
if dir.length() > 0.01:
dir = dir.normalized()
velocity.x = dir.x * WALK_SPEED
velocity.z = dir.z * WALK_SPEED
else:
velocity.x = 0.0
velocity.z = 0.0
move_and_slide()
if is_on_floor() and velocity.y < 0.0:
velocity.y = 0.0
func get_server_state() -> Dictionary:
return {
"position": global_position,
"rotation_y": rotation.y,
"velocity": velocity,
"on_floor": is_on_floor(),
"loadout": loadout,
}
@rpc("any_peer", "call_remote", "reliable")
func rpc_request_input(input: Dictionary, seq: int) -> void:
var d := input.duplicate()
d["seq"] = seq
_input_queue.append(d)
@rpc("any_peer", "call_remote", "reliable")
func rpc_set_loadout(p_loadout: Dictionary) -> void:
var sender := multiplayer.get_remote_sender_id()
if sender == peer_id or sender == 1 or sender == 0:
loadout = p_loadout.duplicate(true)