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_BATCH_SIZE: int = 8
|
||||||
const MOVEMENT_SNAPSHOT_FIELD_COUNT: int = 12
|
const MOVEMENT_SNAPSHOT_FIELD_COUNT: int = 12
|
||||||
const MAX_MOVEMENT_INPUT_SEQUENCE: int = 2147483647
|
const MAX_MOVEMENT_INPUT_SEQUENCE: int = 2147483647
|
||||||
|
const MAX_MOVEMENT_ONE_WAY_TRANSIT_SECONDS: float = 0.25
|
||||||
|
|
||||||
signal state_changed(state: State)
|
signal state_changed(state: State)
|
||||||
signal status_message_changed(message: String)
|
signal status_message_changed(message: String)
|
||||||
|
|
@ -1890,6 +1891,9 @@ func receive_movement_snapshots(encoded_snapshots: Array) -> void:
|
||||||
if state != State.JOINED_CLIENT:
|
if state != State.JOINED_CLIENT:
|
||||||
return
|
return
|
||||||
var local_peer_id: int = multiplayer.get_unique_id()
|
var local_peer_id: int = multiplayer.get_unique_id()
|
||||||
|
var estimated_transit_seconds: float = (
|
||||||
|
_estimated_movement_transit_seconds()
|
||||||
|
)
|
||||||
for value: Variant in encoded_snapshots:
|
for value: Variant in encoded_snapshots:
|
||||||
var snapshot: Dictionary = _decode_movement_snapshot(value)
|
var snapshot: Dictionary = _decode_movement_snapshot(value)
|
||||||
if snapshot.is_empty():
|
if snapshot.is_empty():
|
||||||
|
|
@ -1905,9 +1909,25 @@ func receive_movement_snapshots(encoded_snapshots: Array) -> void:
|
||||||
snapshot,
|
snapshot,
|
||||||
_input_sequence,
|
_input_sequence,
|
||||||
INPUT_INTERVAL,
|
INPUT_INTERVAL,
|
||||||
|
estimated_transit_seconds,
|
||||||
)
|
)
|
||||||
else:
|
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:
|
func publish_authoritative_teleport(peer_id: int) -> void:
|
||||||
|
|
@ -2149,6 +2169,11 @@ func _fail(message: String) -> void:
|
||||||
|
|
||||||
|
|
||||||
func _teardown_peer() -> 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():
|
for peer_id: int in _recovery_attempts.keys():
|
||||||
remote_recovery_presentation_changed.emit(
|
remote_recovery_presentation_changed.emit(
|
||||||
peer_id, false, _recovery_attempts[peer_id]
|
peer_id, false, _recovery_attempts[peer_id]
|
||||||
|
|
@ -2166,6 +2191,7 @@ func _teardown_peer() -> void:
|
||||||
_transport.disconnect_transport()
|
_transport.disconnect_transport()
|
||||||
multiplayer.multiplayer_peer = OfflineMultiplayerPeer.new()
|
multiplayer.multiplayer_peer = OfflineMultiplayerPeer.new()
|
||||||
_connection_deadline = 0.0
|
_connection_deadline = 0.0
|
||||||
|
_input_sequence = 0
|
||||||
_input_accumulator = 0.0
|
_input_accumulator = 0.0
|
||||||
_snapshot_accumulator = 0.0
|
_snapshot_accumulator = 0.0
|
||||||
_server_capabilities = PackedStringArray()
|
_server_capabilities = PackedStringArray()
|
||||||
|
|
|
||||||
|
|
@ -78,6 +78,10 @@ func get_avatar(peer_id: int) -> Player:
|
||||||
return _avatars.get(peer_id)
|
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:
|
func set_peer_presentation_visible(peer_id: int, should_be_visible: bool) -> void:
|
||||||
var avatar: Player = _avatars.get(peer_id)
|
var avatar: Player = _avatars.get(peer_id)
|
||||||
if avatar != null and is_instance_valid(avatar):
|
if avatar != null and is_instance_valid(avatar):
|
||||||
|
|
|
||||||
209
player/player.gd
209
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 CHARACTER_CALL_MOUTH_DURATION_SECONDS: float = 0.16
|
||||||
const BASE_REEL_SPEED: float = 0.16
|
const BASE_REEL_SPEED: float = 0.16
|
||||||
const LANDING_DUST_MIN_FALL_SPEED: float = 2.5
|
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_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_CORRECTION_THRESHOLD: float = 0.12
|
||||||
const LOCAL_PREDICTION_SNAP_DISTANCE: float = 2.0
|
const LOCAL_PREDICTION_SNAP_DISTANCE: float = 2.0
|
||||||
const LOCAL_PREDICTION_CORRECTION_WEIGHT: float = 0.18
|
const LOCAL_PREDICTION_CORRECTION_WEIGHT: float = 0.18
|
||||||
|
|
@ -408,6 +415,9 @@ var _network_sprint: bool = false
|
||||||
var _network_sneak: bool = false
|
var _network_sneak: bool = false
|
||||||
var _network_slow_walk: bool = false
|
var _network_slow_walk: bool = false
|
||||||
var _last_network_input_sequence: int = 0
|
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_position: Vector3
|
||||||
var _network_target_velocity: Vector3
|
var _network_target_velocity: Vector3
|
||||||
var _network_target_visual_yaw: float = 0.0
|
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_target_animation_action_elapsed: float = 0.0
|
||||||
var _network_snapshot_ready: bool = false
|
var _network_snapshot_ready: bool = false
|
||||||
var _network_snapshot_age: float = 0.0
|
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_id: StringName = &""
|
||||||
var _animation_action_sequence: int = 0
|
var _animation_action_sequence: int = 0
|
||||||
var _animation_action_elapsed: float = 0.0
|
var _animation_action_elapsed: float = 0.0
|
||||||
|
|
@ -648,6 +661,8 @@ func _physics_process(delta: float) -> void:
|
||||||
if _network_interpolation_enabled:
|
if _network_interpolation_enabled:
|
||||||
_update_network_interpolation(delta)
|
_update_network_interpolation(delta)
|
||||||
return
|
return
|
||||||
|
if _network_authoritative_simulation:
|
||||||
|
_update_network_input_freshness(delta)
|
||||||
if local_control_enabled and _free_camera_active:
|
if local_control_enabled and _free_camera_active:
|
||||||
_update_free_camera_physics()
|
_update_free_camera_physics()
|
||||||
velocity.x = 0.0
|
velocity.x = 0.0
|
||||||
|
|
@ -657,11 +672,21 @@ func _physics_process(delta: float) -> void:
|
||||||
move_and_slide()
|
move_and_slide()
|
||||||
_network_jump_pending = false
|
_network_jump_pending = false
|
||||||
return
|
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 = (
|
var jump_requested: bool = (
|
||||||
_is_movement_input_enabled()
|
_is_movement_input_enabled()
|
||||||
and not _free_camera_active
|
and not _free_camera_active
|
||||||
and (
|
and (
|
||||||
(local_control_enabled and Input.is_action_just_pressed("jump"))
|
local_jump_pressed
|
||||||
or (_network_authoritative_simulation and _network_jump_pending)
|
or (_network_authoritative_simulation and _network_jump_pending)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
@ -1436,6 +1461,7 @@ func set_local_control(enabled: bool) -> void:
|
||||||
_camera.current = enabled
|
_camera.current = enabled
|
||||||
if not enabled:
|
if not enabled:
|
||||||
_set_camera_dragging(false)
|
_set_camera_dragging(false)
|
||||||
|
_clear_local_network_jump_intent()
|
||||||
|
|
||||||
|
|
||||||
func set_network_peer_id(peer_id: int) -> void:
|
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)
|
set_local_control(false)
|
||||||
_network_authoritative_simulation = authoritative_simulation
|
_network_authoritative_simulation = authoritative_simulation
|
||||||
_network_interpolation_enabled = not 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_ready = false
|
||||||
_network_snapshot_age = 0.0
|
_network_snapshot_age = 0.0
|
||||||
|
_network_snapshot_jitter = 0.0
|
||||||
_network_target_grounded = false
|
_network_target_grounded = false
|
||||||
_network_target_locomotion_state = LocomotionState.IDLE
|
_network_target_locomotion_state = LocomotionState.IDLE
|
||||||
_network_target_animation_action_id = &""
|
_network_target_animation_action_id = &""
|
||||||
|
|
@ -1460,14 +1495,35 @@ func configure_network_remote(authoritative_simulation: bool) -> void:
|
||||||
_camera.current = false
|
_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:
|
func capture_network_input(sequence: int) -> Dictionary:
|
||||||
if _sitting_intent_pending and _sitting_intent_sequence < 0:
|
if _sitting_intent_pending and _sitting_intent_sequence < 0:
|
||||||
_sitting_intent_sequence = sequence
|
_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 (
|
if (
|
||||||
not _is_movement_input_enabled()
|
not _is_movement_input_enabled()
|
||||||
or _water_recovery_active
|
or _water_recovery_active
|
||||||
or _free_camera_active
|
or _free_camera_active
|
||||||
):
|
):
|
||||||
|
_clear_local_network_jump_intent()
|
||||||
return {
|
return {
|
||||||
"sequence": sequence,
|
"sequence": sequence,
|
||||||
"axis": [0.0, 0.0],
|
"axis": [0.0, 0.0],
|
||||||
|
|
@ -1488,11 +1544,16 @@ func capture_network_input(sequence: int) -> Dictionary:
|
||||||
"move_forward",
|
"move_forward",
|
||||||
"move_backward"
|
"move_backward"
|
||||||
)
|
)
|
||||||
|
if (
|
||||||
|
_local_network_jump_intent_pending
|
||||||
|
and _local_network_jump_intent_sequence < 0
|
||||||
|
):
|
||||||
|
_local_network_jump_intent_sequence = sequence
|
||||||
return {
|
return {
|
||||||
"sequence": sequence,
|
"sequence": sequence,
|
||||||
"axis": [axis.x, axis.y],
|
"axis": [axis.x, axis.y],
|
||||||
"camera_yaw": _camera_yaw.global_rotation.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"),
|
"sprint": Input.is_action_pressed("sprint"),
|
||||||
"sneak": Input.is_action_pressed("sneak"),
|
"sneak": Input.is_action_pressed("sneak"),
|
||||||
"slow_walk": Input.is_action_pressed("slow_walk"),
|
"slow_walk": Input.is_action_pressed("slow_walk"),
|
||||||
|
|
@ -1510,9 +1571,14 @@ func apply_authoritative_network_input(data: Dictionary) -> void:
|
||||||
if axis.size() != 2:
|
if axis.size() != 2:
|
||||||
return
|
return
|
||||||
_last_network_input_sequence = sequence
|
_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_axis = Vector2(float(axis[0]), float(axis[1])).limit_length(1.0)
|
||||||
_network_camera_yaw = float(data.get("camera_yaw", 0.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_sprint = bool(data.get("sprint", false))
|
||||||
_network_sneak = bool(data.get("sneak", false))
|
_network_sneak = bool(data.get("sneak", false))
|
||||||
_network_slow_walk = bool(data.get("slow_walk", 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)
|
var parsed: Dictionary = _parse_network_snapshot(snapshot)
|
||||||
if parsed.is_empty():
|
if parsed.is_empty():
|
||||||
return
|
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_velocity = parsed["velocity"]
|
||||||
_network_target_visual_yaw = parsed["visual_yaw"]
|
_network_target_visual_yaw = parsed["visual_yaw"]
|
||||||
_apply_network_target_animation_state(parsed["animation_state"])
|
_apply_network_target_animation_state(parsed["animation_state"])
|
||||||
|
|
@ -1568,6 +1660,7 @@ func apply_local_prediction_correction(
|
||||||
snapshot: Dictionary,
|
snapshot: Dictionary,
|
||||||
latest_input_sequence: int = 0,
|
latest_input_sequence: int = 0,
|
||||||
input_interval_seconds: float = 0.0,
|
input_interval_seconds: float = 0.0,
|
||||||
|
estimated_transit_seconds: float = -1.0,
|
||||||
) -> void:
|
) -> void:
|
||||||
var parsed: Dictionary = _parse_network_snapshot(snapshot)
|
var parsed: Dictionary = _parse_network_snapshot(snapshot)
|
||||||
if parsed.is_empty():
|
if parsed.is_empty():
|
||||||
|
|
@ -1581,22 +1674,22 @@ func apply_local_prediction_correction(
|
||||||
if sitting_intent_acknowledged:
|
if sitting_intent_acknowledged:
|
||||||
_sitting_intent_pending = false
|
_sitting_intent_pending = false
|
||||||
_sitting_intent_sequence = -1
|
_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:
|
if not _sitting_intent_pending:
|
||||||
_set_sitting(bool(parsed["sitting"]))
|
_set_sitting(bool(parsed["sitting"]))
|
||||||
var authoritative_position: Vector3 = parsed["position"]
|
var authoritative_position: Vector3 = parsed["position"]
|
||||||
if (
|
var transit_seconds: float = resolve_local_prediction_transit_seconds(
|
||||||
acknowledged_input > 0
|
acknowledged_input,
|
||||||
and latest_input_sequence > acknowledged_input
|
latest_input_sequence,
|
||||||
and input_interval_seconds > 0.0
|
input_interval_seconds,
|
||||||
):
|
estimated_transit_seconds,
|
||||||
# The snapshot describes the host's position when an older input was
|
)
|
||||||
# acknowledged. Project it through the measured input-sequence gap so
|
if transit_seconds > 0.0:
|
||||||
# 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,
|
|
||||||
)
|
|
||||||
authoritative_position += (
|
authoritative_position += (
|
||||||
(parsed["velocity"] as Vector3) * transit_seconds
|
(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:
|
func apply_network_teleport(snapshot: Dictionary) -> void:
|
||||||
var parsed: Dictionary = _parse_network_snapshot(snapshot)
|
var parsed: Dictionary = _parse_network_snapshot(snapshot)
|
||||||
if parsed.is_empty():
|
if parsed.is_empty():
|
||||||
|
|
@ -1807,23 +1929,38 @@ func _apply_network_casting(casting: bool) -> void:
|
||||||
func _update_network_interpolation(delta: float) -> void:
|
func _update_network_interpolation(delta: float) -> void:
|
||||||
if not _network_snapshot_ready:
|
if not _network_snapshot_ready:
|
||||||
return
|
return
|
||||||
|
var previous_snapshot_age: float = _network_snapshot_age
|
||||||
_network_snapshot_age = minf(
|
_network_snapshot_age = minf(
|
||||||
_network_snapshot_age + delta,
|
_network_snapshot_age + delta,
|
||||||
NETWORK_EXTRAPOLATION_LIMIT_SECONDS,
|
NETWORK_EXTRAPOLATION_LIMIT_SECONDS,
|
||||||
)
|
)
|
||||||
|
var extrapolation_delta: float = (
|
||||||
|
_network_snapshot_age - previous_snapshot_age
|
||||||
|
)
|
||||||
var predicted_position: Vector3 = (
|
var predicted_position: Vector3 = (
|
||||||
_network_target_position
|
_network_target_position
|
||||||
+ _network_target_velocity * _network_snapshot_age
|
+ _network_target_velocity * _network_snapshot_age
|
||||||
)
|
)
|
||||||
var projected_position: Vector3 = (
|
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:
|
if projected_position.distance_to(predicted_position) > 2.0:
|
||||||
global_position = predicted_position
|
global_position = predicted_position
|
||||||
else:
|
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(
|
global_position = projected_position.lerp(
|
||||||
predicted_position,
|
predicted_position,
|
||||||
1.0 - exp(-10.0 * delta)
|
1.0 - exp(-smoothing_rate * delta)
|
||||||
)
|
)
|
||||||
velocity = _network_target_velocity
|
velocity = _network_target_velocity
|
||||||
_visuals.rotation.y = lerp_angle(
|
_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:
|
func is_local_control_enabled() -> bool:
|
||||||
return local_control_enabled
|
return local_control_enabled
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -32,7 +32,16 @@ func _validate_latency_smoothing() -> void:
|
||||||
await process_frame
|
await process_frame
|
||||||
avatar.set_process(false)
|
avatar.set_process(false)
|
||||||
avatar.set_physics_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(
|
var moving_snapshot: Dictionary = _network_snapshot(
|
||||||
Vector3.ZERO,
|
Vector3.ZERO,
|
||||||
Vector3(4.5, 0.0, 0.0),
|
Vector3(4.5, 0.0, 0.0),
|
||||||
|
|
@ -50,21 +59,148 @@ func _validate_latency_smoothing() -> void:
|
||||||
encoded_snapshots[0]
|
encoded_snapshots[0]
|
||||||
) == moving_snapshot
|
) == 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.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(
|
avatar.apply_local_prediction_correction(
|
||||||
moving_snapshot,
|
moving_snapshot,
|
||||||
10,
|
10,
|
||||||
1.0 / 30.0,
|
1.0 / 30.0,
|
||||||
|
0.1,
|
||||||
)
|
)
|
||||||
assert(avatar.global_position.x > 1.25)
|
assert(avatar.global_position.x < 0.8)
|
||||||
avatar.queue_free()
|
assert(avatar.global_position.x > 0.7)
|
||||||
await process_frame
|
|
||||||
|
|
||||||
|
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(
|
func _network_snapshot(
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue