腾讯互动娱乐工程师解析网游延迟:从C-S通信到系统运作的全面分析

2025-01-11
来源:网络整理

腾讯互动娱乐工程师

网络游戏都采用典型的CS通讯方式。客户端必须不断地与服务器通信才能正常运行。网络通信的行程时间显然是我们不希望出现的延迟。网络传输中的丢包和重传都可以包含在网络延迟的范围内。

但游戏中的延迟远不止网络延迟这么简单。计算机系统(包括手机)的运行和游戏的执行机制都会引入延迟。单击鼠标会生成开火命令。该电信号需要时间来由系统传输和捕获。客户端逻辑线程通常直到下一帧才开始处理该输入。在逻辑处理期间,事件被抛出到渲染线程。渲染线程是最快的。需要下一帧才能渲染结果,渲染结果需要显示在屏幕上,并受到屏幕刷新率的限制。可见,即使是没有网络的纯客户端游戏,延迟也是不可避免的。许多游戏在实现过程中都使用了技术,这进一步加剧了延迟。

衡量游戏好坏的一个重要指标就是玩家常说的“手感”,而手感的一个关键组成部分就是反馈的及时性。对于射击游戏和赛车游戏来说,玩家对于手感有着极高的期望。职业玩家甚至可以感知到10ms的延迟差距,因此游戏开发者会在这方面投入大量的精力。

在网络游戏中,网络延迟是延迟的主要部分。降低网络延迟是优化体验的关键,也是开发者会关注的事情。例如,可以为70%的玩家提供小于35ms的RTT。

2.1 搭建专用网络

网络数据包在传输过程中会经历复杂的路由,导致较高的延迟和更多的不确定性。因此,有实力的厂商都会投资网络基础设施。例如,Riot就建立了自己的ISP。这可以简单理解为:厂商会在玩家附近部署很多接入服务器,内部服务器通过专用高速通道连接,就像建设很多专用高速公路一样,所以整体通信非常稳定高效。

2.2 使用UDP

越来越多的游戏从 TCP 切换到 UDP。消除/简化拥塞控制后,数据包传输的及时性得到了显着提高,尤其是在弱网络环境下。

由于UDP的不可靠特性,一般采用-UDP。实际游戏中一般使用可靠和不可靠的UDP。纯性能消息、状态同步等可能会使用不可靠的UDP,而用户输入和逻辑事件等重要信息可能会使用可靠的UDP。

3. 机制

在网络游戏中,客户端和服务器几乎每一帧都会进行通信。玩家的操作始终来自客户端,权威信息始终来自服务器。但网络通讯不稳定。服务器可能连续5帧都没有收到客户端A发来的号码,但下一帧就一次性收到了5个号码。这种输入的缺乏和突然的增长可能会导致卡顿,从而导致游戏体验不均匀。无论是帧同步还是状态同步,都会面临这个问题。

经典的解决方案是采用一种机制:添加一个,缓存几帧数据,然后以稳定的频率向业务系统提供输入。该机制有效解决了网络传输带来的输入抖动问题,该技术已应用于在线视频播放器中。

3.2 缩小

许多游戏中都有一些机制可以使事情变得顺利,但代价是增加延迟。追求极致体验的游戏就不得不在体积上做出妥协。有的只存储1帧数据,有的甚至完全消除。

在缩小的同时,必须保证流畅的游戏体验,这意味着游戏必须提供非常稳定的帧率,避免故障,并且必须在没有输入的情况下预测玩家的行为。假设玩家连续移动,并且客户端每帧报告移动指令。后来由于网络故障,服务器在几帧内都没有收到新的移动指令。这时,服务器必须尽最大努力进行预测,以便恢复后网络播放器的移动仍然顺利。

帧率影响大吗?帧率达到一定值后还值得继续提高吗?这是一个有些争议的话题。

