Skip to content

Commit 864f2eb

Browse files
committed
fix: 优化高ping下的移动
1 parent 3f62d20 commit 864f2eb

11 files changed

Lines changed: 172 additions & 63 deletions

File tree

crates/client/src/app.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -305,8 +305,12 @@ pub struct Game {
305305
pub interp: PlayerInterp,
306306
/// Chunk 快照接收组装器(Remote 端用,Host/Local 闲置)。
307307
pub chunk_assembler: ChunkAssembler,
308+
/// 本地生成的 PlayerInput 序号。Remote 模式也独立递增,不能借用本地 dummy server tick。
309+
pub local_input_tick: u32,
308310
/// 本地位置预测的输入历史(60Hz 推入,PlayerTick reconcile 时修剪)。
309311
pub input_history: InputHistory,
312+
/// 中等位置误差的剩余软修正量。每个渲染帧应用一小段,避免高 RTT 下画面回弹。
313+
pub pending_position_correction: Vec3,
310314
/// Host 时钟与本地时钟的瞬态偏移(ms):server_time_ms - local_now_ms。
311315
/// PlayerTick 每帧覆盖;远端的 rendering target 用。
312316
pub server_clock_offset_ms: i64,
@@ -448,7 +452,9 @@ impl Game {
448452
remote_players: HashMap::new(),
449453
interp,
450454
chunk_assembler: ChunkAssembler::new(),
455+
local_input_tick: 0,
451456
input_history: InputHistory::new(120),
457+
pending_position_correction: Vec3::ZERO,
452458
server_clock_offset_ms: 0,
453459
storage: None,
454460
known_persisted: HashSet::new(),

crates/client/src/lib.rs

Lines changed: 27 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,9 @@ use crate::camera::CameraMode;
4242
use crate::chunk_loader::affected_chunks;
4343
use crate::input::InputState;
4444
use crate::mesh_jobs::{MeshPriority, MeshRunStats};
45-
use crate::prediction::{PendingAction, PendingKind, reconcile_self};
45+
use crate::prediction::{
46+
PendingAction, PendingKind, ReconcileResult, apply_pending_position_correction, reconcile_self,
47+
};
4648
use crate::raycast::raycast;
4749
use crate::storage::{OpfsStorage, WorldStorage};
4850
use crate::ui::lobby::{
@@ -1934,30 +1936,34 @@ fn render_game_frame(
19341936
game.physics.step(&getter, &game.camera, &neutral, dt);
19351937
}
19361938
}
1939+
apply_pending_position_correction(
1940+
&mut game.physics,
1941+
&mut game.pending_position_correction,
1942+
dt,
1943+
);
19371944
game.camera.position = game.physics.eye_position();
19381945

19391946
// 60Hz 逻辑帧
19401947
game.frame_clock.accumulate(dt);
19411948
let mut steps_consumed: u32 = 0;
19421949
let server_tick_allowed = matches!(game.mode, GameMode::Local | GameMode::Host);
1950+
let mut last_input_tick_to_send = None;
19431951
while game.frame_clock.consume_logic_step() {
19441952
if server_tick_allowed {
19451953
game.server.borrow_mut().tick();
19461954
}
1947-
// 每个逻辑步推一条 input history(Host reconcile 用;Remote 也可靠它追踪本地步数)
1955+
// 每个逻辑步分配本地输入序号。Remote 不能借 dummy server tick,
1956+
// 否则 Host 回播时无法把权威位置和同一条预测记录对齐。
1957+
game.local_input_tick = game.local_input_tick.wrapping_add(1).max(1);
19481958
game.input_history
1949-
.push(game.server.borrow().tick, game.physics.feet_position);
1959+
.push(game.local_input_tick, game.physics.feet_position);
1960+
last_input_tick_to_send = Some(game.local_input_tick);
19501961
steps_consumed += 1;
19511962
}
19521963

1953-
// 每个 logic step 上报一条 PlayerInput
1964+
// 若本帧消费了一个或多个逻辑步,只上报最新位置;tick 仍对应最后一个本地输入序号。
19541965
if steps_consumed > 0 && game.entity_id != 0 {
1955-
let tick = if server_tick_allowed {
1956-
game.server.borrow().tick
1957-
} else {
1958-
// Remote:用自己的 input history 计数(从 physics 最后一次 reconcile 后的步数推导)
1959-
0 // Phase 5 简化:Remote 的 PlayerInput.tick 用 0;Host Server 不依赖 Remote 的 tick 做排序
1960-
};
1966+
let tick = last_input_tick_to_send.unwrap_or(game.local_input_tick);
19611967
game.net.send_client_message(ClientMessage::PlayerInput {
19621968
tick,
19631969
position: game.physics.feet_position,
@@ -2678,12 +2684,21 @@ fn apply_server_message(game: &mut Game, msg: ServerMessage) {
26782684
for snap in &players {
26792685
if snap.entity_id == game.entity_id {
26802686
// 自己的权威位置 → reconcile
2681-
let _r = reconcile_self(
2687+
let result = reconcile_self(
26822688
snap.position,
2683-
server_tick,
2689+
snap.last_input_tick,
26842690
&mut game.physics,
26852691
&mut game.input_history,
26862692
);
2693+
match result {
2694+
ReconcileResult::SoftCorrection(delta) => {
2695+
game.pending_position_correction += delta;
2696+
}
2697+
ReconcileResult::HardCorrection(_) => {
2698+
game.pending_position_correction = glam::Vec3::ZERO;
2699+
}
2700+
ReconcileResult::Ok | ReconcileResult::MissingHistory => {}
2701+
}
26872702
} else {
26882703
// 远端玩家 → 喂入插值缓冲
26892704
game.interp.ingest_tick(

crates/client/src/prediction.rs

Lines changed: 99 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -170,13 +170,29 @@ impl InputHistory {
170170
}
171171
}
172172

173-
/// 丢弃所有 tick ≤ server_tick 的遗留记录(服务端已追上)。
174-
pub fn drop_until(&mut self, server_tick: u32) {
175-
while self.records.front().is_some_and(|r| r.tick <= server_tick) {
173+
/// 丢弃所有 tick ≤ input_tick 的遗留记录(服务端已确认这些输入)。
174+
pub fn drop_until(&mut self, input_tick: u32) {
175+
while self.records.front().is_some_and(|r| r.tick <= input_tick) {
176176
self.records.pop_front();
177177
}
178178
}
179179

180+
/// 查找某个本地输入序号对应的预测位置。
181+
pub fn position_at(&self, input_tick: u32) -> Option<Vec3> {
182+
self.records
183+
.iter()
184+
.find(|r| r.tick == input_tick)
185+
.map(|r| r.position)
186+
}
187+
188+
/// 服务端确认某个输入后,若发现该输入时刻存在差值,后续未确认记录也要平移同样的差值。
189+
/// 否则下一次回执会继续拿“旧基准线”比较,造成重复校正。
190+
pub fn translate_remaining(&mut self, delta: Vec3) {
191+
for record in &mut self.records {
192+
record.position += delta;
193+
}
194+
}
195+
180196
pub fn is_empty(&self) -> bool {
181197
self.records.is_empty()
182198
}
@@ -189,38 +205,70 @@ pub const SOFT_THRESHOLD_M: f32 = 0.1;
189205
pub const HARD_THRESHOLD_M: f32 = 2.0;
190206

191207
/// reconcile_self 的结果——方便调用方区分状态并在 HUD 上显示不同颜色。
192-
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
208+
#[derive(Copy, Clone, Debug, PartialEq)]
193209
pub enum ReconcileResult {
194210
/// 误差可接受,不做修正。
195211
Ok,
196-
/// 误差超过 HARD_THRESHOLD,已把 physics 瞬移回服务端位置。
197-
Snap,
212+
/// 本地已没有对应输入 tick 的历史,通常是旧包或历史过短;忽略以避免高延迟旧回声拉扯。
213+
MissingHistory,
214+
/// 误差中等,调用方应把该差值加入 pending correction,按帧软修正。
215+
SoftCorrection(Vec3),
216+
/// 误差过大,已把当前 physics 位置按同 tick 差值立即平移。
217+
HardCorrection(Vec3),
198218
}
199219

200-
/// 对比服务端权威位置与本地 physics,决定是接受还是 Snap
220+
/// 对比服务端权威位置与同一输入 tick 的本地预测记录,决定是否校正
201221
///
202222
/// * `server_position` — PlayerTick 中与自己 entity_id 对应的权威位置(脚底)
203-
/// * `server_tick` — 该 PlayerTick 的 server tick(用于清历史)
223+
/// * `acked_input_tick` — 服务端已处理到的本地 PlayerInput.tick
204224
/// * `physics` — 本地物理状态(feet_position 在此被修正)
205-
/// * `history` — 输入记录历史(用于清掉 server 已处理过的步数
225+
/// * `history` — 输入记录历史(用于查找同 tick 位置并清掉已确认输入
206226
pub fn reconcile_self(
207227
server_position: Vec3,
208-
server_tick: u32,
228+
acked_input_tick: u32,
209229
physics: &mut LocalPhysics,
210230
history: &mut InputHistory,
211231
) -> ReconcileResult {
212-
history.drop_until(server_tick);
232+
let Some(predicted_position) = history.position_at(acked_input_tick) else {
233+
history.drop_until(acked_input_tick);
234+
return ReconcileResult::MissingHistory;
235+
};
236+
237+
let correction = server_position - predicted_position;
238+
let error = correction.length();
213239

214-
let error = (physics.feet_position - server_position).length();
240+
history.drop_until(acked_input_tick);
215241

216-
if error >= HARD_THRESHOLD_M {
217-
physics.feet_position = server_position;
218-
ReconcileResult::Snap
219-
} else if error < SOFT_THRESHOLD_M {
242+
if error < SOFT_THRESHOLD_M {
220243
ReconcileResult::Ok
244+
} else if error >= HARD_THRESHOLD_M {
245+
physics.feet_position += correction;
246+
history.translate_remaining(correction);
247+
ReconcileResult::HardCorrection(correction)
221248
} else {
222-
// 中等误差:Phase 5 不做软插值(Phase 7 加 blend)
223-
ReconcileResult::Ok
249+
history.translate_remaining(correction);
250+
ReconcileResult::SoftCorrection(correction)
251+
}
252+
}
253+
254+
/// 每渲染帧应用一部分待修正位移,避免中等误差造成肉眼可见瞬移。
255+
pub fn apply_pending_position_correction(
256+
physics: &mut LocalPhysics,
257+
pending_correction: &mut Vec3,
258+
dt: f32,
259+
) {
260+
if pending_correction.length_squared() < 0.000001 {
261+
*pending_correction = Vec3::ZERO;
262+
return;
263+
}
264+
265+
let blend = (dt * 5.0).clamp(0.0, 1.0);
266+
let step = *pending_correction * blend;
267+
physics.feet_position += step;
268+
*pending_correction -= step;
269+
270+
if pending_correction.length_squared() < 0.000001 {
271+
*pending_correction = Vec3::ZERO;
224272
}
225273
}
226274

@@ -254,21 +302,47 @@ mod prediction_tests {
254302
fn reconcile_returns_ok_for_small_error() {
255303
let mut physics = LocalPhysics::new(Vec3::new(10.0, 64.0, 10.0));
256304
let mut history = InputHistory::new(10);
257-
history.push(1, Vec3::new(10.0, 64.0, 10.0));
305+
history.push(2, Vec3::new(10.0, 64.0, 10.0));
258306
// 误差 0.05 < SOFT
259307
let r = reconcile_self(Vec3::new(10.03, 64.0, 10.04), 2, &mut physics, &mut history);
260308
assert_eq!(r, ReconcileResult::Ok);
261309
assert!((physics.feet_position - Vec3::new(10.0, 64.0, 10.0)).length() < 0.001);
262310
}
263311

264312
#[test]
265-
fn reconcile_snaps_for_large_error() {
266-
let mut physics = LocalPhysics::new(Vec3::new(10.0, 64.0, 10.0));
313+
fn reconcile_shifts_current_by_same_tick_error_for_large_error() {
314+
let mut physics = LocalPhysics::new(Vec3::new(30.0, 64.0, 10.0));
267315
let mut history = InputHistory::new(10);
268-
history.push(1, Vec3::new(10.0, 64.0, 10.0));
269-
// 误差 10m > HARD → Snap
316+
history.push(2, Vec3::new(10.0, 64.0, 10.0));
317+
history.push(3, Vec3::new(12.0, 64.0, 10.0));
318+
// 误差 10m > HARD → 当前预测位置按差值平移,而不是退回旧快照位置。
270319
let r = reconcile_self(Vec3::new(20.0, 64.0, 10.0), 2, &mut physics, &mut history);
271-
assert_eq!(r, ReconcileResult::Snap);
272-
assert!((physics.feet_position - Vec3::new(20.0, 64.0, 10.0)).length() < 0.001);
320+
assert_eq!(
321+
r,
322+
ReconcileResult::HardCorrection(Vec3::new(10.0, 0.0, 0.0))
323+
);
324+
assert!((physics.feet_position - Vec3::new(40.0, 64.0, 10.0)).length() < 0.001);
325+
assert_eq!(history.position_at(3), Some(Vec3::new(22.0, 64.0, 10.0)));
326+
}
327+
328+
#[test]
329+
fn reconcile_returns_soft_correction_for_mid_error() {
330+
let mut physics = LocalPhysics::new(Vec3::new(10.5, 64.0, 10.0));
331+
let mut history = InputHistory::new(10);
332+
history.push(7, Vec3::new(10.0, 64.0, 10.0));
333+
334+
let r = reconcile_self(Vec3::new(10.5, 64.0, 10.0), 7, &mut physics, &mut history);
335+
assert_eq!(r, ReconcileResult::SoftCorrection(Vec3::new(0.5, 0.0, 0.0)));
336+
assert!((physics.feet_position - Vec3::new(10.5, 64.0, 10.0)).length() < 0.001);
337+
}
338+
339+
#[test]
340+
fn reconcile_missing_history_does_not_snap_to_stale_echo() {
341+
let mut physics = LocalPhysics::new(Vec3::new(30.0, 64.0, 10.0));
342+
let mut history = InputHistory::new(10);
343+
344+
let r = reconcile_self(Vec3::new(10.0, 64.0, 10.0), 99, &mut physics, &mut history);
345+
assert_eq!(r, ReconcileResult::MissingHistory);
346+
assert!((physics.feet_position - Vec3::new(30.0, 64.0, 10.0)).length() < 0.001);
273347
}
274348
}

crates/core/src/protocol.rs

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,9 @@ use crate::chunk::{ChunkPos, Position};
1212
/// 协议版本号。Hello.version 与之不一致时 Host 拒绝接入。
1313
/// 任何破坏性消息字段变更必须递增此版本。
1414
///
15-
/// v2(Phase 6):Welcome 携带完整 roster + host_entity_id
16-
/// 让新加入的 Remote 一次性建好玩家表(包括早于自己加入的 Host)
17-
pub const PROTOCOL_VERSION: u32 = 2;
15+
/// v3(Phase 8):PlayerSnapshot 携带 last_input_tick
16+
/// 让客户端按同一条本地输入记录做预测协调,避免高延迟时被旧位置回声拉回
17+
pub const PROTOCOL_VERSION: u32 = 3;
1818

1919
/// ChunkSnapshot 单片 payload 上限(字节)。
2020
/// 浏览器 SCTP 用户消息上限约 16 KB;保守留 14 KB,剩余给 frag_index/frag_total/bincode header。
@@ -156,6 +156,9 @@ pub enum RoomEvent {
156156
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
157157
pub struct PlayerSnapshot {
158158
pub entity_id: u32,
159+
/// 服务端已接受到该玩家的最新 PlayerInput.tick。
160+
/// 客户端收到自己的快照时用它查本地 InputHistory,而不是用 server tick 对齐。
161+
pub last_input_tick: u32,
159162
pub position: Vec3,
160163
pub yaw: f32,
161164
pub pitch: f32,
@@ -284,9 +287,9 @@ mod tests {
284287
}
285288

286289
#[test]
287-
fn protocol_version_is_two() {
288-
// Phase 6 起为 2;任何破坏性变更应同步更新此测试与版本号。
289-
assert_eq!(PROTOCOL_VERSION, 2);
290+
fn protocol_version_is_three() {
291+
// Phase 8 起为 3;任何破坏性变更应同步更新此测试与版本号。
292+
assert_eq!(PROTOCOL_VERSION, 3);
290293
}
291294

292295
#[test]
@@ -295,6 +298,7 @@ mod tests {
295298
tick: 60,
296299
players: vec![PlayerSnapshot {
297300
entity_id: 1,
301+
last_input_tick: 9,
298302
position: Vec3::new(1.0, 64.0, 2.0),
299303
yaw: 0.5,
300304
pitch: -0.2,

crates/net/src/transport.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,7 @@ mod tests {
118118
tick: 0,
119119
players: vec![PlayerSnapshot {
120120
entity_id: 1,
121+
last_input_tick: 0,
121122
position: Vec3::ZERO,
122123
yaw: 0.0,
123124
pitch: 0.0,

crates/server/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ impl PlayerEntity {
6666
fn to_snapshot(&self, entity_id: EntityId) -> PlayerSnapshot {
6767
PlayerSnapshot {
6868
entity_id,
69+
last_input_tick: self.last_input_tick,
6970
position: self.position,
7071
yaw: self.yaw,
7172
pitch: self.pitch,

docs/modules/client.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -200,7 +200,9 @@ pub struct Game {
200200
pub interp: PlayerInterp, // [Phase 5 ✅]
201201
pub chunk_assembler: ChunkAssembler, // [Phase 5 ✅]
202202
pub remote_players: HashMap<EntityId, RemotePlayerState>, // [Phase 5 ✅]
203-
pub input_history: InputHistory, // [Phase 5 ✅]
203+
pub local_input_tick: u32, // [Phase 8] 本地输入序号;Remote 也单调递增
204+
pub input_history: InputHistory, // [Phase 5 ✅] 以 local_input_tick 记录预测位置
205+
pub pending_position_correction: Vec3, // [Phase 8] 中等误差按帧软修正,避免高延迟回弹
204206
pub server_clock_offset_ms: i64, // [Phase 5 ✅]
205207
pub chat: ChatHistory, // [Phase 6 ✅] 聊天历史(含本地合成的系统消息)
206208
}

docs/modules/core.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -222,6 +222,9 @@ pub enum RoomEvent {
222222
#[derive(Clone, Serialize, Deserialize)]
223223
pub struct PlayerSnapshot {
224224
pub entity_id: u32,
225+
/// 服务端已接受到该玩家的最新 PlayerInput.tick。
226+
/// 本玩家收到自己的快照时用它与本地预测历史对齐,避免高延迟下用旧回声拉扯当前位置。
227+
pub last_input_tick: u32,
225228
pub position: glam::Vec3,
226229
pub yaw: f32,
227230
pub pitch: f32,

docs/modules/server.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@ pub struct PlayerEntity {
9494
pub position: glam::Vec3,
9595
pub yaw: f32,
9696
pub pitch: f32,
97-
pub last_input_tick: u32, // 用于丢弃过期输入
97+
pub last_input_tick: u32, // 用于丢弃过期输入,并随 PlayerSnapshot 回执给客户端协调
9898
pub joined_at_tick: u32,
9999
}
100100
```
@@ -212,6 +212,8 @@ fn broadcast_tick(&mut self) {
212212
.filter(|(_, p)| force_full || p.has_changed_since_last_broadcast())
213213
.map(|(eid, p)| {
214214
p.record_broadcast();
215+
// PlayerSnapshot 携带 last_input_tick,让客户端用同一输入时刻做协调;
216+
// 高 RTT 下不能用 server tick 去对齐客户端预测历史。
215217
p.to_snapshot(*eid)
216218
})
217219
.collect();

0 commit comments

Comments
 (0)