
游戏后端深入探讨 - 守望先锋

ECS作为网络代码启用器: 守望先锋的实体组件系统架构将整个游戏玩法的网络代码表面减少到仅仅三个系统,尽管有数百个系统,使一个内在复杂的问题变得可处理。
命令帧和自适应滴答率:模拟在固定的16毫秒命令帧上运行(在比赛模式下降至7毫秒)——这是一个关于响应性和服务器成本之间的故意权衡,任何多人工作室都应该理解。
回滚需要一个值得信赖的服务器: 守望先锋的调解系统之所以有效,是因为一个专门的权威服务器持有真实真相。没有它,整个回滚和重放模型根本无法存在。
时间膨胀作为数据包丢失的保护盾:当服务器检测到输入饥饿时,它会向客户端信号,使其模拟稍微快一点,在数据包丢失导致错误预测之前,填满输入缓冲区。这个反馈循环不断地、看不见地运行。
预测所有事物,包括火箭: 守望先锋默认预测所有技能和弹药,仅在必要时选择退出;这是2017年的一种不寻常的做法,使游戏感觉更具响应性。
Tim Ford,现在是Kintsugiyama的工作室负责人,并且自2013年夏季《守望先锋》游戏创立以来,担任过守望先锋的首席游戏玩法程序员,他在2017年的GDC大会上展示了《守望先锋》的游戏架构和网络代码。演讲介绍了暴雪如何在严格的实体组件系统架构之上构建一个以服务器为主导的、重预测的多人游戏系统,以及为何这种架构选择最终成为网络代码成功不可分割的一部分。
间接地,这突显了任何游戏工作室,无论大小,可以在他们的多人游戏中增加的最佳实践,以帮助改善其在线架构。让我们一窥其中的奥秘。
什么是ECS,为什么它对网络代码很重要?
实体组件系统架构将游戏世界分为三个不同的概念。实体仅仅是ID; 它们本身并没有任何意义。组件是附加在实体上的纯数据容器;它们存储游戏状态,但没有行为。系统包含所有逻辑,并针对具有该系统关心的特定组件组合的任何实体运行。
这种严格的分离迫使每个行为都存在于一个明确定义的、独立的地方。正如Ford所说,目标是一个“成功的深渊”,即一种架构的约束下,你几乎自然而然地推向编写一致、可维护、和解耦的代码。在《守望先锋》的产品代码库中,约有46个客户端系统和103种组件类型,仅有三种系统负责游戏玩法的网络代码:移动、武器和状态脚本。引擎的其余部分都不接触。这种孤立使得随着英雄阵容的增加,网络代码可维护。
值得注意的是:ECS是一个值得深入探讨的模式,独立于其自身。我们可能会在专门的文章中介绍它,因为它的影响远远超出网络代码。
命令帧、Tickrate和响应性的成本
《守望先锋》的仿真以每个16毫秒的固定命令帧运行,大约为60Hz。在比赛配置中,这个时间缩短为7ms,更接近128Hz。每帧,客户端尽可能接近当前时刻接受玩家输入并将其发送到服务器。客户端的时钟总是比服务器提前半个往返时间加上一个缓冲命令帧。在160ms的RTT条件下,这大约有96ms的提前时间。
这与成本有关。更高的tickrate(即更短的命令帧)需要每秒更多的服务器CPU周期和更多的每场比赛网络带宽。在比赛模式中,从16ms缩短到7ms不是免费的。它是为竞争游戏保留的有意选择,其中边际响应性提升足以证明额外基础设施成本。对于常规匹配,16ms达到了合适的平衡。
该额外成本与tickrate线性增长。部署最近玩家服务器的基础设施有助于在不单纯通过提升tickrate的情况下保持低延迟。Edgegap的游戏服务器编排平台通过将服务器放置在网络边缘最近的玩家群体位置,从而降低了平均延迟58%。
回滚只在专用服务器上真正有效
“这就是我们不得不解决的最重要的游戏玩法工程师网络代码问题,”正如Ford所描述的那样。目标是创建一个响应迅速的网络动作游戏,这意味着在本地预测玩家动作。但是我们不能信任客户端的模拟权限,因为正如Ford所指出的,“有些客户端是混蛋。”正如我们的文章所强调的,对等网络架构是作弊者的天堂,他们可以操控由玩家托管的游戏服务器,甚至中继也存在安全风险。
解决方案是一台可靠、权威的专用服务器。
当客户端预测错误时,比如它认为玩家在跑动,但服务器确定他们被晕眩,客户端会回滚到服务器授权的移动快照,并重新播放每一个缓冲的输入到当前时刻。因为角色移动模拟高度确定性,重放可靠地再现正确状态。这种修正是平滑的,并且在绝大多数情况下是不可见的。
这种系统在点对点或中继架构中是不存在的。没有回滚的基准真相。在云端运行的专用游戏服务器不仅仅是一种便利;它是整个预测和和解模型的结构性前提。没有它,你要么选择信任客户端,要么让每个玩家在看到他们的输入响应之前等待服务器确认。
关于回滚的更多信息,请务必阅读我们关于回滚网络代码的文章,还有我们比较回滚网络代码与输入延迟的延迟减缓方法。
时间膨胀:主动对抗数据包丢失
当数据包丢失且服务器没有输入进行模拟时,它会复制玩家的最后已知输入并侥幸而为。这会造成预测错误。《守望先锋》的回答是避免这种损害。
当服务器检测到输入饥饿时,会通知客户端,然后客户端开始时间膨胀。客户端将固定的16ms步长视为大约15.2ms,模拟速度略快,并将更多输入倒入网络通道,以在服务器端建立一个缓冲。在条件稳定后,客户端反向膨胀,逐步耗尽该缓冲。此反馈循环在持续运行。
这与Ford追溯到Quake World的技术相结合:一个输入滑动窗口。客户端不是仅仅发送当前帧的输入,而是将自上次服务器确认的运动状态以来的每个输入打包到一个数据包中。因为玩家通常按住键而不是以60Hz频率点击,这种压缩非常高效。如果一个数据包丢失,下一个仍然携带所有缺失的输入,在模拟运行前填补空白。两个互补的系统。各自不新颖,但它们一起使数据包丢失在玩家眼中几乎不可见。
预测一切,包括火箭
大多数多人游戏仅预测玩家移动,而止步于此。《守望先锋》默认预测一切(即移动、所有能力和武器)。团队必须显式选择不预测特定能力。这一理念甚至扩展到像法老之鹰火箭这样的大型可见弹药。
当时,Blizzard尊敬的工作室GDC讨论会明确建议不要这样做。火箭是世界上大的物理物体,不是轨迹弹,预测错误会导致它们在视觉上消失。然而暴雪找到了办法做成这件事。正如Ford所说,“预测火箭很酷。”这让人感觉非常好。偶尔火箭会消失。这有一些论坛帖子。完全值得。
给任何射击游戏开发者的教训:默认预测。选择性退出。响应性提升是真实且可感的,即使玩家无法清楚表达原因。
命中注册:只回滚你需要的部分
当玩家开火时,服务器将场景回滚到射手的参照帧,然后计算是否命中。这个逆向和解是一个成熟的技术。守望先锋独有的是它如何管理其成本。
不是回滚场景中的每个实体,而是首先检查代表每个实体运动大约最后半秒的空间包围体是否与弹道射线相交。只有那些包围体相交的实体才会被完全回滚以进行精确命中计算。在进行任何耗时计算之前,这就已经淘汰了绝大多数候选对象。
在220ms回程时间大约以上,命中影响预测完全被禁用。系统以外推法(例如,基于最后已知轨迹对目标位置进行死算预估)代替回顾目标以至于成功躲避到掩护后的受害者依然会死亡。这是一个故意的公平限制。
延迟:一个副作用,一个调用点
守望先锋推迟大的副作用,而不是无论何时何地调用。原则简单:当一个行为触发一大块工作时,询问该工作是否需要立即执行。通常,不需要。
最明显的例子是冲击效果。引擎中的多个系统需要生成表面冲击,从命中扫描子弹到爆炸性弹药到光束武器。每次生成都涉及实体生命周期、场景管理和资源管理。守望先锋宁愿将这个逻辑分散到十几个调用点,而是将待处理的接触记录排入一个单例组件。在渲染准备之前,每帧都有一个系统处理完整批次。
正如Ford所总结的教训:“如果在一个调用点中表达行为,即在该调用点中所有主要行为副作用进行了局部化,那么行为复杂性要小得多。”收益不仅体现在清晰度提高,还有更好的缓存本地性、对效果创造施加每帧预算的能力,以及在多效果同时请求时平滑尖峰的能力。
为多人游戏开发者提供的额外见解
显式组件访问使得多线程成为可能。如果系统提前声明它们读取而不是写入哪些组件,则调度程序可以安全地并行运行非冲突系统。Ford回顾称,守望先锋的临时方法使这一机会变得模糊。从一开始强制严格的元组定义会更早浮现复杂性,并且使上下文安全的并行化变得容易。
仿真时钟必须是固定的,即使渲染器不是。 守望先锋的仿真以60Hz的频率运行。如果渲染器降到30fps,引擎当帧运行两个仿真tick。仿真要比渲染便宜得多,所以这可控,但出于确定性考虑,固定仿真频率是不可协商的。而且如果服务器不能跟上,Ford警告说它会进入“死亡螺旋”――每个延迟帧强制更多tick,直到服务器完全崩溃。优化不是可选的。
架构规则需要时间发现。 守望先锋团队花费大约一年半才确定了他们的ECS规则。在这些规则之前或违反这些规则的代码是最多bug和维护负担的持续来源。尽早定义和执行你的约定规则。
---
这篇文章基于并引用了由Tim Ford在2017年GDC上的原创演讲,发布在YouTube。原始内容的所有权利归其各自所有者所有。
书写者
Edgegap团队