无论是否值得,毫无疑问,更高的帧速率会带来好处。帧率越高,系统运行的延迟越低,响应越及时,表现也会更加流畅、自然。游戏客户端帧率超过 100 的情况并不罕见,但服务器帧率几乎总是低于 64。服务器的帧率是否需要更高?

Riot 表示这是必要的,并且服务器以 的速度运行,这令人印象深刻。客户端和服务端都在模拟角色的运动,它们的计算结果必然会出现分歧。假设服务器每秒1帧,则两端位置最大相差1米,然后强制修正一致。如果两端都是128帧每秒,那么两端的位置最大差异可能只有10厘米,然后强制修正为一致。高帧率就像小步快跑,每次都会犯一些小错误,然后不断纠正它们,所以整体表现会很流畅。另一方面,即使一个客户端仅以 60 帧的频率报告移动指令,其他 9 个客户端仍然可以以 128 帧的频率从服务器接收位置更新,因此其他 9 个人看到的角色动作仍然是非常光滑。

在《中》中,客户端和服务器的运动和物理保持在128帧上运行,这使得两端的每一帧都可以一对一匹配,这极大地方便了服务器的命中判定。服务器只需将其他人的位置回滚固定数量的帧以匹配客户端上的位置。

高帧率缩短了系统运行的延迟,提供更流畅、更细腻、一致的角色动作,这对于FPS游戏来说非常有意义。射击游戏给予玩家的反应时间极短。一秒就可以决定生死。职业玩家对10毫秒的延迟非常敏感。每个人的动作都非常流畅,这对于玩家跟踪和预测彼此的移动轨迹非常有帮助,因此玩家的射击体验会好很多。如果角色移动时快时慢,甚至瞬移,玩家的射击体验就会大打折扣。

然而,一切都是有代价的。实现如此高的帧速率本质上是困难的,并且需要付出大量的努力来优化性能。即使技术上可行,运行服务器的成本也非常高。

帧同步要求每个客户端的计算输入严格一致,从而保证每个世界始终处于相同的状态。这意味着他们的输入必须来自同一台服务器,这意味着玩家输入和逻辑执行之间存在网络延迟。这种延迟通常是几十毫秒,立刻就会让玩家感觉到“游戏感觉不太好”。

大多数FPS游戏都使用状态同步,因为这样很容易进行客户端预测并实现更好的时效性。玩家按下按钮后,客户端将输入发送到服务器。同时,客户端直接在本地处理输入并生成部分结果/性能,而不需要等待服务器的返回。这是客户的预测。服务器处理完输入后,会将结果通知给客户端。如果本地的计算结果与服务器发出的结果不一致,则以服务器的结果为准,因此客户端必须纠正错误。

由于状态同步,即使客户端本地计算错误,也总能恢复到一致且正确的状态。 UE引擎的移动同步就是这样完成的。

游戏帧数显示_帧数显示游戏怎么设置_玩游戏帧数显示

对于网络游戏来说,重要信息必须由服务器确定,不能由客户端独立确定,否则就会出现作弊泛滥的情况,比如子弹命中的确定、造成的伤害量、技能的有效性等但如果玩家的输入总是要等待服务器返回才反馈给玩家,那么玩家就会感觉游戏体验很差。公平性和及时性都很重要,但它们之间存在冲突,需要进行一些谨慎的特殊处理。

6.1 投掷手榴弹

接下来我们看一个实际的例子(这里的例子摘自Halo的分享)。假设玩家投掷手榴弹的大致流程如下:

按钮按下是客户端行为,向前晃动动画可以视为固定延迟,飞出的手榴弹则代表法术施法和效果的正确结束。

我们是网络游戏,生成手雷这么重要的事情必须由服务器来决定,所以我们自然会这样做:

图中黄色文字标记的是延迟的位置。玩家按下按钮一段时间后,会出现向前平移动画。这样的体验当然是无法接受的,我们需要早点给玩家反馈。

如果想要及时反馈,按下按钮后可以直接播放预摇动画,时间到后直接飞出手榴弹:

