PC3 Phase 2.1: server-authoritative sim + prediction/reconciliation with replay
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
This commit is contained in:
@@ -0,0 +1,58 @@
|
||||
extends SceneTree
|
||||
## One client process for the 2-player multiplayer test
|
||||
## (scripts/run_multiplayer_2p_test.sh). Two real OS processes = real network.
|
||||
##
|
||||
## godot --headless --path . --script res://scripts/mp_test_client.gd -- --role a --port 7777
|
||||
##
|
||||
## role "a": moves forward for ~2 s, then prints
|
||||
## RESULT a remotes=<n> moved=<bool> and exits 0 iff it both moved
|
||||
## and saw >=1 remote player.
|
||||
## role "b": connects, holds still for ~4 s, prints RESULT b connected exits 0.
|
||||
|
||||
var role := "b"
|
||||
var port := 7777
|
||||
|
||||
|
||||
func _initialize() -> void:
|
||||
var args := OS.get_cmdline_user_args()
|
||||
for i in args.size():
|
||||
if args[i] == "--role" and i + 1 < args.size():
|
||||
role = args[i + 1]
|
||||
elif args[i] == "--port" and i + 1 < args.size():
|
||||
port = int(args[i + 1])
|
||||
_run()
|
||||
|
||||
|
||||
func _run() -> void:
|
||||
await process_frame
|
||||
var net := NetworkClient.new()
|
||||
root.add_child(net)
|
||||
var ok: bool = await net.connect_to_server("127.0.0.1", port)
|
||||
if not ok:
|
||||
print("RESULT %s connect_failed" % role)
|
||||
quit(1)
|
||||
return
|
||||
for i in 15:
|
||||
await process_frame
|
||||
|
||||
if role == "b":
|
||||
# Hold still and stay connected long enough for A to boot, connect,
|
||||
# and observe us (A's Godot startup alone is a couple of seconds).
|
||||
print("RESULT b connected")
|
||||
for i in 1200:
|
||||
await process_frame
|
||||
net.disconnect_from_server()
|
||||
quit(0)
|
||||
return
|
||||
|
||||
# role a: move forward, watch for a remote and our own displacement.
|
||||
var start: Vector3 = net.position
|
||||
var max_remotes := 0
|
||||
for i in 90:
|
||||
net.predict_and_send(1.0 / 30.0, 0.0, -1.0, false, 0.0)
|
||||
await process_frame
|
||||
max_remotes = maxi(max_remotes, net.get_remote_states().size())
|
||||
var moved: bool = net.position.distance_to(start) > 1.0
|
||||
print("RESULT a remotes=%d moved=%s" % [max_remotes, str(moved)])
|
||||
net.disconnect_from_server()
|
||||
quit(0 if (moved and max_remotes >= 1) else 1)
|
||||
Reference in New Issue
Block a user