射击游戏的延迟对抗
实现网络实时对战游戏的时候,延迟是一个躲不开的难题,总是需要各种经验和技巧。只要是发布在互联网上的游戏,延迟就客观存在,以目前基础技术来说延迟不可能消除。假设光纤以光速一秒绕地球7圈半来说,1000ms/7.5=133ms,就算用光纤直连也有133ms的延迟(严谨一点的话应该算半球,也有66ms),而且互联网庞大且复杂,还要把同一根光纤的网络拥堵和路由器协调运算时间算进去。要做世界服务器网络延迟不可避免,除了搭建区间服务器外,就是需要做延迟对抗的优化。
在快节奏的游戏中,延迟的存在必然影响手感,就算几毫秒也能感受出来。如果让玩家感受到延迟的存在,就会觉得输入反应很慢。网络必然会有波动,不稳定的网络会让玩家感觉一卡一卡的,不流畅感会严重影响玩家感受。每个玩家的延迟都不一样,高延迟玩家有可能影响低延迟玩家的体验,这是非常不公平的,一些ACT游戏比如黑暗之魂中的“延迟战士”,高延迟的玩家因为延迟而低延迟玩家看不到对方真实位置,却能轻易击杀对方,甚至有人会恶意把自己延迟调高。现在很多非重度PVP游戏这方面都没做得很好。不过也没法做到完全公平,后面会提及到。所见即所得,比如玩家的准星明明打中,特效也出来了,对面却没扣血。逻辑的触发都是基于玩家所见,不能因为延迟的存在而忽略所见合理性。
基本Client-Server模型
常说的CS同步方式,即Client-Server模型,其实就是来自CS(不是🤣)。Valve的Source引擎,也就是半条命/CS的引擎,网络模型被几乎所有的现代射击游戏都被使用参考和迭代。里面包含平滑插值,输入预测和延迟补偿的那几个概念。Valve的游戏一般每秒以20-60个数据包,也即是20Hz到60Hz的频率进行网络同步。同时缓存100毫秒的快照,用作网络回滚。详情可以查看Source引擎相关文档:https://developer.valvesoftware.com/wiki/Source_Multiplayer_Networking
当然上面的TickRate都是Source的默认值,或者说是老版CS的值。不同的游戏设定的TickRate都不一样,或者是为了竞技性,或者是削弱竞技性,或者是为了成本,高Tick必然能改善游戏体验,包括能降低错误预测带来的影响,同步位置更准确,游戏更公平,后面会提及,当然肯定会提高服务器成本。
Counter-Strike: Source – 66 |
---|
Team Fortress 2 – 66 |
Left 4 Dead – 30 |
Division 2 – 30 |
Overwatch – 60 |
Halo Reach – 60 |
Counter-Strike: Global Offensive – 可调整,一般60,现在很多私人服务器128 |
Valorant – 128 |
拳头的Valorant的TickRate达到惊人的128,这也是他们对外宣传重点。
不管同步频率是多少,同步始终是离散的假设20Hz的同步频率,那就是每50ms同步一次,必然会让游戏感觉断断续续和抖动,可以想象一下20FPS的游戏的流畅感有多差。网络丢包的情况影响更大,快照之间必须有平滑插值
插值平常又分为内插值和外插值(预测):
- 内插值(Interpolation)指的是在已知快照之间进行插值,插值方法如线性插值、指数插值等。
- 外插值(Extrapolation)指的是根据已知快照,推断丢失快照的值(如丢包),或者是推测未来快照(如输入预测),插值方法如Dead Reckoning算法,或者是现在已被多次落地的神经网络算法。可见论文:https://uwspace.uwaterloo.ca/bitstream/handle/10012/16960/Walker_Tristan.pdf?sequence=3
Dead Reckoning预测算法Projective Velocity Blending的实现
然后当然,实时游戏都习惯使用UDP来作为收发网络包。TCP虽然是可靠的但是因为重发机制导致传输效率低下,UDP更适合实时游戏。现在有很多可靠的UDP实现,如KCP,都是为了解决冗余的TCP包及其繁琐的连接校验流程,并且有足够的参数让对应的游戏有优化空间,但是可靠UDP依然有丢包的延迟损耗。如果对延迟做到极致,不可靠UDP最合适,不可靠UDP不对数据包进行重发,而是忽略该包,服务器做输入外插值,尽可能减少来回的延迟。守望先锋使用的是不可靠UDP,很多FPS游戏都是不可靠UDP。
预测与回滚
射击游戏的网络同步最经典的分享可不得不提及守望先锋在GDC2017上的分享。
下面是猎空的一个演示。客户端每次输入,包括移动,射击可破坏的物体,技能都能做到输入即马上有反应,手感极佳。这就是客户端的输入预测。
然后是在延迟250ms的时候的演示,可以看到几乎跟之前一样,没有任何手感不好的情况,感受不到任何延迟。
守望先锋猎空演示:不管是单机状态下,还是250ms延迟下上面这种操作手感都是一模一样的
而下面的演示例子则是技能释放时进行输入预测,但是被拉回的情况。可以看到温斯顿在200ms延迟的情况下出现了错误的预测,在起跳的时候被美冻住了。原本要起跳在空中被拉回到地面,同时本来的技能CD被瞬间刷新。
要保证手感做的事情则是客户端先行,客户端的本地输入总是领先服务器。守望先锋的客户端领先buffer + half RTT的时间于服务器,half RTT一般指的是网络往返时延的一半,buffer则是一个动态可控制参数,有点类似于TCP协议里的滑动窗口概念。buffer的目的是为了让服务器缓存一定的命令再发送,避免网络波动导致发送的输入结果的波动。
但是为了尽可能减少服务器对客户端的落后,buffer应该尽可能小。这里守望先锋的RTT定义是Ping时间,不过后来改成了Lag,定义为Ping+命令执行时间。
基本指令环为:客户端按照一定帧率获取输入,这里假设每帧——通过每帧输入,客户端先行执行GamePlay逻辑,同时发送输入指令到服务器——经过一定延迟后服务器受到输入指令并执行GamePlay逻辑并给客户端广播执行结果——经过一定延迟后客户端受到执行效果。
然而客户端先行肯定会引起延迟范围内与服务器没有同步执行的情况:假设上述例子猎空在移动的时候被眩晕,导致移动终止,那么客户端必须把先行部分纠正拉回。
上面预测,裂空再跑,可以看到24帧的时候还在跑的状态。此时17帧服务器算到结果是猎空吃了一个麦克雷的闪光弹,但是客户端因为延迟目前预测不到。客户端在收到服务器17帧的纠正包后,先把裂空拉回到当时的位置,会把17帧之后的所有输入重播一遍。
客户端在17帧依然继续做预测,猎空一直都在眩晕状态,预测到33帧的时候眩晕结果,可以继续跑,同时服务器也模拟了同样的结果,收到包也无需纠正。
处理丢包
守望先锋对出现丢包的情况做了一些外插值和动态处理。服务器在20帧意识到客户端没有输入数据包,此时客户端会把之前的输入复制过来,并通知客户端丢包了。
客户端收到包,会提高他的模拟速度,如从16ms提高到15ms,以模拟更多的输入指令包。同时服务器会提高buffer大小,目的能在丢包的时候处理更多的输入,避免复制输入造成的错误模拟。
如图客户端会模拟更快,不过他的先行时间是一样的因为他的模拟速度快了。这里的服务器buffer虽然变大了,但是因为客户端发送频率提高了,实际上延迟是一样的。当服务器检测到客户端没有丢包正常了,就会慢慢缩小buffer,同时客户端也回复到原来的发送频率。而实际上,丢失的输入,用复制上一帧的方式来填补,是一种外插值,容易造成结果错误,对玩家体验不好。要避免这种做法,可以在客户端发送的每个包里保存之前15帧的输入,来填补丢失的包。
Halo的输入预测
在GDC2011有个Halo Reach的分享,讲的也是本地预测的实例。其预测部分也做了有趣而合理的处理。
如上图这是一个扔手雷的流程,客户端按下输入键,通知服务器要扔手雷,并等待服务器返回才开始播放扔手雷动画。黄色字体的部分就是延迟,会让玩家感受到,体验很差。
然后后面这张图是不等待服务器返回,直接播放扔手雷动画。这样确实能隐藏延迟,这是一种不经过服务器允许就自行播放的做法,会造成玩家流程不连续的体验。譬如客户端已经播放了扔手雷的动画,还生成了手雷和特效,服务器却认为手雷扔出失败,需要客户端删除手雷和特效,可能造成延迟严重的人各种表现错误。
然后下面是最终使用的方案:按下按键通知服务器,马上播放手雷动画,服务器收到了开始模拟动画并创建手雷弹道,返回客户端,客户端一定时间后才创建手雷。这样保证了避免回滚造成的表现混乱,也能降低玩家的延迟感知——游戏中扔手雷的手臂占了三分之一屏幕,玩家是很难感受到手雷生成的延迟的。
Halo还有其他例子——背刺和无敌盾,具体可以看GDC2011视频。
延迟补偿
延迟补偿算是多人射击游戏特有而重要的一种提高射击体验的手段,概括来说就是为了解决玩家射击的目标,或者说发出射击指令时射击的目标,经过延迟后可能目标已经不在目标点了,也即是玩家射击的目标其实对服务器来说是数秒前的位置。还是以守望先锋的分享为例:玩家向死神射击,这时候根据本地的结果,应该是打到墙上了。但是因为延迟的存在,服务器会检测到客户端打中了。这样会不符合玩家预期,并非所见即所得。
Source引擎的延迟补偿有个理论公式:
补偿时间 = 当前服务器时间 - 延迟 - 客户端修正插值
Valve的做法是:服务器会保存所有玩家一秒的历史快照,执行射击命令时把所有其他玩家移回到补偿时间,执行射击扣血并移回来。
补偿时间如上所示的公式,注意这是预测的时间,有可能有一定的误差。如图红色是客户端的hitbox,服务器上人已经往前走了一段距离了,补偿返回的结果是蓝色的hitbox,存在一定的误差。
为什么客户端不直接告诉服务器要回滚的时间点这样更准确?因为客户端不可信,客户端可以控制回滚的时间从而实现作弊。
然而对于守望先锋来说,它技能复杂,环境也复杂,这种延迟补偿是不够的。守望先锋是有运动的运载目标,移动平台和盾的。这些都有可能导致阻挡击中目标。所以会导致守望先锋要回滚的东西很多。使用快照Volume,保存半秒的物体活动范围,如果射击的射线与Volume相交,则进行回滚,否则无须回滚,减轻服务器压力。
延迟过高的时候,延迟补偿会对受击者体验及差,过大的补偿可能让明明回到掩体后面的射击者被击中。守望中超过220ms会让射击者不在进行延迟补偿,并且因为延迟过大,本地的射击预测就没什么必要了大部分都是错误的,会关闭射击预测。
总结
多人游戏延迟实际上不同的游戏还有很多各种各样的Trick,目的都是为了玩家更好的体验。根据HaloReach的分享,总结了比较有价值的几条思路:
1、划分好游戏那部分需要服务器准确验证
需要区分好那部分要服务器裁决的,裁决越多,游戏越公平。可预测的地方越多,游戏响应性越强,手感越好。把这两部分划分好非常重要。
2、总是要认真思考:在哪里隐藏延迟?
学会在哪里隐藏延迟合适:有延迟的部分就有服务器验证,没有的验证的部分就没有延迟。
3、不要害怕改变游戏机制,来改善游戏网络体验
halo的工程师举了个例子:两个玩家在同一时间砍了对面, 以策划预期来说,血多的活下来,血少的死了。但是互联网来说这难以控制,因为两台客户端延迟不一样,所以他们通过三个月的测试和玩家邮件反馈,做了简化的处理:这种情况同时死亡。策划会觉得这样毫无策略深度,但这样在网络上工作得更好玩家感受上更公平
4、预留多时间来迭代网络体验
halo所说,他们在alpha版本,测试你游戏的什么情况都有:在林间小屋玩你游戏的、在车上玩你游戏的还有在树上玩的还有各种IPS运营商差异,各种高延迟高丢包的情况,能出现各种奇奇怪怪的bug。预留更多时间来迭代,改善玩家的网络体验,不断跟测试玩家交流。
守望先锋发布的时候,网络技术负责人还组织了一次对玩家的网络延迟科普,描述守望先锋是怎么做延迟预测和补偿的。演讲非常通俗,同时也征求玩家意见。而且不少游戏,ow、halo、tf、valorant、cs:每个游戏的延迟对抗做法都不一样,有些追求公平,如valorant,不计成本的解决Peeker’s advantage问题。有的追求爽快感。不断跟玩家沟通,在不违背游戏本身设计初衷的前提下,修正网络体验,在公平与爽快,稳定与合理,艺术与科学中找到一个平衡点。