这违反了服务器决策的原则,显然是不可接受的。服务器的前摇过程还没有完成,但是客户端已经飞出了手榴弹。如果服务器的前摇过程被其他人打断,客户端必须删除飞出的手榴弹。这种奇怪的行为会让玩家非常困惑和不满。

Halo的最终实现是:

按下按钮后,客户端立即播放前摇动画,但要等待服务器进程结束才通知客户端飞出手榴弹。相当于客户端的前挥杆拉长了。这样做既解决了上述问题,又提供了良好的用户体验:

6.2 开启无敌

再比如光环释放无敌技能的过程。我们仍然期望按下按钮后立即播放向前平移动画,但我们不能再像上面那样延迟无敌效果。因为无敌状态对于施法者来说是如此的重要,所以0.1秒的延迟可能就是生与死的区别。玩家们对于这种延迟非常在意,很难容忍。这个时候还能把延迟藏到哪里呢?

为了给施法者提供极致的体验,这里对游戏机制进行了修改——缩短了服务器端的施法延迟,也就是将网络延迟隐藏在这里。

这种对游戏机制的修改是一种特殊情况,应谨慎使用。虽然施法者的经验有所提升,但这对于对手来说却是不公平的。这里之所以可以这样做,是因为玩家一般无法接受自己的无敌出现得晚,但对对方的无敌出现得早却相对能容忍。

7.1 你在屏幕上看到的一切都是幻觉

我们来看看射击游戏中的一个经典问题——命中判定。由于延迟的存在,问题变得更加困难。

子弹是否击中敌人,子弹击中头部还是脚部,都是非常关键的信息。命中判定可以在客户端进行,然后服务器进行验证,也可以只在服务器进行。无论使用哪种方法,我们都必须面对这样一个事实:玩家屏幕上看到的敌人位置很可能不是敌人此刻的真实位置,或者不是对手在其他世界的位置。

在同一个游戏中,每个客户端和服务器都是一个独立的世界。理想情况下,所有世界都应该同步且一致。在任何给定时刻,玩家A在每个世界中的位置和姿势(Pose)应该是相同的。但由于拖延,这个理想的情况永远无法实现!

简化的现实模拟如下所示:

一个方框代表游戏运行的一帧。图中只画出了部分框架,假设客户端和服务器的逻辑框架能够稳定地一一对应。红线代表客户端上报的当前帧的位置/运动,蓝线代表每帧中服务器发送的其他字符的位置/运动。假设A和B都在连续运动:

这就是很多游戏的角色位置模拟过程。在任何时刻,客户端自己的位置总是领先于服务器,但它看到的其他人的位置却落后于服务器。这就导致——在战斗中,我们总是用自己在未来的位置来对抗敌人在过去的位置。例如,客户端A在时间t3瞄准B并射击。当服务器接收到射击事件时,已经是第i帧了,但是t3时刻和t4时刻玩家B在服务器上的位置不是PBe。

假设命中确定是在客户端完成的。在t3时刻,客户端A认为自己击中了B,但是服务器需要验证这次击球的合法性。当服务器收到投篮事件时,玩家B的位置已变为PBi。这时候服务器很可能认为A打不到B,把命中判断留给服务器,你还是遇到同样的问题。

不同世界之间的不同步是由延迟造成的。延迟的结果就是:只要敌人继续移动并且移动得比较快,你的瞄准射击就永远不会击中敌人。

7.2 服务器回滚玩家位置

解决命中问题的经典方法是让服务器回滚玩家的位置。服务器记录了一段时间内每个人的历史位置和姿势。当服务器接收到A的投篮事件时,将除A之外的所有玩家移动回合适的历史位置,使得服务器上其他人的位置与A一致。其他赶来的人,情况也基本相同。这样在客户端能打到的,在服务器上也能打到。如图所示:

游戏帧数显示_玩游戏帧数显示_帧数显示游戏怎么设置

