Stabilize airborne sitting prediction (#117)
This commit is contained in:
parent
bfbcba7109
commit
62af5e7086
2 changed files with 198 additions and 18 deletions
|
|
@ -592,7 +592,9 @@ var _character_animation_name: StringName = &""
|
|||
var _sitting: bool = false
|
||||
var _sit_after_landing: bool = false
|
||||
var _sitting_intent_pending: bool = false
|
||||
var _sitting_intent_target: bool = false
|
||||
var _sitting_intent_sequence: int = -1
|
||||
var _latest_authoritative_sitting_ack: int = -1
|
||||
var _held_fish_visible: bool = false
|
||||
var _pending_held_fish_texture: Texture2D
|
||||
var _pending_held_fish_scale: Vector3 = Vector3.ONE
|
||||
|
|
@ -1084,7 +1086,9 @@ func _simulate_movement_physics(delta: float) -> void:
|
|||
_set_sitting(false)
|
||||
if _sit_after_landing and not jump_requested and _can_begin_sitting():
|
||||
_sit_after_landing = false
|
||||
_set_sitting(true, local_control_enabled)
|
||||
# Landing resolves the already-queued desire to sit; it is not a new
|
||||
# input edge and must not restart its acknowledgement window.
|
||||
_set_sitting(true)
|
||||
if _sitting:
|
||||
if jump_requested:
|
||||
_set_sitting(false, local_control_enabled)
|
||||
|
|
@ -1740,7 +1744,9 @@ func _complete_retract_animation() -> void:
|
|||
func toggle_sitting() -> void:
|
||||
if _sit_after_landing:
|
||||
_sit_after_landing = false
|
||||
_set_sitting(false, local_control_enabled)
|
||||
_set_sitting(false)
|
||||
if local_control_enabled:
|
||||
_queue_local_sitting_intent(false)
|
||||
return
|
||||
var should_sit: bool = not _sitting
|
||||
if should_sit:
|
||||
|
|
@ -1756,10 +1762,12 @@ func toggle_sitting() -> void:
|
|||
return
|
||||
if not _can_begin_sitting():
|
||||
_sit_after_landing = true
|
||||
# Queue and repeat the intent until an owner snapshot acknowledges it.
|
||||
# Previously this branch returned before marking a local sitting intent,
|
||||
# so a delayed authoritative audit could race the deferred sit.
|
||||
_set_sitting(false, local_control_enabled)
|
||||
# Keep the desired state distinct from the physical pose. The host can
|
||||
# acknowledge this input while its own capsule is still airborne, so a
|
||||
# standing audit is not yet confirmation that the request was resolved.
|
||||
_set_sitting(false)
|
||||
if local_control_enabled:
|
||||
_queue_local_sitting_intent(true)
|
||||
return
|
||||
_set_sitting(should_sit, local_control_enabled)
|
||||
|
||||
|
|
@ -1778,8 +1786,7 @@ func _set_sitting(
|
|||
is_local_intent: bool = false,
|
||||
) -> void:
|
||||
if is_local_intent:
|
||||
_sitting_intent_pending = true
|
||||
_sitting_intent_sequence = -1
|
||||
_queue_local_sitting_intent(should_sit)
|
||||
if _sitting == should_sit:
|
||||
return
|
||||
_sitting = should_sit
|
||||
|
|
@ -1793,6 +1800,17 @@ func _set_sitting(
|
|||
_update_character_animation()
|
||||
|
||||
|
||||
func _queue_local_sitting_intent(should_sit: bool) -> void:
|
||||
# Physics may resolve a deferred sit on the same frame that input is being
|
||||
# captured. Do not move the sequence watermark when it is still the same
|
||||
# logical request, or no delayed owner audit could ever catch up to it.
|
||||
if _sitting_intent_pending and _sitting_intent_target == should_sit:
|
||||
return
|
||||
_sitting_intent_pending = true
|
||||
_sitting_intent_target = should_sit
|
||||
_sitting_intent_sequence = -1
|
||||
|
||||
|
||||
func is_sitting() -> bool:
|
||||
return _sitting
|
||||
|
||||
|
|
@ -1802,7 +1820,10 @@ func _can_begin_sitting() -> bool:
|
|||
|
||||
|
||||
func _cancel_sitting_for_jump() -> void:
|
||||
if not _sitting and not _sit_after_landing:
|
||||
var has_pending_sit_request: bool = (
|
||||
_sitting_intent_pending and _sitting_intent_target
|
||||
)
|
||||
if not _sitting and not _sit_after_landing and not has_pending_sit_request:
|
||||
return
|
||||
_sit_after_landing = false
|
||||
_set_sitting(false, local_control_enabled)
|
||||
|
|
@ -1818,6 +1839,8 @@ func get_network_sitting_intent() -> bool:
|
|||
# the stale seated bit as soon as the reliable jump intent is queued.
|
||||
if _local_network_jump_intent_pending:
|
||||
return false
|
||||
if _sitting_intent_pending:
|
||||
return _sitting_intent_target
|
||||
return _sitting or _sit_after_landing
|
||||
|
||||
|
||||
|
|
@ -1850,6 +1873,10 @@ func _apply_local_prediction_sitting_state(should_sit: bool) -> void:
|
|||
_sit_after_landing = false
|
||||
_set_sitting(false)
|
||||
return
|
||||
if not should_sit:
|
||||
_sit_after_landing = false
|
||||
_set_sitting(false)
|
||||
return
|
||||
if should_sit and not _can_begin_sitting():
|
||||
_sit_after_landing = true
|
||||
_set_sitting(false)
|
||||
|
|
@ -2360,7 +2387,9 @@ func reset_network_movement_state() -> void:
|
|||
end_animation_action()
|
||||
_sit_after_landing = false
|
||||
_sitting_intent_pending = false
|
||||
_sitting_intent_target = false
|
||||
_sitting_intent_sequence = -1
|
||||
_latest_authoritative_sitting_ack = -1
|
||||
_set_sitting(false)
|
||||
_network_axis = Vector2.ZERO
|
||||
_network_jump_pending = false
|
||||
|
|
@ -2459,8 +2488,6 @@ func apply_network_space_transition(
|
|||
|
||||
|
||||
func capture_network_input(sequence: int) -> Dictionary:
|
||||
if _sitting_intent_pending and _sitting_intent_sequence < 0:
|
||||
_sitting_intent_sequence = sequence
|
||||
if (
|
||||
local_control_enabled
|
||||
and _is_movement_input_enabled()
|
||||
|
|
@ -2468,6 +2495,13 @@ func capture_network_input(sequence: int) -> Dictionary:
|
|||
and Input.is_action_just_pressed("jump")
|
||||
):
|
||||
_queue_local_network_jump_intent()
|
||||
# Input capture and physics do not have a guaranteed ordering within the
|
||||
# rendered frame. Cancel the desired sit here as well as in physics so an
|
||||
# unusually fast owner audit cannot acknowledge the jump first and expose
|
||||
# the old seated level bit for one outgoing packet.
|
||||
_cancel_sitting_for_jump()
|
||||
if _sitting_intent_pending and _sitting_intent_sequence < 0:
|
||||
_sitting_intent_sequence = sequence
|
||||
if (
|
||||
not _is_movement_input_enabled()
|
||||
or _water_recovery_active
|
||||
|
|
@ -2723,10 +2757,18 @@ func apply_local_prediction_correction(
|
|||
_reset_local_prediction_error()
|
||||
return
|
||||
var acknowledged_input: int = parsed["acknowledged_input"]
|
||||
var authoritative_sitting: bool = bool(parsed["sitting"])
|
||||
var sitting_audit_is_stale: bool = (
|
||||
acknowledged_input < _latest_authoritative_sitting_ack
|
||||
)
|
||||
if not sitting_audit_is_stale:
|
||||
_latest_authoritative_sitting_ack = acknowledged_input
|
||||
var sitting_intent_acknowledged: bool = (
|
||||
_sitting_intent_pending
|
||||
not sitting_audit_is_stale
|
||||
and _sitting_intent_pending
|
||||
and _sitting_intent_sequence >= 0
|
||||
and acknowledged_input >= _sitting_intent_sequence
|
||||
and authoritative_sitting == _sitting_intent_target
|
||||
)
|
||||
if sitting_intent_acknowledged:
|
||||
_sitting_intent_pending = false
|
||||
|
|
@ -2737,8 +2779,8 @@ func apply_local_prediction_correction(
|
|||
and acknowledged_input >= _local_network_jump_intent_sequence
|
||||
):
|
||||
_clear_local_network_jump_intent()
|
||||
if not _sitting_intent_pending:
|
||||
_apply_local_prediction_sitting_state(bool(parsed["sitting"]))
|
||||
if not _sitting_intent_pending and not sitting_audit_is_stale:
|
||||
_apply_local_prediction_sitting_state(authoritative_sitting)
|
||||
var authoritative_position: Vector3 = parsed["position"]
|
||||
var transit_seconds: float = resolve_local_prediction_transit_seconds(
|
||||
acknowledged_input,
|
||||
|
|
|
|||
|
|
@ -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