不止是提高帧率,你还需要知道这些缓解射击游戏延迟问题的方法

作者:Walter,腾讯互动娱乐 工程师
来源: 腾讯游戏学堂

导读:在射击游戏中,延迟的出现在所难免。如何最大化降低延迟,为玩家提供良好的的射击手感,始终是开发者们需要研究的问题。本文将为大家提供多种切实有效的方案,用来解决这一痛点,希望能够对你有所帮助。

1、认识游戏中的延迟

网游都是采用典型的C-S通信模式,客户端必须持续地与服务器通信才能正常运作,网络通信的round-trip time明显就是我们不想要的延迟。网络传输的丢包、重传等,都可以纳入网络延迟的范畴。

但游戏中的延迟远不止网络延迟这么简单,计算机系统(包括手机)的运作、游戏的实现机制都会引入延迟。点击鼠标产生开火指令,这个电信号的传输、被系统捕获都需要时间,客户端逻辑线程通常要到下一帧才会开始处理这次输入,逻辑处理中抛事件到渲染线程,渲染线程最快也要下一帧才能去渲染结果,渲染结果要显示到屏幕上又受限于屏幕的刷新率。可见,即使是没有网络的纯客户端游戏,延迟都不可避免地存在。很多游戏在实现中会用到Buffering技术,这还会进一步加剧延迟。

衡量游戏品质的一项重要指标就是玩家常说的“手感”,而手感的关键组成部分就是反馈的及时性。对于射击游戏、竞速游戏,玩家对手感有着极高的期待,职业玩家甚至能感知到10ms级别的延迟差距,所以游戏开发者都会在这方面下很大的功夫。

2、降低网络延迟

网络游戏中,网络延迟是延迟的主体部分,降低网络延迟是优化体验的关键,也是开发商都会重视的地方。例如,Valorant 做到了为70%玩家提供小于35ms的RTT。

2.1 搭建专用网络

网络包在传输中要经过复杂的路由,这导致了高延迟以及更多的不确定性。所以,有实力的厂商都会在网络基础设施上做投入,例如Riot有去搭建自己的ISP。这一点可以简单理解为:厂商会围绕玩家就近部署很多接入服务器,内部的服务器之间由专用的高速通道相连,就像修了很多专用的高速公路,所以整体的通信非常稳定、高效。


2.2 使用UDP

越来越多的游戏都从TCP转向了UDP,在剔除/简化了拥塞控制以后,包传输的及时性有了明显的提升,尤其是在弱网环境下。

由于UDP不可靠的特性,一般都会实现Reliable-UDP。实际游戏中,一般会混用可靠与不可靠的UDP,纯表现的消息、状态同步等可能会用不可靠的UDP,用户输入、逻辑事件等重要的信息则会用可靠的UDP。

3、使Buffer尽可能小

3.1 Buffering机制

网络游戏中,客户端和服务器几乎每一帧都在通信,玩家的操作永远来自于客户端,权威的信息永远来自于服务器。但网络通信是不稳定的,服务器可能连续5帧都没有收到客户端A的数,但在下一帧又一口气收到5个。这种输入的缺失和爆发增长会导致卡顿,导致不平滑的游戏体验。无论是帧同步还是状态同步,都会面对这个问题。

经典的解决办法是使用buffering机制:增加一个buffer,缓存几帧数据以后再以稳定的频率向业务系统提供输入。这个机制良好地解决了网络传输带来的输入抖动问题,网络视频播放器中一直在使用该技术。


3.2 缩小buffer

很多游戏中都有用buffering机制,它确实让游戏变得平滑了,但却以增加延迟为代价。追求极致体验的游戏都不得不在buffer的大小上做折中,有的只存1帧的数据,有的甚至完全消除buffer。

缩小buffer的同时还要保证平滑的游戏体验,这意味着游戏要提供非常稳定的帧率、避免毛刺,还必须要在没有输入的情况下自己去预测玩家的行为。假设玩家在持续地移动,客户端每帧都在上报移动指令,后来由于网络故障,服务器在几帧内都未收到新的移动指令,这时服务器要尽最大的努力去预测,从而使网络恢复以后玩家的移动仍然是平滑的。