但这个解决方案有一个问题:这对受到打击的一方不公平。例如,在时间t3,玩家B显然正在躲藏。在客户端B的世界中,玩家A无法击中他(位置PAe和位置PBi)。结果,如果你在时间 t3 开火,你仍然会自杀。

为了解决这个问题,一般需要添加回滚长度的约束。例如,最多只允许回滚1个RTT。

UE4默认的运动同步流程是:客户端和服务端都模拟角色的运动。客户端每帧报告其运动结果和当前运动输入。服务器在接收到运动输入后也会模拟运动。如果服务器的移动结果与客户端报告的结果误差在一定的容忍范围内,则直接采用客户端的结果,使得玩家在服务器上的位置等于客户端报告的位置。如果误差超出容忍范围,则以服务器为准,客户端位置将被强制重置。当容差范围设置为较大值时,角色的位置将在服务器上显着重置。当与其他客户端同步时,角色的移动将不可避免地发生波动,无论是通过直接位置重置还是通过插值(加速并移动过去)。无论采用哪种处理方式,最终的结果都是:一名球员B被卡住,然后其他9人看到的B的动作并不流畅。

我认为如果一个人被卡住,其他 9 个人的瞄准体验就会恶化,这是不可接受的。解决方案是:如果服务器没有接收到运动输入,它仍然会根据当前状态进行每一帧的预测。当出现分歧时,始终以服务器的模拟为准,每帧都会发送角色的位置。这样,无论个体被卡住多严重,其他9个人看到的角色动作永远都是顺畅的,但被卡住的人却会频繁被拉扯。

9.1 什么是 s?

这是战术射击游戏中的经典话题。玩家可以通过在拐角处来回徘徊来获得先发优势(他们会先看到对手),这是网络延迟造成的不公平现象。

如图所示,从角落里冲出来的人会先看到对方,而蹲在角落里的人显然会稍后看到对方。注意:这里使用了一个比较大的ping值来演示效果,但实际中可能不会那么明显。

造成这个问题的本质原因是网络延迟。如上所述:如果你在移动,你在客户身上的位置领先于所有其他世界,而静止的人的位置则领先于其他所有世界。是一致的。移动者的位置需要同步到其他客户端,需要通过网络传输:

9.2 影响有多大?

这个优势其实可以用一个数学公式来描述:

如图所示,如果你想击败对手,你必须在击杀射击之前向服务器发送开火命令,这样你才能先开火。那么我们可以得到如下关系:

时间 < 时间 - (RTT++)

因此,一方领先的时间主要取决于一方的网络延迟,以及延迟。

更进一步,我们可以用具体的数字来量化这一优势。按照帧率60、单边通信时延30ms计算,方可以大致获得先发优势。

9.3 技术层面可以做什么?

① 减少网络延迟。无非就是就近部署,搭建高速专网,使用UDP,就像上面提到的那样。

② 减少延迟。上面已经讨论了最小化尺寸。

③ 高帧率也有帮助。在 中,如果客户端可以运行 140 帧,则这个差异可以减少到 71ms。

9.4 从设计角度来看的弱化

从技术上来说,没有办法完全消除这个问题,也很难实现显着的改进,至少成本很高。但在设计方面我们有很多方法可以优化:

地图设计:比如一个区域只有一两个入口(供其他人窥视),但场地内有多个地方可以设伏并守卫入口。这样的设计使得“”的立场具有高度的不确定性,但“”的立场却具有高度的确定性,这就增加了当事人变相的难度。

移动时射击的准确性降低。每场比赛在这一点上的取舍都不同,在这方面也比较狠。移动时几乎很难击中。

让的身体先曝光,但摄像机随后曝光。这使得一个人的身体的一部分在相机看到之前暴露出来。如果人移动速度较慢,系统运行帧率较高,身体变大,将有助于首先暴露部分身体。

来源和参考文献:

分享