WIP checkpoint: Client-side position sync + prediction/reconciliation
This commit is contained in:
@@ -0,0 +1,101 @@
|
||||
extends SceneTree
|
||||
## Real integration test for NetworkClient (client-side prediction +
|
||||
## reconciliation) against an actual dedicated server process.
|
||||
##
|
||||
## This does NOT boot its own server -- run scenes/server.tscn as a separate
|
||||
## process first (see docs/architecture/networking.md's ENet/port setup):
|
||||
## godot --headless --path . scenes/server.tscn &
|
||||
## then, from the same project directory:
|
||||
## godot --headless --path . --script res://scripts/smoke_test_network_client.gd
|
||||
##
|
||||
## Verifies: the client can actually connect over ENet, local prediction
|
||||
## moves the player immediately on input (no waiting for the server),
|
||||
## server snapshots actually arrive and update the client's authoritative
|
||||
## view, and a deliberately-desynced predicted position gets corrected by
|
||||
## reconciliation.
|
||||
|
||||
var passed := 0
|
||||
var failed := 0
|
||||
|
||||
|
||||
func _initialize() -> void:
|
||||
_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", 7777)
|
||||
if not _check("connected to the real dedicated server", ok):
|
||||
printerr(" Is `godot --headless --path . scenes/server.tscn` running on port 7777?")
|
||||
_finish()
|
||||
return
|
||||
|
||||
# Give the server a few frames to process the connection and spawn our
|
||||
# player node before we start sending input.
|
||||
for i in 10:
|
||||
await process_frame
|
||||
|
||||
# 1. Prediction: pressing "forward" should move the local position
|
||||
# immediately, without waiting for any server round trip.
|
||||
var pos_before: Vector3 = net.position
|
||||
for i in 10:
|
||||
net.predict_and_send(1.0 / 30.0, 0.0, -1.0, false) # move -Z (forward)
|
||||
await process_frame
|
||||
var pos_after_predict: Vector3 = net.position
|
||||
_check("local prediction moved the player without waiting for the server",
|
||||
pos_after_predict.distance_to(pos_before) > 0.1)
|
||||
|
||||
# 2. Snapshots actually arrive from the real server and get processed --
|
||||
# wait for at least one reconciliation-eligible snapshot by giving it
|
||||
# real time (server broadcasts at its own tick rate).
|
||||
var got_snapshot := false
|
||||
net.reconciled.connect(func(_err): got_snapshot = true)
|
||||
for i in 60:
|
||||
net.predict_and_send(1.0 / 30.0, 0.0, -1.0, false)
|
||||
await process_frame
|
||||
# Reconciliation only actually fires on a real mismatch -- absence of a
|
||||
# large mismatch is a PASS outcome too (means prediction tracked the
|
||||
# server closely), so this alone isn't proof snapshots arrived. Check
|
||||
# that directly instead:
|
||||
_check("prediction buffer has entries (input was actually sent)", net._prediction_buffer.size() > 0)
|
||||
|
||||
# 3. Deliberately desync prediction from reality, then confirm the next
|
||||
# snapshot corrects it -- the real proof reconciliation logic works,
|
||||
# not just that it never had to fire.
|
||||
net.position = Vector3(9999, 9999, 9999)
|
||||
var desynced_pos := net.position
|
||||
var reconciled_this_round := false
|
||||
var correction_seen: float = -1.0
|
||||
var reconcile_handler := func(err): reconciled_this_round = true; correction_seen = err
|
||||
net.reconciled.connect(reconcile_handler)
|
||||
for i in 60:
|
||||
await process_frame
|
||||
if reconciled_this_round:
|
||||
break
|
||||
net.reconciled.disconnect(reconcile_handler)
|
||||
_check("a deliberately desynced position gets corrected by the next snapshot",
|
||||
reconciled_this_round and net.position.distance_to(desynced_pos) > 100.0)
|
||||
if reconciled_this_round:
|
||||
print(" correction distance was ~", correction_seen)
|
||||
|
||||
net.disconnect_from_server()
|
||||
_finish()
|
||||
|
||||
|
||||
func _check(description: String, condition: bool) -> bool:
|
||||
if condition:
|
||||
print(" PASS: ", description)
|
||||
passed += 1
|
||||
else:
|
||||
printerr(" FAIL: ", description)
|
||||
failed += 1
|
||||
return condition
|
||||
|
||||
|
||||
func _finish() -> void:
|
||||
print("\n=== SMOKE RESULT: %d/%d passed ===" % [passed, passed + failed])
|
||||
quit(0 if failed == 0 else 1)
|
||||
Reference in New Issue
Block a user