fix: improve movement under network latency
This commit is contained in:
parent
72770cbfd2
commit
c260472d8a
4 changed files with 364 additions and 31 deletions
|
|
@ -12,6 +12,7 @@ const SNAPSHOT_INTERVAL: float = 1.0 / 30.0
|
|||
const MOVEMENT_SNAPSHOT_BATCH_SIZE: int = 8
|
||||
const MOVEMENT_SNAPSHOT_FIELD_COUNT: int = 12
|
||||
const MAX_MOVEMENT_INPUT_SEQUENCE: int = 2147483647
|
||||
const MAX_MOVEMENT_ONE_WAY_TRANSIT_SECONDS: float = 0.25
|
||||
|
||||
signal state_changed(state: State)
|
||||
signal status_message_changed(message: String)
|
||||
|
|
@ -1890,6 +1891,9 @@ func receive_movement_snapshots(encoded_snapshots: Array) -> void:
|
|||
if state != State.JOINED_CLIENT:
|
||||
return
|
||||
var local_peer_id: int = multiplayer.get_unique_id()
|
||||
var estimated_transit_seconds: float = (
|
||||
_estimated_movement_transit_seconds()
|
||||
)
|
||||
for value: Variant in encoded_snapshots:
|
||||
var snapshot: Dictionary = _decode_movement_snapshot(value)
|
||||
if snapshot.is_empty():
|
||||
|
|
@ -1905,9 +1909,25 @@ func receive_movement_snapshots(encoded_snapshots: Array) -> void:
|
|||
snapshot,
|
||||
_input_sequence,
|
||||
INPUT_INTERVAL,
|
||||
estimated_transit_seconds,
|
||||
)
|
||||
else:
|
||||
avatar.push_network_snapshot(snapshot)
|
||||
avatar.push_network_snapshot(
|
||||
snapshot,
|
||||
estimated_transit_seconds,
|
||||
)
|
||||
|
||||
|
||||
func _estimated_movement_transit_seconds() -> float:
|
||||
if state != State.JOINED_CLIENT:
|
||||
return -1.0
|
||||
var round_trip_msec: int = get_peer_rtt_ms(get_local_peer_id())
|
||||
if round_trip_msec < 0:
|
||||
return -1.0
|
||||
return minf(
|
||||
float(round_trip_msec) / 2000.0,
|
||||
MAX_MOVEMENT_ONE_WAY_TRANSIT_SECONDS,
|
||||
)
|
||||
|
||||
|
||||
func publish_authoritative_teleport(peer_id: int) -> void:
|
||||
|
|
@ -2149,6 +2169,11 @@ func _fail(message: String) -> void:
|
|||
|
||||
|
||||
func _teardown_peer() -> void:
|
||||
var local_avatar: Player
|
||||
if _spawn_service != null:
|
||||
local_avatar = _spawn_service.get_local_player()
|
||||
if local_avatar != null:
|
||||
local_avatar.reset_network_movement_state()
|
||||
for peer_id: int in _recovery_attempts.keys():
|
||||
remote_recovery_presentation_changed.emit(
|
||||
peer_id, false, _recovery_attempts[peer_id]
|
||||
|
|
@ -2166,6 +2191,7 @@ func _teardown_peer() -> void:
|
|||
_transport.disconnect_transport()
|
||||
multiplayer.multiplayer_peer = OfflineMultiplayerPeer.new()
|
||||
_connection_deadline = 0.0
|
||||
_input_sequence = 0
|
||||
_input_accumulator = 0.0
|
||||
_snapshot_accumulator = 0.0
|
||||
_server_capabilities = PackedStringArray()
|
||||
|
|
|
|||
|
|
@ -78,6 +78,10 @@ func get_avatar(peer_id: int) -> Player:
|
|||
return _avatars.get(peer_id)
|
||||
|
||||
|
||||
func get_local_player() -> Player:
|
||||
return _local_player
|
||||
|
||||
|
||||
func set_peer_presentation_visible(peer_id: int, should_be_visible: bool) -> void:
|
||||
var avatar: Player = _avatars.get(peer_id)
|
||||
if avatar != null and is_instance_valid(avatar):
|
||||
|
|
|
|||
207
player/player.gd
207
player/player.gd
|
|
@ -104,8 +104,15 @@ const CHARACTER_CALL_MOUTH_ID: String = "open_ah"
|
|||
const CHARACTER_CALL_MOUTH_DURATION_SECONDS: float = 0.16
|
||||
const BASE_REEL_SPEED: float = 0.16
|
||||
const LANDING_DUST_MIN_FALL_SPEED: float = 2.5
|
||||
const NETWORK_EXTRAPOLATION_LIMIT_SECONDS: float = 0.5
|
||||
const NETWORK_EXTRAPOLATION_LIMIT_SECONDS: float = 0.25
|
||||
const NETWORK_EXPECTED_SNAPSHOT_INTERVAL_SECONDS: float = 1.0 / 30.0
|
||||
const NETWORK_SNAPSHOT_JITTER_LIMIT_SECONDS: float = 0.1
|
||||
const NETWORK_SNAPSHOT_JITTER_WEIGHT: float = 0.15
|
||||
const NETWORK_REMOTE_SMOOTHING_RATE: float = 12.0
|
||||
const NETWORK_REMOTE_JITTER_SMOOTHING_RATE: float = 6.0
|
||||
const NETWORK_INPUT_STALE_TIMEOUT_SECONDS: float = 0.25
|
||||
const LOCAL_PREDICTION_EXTRAPOLATION_LIMIT_SECONDS: float = 0.25
|
||||
const LOCAL_PREDICTION_FALLBACK_TRANSIT_RATIO: float = 0.5
|
||||
const LOCAL_PREDICTION_CORRECTION_THRESHOLD: float = 0.12
|
||||
const LOCAL_PREDICTION_SNAP_DISTANCE: float = 2.0
|
||||
const LOCAL_PREDICTION_CORRECTION_WEIGHT: float = 0.18
|
||||
|
|
@ -408,6 +415,9 @@ var _network_sprint: bool = false
|
|||
var _network_sneak: bool = false
|
||||
var _network_slow_walk: bool = false
|
||||
var _last_network_input_sequence: int = 0
|
||||
var _network_input_age: float = 0.0
|
||||
var _network_input_stale: bool = false
|
||||
var _network_jump_intent_active: bool = false
|
||||
var _network_target_position: Vector3
|
||||
var _network_target_velocity: Vector3
|
||||
var _network_target_visual_yaw: float = 0.0
|
||||
|
|
@ -418,6 +428,9 @@ var _network_target_animation_action_sequence: int = 0
|
|||
var _network_target_animation_action_elapsed: float = 0.0
|
||||
var _network_snapshot_ready: bool = false
|
||||
var _network_snapshot_age: float = 0.0
|
||||
var _network_snapshot_jitter: float = 0.0
|
||||
var _local_network_jump_intent_pending: bool = false
|
||||
var _local_network_jump_intent_sequence: int = -1
|
||||
var _animation_action_id: StringName = &""
|
||||
var _animation_action_sequence: int = 0
|
||||
var _animation_action_elapsed: float = 0.0
|
||||
|
|
@ -648,6 +661,8 @@ func _physics_process(delta: float) -> void:
|
|||
if _network_interpolation_enabled:
|
||||
_update_network_interpolation(delta)
|
||||
return
|
||||
if _network_authoritative_simulation:
|
||||
_update_network_input_freshness(delta)
|
||||
if local_control_enabled and _free_camera_active:
|
||||
_update_free_camera_physics()
|
||||
velocity.x = 0.0
|
||||
|
|
@ -657,11 +672,21 @@ func _physics_process(delta: float) -> void:
|
|||
move_and_slide()
|
||||
_network_jump_pending = false
|
||||
return
|
||||
var local_jump_pressed: bool = (
|
||||
local_control_enabled
|
||||
and Input.is_action_just_pressed("jump")
|
||||
)
|
||||
if (
|
||||
local_jump_pressed
|
||||
and _is_movement_input_enabled()
|
||||
and not _free_camera_active
|
||||
):
|
||||
_queue_local_network_jump_intent()
|
||||
var jump_requested: bool = (
|
||||
_is_movement_input_enabled()
|
||||
and not _free_camera_active
|
||||
and (
|
||||
(local_control_enabled and Input.is_action_just_pressed("jump"))
|
||||
local_jump_pressed
|
||||
or (_network_authoritative_simulation and _network_jump_pending)
|
||||
)
|
||||
)
|
||||
|
|
@ -1436,6 +1461,7 @@ func set_local_control(enabled: bool) -> void:
|
|||
_camera.current = enabled
|
||||
if not enabled:
|
||||
_set_camera_dragging(false)
|
||||
_clear_local_network_jump_intent()
|
||||
|
||||
|
||||
func set_network_peer_id(peer_id: int) -> void:
|
||||
|
|
@ -1450,8 +1476,17 @@ func configure_network_remote(authoritative_simulation: bool) -> void:
|
|||
set_local_control(false)
|
||||
_network_authoritative_simulation = authoritative_simulation
|
||||
_network_interpolation_enabled = not authoritative_simulation
|
||||
_network_axis = Vector2.ZERO
|
||||
_network_jump_pending = false
|
||||
_network_sprint = false
|
||||
_network_sneak = false
|
||||
_network_slow_walk = false
|
||||
_network_input_age = 0.0
|
||||
_network_input_stale = false
|
||||
_network_jump_intent_active = false
|
||||
_network_snapshot_ready = false
|
||||
_network_snapshot_age = 0.0
|
||||
_network_snapshot_jitter = 0.0
|
||||
_network_target_grounded = false
|
||||
_network_target_locomotion_state = LocomotionState.IDLE
|
||||
_network_target_animation_action_id = &""
|
||||
|
|
@ -1460,14 +1495,35 @@ func configure_network_remote(authoritative_simulation: bool) -> void:
|
|||
_camera.current = false
|
||||
|
||||
|
||||
func reset_network_movement_state() -> void:
|
||||
_clear_local_network_jump_intent()
|
||||
_network_axis = Vector2.ZERO
|
||||
_network_jump_pending = false
|
||||
_network_sprint = false
|
||||
_network_sneak = false
|
||||
_network_slow_walk = false
|
||||
_network_input_age = 0.0
|
||||
_network_input_stale = false
|
||||
_network_jump_intent_active = false
|
||||
_last_network_input_sequence = 0
|
||||
|
||||
|
||||
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()
|
||||
and not _free_camera_active
|
||||
and Input.is_action_just_pressed("jump")
|
||||
):
|
||||
_queue_local_network_jump_intent()
|
||||
if (
|
||||
not _is_movement_input_enabled()
|
||||
or _water_recovery_active
|
||||
or _free_camera_active
|
||||
):
|
||||
_clear_local_network_jump_intent()
|
||||
return {
|
||||
"sequence": sequence,
|
||||
"axis": [0.0, 0.0],
|
||||
|
|
@ -1488,11 +1544,16 @@ func capture_network_input(sequence: int) -> Dictionary:
|
|||
"move_forward",
|
||||
"move_backward"
|
||||
)
|
||||
if (
|
||||
_local_network_jump_intent_pending
|
||||
and _local_network_jump_intent_sequence < 0
|
||||
):
|
||||
_local_network_jump_intent_sequence = sequence
|
||||
return {
|
||||
"sequence": sequence,
|
||||
"axis": [axis.x, axis.y],
|
||||
"camera_yaw": _camera_yaw.global_rotation.y,
|
||||
"jump": Input.is_action_just_pressed("jump"),
|
||||
"jump": _local_network_jump_intent_pending,
|
||||
"sprint": Input.is_action_pressed("sprint"),
|
||||
"sneak": Input.is_action_pressed("sneak"),
|
||||
"slow_walk": Input.is_action_pressed("slow_walk"),
|
||||
|
|
@ -1510,9 +1571,14 @@ func apply_authoritative_network_input(data: Dictionary) -> void:
|
|||
if axis.size() != 2:
|
||||
return
|
||||
_last_network_input_sequence = sequence
|
||||
_network_input_age = 0.0
|
||||
_network_input_stale = false
|
||||
_network_axis = Vector2(float(axis[0]), float(axis[1])).limit_length(1.0)
|
||||
_network_camera_yaw = float(data.get("camera_yaw", 0.0))
|
||||
_network_jump_pending = bool(data.get("jump", false))
|
||||
var jump_intent_active: bool = bool(data.get("jump", false))
|
||||
if jump_intent_active and not _network_jump_intent_active:
|
||||
_network_jump_pending = true
|
||||
_network_jump_intent_active = jump_intent_active
|
||||
_network_sprint = bool(data.get("sprint", false))
|
||||
_network_sneak = bool(data.get("sneak", false))
|
||||
_network_slow_walk = bool(data.get("slow_walk", false))
|
||||
|
|
@ -1546,11 +1612,37 @@ func make_network_snapshot(peer_id: int) -> Dictionary:
|
|||
}
|
||||
|
||||
|
||||
func push_network_snapshot(snapshot: Dictionary) -> void:
|
||||
func push_network_snapshot(
|
||||
snapshot: Dictionary,
|
||||
estimated_transit_seconds: float = -1.0,
|
||||
) -> void:
|
||||
var parsed: Dictionary = _parse_network_snapshot(snapshot)
|
||||
if parsed.is_empty():
|
||||
return
|
||||
_network_target_position = parsed["position"]
|
||||
if _network_snapshot_ready:
|
||||
var arrival_error: float = absf(
|
||||
_network_snapshot_age
|
||||
- NETWORK_EXPECTED_SNAPSHOT_INTERVAL_SECONDS
|
||||
)
|
||||
_network_snapshot_jitter = lerpf(
|
||||
_network_snapshot_jitter,
|
||||
minf(arrival_error, NETWORK_SNAPSHOT_JITTER_LIMIT_SECONDS),
|
||||
NETWORK_SNAPSHOT_JITTER_WEIGHT,
|
||||
)
|
||||
var transit_seconds: float = (
|
||||
clampf(
|
||||
estimated_transit_seconds,
|
||||
0.0,
|
||||
NETWORK_EXTRAPOLATION_LIMIT_SECONDS,
|
||||
)
|
||||
if is_finite(estimated_transit_seconds)
|
||||
and estimated_transit_seconds >= 0.0
|
||||
else 0.0
|
||||
)
|
||||
_network_target_position = (
|
||||
(parsed["position"] as Vector3)
|
||||
+ (parsed["velocity"] as Vector3) * transit_seconds
|
||||
)
|
||||
_network_target_velocity = parsed["velocity"]
|
||||
_network_target_visual_yaw = parsed["visual_yaw"]
|
||||
_apply_network_target_animation_state(parsed["animation_state"])
|
||||
|
|
@ -1568,6 +1660,7 @@ func apply_local_prediction_correction(
|
|||
snapshot: Dictionary,
|
||||
latest_input_sequence: int = 0,
|
||||
input_interval_seconds: float = 0.0,
|
||||
estimated_transit_seconds: float = -1.0,
|
||||
) -> void:
|
||||
var parsed: Dictionary = _parse_network_snapshot(snapshot)
|
||||
if parsed.is_empty():
|
||||
|
|
@ -1581,22 +1674,22 @@ func apply_local_prediction_correction(
|
|||
if sitting_intent_acknowledged:
|
||||
_sitting_intent_pending = false
|
||||
_sitting_intent_sequence = -1
|
||||
if (
|
||||
_local_network_jump_intent_pending
|
||||
and _local_network_jump_intent_sequence >= 0
|
||||
and acknowledged_input >= _local_network_jump_intent_sequence
|
||||
):
|
||||
_clear_local_network_jump_intent()
|
||||
if not _sitting_intent_pending:
|
||||
_set_sitting(bool(parsed["sitting"]))
|
||||
var authoritative_position: Vector3 = parsed["position"]
|
||||
if (
|
||||
acknowledged_input > 0
|
||||
and latest_input_sequence > acknowledged_input
|
||||
and input_interval_seconds > 0.0
|
||||
):
|
||||
# The snapshot describes the host's position when an older input was
|
||||
# acknowledged. Project it through the measured input-sequence gap so
|
||||
# ordinary round-trip latency is not mistaken for prediction error.
|
||||
var sequence_gap: int = latest_input_sequence - acknowledged_input
|
||||
var transit_seconds: float = minf(
|
||||
float(sequence_gap) * input_interval_seconds,
|
||||
LOCAL_PREDICTION_EXTRAPOLATION_LIMIT_SECONDS,
|
||||
var transit_seconds: float = resolve_local_prediction_transit_seconds(
|
||||
acknowledged_input,
|
||||
latest_input_sequence,
|
||||
input_interval_seconds,
|
||||
estimated_transit_seconds,
|
||||
)
|
||||
if transit_seconds > 0.0:
|
||||
authoritative_position += (
|
||||
(parsed["velocity"] as Vector3) * transit_seconds
|
||||
)
|
||||
|
|
@ -1612,6 +1705,35 @@ func apply_local_prediction_correction(
|
|||
)
|
||||
|
||||
|
||||
static func resolve_local_prediction_transit_seconds(
|
||||
acknowledged_input: int,
|
||||
latest_input_sequence: int,
|
||||
input_interval_seconds: float,
|
||||
estimated_transit_seconds: float,
|
||||
) -> float:
|
||||
if is_finite(estimated_transit_seconds) and estimated_transit_seconds >= 0.0:
|
||||
return minf(
|
||||
estimated_transit_seconds,
|
||||
LOCAL_PREDICTION_EXTRAPOLATION_LIMIT_SECONDS,
|
||||
)
|
||||
if (
|
||||
acknowledged_input <= 0
|
||||
or latest_input_sequence <= acknowledged_input
|
||||
or input_interval_seconds <= 0.0
|
||||
):
|
||||
return 0.0
|
||||
# The input gap spans approximately the full round trip: from the input
|
||||
# acknowledged by the host to the newest input at snapshot receipt. Only
|
||||
# half of that interval lies between the host snapshot and the client now.
|
||||
var sequence_gap: int = latest_input_sequence - acknowledged_input
|
||||
return minf(
|
||||
float(sequence_gap)
|
||||
* input_interval_seconds
|
||||
* LOCAL_PREDICTION_FALLBACK_TRANSIT_RATIO,
|
||||
LOCAL_PREDICTION_EXTRAPOLATION_LIMIT_SECONDS,
|
||||
)
|
||||
|
||||
|
||||
func apply_network_teleport(snapshot: Dictionary) -> void:
|
||||
var parsed: Dictionary = _parse_network_snapshot(snapshot)
|
||||
if parsed.is_empty():
|
||||
|
|
@ -1807,23 +1929,38 @@ func _apply_network_casting(casting: bool) -> void:
|
|||
func _update_network_interpolation(delta: float) -> void:
|
||||
if not _network_snapshot_ready:
|
||||
return
|
||||
var previous_snapshot_age: float = _network_snapshot_age
|
||||
_network_snapshot_age = minf(
|
||||
_network_snapshot_age + delta,
|
||||
NETWORK_EXTRAPOLATION_LIMIT_SECONDS,
|
||||
)
|
||||
var extrapolation_delta: float = (
|
||||
_network_snapshot_age - previous_snapshot_age
|
||||
)
|
||||
var predicted_position: Vector3 = (
|
||||
_network_target_position
|
||||
+ _network_target_velocity * _network_snapshot_age
|
||||
)
|
||||
var projected_position: Vector3 = (
|
||||
global_position + _network_target_velocity * delta
|
||||
global_position + _network_target_velocity * extrapolation_delta
|
||||
)
|
||||
if projected_position.distance_to(predicted_position) > 2.0:
|
||||
global_position = predicted_position
|
||||
else:
|
||||
var jitter_ratio: float = clampf(
|
||||
_network_snapshot_jitter
|
||||
/ NETWORK_SNAPSHOT_JITTER_LIMIT_SECONDS,
|
||||
0.0,
|
||||
1.0,
|
||||
)
|
||||
var smoothing_rate: float = lerpf(
|
||||
NETWORK_REMOTE_SMOOTHING_RATE,
|
||||
NETWORK_REMOTE_JITTER_SMOOTHING_RATE,
|
||||
jitter_ratio,
|
||||
)
|
||||
global_position = projected_position.lerp(
|
||||
predicted_position,
|
||||
1.0 - exp(-10.0 * delta)
|
||||
1.0 - exp(-smoothing_rate * delta)
|
||||
)
|
||||
velocity = _network_target_velocity
|
||||
_visuals.rotation.y = lerp_angle(
|
||||
|
|
@ -1833,6 +1970,36 @@ func _update_network_interpolation(delta: float) -> void:
|
|||
)
|
||||
|
||||
|
||||
func _update_network_input_freshness(delta: float) -> void:
|
||||
_network_input_age = minf(
|
||||
_network_input_age + delta,
|
||||
NETWORK_INPUT_STALE_TIMEOUT_SECONDS + 1.0,
|
||||
)
|
||||
if (
|
||||
_network_input_stale
|
||||
or _network_input_age <= NETWORK_INPUT_STALE_TIMEOUT_SECONDS
|
||||
):
|
||||
return
|
||||
_network_input_stale = true
|
||||
_network_axis = Vector2.ZERO
|
||||
_network_jump_pending = false
|
||||
_network_sprint = false
|
||||
_network_sneak = false
|
||||
_network_slow_walk = false
|
||||
|
||||
|
||||
func _queue_local_network_jump_intent() -> void:
|
||||
if _local_network_jump_intent_pending:
|
||||
return
|
||||
_local_network_jump_intent_pending = true
|
||||
_local_network_jump_intent_sequence = -1
|
||||
|
||||
|
||||
func _clear_local_network_jump_intent() -> void:
|
||||
_local_network_jump_intent_pending = false
|
||||
_local_network_jump_intent_sequence = -1
|
||||
|
||||
|
||||
func is_local_control_enabled() -> bool:
|
||||
return local_control_enabled
|
||||
|
||||
|
|
|
|||
|
|
@ -32,7 +32,16 @@ func _validate_latency_smoothing() -> void:
|
|||
await process_frame
|
||||
avatar.set_process(false)
|
||||
avatar.set_physics_process(false)
|
||||
avatar.configure_network_remote(false)
|
||||
_validate_compact_snapshot_encoding()
|
||||
_validate_transit_estimation()
|
||||
_validate_remote_snapshot_smoothing(avatar)
|
||||
_validate_reliable_jump_intent(avatar)
|
||||
_validate_stale_input_expiry(avatar)
|
||||
avatar.queue_free()
|
||||
await process_frame
|
||||
|
||||
|
||||
func _validate_compact_snapshot_encoding() -> void:
|
||||
var moving_snapshot: Dictionary = _network_snapshot(
|
||||
Vector3.ZERO,
|
||||
Vector3(4.5, 0.0, 0.0),
|
||||
|
|
@ -50,21 +59,148 @@ func _validate_latency_smoothing() -> void:
|
|||
encoded_snapshots[0]
|
||||
) == moving_snapshot
|
||||
)
|
||||
avatar.push_network_snapshot(moving_snapshot)
|
||||
for _step: int in 12:
|
||||
avatar.call("_update_network_interpolation", 1.0 / 30.0)
|
||||
assert(avatar.global_position.x > 1.5)
|
||||
|
||||
|
||||
func _validate_transit_estimation() -> void:
|
||||
assert(is_equal_approx(
|
||||
Player.resolve_local_prediction_transit_seconds(
|
||||
10,
|
||||
16,
|
||||
1.0 / 30.0,
|
||||
0.075,
|
||||
),
|
||||
0.075,
|
||||
))
|
||||
# Six outstanding 30 Hz inputs span about 200 ms round trip. The fallback
|
||||
# must use only the approximately 100 ms one-way half of that gap.
|
||||
assert(is_equal_approx(
|
||||
Player.resolve_local_prediction_transit_seconds(
|
||||
10,
|
||||
16,
|
||||
1.0 / 30.0,
|
||||
-1.0,
|
||||
),
|
||||
0.1,
|
||||
))
|
||||
assert(is_equal_approx(
|
||||
Player.resolve_local_prediction_transit_seconds(
|
||||
1,
|
||||
100,
|
||||
1.0 / 30.0,
|
||||
-1.0,
|
||||
),
|
||||
Player.LOCAL_PREDICTION_EXTRAPOLATION_LIMIT_SECONDS,
|
||||
))
|
||||
|
||||
|
||||
func _validate_remote_snapshot_smoothing(avatar: Player) -> void:
|
||||
avatar.configure_network_remote(false)
|
||||
avatar.global_position = Vector3(1.35, 0.0, 0.0)
|
||||
var moving_snapshot: Dictionary = _network_snapshot(
|
||||
Vector3.ZERO,
|
||||
Vector3(4.5, 0.0, 0.0),
|
||||
1,
|
||||
)
|
||||
avatar.push_network_snapshot(moving_snapshot, 0.1)
|
||||
assert(is_equal_approx(avatar.global_position.x, 0.45))
|
||||
assert(is_equal_approx(
|
||||
float(avatar.get("_network_target_position").x),
|
||||
0.45,
|
||||
))
|
||||
avatar.call("_update_network_interpolation", 0.12)
|
||||
var before_delayed_snapshot: Vector3 = avatar.global_position
|
||||
var delayed_snapshot: Dictionary = _network_snapshot(
|
||||
Vector3(0.54, 0.0, 0.0),
|
||||
Vector3(4.5, 0.0, 0.0),
|
||||
2,
|
||||
)
|
||||
avatar.push_network_snapshot(delayed_snapshot, 0.1)
|
||||
# Snapshot receipt updates the target without teleporting a presented
|
||||
# remote avatar, even after a jittered packet interval.
|
||||
assert(avatar.global_position == before_delayed_snapshot)
|
||||
assert(float(avatar.get("_network_snapshot_jitter")) > 0.0)
|
||||
avatar.call("_update_network_interpolation", 1.0 / 60.0)
|
||||
assert(
|
||||
avatar.global_position.distance_to(before_delayed_snapshot) < 0.12
|
||||
)
|
||||
# Simulate a burst of dropped snapshots. Extrapolation must stop at the
|
||||
# tight limit instead of allowing the remote avatar to run indefinitely.
|
||||
for _step: int in 20:
|
||||
avatar.call("_update_network_interpolation", 1.0 / 30.0)
|
||||
assert(is_equal_approx(
|
||||
float(avatar.get("_network_snapshot_age")),
|
||||
Player.NETWORK_EXTRAPOLATION_LIMIT_SECONDS,
|
||||
))
|
||||
var maximum_extrapolated_x: float = (
|
||||
0.54
|
||||
+ 4.5 * (0.1 + Player.NETWORK_EXTRAPOLATION_LIMIT_SECONDS)
|
||||
)
|
||||
assert(avatar.global_position.x <= maximum_extrapolated_x + 0.1)
|
||||
|
||||
avatar.set_local_control(true)
|
||||
avatar.global_position = Vector3(0.8, 0.0, 0.0)
|
||||
avatar.apply_local_prediction_correction(
|
||||
moving_snapshot,
|
||||
10,
|
||||
1.0 / 30.0,
|
||||
0.1,
|
||||
)
|
||||
assert(avatar.global_position.x > 1.25)
|
||||
avatar.queue_free()
|
||||
await process_frame
|
||||
assert(avatar.global_position.x < 0.8)
|
||||
assert(avatar.global_position.x > 0.7)
|
||||
|
||||
|
||||
func _validate_reliable_jump_intent(avatar: Player) -> void:
|
||||
avatar.set_local_control(true)
|
||||
avatar.call("_queue_local_network_jump_intent")
|
||||
var first: Dictionary = avatar.capture_network_input(20)
|
||||
var repeated: Dictionary = avatar.capture_network_input(21)
|
||||
assert(bool(first["jump"]))
|
||||
assert(bool(repeated["jump"]))
|
||||
assert(int(avatar.get("_local_network_jump_intent_sequence")) == 20)
|
||||
avatar.apply_local_prediction_correction(
|
||||
_network_snapshot(avatar.global_position, Vector3.ZERO, 20),
|
||||
21,
|
||||
1.0 / 30.0,
|
||||
0.0,
|
||||
)
|
||||
assert(not bool(avatar.capture_network_input(22)["jump"]))
|
||||
|
||||
avatar.configure_network_remote(true)
|
||||
var first_host_jump: Dictionary = _movement_input(30, false, false)
|
||||
first_host_jump["jump"] = true
|
||||
avatar.apply_authoritative_network_input(first_host_jump)
|
||||
assert(bool(avatar.get("_network_jump_pending")))
|
||||
avatar.set("_network_jump_pending", false)
|
||||
var repeated_host_jump: Dictionary = _movement_input(31, false, false)
|
||||
repeated_host_jump["jump"] = true
|
||||
avatar.apply_authoritative_network_input(repeated_host_jump)
|
||||
assert(not bool(avatar.get("_network_jump_pending")))
|
||||
avatar.apply_authoritative_network_input(
|
||||
_movement_input(32, false, false)
|
||||
)
|
||||
var next_host_jump: Dictionary = _movement_input(33, false, false)
|
||||
next_host_jump["jump"] = true
|
||||
avatar.apply_authoritative_network_input(next_host_jump)
|
||||
assert(bool(avatar.get("_network_jump_pending")))
|
||||
avatar.reset_network_movement_state()
|
||||
assert(not bool(avatar.get("_network_jump_pending")))
|
||||
assert(not bool(avatar.get("_local_network_jump_intent_pending")))
|
||||
assert(int(avatar.get("_last_network_input_sequence")) == 0)
|
||||
|
||||
|
||||
func _validate_stale_input_expiry(avatar: Player) -> void:
|
||||
avatar.configure_network_remote(true)
|
||||
avatar.apply_authoritative_network_input(_movement_input(40, true))
|
||||
assert((avatar.get("_network_axis") as Vector2).length_squared() > 0.0)
|
||||
avatar.call(
|
||||
"_update_network_input_freshness",
|
||||
Player.NETWORK_INPUT_STALE_TIMEOUT_SECONDS + 0.01,
|
||||
)
|
||||
assert((avatar.get("_network_axis") as Vector2) == Vector2.ZERO)
|
||||
assert(not bool(avatar.get("_network_sprint")))
|
||||
assert(bool(avatar.get("_network_input_stale")))
|
||||
avatar.apply_authoritative_network_input(_movement_input(41, false))
|
||||
assert(not bool(avatar.get("_network_input_stale")))
|
||||
assert((avatar.get("_network_axis") as Vector2).length_squared() > 0.0)
|
||||
|
||||
|
||||
func _network_snapshot(
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue