Improve multiplayer movement reconciliation

This commit is contained in:
Alexander Sellite 2026-08-23 23:04:14 -04:00
parent 348ae261e1
commit a3aea98982
3 changed files with 240 additions and 89 deletions

View file

@ -71,6 +71,7 @@ func _validate_compact_snapshot_encoding() -> void:
1,
)
assert(NetworkSession.MOVEMENT_SNAPSHOT_BATCH_SIZE == 8)
assert(NetworkSession.OWNER_SNAPSHOT_DIVISOR == 3)
var encoded_snapshots: Array = []
for _peer: int in NetworkSession.MOVEMENT_SNAPSHOT_BATCH_SIZE:
encoded_snapshots.append(
@ -283,16 +284,72 @@ func _validate_remote_snapshot_smoothing(avatar: Player) -> void:
avatar.set_local_control(true)
avatar.global_position = Vector3(0.8, 0.0, 0.0)
var replay_input: Dictionary = _movement_input(2, false)
replay_input["axis"] = [1.0, 0.0]
var pending_inputs: Array[Dictionary] = [replay_input]
avatar.apply_local_prediction_correction(
moving_snapshot,
pending_inputs,
2,
1.0 / 30.0,
0.1,
0.1,
)
# Ordinary host/client disagreement is expected while a packet is in flight.
# It must never tug the locally controlled body or camera around.
assert(is_equal_approx(avatar.global_position.x, 0.8))
assert(
int(avatar.get("_local_prediction_soft_corrections")) == 0
)
assert(
int(avatar.get("_local_prediction_hard_corrections")) == 0
)
# A larger but still plausible mismatch must persist across multiple audits
# before a small correction is allowed. Preserve both visible character and
# camera positions while the collision body catches up.
avatar.global_position = Vector3(3.0, 0.0, 0.0)
var visual_position: Vector3 = avatar.get_node("Visuals").global_position
var camera_position: Vector3 = avatar.get_node("CameraYaw").global_position
var drift_snapshot: Dictionary = _network_snapshot(
Vector3.ZERO,
Vector3.ZERO,
20,
)
for _audit: int in 3:
avatar.apply_local_prediction_correction(
drift_snapshot,
20,
1.0 / 30.0,
0.0,
0.1,
)
assert(is_equal_approx(avatar.global_position.x, 3.0))
avatar.apply_local_prediction_correction(
drift_snapshot,
20,
1.0 / 30.0,
0.0,
0.1,
)
assert(avatar.global_position.x < 3.0)
assert(avatar.global_position.x >= 2.85 - 0.001)
assert(avatar.get_node("Visuals").global_position == visual_position)
assert(avatar.get_node("CameraYaw").global_position == camera_position)
assert(
int(avatar.get("_local_prediction_soft_corrections")) == 1
)
# Genuine divergence still recovers immediately, as do the separate reliable
# teleport and water-recovery paths used by gameplay transitions.
avatar.global_position = Vector3(10.0, 0.0, 0.0)
avatar.apply_local_prediction_correction(
drift_snapshot,
20,
1.0 / 30.0,
0.0,
0.1,
)
assert(avatar.global_position == Vector3.ZERO)
assert(
int(avatar.get("_local_prediction_hard_corrections")) == 1
)
assert(avatar.global_position.x < 0.8)
assert(avatar.global_position.x > 0.0)
func _validate_reliable_jump_intent(avatar: Player) -> void:
@ -305,8 +362,10 @@ func _validate_reliable_jump_intent(avatar: Player) -> void:
assert(int(avatar.get("_local_network_jump_intent_sequence")) == 20)
avatar.apply_local_prediction_correction(
_network_snapshot(avatar.global_position, Vector3.ZERO, 20),
[],
20,
1.0 / 30.0,
0.0,
0.1,
)
assert(not bool(avatar.capture_network_input(22)["jump"]))