4、更高的帧率

帧率的影响到底大不大?帧率高到一定值以后是否值得继续提高?这是一个有一定争议的话题。

不管是否值得,但毫无疑问,更高的帧率必然是有好处的。帧率越高,系统运作的延迟就越低,响应越及时,表现也会更平滑、更自然。客户端帧率超过100的游戏并不少见,但服务器帧率几乎都小于64。服务器的帧率有必要更高吗?

Riot说有必要,Valorant 的服务器是以128帧运行的,这让人印象深刻。客户端与服务器都在模拟角色的移动,它们的运算结果会无可避免地出现分歧。假设服务器是1秒1帧,两端位置的最大分歧是1米,然后被强制纠正到一致。若两端都是1秒128帧,两端位置的最大分歧可能就只有10厘米了,然后也被强制纠正到一致。帧率高了就像是小步快跑,每次产生很小的误差,然后不断地纠正,所以整体的表现会很平滑。另一方面,即使某个客户端只以60帧的频率上报移动指令,其他9个客户端仍然可以以128帧的频率收到服务器下发的位置更新,所以其他9个人看到的人物移动仍然是非常平滑的。

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

高帧率缩短了系统运作的延迟,提供了更加平滑、细腻且一致的角色移动,这对FPS游戏是很有意义的。射击游戏给玩家的反应时间是极短的,1秒就决定了生死,职业玩家对10毫秒的延迟都很敏感。每个人都是非常平滑地移动,这是很利于玩家追踪和预测对方移动轨迹的,所以玩家的射击体验会好很多。要是人物的移动忽快忽慢,甚至还有位置瞬移,玩家的射击体验就会大打折扣。

但是,一切皆有代价。要做到这样的高帧率本身就很困难,需要花很大的力气去优化性能。就算技术上做到了,服务器运行的成本也是非常高的。

5、状态同步+客户端预测+纠正

帧同步要求每个客户端的运算输入严格一致,这样才能保证每个世界始终处于一样的状态。这意味着它们的输入一定来自于同一个服务器,意味着玩家输入到逻辑执行之间要包含网络延迟,这个延迟通常是几十毫秒,这会立即让玩家觉得“游戏的手感不好”。

FPS游戏大多都是使用状态同步,因为这样很容易做客户端预测,从而实现更好的及时性。玩家按键操作之后,客户端把该输入发给服务器,同时客户端本地直接处理该输入、产生部分结果/表现,而不用等待服务器的返回,这就是客户端预测。服务器处理完输入以后会通知客户端结果,如果本地运算结果与服务器下发的不一致,肯定要以服务器的为准,故客户端要做纠错处理。

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

6、隐藏log

对于网络游戏,重要的信息必须以服务器为准、不能客户端自主决定,否则会外挂泛滥,例如子弹命中的判定、造成的伤害量、技能的生效等等。但如果玩家的输入总是要等服务器返回后才给玩家反馈的话,玩家会觉得游戏的体验很差。公平性与及时性都很重要,但却存在冲突,需要我们做一些细致的特殊处理。

6.1 扔手雷

接下来,我们来看一个实际的例子(此处的例子节选自 Halo 的分享),假设玩家扔手雷的粗略过程如下:


按键是客户端行为,前摇动画可以看作是一段固定的延迟,飞出手雷代表施法的正确结束、产生了效果。

我们是网游,生成手雷这么重要的事情当然得服务器说了算,所以我们会很自然地这样做:


图中黄色的文字标记出了延迟的位置,玩家按键以后 隔一段时间才出现前摇动画,这体验当然是不可接受的,我们要更早地给玩家反馈。

想要及时反馈,可以在按键后直接播前摇动画,并且在时间结束后直接飞出手雷:


这违背了服务器做决策的原则,明显更无法接受。服务器上前摇过程还未走完,但客户端已经飞出手雷了,若服务器上前摇过程被他人打断,客户端必须要把已经飞出的手雷给删掉,这种诡异的表现会让玩家非常困惑和不满。

Halo 最终的实现方案是:


按键后,客户端立即播放前摇动画,但等服务器流程结束后再通知客户端飞出手雷。相当于在客户端把前摇给拉长了。这样做解决了上述问题,提供了良好的用户体验:

玩家按键后立即获得了反馈,所以操作体验很好。

手雷的生成遵从了服务器的权威,杜绝了被打断导致的奇怪表现。

虽然手雷的飞出有延迟,但动画末尾的手臂占据了大部分画面,玩家几乎感知不到这个延迟。即使玩家发现了这个延迟,影响也不大。

6.2 开启无敌

另外一个例子,是 Halo 释放无敌技能的过程。我们仍然期望按键后就立即播放前摇动画,但却不能再像上面一样延迟出现无敌效果。因为无敌状态对施法者太重要了,晚0.1秒就可能是生与死的差别,玩家对这段延迟非常在意、几乎无法容忍。这时还可以把延迟藏在哪呢?


为了给施法者提供极致的体验,这里修改了游戏机制——把服务器端的施法延迟给缩短了,即把网络延迟藏在了这里。

这种修改游戏机制的做法是特例,应该谨慎使用。虽然施法方的体验变好了,但对他的对手是不公平的。这里之所以可以这么做,是因为玩家普遍不能接受自己的无敌晚一点出现,但却相对能容忍对方的无敌早一点出现。

7、在割裂的世界中愉快地玩耍

7.1 屏幕上看到的都是假象

我们再来看看射击游戏中的经典问题——命中判定。延迟的存在,使这个问题变得更加棘手。

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

同一局游戏内,每个客户端和服务器都各自是一个独立的世界。理想情况下 所有世界都应该是同步的、一致的,在任一给定时刻,玩家A在每个世界中的位置和姿态(Pose)都应该是相同的。但由于延迟的存在,这种理想情况永远都不可能实现!

一个简化后的、现实中的模拟如下图:


一个框代表游戏运行的一帧,图中只画出了部分帧,并假设客户端与服务器的逻辑帧能稳定地一一对应。红线代表客户端上报的自己当前帧的位置/移动,蓝线代表服务器每帧下发的其他角色的位置/移动。

假设A和B都在持续地移动:

t1时刻,客户端A把自己本帧移动的结果(PAe)上报给服务器,客户端B同样把PBe上报给服务器。

t2时刻,服务器上A和B的位置分别为PAe和PBe。服务器把当前帧的位置信息同步给所有客户端。

t3时刻,客户端A收到玩家B的位置为PBe,但自己已经移动到了PAi。

这是很多游戏的角色位置模拟过程,在任一时刻,客户端看到的自己的位置始终是领先于服务器的,但看到的其他人的位置又都是落后于服务器的。这导致——战斗中,我们始终是在用将来位置的自己去打过去位置的敌人。例如客户端A在t3时刻瞄准了B,并开枪射击,服务器收到射击事件时已经是第i帧了,但t3时刻和t4时刻 玩家B在服务器上的位置都不是PBe。

假设命中判定在客户端做,t3时刻客户端A认为自己打中了B,但服务器需要校验这一次射击的合法性,服务器收到射击事件时 玩家B的位置已经变成PBi了,这时服务器很可能认为A打不中B。把命中判定交给服务器来做,仍然有同样的问题。

不同世界间的不同步是由延迟导致的,延迟导致的结果是:只要敌人持续在移动,并且移动速度比较快,你瞄准后的射击永远也打不中敌人。

7.2 服务器回退玩家的位置

解决命中问题的经典方案是——服务器做玩家位置回退。服务器记录一段时间内每个人的历史位置和Pose,收到A的射击事件时,服务器把A之外的所有玩家都挪回到一个恰当的历史位置去,使服务器上其他人的位置与A看到的其他人位置基本一致。这样,客户端上能打中的 服务器上就也能打中了。如图所示:


但这种方案也有问题:它对被打的那一方不公平。例如t3时刻,玩家B明明已经躲起来了,在客户端B的世界里 玩家A是打不到自己的(位置PAe与位置PBi),结果你t3时刻的开火还是打死了自己。

要解决这个问题,一般是给回退的时长加一个约束,例如最多只允许回退一个RTT。

8、让网络卡的人自己拉扯

