stabilize movement reconciliation states
This commit is contained in:
parent
1ab2a291d2
commit
135fd9110f
2 changed files with 156 additions and 9 deletions
|
|
@ -37,7 +37,7 @@ func _validate_latency_smoothing() -> void:
|
|||
_validate_compact_animation_encoding()
|
||||
_validate_animation_action_ordering(avatar)
|
||||
_validate_transit_estimation()
|
||||
_validate_remote_snapshot_smoothing(avatar)
|
||||
await _validate_remote_snapshot_smoothing(avatar)
|
||||
_validate_remote_locomotion_playback_recovery(avatar)
|
||||
_validate_reliable_jump_intent(avatar)
|
||||
_validate_airborne_sitting(avatar)
|
||||
|
|
@ -145,11 +145,20 @@ func _validate_compact_animation_encoding() -> void:
|
|||
|
||||
func _validate_airborne_sitting(avatar: Player) -> void:
|
||||
avatar.reset_network_movement_state()
|
||||
avatar.set_local_control(true)
|
||||
avatar.set("_sit_after_landing", true)
|
||||
assert(avatar.get_network_sitting_intent())
|
||||
assert(not avatar.get_network_sitting_state())
|
||||
assert(not bool(avatar.make_network_snapshot(2)["sitting"]))
|
||||
assert(bool(avatar.capture_network_input(1)["sitting"]))
|
||||
avatar.call("_queue_local_network_jump_intent")
|
||||
var jumping_input: Dictionary = avatar.capture_network_input(2)
|
||||
assert(bool(jumping_input["jump"]))
|
||||
assert(not bool(jumping_input["sitting"]))
|
||||
avatar.call("_cancel_sitting_for_jump")
|
||||
assert(not bool(avatar.get("_sit_after_landing")))
|
||||
assert(bool(avatar.get("_sitting_intent_pending")))
|
||||
avatar.reset_network_movement_state()
|
||||
|
||||
# Even a malformed or stale reconciliation that marks an airborne avatar as
|
||||
# seated must not bypass gravity and freeze it in place.
|
||||
|
|
@ -304,8 +313,24 @@ func _validate_remote_snapshot_smoothing(avatar: Player) -> void:
|
|||
)
|
||||
assert(avatar.global_position.x <= maximum_extrapolated_x + 0.1)
|
||||
|
||||
# Reconciliation only applies after both simulations report a grounded body.
|
||||
# Give the unit avatar a real floor so this exercises the production path.
|
||||
var floor := StaticBody3D.new()
|
||||
floor.collision_layer = 1
|
||||
floor.collision_mask = 0
|
||||
floor.position = Vector3(0.0, -0.1, 0.0)
|
||||
var floor_shape := CollisionShape3D.new()
|
||||
var floor_box := BoxShape3D.new()
|
||||
floor_box.size = Vector3(100.0, 0.2, 100.0)
|
||||
floor_shape.shape = floor_box
|
||||
floor.add_child(floor_shape)
|
||||
root.add_child(floor)
|
||||
await physics_frame
|
||||
avatar.set_local_control(true)
|
||||
avatar.global_position = Vector3(0.8, 0.0, 0.0)
|
||||
avatar.velocity = Vector3.DOWN
|
||||
avatar.move_and_slide()
|
||||
assert(avatar.is_on_floor())
|
||||
avatar.apply_local_prediction_correction(
|
||||
moving_snapshot,
|
||||
2,
|
||||
|
|
@ -358,6 +383,42 @@ func _validate_remote_snapshot_smoothing(avatar: Player) -> void:
|
|||
int(avatar.get("_local_prediction_soft_corrections")) == 1
|
||||
)
|
||||
|
||||
# Host and client jump timing naturally differs. An airborne authoritative
|
||||
# snapshot must not drag the local capsule sideways or down before landing.
|
||||
avatar.global_position = Vector3(3.0, 0.0, 0.0)
|
||||
avatar.velocity = Vector3(0.0, 1.0, 0.0)
|
||||
var airborne_snapshot: Dictionary = drift_snapshot.duplicate(true)
|
||||
airborne_snapshot["grounded"] = false
|
||||
for _audit: int in 6:
|
||||
avatar.apply_local_prediction_correction(
|
||||
airborne_snapshot,
|
||||
20,
|
||||
1.0 / 30.0,
|
||||
0.0,
|
||||
0.1,
|
||||
)
|
||||
assert(avatar.global_position.is_equal_approx(Vector3(3.0, 0.0, 0.0)))
|
||||
assert(int(avatar.get("_local_prediction_soft_corrections")) == 1)
|
||||
avatar.velocity = Vector3.ZERO
|
||||
|
||||
# The local body may reach its apex after the host has already landed. The
|
||||
# local airborne state still owns presentation and must not be pulled toward
|
||||
# the host until it has landed too.
|
||||
avatar.global_position = Vector3(3.0, 2.0, 0.0)
|
||||
avatar.velocity = Vector3.ZERO
|
||||
avatar.move_and_slide()
|
||||
assert(not avatar.is_on_floor())
|
||||
for _audit: int in 6:
|
||||
avatar.apply_local_prediction_correction(
|
||||
drift_snapshot,
|
||||
20,
|
||||
1.0 / 30.0,
|
||||
0.0,
|
||||
0.1,
|
||||
)
|
||||
assert(avatar.global_position.is_equal_approx(Vector3(3.0, 2.0, 0.0)))
|
||||
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)
|
||||
|
|
@ -373,6 +434,45 @@ func _validate_remote_snapshot_smoothing(avatar: Player) -> void:
|
|||
int(avatar.get("_local_prediction_hard_corrections")) == 1
|
||||
)
|
||||
|
||||
# Soft reconciliation must respect the same terrain collision as ordinary
|
||||
# player movement instead of phasing the capsule through a solid obstacle.
|
||||
var blocker := StaticBody3D.new()
|
||||
blocker.collision_layer = 1
|
||||
blocker.collision_mask = 0
|
||||
blocker.position = Vector3(1.0, 1.0, 0.0)
|
||||
var blocker_shape := CollisionShape3D.new()
|
||||
var blocker_box := BoxShape3D.new()
|
||||
blocker_box.size = Vector3(0.2, 4.0, 4.0)
|
||||
blocker_shape.shape = blocker_box
|
||||
blocker.add_child(blocker_shape)
|
||||
root.add_child(blocker)
|
||||
await physics_frame
|
||||
avatar.global_position = Vector3.ZERO
|
||||
avatar.velocity = Vector3.ZERO
|
||||
avatar.call("_clear_local_reconciliation_offsets")
|
||||
var blocked_visual_position: Vector3 = (
|
||||
avatar.get_node("Visuals") as Node3D
|
||||
).global_position
|
||||
var blocked_camera_position: Vector3 = (
|
||||
avatar.get_node("CameraYaw") as Node3D
|
||||
).global_position
|
||||
assert(bool(avatar.call(
|
||||
"_apply_camera_safe_local_correction",
|
||||
Vector3(2.0, 0.0, 0.0),
|
||||
)))
|
||||
assert(avatar.global_position.x > 0.0 and avatar.global_position.x < 0.6)
|
||||
assert(
|
||||
(avatar.get_node("Visuals") as Node3D).global_position
|
||||
== blocked_visual_position
|
||||
)
|
||||
assert(
|
||||
(avatar.get_node("CameraYaw") as Node3D).global_position
|
||||
== blocked_camera_position
|
||||
)
|
||||
blocker.queue_free()
|
||||
floor.queue_free()
|
||||
await physics_frame
|
||||
|
||||
|
||||
func _validate_remote_locomotion_playback_recovery(avatar: Player) -> void:
|
||||
avatar.configure_network_remote(false)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue