Client-side position sync + prediction/reconciliation #52
@@ -1,5 +1,5 @@
|
|||||||
63f7b34db8d8cdea90c76aacccf841ec
|
63f7b34db8d8cdea90c76aacccf841ec
|
||||||
::res://::1787274081
|
::res://::1787274847
|
||||||
export_presets.cfg::TextFile::-1::1787272930::0::1::::<><><>0<>0<><>::
|
export_presets.cfg::TextFile::-1::1787272930::0::1::::<><><>0<>0<><>::
|
||||||
README.md::TextFile::-1::1787272930::0::1::::<><><>0<>0<><>::
|
README.md::TextFile::-1::1787272930::0::1::::<><><>0<>0<><>::
|
||||||
::res://addons/::1787272930
|
::res://addons/::1787272930
|
||||||
@@ -44,7 +44,7 @@ server_player.tscn::PackedScene::-1::1787274081::0::1::::<><><>0<>0<><>::res://s
|
|||||||
world_list.tscn::PackedScene::-1::1787272930::0::1::::<><><>0<>0<><>::res://scripts/world_list.gd
|
world_list.tscn::PackedScene::-1::1787272930::0::1::::<><><>0<>0<><>::res://scripts/world_list.gd
|
||||||
::res://scenes/worlds/::1787272930
|
::res://scenes/worlds/::1787272930
|
||||||
test_level.tscn::PackedScene::-1::1787272930::0::1::::<><><>0<>0<><>::
|
test_level.tscn::PackedScene::-1::1787272930::0::1::::<><><>0<>0<><>::
|
||||||
::res://scripts/::1787274812
|
::res://scripts/::1787274976
|
||||||
api_client.gd::GDScript::5412288119287126975::1787272930::0::1::::<>Node<><>0<>0<><>::
|
api_client.gd::GDScript::5412288119287126975::1787272930::0::1::::<>Node<><>0<>0<><>::
|
||||||
chunk.gd::GDScript::7364239597441807588::1787272930::0::1::::Chunk<>Node3D<><>0<>0<><>::
|
chunk.gd::GDScript::7364239597441807588::1787272930::0::1::::Chunk<>Node3D<><>0<>0<><>::
|
||||||
chunk_data_decoder.gd::GDScript::3922068956043581888::1787272930::0::1::::ChunkDataDecoder<>RefCounted<><>0<>0<><>::
|
chunk_data_decoder.gd::GDScript::3922068956043581888::1787272930::0::1::::ChunkDataDecoder<>RefCounted<><>0<>0<><>::
|
||||||
@@ -52,7 +52,7 @@ chunk_scene.gd::GDScript::9207602004456600331::1787272930::0::1::::ChunkScene<>N
|
|||||||
chunk_stream_manager.gd::GDScript::2366645676568508746::1787272930::0::1::::ChunkStreamManager<>Node<><>0<>0<><>::
|
chunk_stream_manager.gd::GDScript::2366645676568508746::1787272930::0::1::::ChunkStreamManager<>Node<><>0<>0<><>::
|
||||||
dynamic_object_node.gd::GDScript::8863349716576659118::1787272930::0::1::::DynamicObjectNode<>Node3D<><>0<>0<><>::
|
dynamic_object_node.gd::GDScript::8863349716576659118::1787272930::0::1::::DynamicObjectNode<>Node3D<><>0<>0<><>::
|
||||||
main_menu.gd::GDScript::2834562513277893052::1787272930::0::1::::<>Control<><>0<>0<><>::
|
main_menu.gd::GDScript::2834562513277893052::1787272930::0::1::::<>Control<><>0<>0<><>::
|
||||||
network_client.gd::GDScript::5408296350353838185::1787274408::0::1::::NetworkClient<>Node<><>0<>0<><>::
|
network_client.gd::GDScript::5408296350353838185::1787274976::0::1::::NetworkClient<>Node<><>0<>0<><>::
|
||||||
network_player_proxy.gd::GDScript::4787436336771232797::1787274812::0::1::::<>Node<><>0<>0<><>::
|
network_player_proxy.gd::GDScript::4787436336771232797::1787274812::0::1::::<>Node<><>0<>0<><>::
|
||||||
network_receiver_proxy.gd::GDScript::6742760436861380520::1787274804::0::1::::<>Node<><>0<>0<><>::
|
network_receiver_proxy.gd::GDScript::6742760436861380520::1787274804::0::1::::<>Node<><>0<>0<><>::
|
||||||
player.gd::GDScript::4510795778937304264::1787274081::0::1::::Player<>CharacterBody3D<><>0<>0<><>::
|
player.gd::GDScript::4510795778937304264::1787274081::0::1::::Player<>CharacterBody3D<><>0<>0<><>::
|
||||||
|
|||||||
+21
-22
@@ -200,36 +200,35 @@ func _handle_snapshot(snapshot_data: Dictionary) -> void:
|
|||||||
return
|
return
|
||||||
|
|
||||||
var authoritative_pos: Vector3 = (my_state as Dictionary).get("position", position)
|
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
|
# Real limitation, noted explicitly rather than silently faked: the
|
||||||
# tick -- the buffer is seq-ordered, so the first entry >= acked_seq is
|
# server's snapshot_state (scripts/server.gd's _broadcast_snapshot) does
|
||||||
# the comparison point. If the server hasn't caught up to any of our
|
# not currently echo back "last input seq processed for this peer" --
|
||||||
# sent input yet (empty/very early buffer), nothing to reconcile.
|
# its `tick` field is the server's own internal simulation tick counter,
|
||||||
var predicted_entry: Dictionary = {}
|
# a completely different clock from this client's per-input `_seq`. True
|
||||||
for entry in _prediction_buffer:
|
# tick-correlated rollback (docs/architecture/networking.md §5.2 step 4,
|
||||||
if entry["seq"] >= acked_seq:
|
# "re-simulate any inputs that occurred after the server's tick") needs
|
||||||
predicted_entry = entry
|
# that ack echoed back per-peer -- a real, scoped follow-up, not
|
||||||
break
|
# something to fake here. Until then, reconcile against the MOST RECENT
|
||||||
if predicted_entry.is_empty():
|
# 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
|
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:
|
if error <= RECONCILE_ERROR_THRESHOLD:
|
||||||
return # Close enough -- prediction was correct, nothing to do.
|
return # Close enough -- prediction was correct, nothing to do.
|
||||||
|
|
||||||
# Correct: snap to the authoritative position, then replay every input
|
# Correct: snap to the authoritative position. Without a real ack-seq
|
||||||
# sent after the acknowledged tick so the visible result still reflects
|
# from the server there's no reliable "replay inputs after this point"
|
||||||
# input the server hasn't processed yet (§5.2 step 4).
|
# 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
|
position = authoritative_pos
|
||||||
velocity = Vector3.ZERO
|
velocity = Vector3.ZERO
|
||||||
var replay_from_index := _prediction_buffer.find(predicted_entry) + 1
|
_prediction_buffer.clear()
|
||||||
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
|
|
||||||
|
|
||||||
reconciled.emit(error)
|
reconciled.emit(error)
|
||||||
|
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ var owner_client: Node = null
|
|||||||
## config, a mismatch here would silently drop the call.
|
## config, a mismatch here would silently drop the call.
|
||||||
@rpc("any_peer", "call_remote", "reliable")
|
@rpc("any_peer", "call_remote", "reliable")
|
||||||
func _receive_snapshot(snapshot_data: Dictionary) -> void:
|
func _receive_snapshot(snapshot_data: Dictionary) -> void:
|
||||||
|
print("DEBUG proxy received snapshot: ", snapshot_data)
|
||||||
if owner_client and is_instance_valid(owner_client):
|
if owner_client and is_instance_valid(owner_client):
|
||||||
owner_client._handle_snapshot(snapshot_data)
|
owner_client._handle_snapshot(snapshot_data)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user