了解Edgegap的工作原理

了解Edgegap的工作原理

网络异步(Desync)详解:原因、拉回(Rubberbanding)及服务端解决方案

已发布

已发布

已发布

主图标题为“游戏服务器同步失步”,副标题为“原因、橡皮筋现象及服务器端修复”。属于 Edgegap “洞察系列”的一部分

核心洞察

核心洞察

核心洞察

  • 不同步是分歧,而非延迟:滞后(Lag)意味着信息到达较晚。不同步(Desync)意味着两台机器保存着相同世界的不同版本,这是一个具有独立解决方案的独立故障。

  • 存在两种截然不同的类型:复制不同步(Replication desync),即权威服务器纠正预测错误的客户端;以及确定性不同步(Determinism desync),即对等端模拟产生分歧,导致会话直接停止。

  • “橡皮筋”效应有两个控制杠杆:客户端预测错误的频率(由延迟决定),以及每次纠正的明显程度(由网络代码决定)。调整其中任何一个,玩家看到的“橡皮筋”现象都会减少。

  • tick 率并不是这两个杠杆之一:提高 tick 率并不会使预测更准确,而且在已经丢包的连接上,它反而会使画面异常情况更加严重。

  • “发生不同步”是校验和失败:帧同步(Lockstep)游戏在每回合都会比较状态哈希值,因此一个分歧的浮点数或一个未同步的随机调用都会导致会话终止。

在 GDC 2001 上发表的两篇论文至今仍然解释了二十五年后多人游戏中出现的大多数问题。时任 Valve 开发人员的 Yahn W. Bernier 发表了《客户端/服务器游戏内协议设计与优化中的延迟补偿方法》,描述了《半条命》如何通过预测和服务器端滞后补偿来隐藏网络延迟。当时在 Ensemble Studios 工作室的 Paul Bettner 和 Mark Terrano 发表了《28.8 调制解调器上的 1500 名弓箭手:《帝国时代》及以后的网络编程》,描述了一款实时战略游戏如何通过拨号调制解调器保持数千个单位的同步,以及在没有同步时会发生什么。

在这两篇论文中,他们描述了两种完全不同的故障模式。玩家和开发人员现在用同一个词来称呼这两种模式:去同步(desync)。

弄清楚你遇到的是哪一种是解决问题的大部分工作,因为两者的修复方法并没有重叠。

多人游戏中的去同步(Desync)是什么?

Desync 是去同步(desynchronization)的缩写,是指同一会话中的两台机器对游戏世界状态的理解不一致。你的客户端认为你正站在墙后。而服务器认为你仍站在门口。两者在内部都是自洽的。但只有其中一个是具有权威性的。

任何玩过网游的人对这些症状都不陌生。子弹穿过了明明处于准星中央的玩家。在到达掩体整整一秒后才受到伤害。角色无缘无故地向后滑动了三米。一扇门打开、关闭,然后再次打开。在回合制和模拟游戏中,症状更为直接:游戏停止运行,并告诉你发生了去同步。

这些看起来像是不同的 Bug。但在底层,它们是同一种类型的问题,只是呈现在两种非常不同的网络架构中。

去同步与延迟:有什么区别?

延迟(Lag)是一个时间问题。信息到达得晚,但大家最终会对发生的事情达成一致。一个 200 毫秒 Ping 值但从未发生去同步的游戏,虽然反应迟钝,但完全具有一致性。

去同步(Desync)是一个正确性问题。机器得出了不同的答案,如果不进行干预,它们将继续发散。

延迟是慢。去同步是错。

两者是有联系的,因为延迟是让发散更容易发生的条件之一,但它们不能混为一谈。你可能会因为浮点数 Bug 在 20 毫秒 Ping 值下发生去同步,你也可能在 150 毫秒 Ping 值下玩游戏而完全没有去同步。


Branch diagram of the two kinds of desync. Replication desync shows as characters snapping back and shots passing through, happens in authoritative-server games with client-side prediction, is caused by the client predicting wrong, and is fixed in netcode with prediction, rollback, resimulation and smoothing. Determinism desync stops the session with the error “a desync has occurred”, happens in lockstep games, is caused by a checksum mismatch from float divergence or unsynchronized randomness, and is fixed with deterministic math, synchronized RNG and version gates.

同步/复制去同步:当服务器否决客户端时

大多数动作游戏、射击游戏和战术竞技游戏都使用权威服务器。服务器拥有真实的游戏状态,客户端发送输入,服务器回传真相。

