Chancy game dev notes · 全栈工程师

服务器说了算,然后手感就没了

权威服务器只是基线。预测、插值、延迟补偿这三层补丁是怎么叠上去的,以及它们各自的账单。

· 5 分钟读完

#netcode #realtime

我第一次写联机逻辑时的认知很朴素:服务器是权威,客户端只是显示器。客户端把输入发上去,服务器算完再把结果广播回来,谁也别想作弊。

这个模型是对的,也是不能用的。

100ms 不是延迟,是"没有手感"

Gabriel Gambetta 那篇常被引用的客户端-服务器架构里有个很朴素的算术:单程 50ms 是当时一个还不错的往返水平,也就是说你按下按键之后,100ms 才在自己屏幕上看到反馈。

100ms 有多长?大概是人能明确感知到"这不是我按的,是机器替我按的"的门槛。本地单机游戏的反馈是 16ms 一帧,玩家的滑动、连点、方向微调全都发生在那个尺度上;一个"服务器权威"的射击游戏把反馈整体往后推了 6 帧,还要再加上抖动。所以纯权威模型下,手感不是"稍差",而是从根上不存在。

于是有了三层补丁。它们不是可选优化,是同一个延迟问题的三个正交切面:自己怎么看自己、别人怎么看、命中怎么算。

第一层:客户端预测 + 服务器校正

所谓预测,做法其实很机械:本地立刻执行输入,同时把这条输入压进一个待确认队列,每条带一个序号。

seq 41  JUMP          ← 已发服务器
seq 42  MOVE (0.2, 0)
seq 43  FIRE          ← 本地已执行,服务器尚未确认

服务器回来 state@seq42,客户端就丢掉队列里 42 及以前的输入,保留 43,从服务器状态开始把剩余输入重放一遍。逻辑上等价于"服务器是真相,我只是提前算了一遍它的作业"。

真正的坑在校正那一步。重放之后的位置和服务器给你的位置几乎永远差一点,差一点就要修。硬拉会让镜头抽一下,玩家立刻看出来;Gambetta 给的样例是用一个约 100ms 的渐变动画去吸收偏差(文中场景是 250ms 延迟)。而所谓橡皮筋(rubber-banding)体验,本质就是重放误差被积累到一次吸收不掉了 —— 通常因为队列拉太长、或者你的输入里混进了时间相关的东西(dt 不固定,重放就必然不准)。

这一层有个隐含前提,很多人踩过才知道:只有本地能确定性重放的逻辑才能预测。服务器如果用了随机数、用了别的浮点顺序、或者有个全局状态参与,你的重放结果就和它不同 —— 那就不是校正,是 desync。

第二层:插值,意味着"别人永远是过去的"

服务器不必为每个玩家每帧发状态。Gambetta 的示例里服务器10Hz 广播快照,也就是每 100ms 一发。客户端要做得好看,就得故意渲染在过去:攒够两个快照,在它们之间做 lerp,画面时间比当前时间落后大约 1~2 个快照周期。

这层的结论我抄下来贴在显示器边上:

你看自己是"现在",你看别人是"过去"。

于是"我明明打到他了"这件事,在物理上是必然发生的,不是 bug。你不做点什么,服务器就会拿"现在的他人位置"去判"我此刻这发子弹"的命中,而这个人在我屏幕上明明是两帧之前的位置。

第三层:延迟补偿,服务器同时维护多份真相

处理方式是把判定拉回客户端的时间。Mitchell Robinson 那篇多人延迟补偿给了一组很实用的参数:服务器每隔 50ms 存一份相关实体的历史状态,超过 400ms 的丢弃 —— 这个上限就是 rewind 窗口。收到射击请求时,按客户端报告的射击时刻把世界"回溯"到那一帧,再在那份历史轨迹里推进投射物判定命中。

它有效,而且代价是明码标价的公平性问题:高延迟玩家可以打中你,而低延迟玩家自己的子弹反而显得迟到。窗口开大一点,前者更宽容、后者更窝火;关小一点则相反。我的经验是把它当成一个纯产品参数而不是技术参数来定 —— 竞技向就收紧窗口,合作向就放松,别指望工程把设计上的取舍抹平。

顺带一句:Valve 那两篇 Source Multiplayer Networking 与 Latency Compensating Methods 是回溯判定的原始出处,值得完整读一遍 —— 站点反爬严,我这次没能抓到正文,所以这里的参数全部来自上面那篇能核对的来源。

三层叠起来,账单长什么样

层换来什么付出什么
客户端预测自己的输入立刻有反馈未确认输入队列 + 重放,逻辑必须可确定性重放
实体插值别人动起来不闪烁别人被固定推迟 100~200ms 显示
延迟补偿"我打到他了"变成合法请求服务器要存历史状态,且公平性向高延迟侧倾斜

我现在的判断是:先接受这三层是必需的,再考虑能不能不用它们。有些品类确实能躲 —— 回合制、慢节奏、格斗游戏用回滚、RTS 用帧同步 —— 但那都是换了一套代价,不是没有代价。下一篇写帧同步那几个坑。

参考来源