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 ## 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(), } @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)