在你的角色移动之前等待一个往返时间感觉会非常糟糕。这种方法正是 SnapNet 的文档所称的输入延迟,在这种模式下,客户端“将他们的输入发送到服务器,然后当他们收到来自服务器的模拟结果时,向玩家展示这些结果。”它很简单,而且永远不会预测错误。但它也将你游戏的响应能力直接与每个玩家的 Ping 值绑定在一起。

因此,大多数快节奏游戏都会采用预测。正如 Gabriel Gambetta 在他的《快节奏多人游戏(Fast-Paced Multiplayer)系列》中所描述的,“我们可以将输入发送到服务器,并立即在客户端处理它们,也就是说,我们预测服务器处理输入后的游戏状态会是什么。”

当预测与服务器随后确认的情况相吻合时,玩家什么异样都感觉不到。预测在起作用时是无形的。

当预测不匹配时,服务器的版本会胜出,客户端必须被拉回到一致的状态。这就是同步/复制去同步(replication desync),玩家会将其视为一次位置修正(纠偏)。

Bernier 的论文涵盖了同一取舍的另一半。由于每个客户端呈现的都是世界稍有延迟的视图,服务器会倒回自己的历史记录,根据射击者实际看到的情况来评估射击。正是这种技术让扣动扳机的人觉得命中判定是公平的。这也是为什么被射击的人有时会在他们认为自己已经到达掩体后才倒地。这里的意见分歧并不是故障。这是关于尊重谁的视图的刻意选择。

为什么会出现“橡皮筋”现象(回弹)?

橡皮筋现象(Rubberbanding)是预测错误被纠正时的直观表现。Gambetta 对这种原始行为的描述非常精确:角色“向右移动了两个方格,在那里停留了 50 毫秒,向左跳了一个方格,在那里停留了 100 毫秒,然后向右跳了一个方格。”

SnapNet 的核心概念文档直白地指出了这种折中:“当客户端对模拟结果预测错误时,可能会出现视觉瑕疵和闪烁,”它们通常“表现为物体瞬移或凭空出现/消失。”

这意味着回弹永远不是根本问题。它是两个输入相乘的结果:客户端猜错的频率,以及每次猜错在屏幕上呈现的糟糕程度。延迟、抖动和丢包推动了前者。网络代码(Netcode)推动了后者。两者都是实实在在的杠杆,而一个只了解其中之一的团队将会在错误的工作上浪费数月时间。

第一根杠杆:降低延迟

预测误差随往返时间而变化。客户端与服务器之间的每一毫秒,都是客户端在没有确认、完全凭自己的猜测独自前进的又一帧。

在 60 Hz 的模拟速率下,距离其服务器 30 毫秒的客户端运行时间大约比真实情况提前两帧。在 180 毫秒时,它运行时间提前大约十一帧。同样的代码,同样的引擎,同样的玩家。暴露程度高出五倍。

这个比例就是为什么一款网络代码平庸的游戏在 50 毫秒时感觉还可以,而在 150 毫秒时就会崩溃。位置修正一直在发生。在近距离时,它们足够小,以至于在有人注意到之前,插值就已经吸收了它们。拉大差距,同样的修正就会变成肉眼可见的瞬移。丢包和抖动会使情况更加恶化,因为丢失输入数据包意味着服务器在完全没有玩家操作的情况下向前推进。

因此,对于大多数工作室来说,最廉价的解决方案根本不需要代码。把服务器放得更近。将服务器部署到 615 多个位置而不是少数几个大区域,可以让平均延迟降低 58%,并为 78% 的玩家群体提供低于 50 毫秒的连接。一个将其中位数往返时间减半的团队,在不发布补丁的情况下,就已经将其网络代码必须解决的问题规模大致减半。关于这种方法的完整论证,请参见《要实现真正的延迟降低,唯一的答案是增加节点位置》。

为什么提高 Tick Rate 是错误的基础设施杠杆

寻求基础设施修复的工作室通常首先会想到 Tick Rate(滴答率/服务器刷新率)。这是错误的。

Tick Rate 决定了服务器解析和广播世界的频率。提高它可以获得更精细的时间分辨率,这对于命中判定的精度确实很重要。它对预测准确性没有任何帮助,因为准确性取决于你的模拟代码以及哪些输入到达了服务器,而不是取决于服务器说话的频率。

它确实能可靠做到的是增加消息量。一个因为连接丢包而产生回弹的玩家,现在会收到两倍多的数据包供其丢弃。修正到达得更频繁,视觉瑕疵也会变得更糟。

