客户端直接显示服务器位置会导致“瞬移”和“拉扯”,因网络延迟使serverPosition总是过时;正确做法是用航位推测(Dead Reckoning)预测当前位置,并以服务器数据渐进纠正。

为什么客户端直接显示服务器位置会导致“瞬移”和“拉扯”
因为网络延迟和帧率不一致,客户端收到的服务器位置总是旧的。如果直接把 serverPosition 赋给渲染对象,角色就会在每帧更新时突然跳到一个新位置——这就是俗称的“拉扯”。真正的平滑,不是靠插值“美化”,而是用航位推测(Dead Reckoning)主动预测当前位置,再用服务器数据做渐进式纠正。
Dead Reckoning 的核心三要素必须同步
只传位置是不够的。客户端要预测,至少需要三个量:上一帧的 position、velocity 和 acceleration(或更简化的 direction + speed)。服务器每次发包必须包含这组状态,且时间戳(timestamp)要精确到毫秒级。常见错误是只发位置+ID,结果客户端只能做线性外推,遇到急停、转向就严重偏移。
- 推荐结构体:
struct EntityState { Vec2 pos; Vec2 vel; uint64_t timestamp_ms; }; - 服务器发送频率建议 20–30 Hz,低于 15 Hz 会导致预测失准;高于 60 Hz 则带宽浪费明显
- 客户端必须用本地高精度时钟(如
std::chrono::steady_clock)计算dt = now - packet.timestamp_ms,不能依赖帧时间
插值 + 外推混合策略:分阶段处理网络延迟
单纯外推会漂移,单纯插值会滞后。实际工程中采用“双缓存+混合”:维护两个服务端状态(prevServerState 和 currServerState),在两者之间插值;同时用 currServerState 的速度继续外推未来几帧,直到下一个包到达。
- 插值阶段(已收包区间):
interpolated = lerp(prev.pos, curr.pos, alpha),其中alpha = (now - prev.ts) / (curr.ts - prev.ts) - 外推阶段(等待新包):
predicted = curr.pos + curr.vel * (now - curr.ts),上限建议设为 100 ms,超时则回滚或冻结 - 纠正触发点:当新包到达,若
predicted与新serverPosition距离 > 0.5 单位(按游戏尺度调),说明漂移过大,应立即 snap 并重置预测起点
容易被忽略的底层细节
很多实现卡在“看起来动了但手感不对”,问题往往不在算法,而在时序和数值稳定性。比如浮点累积误差会让 vel 慢慢偏离真实值;又比如没对齐物理更新步长,导致预测和本地物理模拟不同步。
立即学习“C++免费学习笔记(深入)”;
- 所有运动状态(pos/vel)必须用 double 或 fixed-point 存储,避免 float 在长距离移动中的精度坍塌
- 预测计算必须和物理更新使用同一时间基准(例如都基于固定步长 16.67 ms 的
physicsTick),不能混用 real-time 和 frame-time - 服务器做状态压缩时,不要四舍五入坐标到厘米级——小数点后两位可能刚好切掉关键抖动,建议保留 3–4 位有效数字并用 delta 编码



