UE4默认的移动同步过程是:客户端和服务器都模拟角色的移动,客户端每帧上报自己移动后的结果和当前的移动输入,服务器收到移动输入后也去模拟移动。若服务器的移动结果与客户端上报的结果误差在一定容忍范围内,则直接采纳客户端的结果,使服务器上玩家的位置等于客户的上报的位置。若误差超出了容忍范围,则以服务器为准,强制重置客户端的位置。当容忍范围设置得较大的时候,服务器上会大幅度重置角色的位置,当同步到其他客户端以后,这个角色的移动必然会出现波动,要么是直接位置重置,要么是插值过去(加速移动过去)。无论用哪种处理方式,最终的结果都是:一个玩家B卡了,然后其他9个人看到的B的移动都是不平滑的。

Valorant 认为:一个人卡了,却导致其他9个人的瞄准体验变差了,这是不可接受的。Valorant 的方案是:若服务器未收到移动输入,仍然根据当前的状态每帧做预测,当出现分歧时始终以服务器的模拟为准,并且每帧都下发角色的位置。这样做,无论个人怎么卡,其他9个人看到的角色移动始终是平滑的,但卡的那个人自己会被频繁地拉扯。

9、解决Peeker' s advantage

9.1 什么是Peeker’s advantage?

这是战术射击游戏的经典话题,玩家在转角处来回晃悠可以获得先手优势(会先看到对方),是一种网络延迟导致的不公平现象。


如图所示,冲出角落的人会先看到对方,而蹲角落的人明显要晚一些看到对方。说明:这里是用了比较大的ping值来演示该效果,实际中不一定有这么明显。

产生这个问题的本质原因就是网络延迟,正如上面已经讲过的:若是自己在移动,自己客户端上自己的位置是领先于其他所有世界的,而静止不动的人的位置在所有世界中都是一致的。移动的人的位置要同步到其他客户端,这是需要经过网络传输的:


9.2 它到底有多大的影响?

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


如图,holder要想打赢对方,就必须要在server applies kill shot之前把开火指令发送到服务器,这样才能实现比peeker先开火。进而可以得到这样的关系:

holder reaction time < peeker reaction time - (holder RTT + holder buffering latency + server buffering latency)

所以peeker方领先的时间主要取决于holder方的网络延迟,以及buffering延迟。

更进一步,我们可以带入具体的数字来量化这个优势。按帧率60、单边通信30ms延迟来算,peeker方大约可以获得140ms的先手优势。

9.3 技术层面能做什么?

① 降低网络延迟。无非就是就近部署、搭建高速专用网络、使用UDP,上面已经讲过了。
② 降低buffering延迟。最小化buffer大小,上面也已经讲过了。
③ 高帧率也是有帮助的。在 Valorant 中,如果客户端能跑140帧的话,这个差值可以降低到71ms。

9.4 从设计的角度弱化Peeker’s advantage

技术上没办法彻底杜绝这个问题,而且也很难取得明显的改善,至少代价是很大的。但从设计上 我们有很多方法来优化它:

地图设计:例如一个区域只留一两个进入的入口(供他人peek around),但场地内有多个地方可以设伏、蹲守入口。这样的设计使得holder的位置高度不确定,但peeker的位置高度确定,变相地增加了peeker方的难度。

降低移动中射击的准确率。在这一点上每个游戏的取舍都不同,Valorant在这方面做得比较狠,移动中几乎很难打中。

让peeker的身体先暴露,但相机晚一点暴露。这使得peeker先暴露出自己的部分身体,然后相机才能看到holder。人的移速慢一点、系统运行帧率高一点、身体做大一点,都会有利于peeker先暴露出部分身体。

出处与参考:
第六节摘自Halo的GDC分享:https://youtu.be/h47zZrqjgLc
文中提到的Valorant的部分主要摘自Valorant官方博客:https://technology.riotgames.com/news/peeking-valorants-netcode
感兴趣的同学可以看一下这个对高帧率的对比测试:https://youtu.be/OX31kZbAXsA

本文转自: 腾讯游戏学堂,作者:Walter,腾讯互动娱乐 工程师,转载此文目的在于传递更多信息,版权归原作者所有。如不支持转载,请联系小编demi@eetrend.com删除。

最新文章