Tick Rate 是一个关于精度和成本的决定,我们在《游戏服务器 Tick Rate 详解》中对其进行了剖析。延迟是影响去同步的基础设施杠杆。而 Tick Rate 不是。

第二根杠杆:修复网络代码

较低的延迟可以缩小问题的规模。它永远无法消除问题,因为即使在 20 毫秒时,客户端仍在进行预测,而一款不带预测功能就上线的游戏在局域网(LAN)上也会让人感觉迟钝。剩下的工作由四种机制来完成。

客户端预测在玩家按下按键的瞬间在本地运行模拟,因此无论 Ping 值如何,输入都会立即得到响应。其他一切都建立在它的基础之上。

回滚(Rollback)处理服务器的权威状态到达并产生分歧的时刻。客户端不会直接瞬移到新状态并丢弃之后的所有内容,而是会倒回到最后确认的帧。

重新模拟(Resimulation)从该确认的状态向前重新播放中间的帧,应用玩家在此期间做出的输入。SnapNet 将该序列描述为“将客户端的模拟回滚到第 1 帧的结果,重新模拟第 2 帧和第 3 帧,最后模拟新帧。”做得好的话,大多数修正都会在玩家毫无察觉的情况下完成,因为重新播放的结果与他们所处的实际位置非常接近。

表现平滑(Presentation smoothing)处理经历了上述过程后残留的问题。将表现层/视觉代码与模拟代码分开,意味着纠正后的位置可以在几个帧中通过视觉插值来过渡,而不是直接瞬移。SnapNet 将模拟和表现分离开来就是为了这一点:游戏逻辑代码在每个渲染帧中可能会运行多次,而“任何仅用于表现且不影响模拟的逻辑仍然可以每帧只运行一次”。

还有第五点考量,解释了为什么某些修正产生的影响更大。输入的权威性不会永远保持不变。从玩家按下按键的那一刻起,该输入的精确度就会在穿越网络以及玩家在其上不断发出新输入的过程中衰减。20 毫秒前射出的一枪是对世界状态的强有力声明。而 200 毫秒后仍未确认的同一枪——此时玩家已经进行了横移、转向并又开火了两次——则要弱得多。如果和解机制以相同的置信度对待两者,要么会过度修正近期的输入,要么会固执地维护过期的输入。

这一切都需要消耗 CPU。正如 SnapNet 所指出的,和解“会带来沉重的 CPU 成本”,因为单个渲染帧可能需要倒回并重新播放多个模拟帧。为了帮助团队在下定决心前权衡输入延迟与回滚,我们在《如何缓解多人游戏中的延迟:输入延迟 vs 回滚》中对两者进行了直接对比。

这两根杠杆都不能替代彼此。在 200 毫秒的延迟下,优秀的网络代码仍然会产生可见的修正。在没有预测的游戏中,即使拥有完美的路由,感觉依然是不灵敏的。发布流畅多人游戏的工作室两手都要抓。

确定性去同步:为什么会显示“发生去同步”

第二类去同步的工作原理与第一类完全不同,它是目前互联网上抱怨最多、最响亮的原因。

策略游戏、4X 游戏、宏大战略模拟游戏、体育游戏以及许多合作游戏根本不传输世界状态。它们使用的是帧步锁。每台机器运行相同的模拟,线上传输的唯一内容是玩家的输入。Bettner 和 Terrano 就是利用这种方法在 28.8k 调制解调器上移动 1500 个单位的,如果是传输位置数据,这绝对是无法实现的。

局限性是绝对的。每台机器必须永远计算出位一致(bit-identical)的结果。为了验证这一点,帧步锁游戏会定期对它们的状态进行校验和(checksum)并进行比较。不匹配是无法通过请求重新发送来修复的,因为没有可用于重新发送的权威副本。因此,游戏会停止运行并报告错误。

这就是“发生去同步(a desync has occurred)”的意思。校验和比较失败了。

Bettner 和 Terrano 记录了这是多么的脆弱。他们写道:“在创建随机地图时,如果一只鹿的位置稍微偏了一点,它的寻食行为就会稍有不同,几分钟后,一个村民的路径就会偏离一点,或者他的长矛没刺中,没有带肉回家。”用他们的话说,核心难点在于:“极其微妙的差异会随着时间的推移而成倍增加。”

他们的团队积极地进行校验和检查,但仍然无法完全解决。 “尽管我们对世界、物体、寻路、目标定位和所有其他系统都进行了校验和检查,但似乎总还有一件事悄无声息地溜了过去。”

