格斗游戏为什么愿意为一帧付重算的钱
回滚不是网络技巧,是架构约束:它要求逻辑能从快照恢复。同一套技术在格斗是圣杯,在 VR 是一帧跑 25 次物理的黑洞。
格斗游戏玩家对"一帧"有一种外人看不懂的执念。60Hz 下一帧 16.6ms,一个确反(punish)窗口常常只有几帧 —— 也就是说,网络方案里任何"多等几帧"都是直接把游戏性等没了。
Infil 那篇 Fightin' Words: Netcode 把两条路讲得很干净:
- delay-based:干脆集体多等几帧,等到所有人的输入都确定齐了再往下走。实现简单,代价是每一帧都固定变慢,网络好的人也一起慢。
- rollback:先猜。用"上一帧这个玩家的输入"推测他这一帧的输入,直接往前跑;等真实输入到了,如果猜错,就把世界回滚到那一帧、用正确输入重跑。代价是猜错时的重算(resimulation)开销。
一个是把成本平摊给所有人、每帧都付一点;一个是把成本集中在"猜错的那些帧"、赌它不常发生。格斗游戏选了后者,因为它对最低延迟的执念高于对 CPU 的执念。
回滚真正的门槛不是网络
工业界的参照实现是 GGPO。它的术语值得抄下来,因为这三个词是绑在一起的:input prediction(输入预测)+ speculative execution(推测执行)+ rollback(回滚)。
而它对你代码的要求,是我认为这篇文章最该被工程同行读到的一点:回滚要求游戏逻辑可以从一份快照恢复到任意历史帧,然后确定性地重跑。
这条约束会一路传导到你架构的最底层:
- 模拟必须是固定步长的。变长
dt会让"重跑第 7 帧"这件事没有定义。 - 状态必须集中在可序列化的容器里,不能有"藏在一个对象构造函数里、或者依赖外部系统当前时间"的隐状态。
- 不能有副作用。随机数必须是显式种子、按帧记录的;不能在读状态的时候打日志、不能依赖容器遍历顺序、不能在逻辑里调系统时间。
- 逻辑层和表现层必须切开。特效、音效、镜头是"发生了就发生了"的东西,它们不参与回滚,只能被重放事件驱动。
说白了:rollback 不是一个网络库能替你解决的问题,它是一份架构税,在你写第一行业务代码之前就已经决定了代码长什么样。
顺带说,我对"表现层不参与回滚"这一点在意的原因是它在手感上真会露馅:回滚时画面时间必须跟着退,否则音效会重播 —— Kyle Pittman 在 Implementing Roll-back Network Synchronization 里给的做法就是把音效/特效挂到一个会回退的时间系统上,避免重复播放。网络架构和"音游判定准不准"最后是同一件事。
同一套技术,在 VR 里是性能黑洞
然后是我最喜欢的一组反例。Glenn Fiedler 的 Networked Physics in Virtual Reality(2018)里有个算术:VR 要 90Hz 物理 + 延迟补偿,为了覆盖一帧的往返,你得在一帧之内跑大约 25 个物理步。
回滚和帧同步在 60Hz 格斗游戏里是圣杯,因为重算几帧的代价还容得下;在 90Hz 且每帧预算被头显和双眼渲染吃光的地方,重算 25 帧直接出局。所以那篇文章的结论是换一套完全不同的模型:分布式模拟 + 用递增 sequence 序号来裁决控制权冲突 —— 不预测、不回滚,让每个客户端模拟自己那部分,冲突时按序号定谁说了算。
我的 takeaway 不是"VR 该怎么做",而是这个:
任何网络方案的可行性都由"帧预算 ÷ 单步模拟成本"决定,而不是由它听起来多先进决定。
选型的时候先算这一除法,再去读别人的架构。格斗游戏能算过来是因为它的模拟状态极小(两个角色),VR 算不过来是因为它必须跟上渲染帧率 —— 一个是状态量,一个是预算量,两个都得看。
三条路怎么挑
| 方案 | 延迟表现 | 主要代价 | 隐性前提 |
|---|---|---|---|
| delay-based | 每帧固定慢 N 帧 | 所有玩家一起慢,包括局域网 | 无 |
| rollback | 网络好时几乎无感 | 猜错帧的重算 CPU | 逻辑必须可从快照确定性重跑 |
| 分布式模拟 | 取决于冲突裁决 | 一致性弱,控制权要设计 | 状态要能按所有权切分 |
如果你正在一个已经有大量隐状态、到处 new 出对象、逻辑里混着 Unity/引擎 API 的项目里考虑上回滚 —— 先停一下,你真正的工作量不在网络层,在那次重构上。而那次重构值不值,取决于上面那张表第三列。
参考来源
- Fightin' Words: Netcode — Infil
- GGPO — pond3r(回滚网络代码的工业参照实现)
- Networked Physics in Virtual Reality — Glenn Fiedler, Gaffer on Games(2018)
- Implementing Roll-back Network Synchronization — Kyle Pittman