682f1f89f7
Made the multiplayer core actually work (it had never run a real sim): - project.godot: physics pinned to 30 Hz -- the shared sim/snapshot/prediction rate. - server.gd: loads the world's level (real ground + collision), spawns a server Player per peer at the level's PlayerSpawn, broadcasts a full snapshot every physics frame with each peer's state AND last_seq (the last input seq applied for that peer). Accepts `--port/--world/--status-file` args (status file = player count for a future idle reaper); defaults keep `godot res://scenes/server.tscn` working standalone. - player_spawner.gd (server Player): rewrote -- drains its whole input queue each physics frame in seq order, integrates gravity/walk/jump with the shared constants, move_and_slide(), tracks last_applied_seq. No more no-op process_server_input(). - network_client.gd: rewrote reconciliation -- drops inputs the server has acked (last_seq), re-simulates the still-in-flight ones from the authoritative state, snaps only on real drift. Exposes get_remote_states() (peer_id -> latest snapshot state) for the game to render other players. predict_and_send() now also sends body yaw. - proxies trimmed to the RPC set the rewrite actually uses. - scripts/mp_test_client.gd + run_multiplayer_2p_test.sh: two real client processes vs one real server -- client A sees client B and its own movement round-trips. PASS. - run_checkpoint_tests.py: also run the 2p shell test. smoke_test_network_client 4/4, player 15/15, avatar_in_world 10/10, all green. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015jtxAiu5dAW4WxCNdGkrkJ
76 lines
2.2 KiB
GDScript
76 lines
2.2 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
|
|
|
|
## 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
|
|
|
|
|
|
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)
|