自 2001 年以来,常见的罪魁祸首并没有发生改变。在不同的 CPU 架构、编译器或平台之间四舍五入结果不同的浮点数数学计算。在不同的机器上消耗次数不同的随机数生成器,作者直接指出了这个问题,并指出“程序员不习惯在模拟中编写使用相同次数随机调用的代码。”未初始化的内存。容器迭代顺序。玩家之间的模组(Mod)或补丁版本不匹配,这就是为什么打过模组的宏大战略游戏会如此频繁地去同步。

前面章节提到的两根杠杆在这里都不适用。降低延迟没有任何作用,因为在传输过程中没有丢失任何东西。更好的预测没有任何作用,因为没有什么可预测的。这两台机器做了不同的数学运算,15 毫秒 Ping 值的会话与 150 毫秒的一样容易崩溃。如果你的游戏将去同步报告为硬性错误而不是卡顿,那么修复工作必须在模拟代码中进行,而别无他法。

专用服务器能消除去同步吗?

是的,而且是出于比大多数团队预期的更为根本的原因。

显而易见的部分是恢复。当客户端的状态偏离真相时,权威服务器可以停止发送增量数据(Delta)——这是通常只传输自上一个 Tick 以来发生变化的内容的高效方法——而是推送完整状态,并带有将其视为规范状态的指令。客户端有偏差的副本就会被直接覆盖。这会消耗带宽,这正是你在其余时间发送增量数据的原因,但它将一个无法恢复的会话变成了片刻的停顿。

不那么明显的部分是架构上的。锁步去同步无法恢复,正是因为没有任何一台机器有资格去否决其他机器。在中间放一个专用的权威服务器,这个条件就不复存在了。从结构上讲,存在一个单一真理来源,因此导致《文明》会话结束的那种故障模式不会以同样的方式出现。

点对点(P2P)和主机迁移架构失去了这两个特性。每个客户端都成为了潜在的真理来源,主机的机器性能变成了每个人的问题,而比赛中途的迁移则将主导权交给了可能处于比离线的主机更糟糕状态的机器。我们在《权威服务器、中继、点对点》中分析了这些模型之间的权衡。

专用服务器并不能让预测变得多余,它本身也不能缩短与玩家的距离。它能带给你的是一个由你掌控、可以部署检测手段并且可以强行纠正的状态。

一个澄清:“Desync” 也意味着一种外挂

在对堆积如山的玩家报告进行分类筛选之前,这个词的第三种含义也值得了解。

在竞技射击游戏社区中,它通常是延迟补偿视觉瑕疵的代名词。《彩虹六号:围攻》就是一个现成的例子,对该游戏网络代码的分析发现,即使躲在掩体后,低 Ping 值的玩家在与高 Ping 值玩家交火时仍会处于劣势,同时对手实际面对的方向也会出现偏差。这就是客户端/服务器复制去同步,只是玩家用他们自己的词汇来描述它。

在其他地方,它意味着蓄意的作弊行为。在 2025 年 10 月,Roblox 开发人员 loleris 在官方开发者论坛上报告了一种正在游戏中蔓延的“去同步”外挂,在这种外挂中,作弊者的位置更新仍然能到达服务器,但停止向其他客户端同步。正如该贴子所指出的,其结果是“服务器看到你在圈内,但所有其他客户端看到你在很远的地方”,这使得该玩家实际上处于隐身和无敌状态。Roblox 官方确认他们可以重现该问题并发布了修复补丁。

同一个词,截然相反的意图。一个抱怨去同步的帖子可能是在描述你的网络代码,或者有人在滥用它。

—

本文引用并参考了:Yahn W. Bernier 的《客户端/服务器游戏内协议设计与优化中的延迟补偿方法》(GDC 2001);Paul Bettner 和 Mark Terrano 的《28.8 调制解调器上的 1500 名弓箭手:《帝国时代》及以后的网络编程》(GDC 2001,发表于 Game Developer);Gabriel Gambetta 的《快节奏多人游戏(Fast-Paced Multiplayer)》系列;以及 High Horse Entertainment 的 SnapNet 核心概念文档。原内容的所有权利归其各自所有者所有。

书写者

Jakub Motyl(产品经理)和 Gabriel Parent(总监)

Get your Game Online Easily & in Minutes

立即开始集成!

轻松在线游戏
且在几分钟内

Get your Game Online Easily & in Minutes