Stabilize airborne sitting prediction (#117)
This commit is contained in:
parent
bfbcba7109
commit
62af5e7086
2 changed files with 198 additions and 18 deletions
|
|
@ -41,7 +41,7 @@ func _validate_latency_smoothing() -> void:
|
|||
await _validate_remote_snapshot_smoothing(avatar)
|
||||
_validate_remote_locomotion_playback_recovery(avatar)
|
||||
_validate_reliable_jump_intent(avatar)
|
||||
_validate_airborne_sitting(avatar)
|
||||
await _validate_airborne_sitting(avatar)
|
||||
_validate_stale_input_expiry(avatar)
|
||||
avatar.queue_free()
|
||||
await process_frame
|
||||
|
|
@ -193,21 +193,157 @@ func _validate_compact_animation_encoding() -> void:
|
|||
|
||||
|
||||
func _validate_airborne_sitting(avatar: Player) -> void:
|
||||
# Give the prediction unit a real floor so host/client landing can be offset
|
||||
# while the owner-audit acknowledgement ordering is exercised faithfully.
|
||||
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.reset_network_movement_state()
|
||||
avatar.set_local_control(true)
|
||||
avatar.global_position = Vector3(0.0, 2.0, 0.0)
|
||||
avatar.velocity = Vector3(0.0, -1.0, 0.0)
|
||||
avatar.move_and_slide()
|
||||
assert(not avatar.is_on_floor())
|
||||
avatar.toggle_sitting()
|
||||
assert(avatar.get_network_sitting_intent())
|
||||
assert(not avatar.get_network_sitting_state())
|
||||
assert(bool(avatar.get("_sitting_intent_pending")))
|
||||
assert(bool(avatar.get("_sitting_intent_target")))
|
||||
assert(not bool(avatar.make_network_snapshot(2)["sitting"]))
|
||||
assert(bool(avatar.capture_network_input(1)["sitting"]))
|
||||
|
||||
# Reproduce the high-RTT owner audit race: the host has reached the ground
|
||||
# and reports the deferred sit while this client's predicted capsule is still
|
||||
# airborne. Repeated seated snapshots must not zero its falling velocity.
|
||||
# The host has received the sit input (and echoes its sequence) but has not
|
||||
# landed yet. Receipt is not completion: keep transmitting the desired state
|
||||
# instead of accepting this temporary standing pose and starting a true/false
|
||||
# feedback loop under high RTT.
|
||||
var host_still_airborne: Dictionary = _network_snapshot(
|
||||
avatar.global_position,
|
||||
avatar.velocity,
|
||||
1,
|
||||
)
|
||||
host_still_airborne["grounded"] = false
|
||||
avatar.apply_local_prediction_correction(
|
||||
host_still_airborne,
|
||||
1,
|
||||
1.0 / 30.0,
|
||||
0.3,
|
||||
0.1,
|
||||
)
|
||||
assert(bool(avatar.get("_sitting_intent_pending")))
|
||||
assert(avatar.get_network_sitting_intent())
|
||||
|
||||
# Let local prediction land first. A newer host audit can still be standing
|
||||
# because the host began simulating the delayed input later. It must neither
|
||||
# stand the local player back up nor change the level-triggered retry bit.
|
||||
avatar.global_position = Vector3.ZERO
|
||||
avatar.velocity = Vector3.DOWN
|
||||
avatar.move_and_slide()
|
||||
assert(avatar.is_on_floor())
|
||||
avatar.call("_simulate_movement_physics", 1.0 / 60.0)
|
||||
assert(avatar.is_sitting())
|
||||
assert(int(avatar.get("_sitting_intent_sequence")) == 1)
|
||||
assert(bool(avatar.capture_network_input(2)["sitting"]))
|
||||
var delayed_standing_audit: Dictionary = _network_snapshot(
|
||||
avatar.global_position,
|
||||
Vector3.ZERO,
|
||||
2,
|
||||
)
|
||||
avatar.apply_local_prediction_correction(
|
||||
delayed_standing_audit,
|
||||
2,
|
||||
1.0 / 30.0,
|
||||
0.3,
|
||||
0.1,
|
||||
)
|
||||
assert(avatar.is_sitting())
|
||||
assert(bool(avatar.get("_sitting_intent_pending")))
|
||||
assert(avatar.get_network_sitting_intent())
|
||||
|
||||
# Once the host reaches the ground, its matching pose confirms and retires
|
||||
# the request without disturbing the already-seated local prediction.
|
||||
var delayed_sit_snapshot: Dictionary = _network_snapshot(
|
||||
avatar.global_position,
|
||||
Vector3.ZERO,
|
||||
2,
|
||||
)
|
||||
delayed_sit_snapshot["sitting"] = true
|
||||
avatar.apply_local_prediction_correction(
|
||||
delayed_sit_snapshot,
|
||||
2,
|
||||
1.0 / 30.0,
|
||||
0.3,
|
||||
0.1,
|
||||
)
|
||||
assert(avatar.is_sitting())
|
||||
assert(not bool(avatar.get("_sitting_intent_pending")))
|
||||
|
||||
# A late owner audit from before the confirmed seated pose cannot overwrite
|
||||
# it even if packet ordering is perturbed by a transport or test harness.
|
||||
avatar.apply_local_prediction_correction(
|
||||
host_still_airborne,
|
||||
2,
|
||||
1.0 / 30.0,
|
||||
0.3,
|
||||
0.1,
|
||||
)
|
||||
assert(avatar.is_sitting())
|
||||
|
||||
# The inverse transition has the same rule: keep the predicted standing pose
|
||||
# through a delayed seated audit until the host confirms the newer request.
|
||||
avatar.toggle_sitting()
|
||||
assert(not avatar.is_sitting())
|
||||
assert(not bool(avatar.get("_sitting_intent_target")))
|
||||
assert(not bool(avatar.capture_network_input(3)["sitting"]))
|
||||
avatar.apply_local_prediction_correction(
|
||||
delayed_sit_snapshot,
|
||||
3,
|
||||
1.0 / 30.0,
|
||||
0.3,
|
||||
0.1,
|
||||
)
|
||||
assert(not avatar.is_sitting())
|
||||
assert(bool(avatar.get("_sitting_intent_pending")))
|
||||
var confirmed_standing: Dictionary = _network_snapshot(
|
||||
avatar.global_position,
|
||||
Vector3.ZERO,
|
||||
3,
|
||||
)
|
||||
avatar.apply_local_prediction_correction(
|
||||
confirmed_standing,
|
||||
3,
|
||||
1.0 / 30.0,
|
||||
0.3,
|
||||
0.1,
|
||||
)
|
||||
assert(not avatar.is_sitting())
|
||||
assert(not bool(avatar.get("_sitting_intent_pending")))
|
||||
avatar.apply_local_prediction_correction(
|
||||
delayed_sit_snapshot,
|
||||
3,
|
||||
1.0 / 30.0,
|
||||
0.3,
|
||||
0.1,
|
||||
)
|
||||
assert(not avatar.is_sitting())
|
||||
|
||||
# Retain the original mid-air gravity regression independently of the
|
||||
# acknowledgement state-machine coverage above.
|
||||
avatar.reset_network_movement_state()
|
||||
avatar.global_position = Vector3(0.0, 2.0, 0.0)
|
||||
avatar.velocity = Vector3(0.0, -1.0, 0.0)
|
||||
avatar.move_and_slide()
|
||||
avatar.toggle_sitting()
|
||||
assert(bool(avatar.capture_network_input(1)["sitting"]))
|
||||
delayed_sit_snapshot = _network_snapshot(
|
||||
avatar.global_position,
|
||||
Vector3.ZERO,
|
||||
1,
|
||||
|
|
@ -264,6 +400,8 @@ func _validate_airborne_sitting(avatar: Player) -> void:
|
|||
assert(bool(avatar.get("_sit_after_landing")))
|
||||
assert(avatar.velocity.y < 4.0)
|
||||
avatar.reset_network_movement_state()
|
||||
floor.queue_free()
|
||||
await physics_frame
|
||||
|
||||
|
||||
func _validate_animation_action_ordering(avatar: Player) -> void:
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue