WIP checkpoint: Client-side position sync + prediction/reconciliation

This commit is contained in:
2026-08-21 01:17:13 +00:00
parent 1a8821e3c8
commit 2dff83bae3
3 changed files with 25 additions and 25 deletions
+21 -22
View File
@@ -200,36 +200,35 @@ func _handle_snapshot(snapshot_data: Dictionary) -> void:
return
var authoritative_pos: Vector3 = (my_state as Dictionary).get("position", position)
var acked_seq: int = snapshot_data.get("tick", 0)
# Find our own predicted position at (approximately) the acknowledged
# tick -- the buffer is seq-ordered, so the first entry >= acked_seq is
# the comparison point. If the server hasn't caught up to any of our
# sent input yet (empty/very early buffer), nothing to reconcile.
var predicted_entry: Dictionary = {}
for entry in _prediction_buffer:
if entry["seq"] >= acked_seq:
predicted_entry = entry
break
if predicted_entry.is_empty():
# Real limitation, noted explicitly rather than silently faked: the
# server's snapshot_state (scripts/server.gd's _broadcast_snapshot) does
# not currently echo back "last input seq processed for this peer" --
# its `tick` field is the server's own internal simulation tick counter,
# a completely different clock from this client's per-input `_seq`. True
# tick-correlated rollback (docs/architecture/networking.md §5.2 step 4,
# "re-simulate any inputs that occurred after the server's tick") needs
# that ack echoed back per-peer -- a real, scoped follow-up, not
# something to fake here. Until then, reconcile against the MOST RECENT
# prediction (not a tick-matched one): still a real error measurement
# and a real correction, just without precise replay-from-exact-tick.
if _prediction_buffer.is_empty():
return
var latest_entry: Dictionary = _prediction_buffer[-1]
var error: float = (predicted_entry["position"] as Vector3).distance_to(authoritative_pos)
var error: float = (latest_entry["position"] as Vector3).distance_to(authoritative_pos)
if error <= RECONCILE_ERROR_THRESHOLD:
return # Close enough -- prediction was correct, nothing to do.
# Correct: snap to the authoritative position, then replay every input
# sent after the acknowledged tick so the visible result still reflects
# input the server hasn't processed yet (§5.2 step 4).
# Correct: snap to the authoritative position. Without a real ack-seq
# from the server there's no reliable "replay inputs after this point"
# window to recompute, so the buffer is cleared and prediction resumes
# fresh from the corrected position on the next input -- still visibly
# responsive (no waiting for a round trip before the *next* input
# applies), just without this pass replaying in-flight inputs.
position = authoritative_pos
velocity = Vector3.ZERO
var replay_from_index := _prediction_buffer.find(predicted_entry) + 1
for i in range(replay_from_index, _prediction_buffer.size()):
var entry: Dictionary = _prediction_buffer[i]
var inp: Dictionary = entry["input"]
_apply_movement(1.0 / 30.0, inp.get("move_x", 0.0), inp.get("move_z", 0.0), inp.get("jump", false))
entry["position"] = position
entry["velocity"] = velocity
_prediction_buffer.clear()
reconciled.emit(error)