Improve multiplayer movement reconciliation
This commit is contained in:
parent
348ae261e1
commit
a3aea98982
3 changed files with 240 additions and 89 deletions
222
player/player.gd
222
player/player.gd
|
|
@ -145,9 +145,14 @@ const NETWORK_MOVEMENT_HISTORY_SECONDS: float = 1.25
|
|||
const NETWORK_MAX_LAG_COMPENSATION_SECONDS: float = 0.75
|
||||
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
|
||||
const LOCAL_PREDICTION_CORRECTION_THRESHOLD: float = 1.5
|
||||
const LOCAL_PREDICTION_SNAP_DISTANCE: float = 6.0
|
||||
const LOCAL_PREDICTION_CORRECTION_DELAY_SECONDS: float = 0.35
|
||||
const LOCAL_PREDICTION_MIN_CORRECTION_AUDITS: int = 3
|
||||
const LOCAL_PREDICTION_SOFT_CORRECTION_RATE: float = 2.5
|
||||
const LOCAL_PREDICTION_MAX_SOFT_CORRECTION_STEP: float = 0.15
|
||||
const LOCAL_PREDICTION_MAX_AUDIT_DELTA_SECONDS: float = 0.15
|
||||
const LOCAL_RECONCILIATION_PRESENTATION_RECENTER_RATE: float = 5.0
|
||||
# The target Android handheld exposes its physical right trigger through
|
||||
# Godot's left-trigger axis. Keep the role named here so the platform mapping
|
||||
# remains isolated from camera behavior.
|
||||
|
|
@ -529,6 +534,13 @@ var _network_snapshot_age: float = 0.0
|
|||
var _network_snapshot_jitter: float = 0.0
|
||||
var _network_simulation_only: bool = false
|
||||
var _local_reconciliation_visual_offset: Vector3 = Vector3.ZERO
|
||||
var _local_reconciliation_camera_offset: Vector3 = Vector3.ZERO
|
||||
var _local_prediction_error_seconds: float = 0.0
|
||||
var _local_prediction_error_audits: int = 0
|
||||
var _local_prediction_error_direction: Vector3 = Vector3.ZERO
|
||||
var _local_prediction_soft_corrections: int = 0
|
||||
var _local_prediction_hard_corrections: int = 0
|
||||
var _local_prediction_largest_error: float = 0.0
|
||||
var _authoritative_movement_history: Array[Dictionary] = []
|
||||
var _local_network_jump_intent_pending: bool = false
|
||||
var _local_network_jump_intent_sequence: int = -1
|
||||
|
|
@ -1995,6 +2007,11 @@ func reset_network_movement_state() -> void:
|
|||
_network_input_stale_timeout_seconds = NETWORK_INPUT_STALE_TIMEOUT_SECONDS
|
||||
_network_jump_intent_active = false
|
||||
_last_network_input_sequence = 0
|
||||
_reset_local_prediction_error()
|
||||
_clear_local_reconciliation_offsets()
|
||||
_local_prediction_soft_corrections = 0
|
||||
_local_prediction_hard_corrections = 0
|
||||
_local_prediction_largest_error = 0.0
|
||||
|
||||
|
||||
func capture_network_input(sequence: int) -> Dictionary:
|
||||
|
|
@ -2235,8 +2252,10 @@ func push_network_snapshot(
|
|||
|
||||
func apply_local_prediction_correction(
|
||||
snapshot: Dictionary,
|
||||
pending_inputs: Array[Dictionary] = [],
|
||||
latest_input_sequence: int = 0,
|
||||
input_interval_seconds: float = 0.0,
|
||||
estimated_transit_seconds: float = -1.0,
|
||||
audit_delta_seconds: float = 0.0,
|
||||
) -> void:
|
||||
var parsed: Dictionary = _parse_network_snapshot(snapshot)
|
||||
if parsed.is_empty():
|
||||
|
|
@ -2258,92 +2277,143 @@ func apply_local_prediction_correction(
|
|||
_clear_local_network_jump_intent()
|
||||
if not _sitting_intent_pending:
|
||||
_set_sitting(bool(parsed["sitting"]))
|
||||
var authoritative_position: Vector3 = parsed["position"]
|
||||
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
|
||||
)
|
||||
var error_offset: Vector3 = authoritative_position - global_position
|
||||
var error_distance: float = error_offset.length()
|
||||
_local_prediction_largest_error = maxf(
|
||||
_local_prediction_largest_error,
|
||||
error_distance,
|
||||
)
|
||||
if error_distance <= LOCAL_PREDICTION_CORRECTION_THRESHOLD:
|
||||
_reset_local_prediction_error()
|
||||
return
|
||||
if error_distance >= LOCAL_PREDICTION_SNAP_DISTANCE:
|
||||
_clear_local_reconciliation_offsets()
|
||||
global_position = authoritative_position
|
||||
velocity = parsed["velocity"]
|
||||
_local_prediction_hard_corrections += 1
|
||||
_reset_local_prediction_error()
|
||||
return
|
||||
var error_direction: Vector3 = error_offset.normalized()
|
||||
if (
|
||||
not _local_prediction_error_direction.is_zero_approx()
|
||||
and _local_prediction_error_direction.dot(error_direction) < 0.5
|
||||
):
|
||||
_reset_local_prediction_error()
|
||||
_local_prediction_error_direction = error_direction
|
||||
_local_prediction_error_audits += 1
|
||||
_local_prediction_error_seconds += clampf(
|
||||
audit_delta_seconds,
|
||||
0.0,
|
||||
LOCAL_PREDICTION_MAX_AUDIT_DELTA_SECONDS,
|
||||
)
|
||||
if (
|
||||
_local_prediction_error_audits
|
||||
< LOCAL_PREDICTION_MIN_CORRECTION_AUDITS
|
||||
or _local_prediction_error_seconds
|
||||
< LOCAL_PREDICTION_CORRECTION_DELAY_SECONDS
|
||||
):
|
||||
return
|
||||
var correction_weight: float = 1.0 - exp(
|
||||
-LOCAL_PREDICTION_SOFT_CORRECTION_RATE
|
||||
* clampf(
|
||||
audit_delta_seconds,
|
||||
0.0,
|
||||
LOCAL_PREDICTION_MAX_AUDIT_DELTA_SECONDS,
|
||||
)
|
||||
)
|
||||
var correction: Vector3 = error_offset * correction_weight
|
||||
if correction.length() > LOCAL_PREDICTION_MAX_SOFT_CORRECTION_STEP:
|
||||
correction = (
|
||||
correction.normalized()
|
||||
* LOCAL_PREDICTION_MAX_SOFT_CORRECTION_STEP
|
||||
)
|
||||
_apply_camera_safe_local_correction(correction)
|
||||
_local_prediction_soft_corrections += 1
|
||||
|
||||
|
||||
func _apply_camera_safe_local_correction(correction: Vector3) -> void:
|
||||
if correction.is_zero_approx():
|
||||
return
|
||||
var previous_visual_position: Vector3 = _visuals.global_position
|
||||
var previous_camera_position: Vector3 = _camera_yaw.global_position
|
||||
var base_visual_local_position: Vector3 = (
|
||||
_visuals.position - _local_reconciliation_visual_offset
|
||||
)
|
||||
var previous_position: Vector3 = global_position
|
||||
global_position = parsed["position"]
|
||||
velocity = parsed["velocity"]
|
||||
if input_interval_seconds > 0.0:
|
||||
for input: Dictionary in pending_inputs:
|
||||
_replay_network_movement_input(input, input_interval_seconds)
|
||||
var correction_distance: float = previous_position.distance_to(
|
||||
global_position
|
||||
var base_camera_local_position: Vector3 = (
|
||||
_camera_yaw.position - _local_reconciliation_camera_offset
|
||||
)
|
||||
global_position += correction
|
||||
_visuals.global_position = previous_visual_position
|
||||
_camera_yaw.global_position = previous_camera_position
|
||||
_local_reconciliation_visual_offset = (
|
||||
_visuals.position - base_visual_local_position
|
||||
)
|
||||
_local_reconciliation_camera_offset = (
|
||||
_camera_yaw.position - base_camera_local_position
|
||||
)
|
||||
if correction_distance <= LOCAL_PREDICTION_SNAP_DISTANCE:
|
||||
_visuals.global_position = previous_visual_position
|
||||
_local_reconciliation_visual_offset = (
|
||||
_visuals.position - base_visual_local_position
|
||||
)
|
||||
else:
|
||||
_local_reconciliation_visual_offset = Vector3.ZERO
|
||||
|
||||
|
||||
func _replay_network_movement_input(
|
||||
data: Dictionary,
|
||||
delta: float,
|
||||
) -> void:
|
||||
var axis_value: Variant = data.get("axis", [])
|
||||
if typeof(axis_value) != TYPE_ARRAY or axis_value.size() != 2:
|
||||
return
|
||||
if bool(data.get("sitting", false)) or _water_recovery_active:
|
||||
velocity = Vector3.ZERO
|
||||
return
|
||||
var input_vector := Vector2(
|
||||
float(axis_value[0]),
|
||||
float(axis_value[1]),
|
||||
).limit_length(1.0)
|
||||
var camera_basis := Basis(
|
||||
Vector3.UP,
|
||||
float(data.get("camera_yaw", 0.0)),
|
||||
)
|
||||
var move_direction: Vector3 = (
|
||||
camera_basis.x * input_vector.x
|
||||
+ camera_basis.z * input_vector.y
|
||||
)
|
||||
move_direction.y = 0.0
|
||||
move_direction = move_direction.normalized()
|
||||
_network_sprint = bool(data.get("sprint", false))
|
||||
_network_sneak = bool(data.get("sneak", false))
|
||||
_network_slow_walk = bool(data.get("slow_walk", false))
|
||||
# Replay the speed authored by this exact pending input. Consulting the
|
||||
# current InputMap here would make an older walk replay as a sprint (or the
|
||||
# reverse) whenever the local button changed while a snapshot was in flight.
|
||||
var replay_speed: float = walk_speed
|
||||
if _network_sneak:
|
||||
replay_speed = sneak_speed
|
||||
elif _network_slow_walk:
|
||||
replay_speed = slow_walk_speed
|
||||
elif _network_sprint:
|
||||
replay_speed = sprint_speed
|
||||
if item_effects != null:
|
||||
replay_speed *= item_effects.get_movement_multiplier()
|
||||
var input_strength: float = minf(input_vector.length(), 1.0)
|
||||
velocity.x = move_direction.x * replay_speed * input_strength
|
||||
velocity.z = move_direction.z * replay_speed * input_strength
|
||||
if not is_on_floor():
|
||||
var gravity_multiplier: float = (
|
||||
upward_gravity_multiplier
|
||||
if velocity.y > 0.0
|
||||
else fall_gravity_multiplier
|
||||
)
|
||||
velocity.y -= _gravity * gravity_multiplier * delta
|
||||
elif bool(data.get("jump", false)):
|
||||
velocity.y = jump_velocity
|
||||
move_and_slide()
|
||||
func _reset_local_prediction_error() -> void:
|
||||
_local_prediction_error_seconds = 0.0
|
||||
_local_prediction_error_audits = 0
|
||||
_local_prediction_error_direction = Vector3.ZERO
|
||||
|
||||
|
||||
func _clear_local_reconciliation_offsets() -> void:
|
||||
if not _local_reconciliation_visual_offset.is_zero_approx():
|
||||
_visuals.position -= _local_reconciliation_visual_offset
|
||||
if not _local_reconciliation_camera_offset.is_zero_approx():
|
||||
_camera_yaw.position -= _local_reconciliation_camera_offset
|
||||
_local_reconciliation_visual_offset = Vector3.ZERO
|
||||
_local_reconciliation_camera_offset = Vector3.ZERO
|
||||
|
||||
|
||||
func _update_local_reconciliation_visuals(delta: float) -> void:
|
||||
if _local_reconciliation_visual_offset.is_zero_approx():
|
||||
if (
|
||||
_local_reconciliation_visual_offset.is_zero_approx()
|
||||
and _local_reconciliation_camera_offset.is_zero_approx()
|
||||
):
|
||||
_local_reconciliation_visual_offset = Vector3.ZERO
|
||||
_local_reconciliation_camera_offset = Vector3.ZERO
|
||||
return
|
||||
var retained_ratio: float = exp(-14.0 * delta)
|
||||
var retained_offset: Vector3 = (
|
||||
var retained_ratio: float = exp(
|
||||
-LOCAL_RECONCILIATION_PRESENTATION_RECENTER_RATE * delta
|
||||
)
|
||||
var retained_visual_offset: Vector3 = (
|
||||
_local_reconciliation_visual_offset * retained_ratio
|
||||
)
|
||||
_visuals.position += retained_offset - _local_reconciliation_visual_offset
|
||||
_local_reconciliation_visual_offset = retained_offset
|
||||
var retained_camera_offset: Vector3 = (
|
||||
_local_reconciliation_camera_offset * retained_ratio
|
||||
)
|
||||
_visuals.position += (
|
||||
retained_visual_offset - _local_reconciliation_visual_offset
|
||||
)
|
||||
_camera_yaw.position += (
|
||||
retained_camera_offset - _local_reconciliation_camera_offset
|
||||
)
|
||||
_local_reconciliation_visual_offset = retained_visual_offset
|
||||
_local_reconciliation_camera_offset = retained_camera_offset
|
||||
|
||||
|
||||
func get_local_prediction_metrics() -> Dictionary:
|
||||
return {
|
||||
"soft_corrections": _local_prediction_soft_corrections,
|
||||
"hard_corrections": _local_prediction_hard_corrections,
|
||||
"largest_error": _local_prediction_largest_error,
|
||||
"out_of_bounds_audits": _local_prediction_error_audits,
|
||||
"out_of_bounds_seconds": _local_prediction_error_seconds,
|
||||
}
|
||||
|
||||
|
||||
static func resolve_network_input_stale_timeout_seconds(
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue