游戏后台状态同步与帧同步

版权声明:本文为博主原创文章,转载请注明出处http://blog.csdn.net/jiange_zh https://blog.csdn.net/jiange_zh/article/details/78509457

最近开始学习一下游戏后台的一些知识,一直很好奇多个玩家之间的数据是如何同步的,查了一下,目前使用的比较多的是状态同步和帧同步。

状态同步

同步的是游戏中的各种状态。

一般的流程是客户端上传操作到服务器,服务器收到后计算游戏行为的结果,即技能逻辑,战斗计算都由服务器运算,然后以广播的方式下发游戏中各种状态,客户端收到状态后,更新自己本地的动作状态、Buff状态,位置等就可以了,但是为了给玩家好的体验,减少同步的数据量,客户端也会做很多的本地运算,减少服务器同步的频率以及数据量,该方式多用于回合制的游戏。

状态同步其实是一种不严谨的同步。它的思想中,不同玩家屏幕上的表现的一致性并不是重要指标, 只要每次操作的结果相同即可。所以状态同步对网络延迟的要求并不高。像玩RPG游戏,200-300ms的延迟也可以接受。 但是在RTS游戏中,50ms的延迟却会很受伤。

举个移动的例子,在状态同步中, 客户端甲上操作要求从A点移动到B点,但在客户端乙上, 甲对象从A移动到C,然后从C点移动到了B。这是因为, 客户端乙收到A的移动状态时, 已经经过了一个延迟。这个过程中,需要客户端乙本地做一些平滑的处理,最终达到移动到B点的结果。(可通过增加动作前后摇来减少延迟——多播一点动画,给服务器多争取一些时间!)

帧同步

RTS(即时战略游戏)游戏常采用的一种同步技术 ,上一种状态同步方式数据量会随着需要同步的单位数量增长,对于RTS游戏来讲动不动就是几百个的单位可以被操作,如果这些都需要同步的话,数据量是不能被接受的,所以帧同步不同步状态,只同步操作。

1、帧率

大家小时候应该看过这样的小人书:快速翻看就可以看到漫画上的人物会动起来。

由于人类眼睛的特殊生理结构,如果所看画面之帧率高于每秒约10-12帧的时候,就会认为是连贯的, 此现象称之为视觉暂留。

游戏中的所有动画也是采用这种方式来渲染,只不过帧率是由GPU来控制,你所看到的画面都是由GPU一帧帧渲染的,比如30帧/s,你所看到的画面就比较流畅了,帧率越高你所看到的越流畅。

2、Lockstep——帧同步

帧同步可以说是通过帧率延伸过来的,你可以把一个游戏看成一个巨大的状态机,所有的参与者都采用同一个逻辑帧率来不断的向前推进。

  • 我们把游戏的前进分为一帧帧,这里的帧和游戏的渲染帧率并不是一个,只是借鉴了帧的概念,自定义的帧,我们称为turn。游戏的过程就是每一个turn不断向前推进,每一个玩家的turn推进速度一致。

  • 每一帧只有当服务器集齐了所有玩家的操作指令,也就是输入确定了之后,才可以进行广播(并不计算游戏行为),进入下一个turn,否则就要等待最慢的玩家,如此才能保证帧一致。

  • Lockstep的游戏是严格按照turn向前推进的,如果有人延迟比较高,其他玩家必须等待该玩家跟上之后再继续计算,不存在某个玩家领先或落后其他玩家若干个turn的情况。使用Lockstep同步机制的游戏中,每个玩家的延迟都等于延迟最高的那个人。

  • 由于大家的turn一致,以及输入固定,所以每一步所有客户端的计算结果都一致的。

这种囚徒模式的帧同步,因为某个玩家有延迟,而导致该帧的同步时间发生延迟,从而导致所有玩家都在等待,出现卡顿现象。

3、乐观锁&断线重连

囚徒模式的帧同步,有一个致命的缺陷就是,若联网的玩家有一个网速慢了,势必会影响其他玩家的体验,因为服务器要等待所有输入达到之后再同步到所有的c端。另外如果中途有人掉线了,游戏就会无法继续或者掉线玩家无法重连,因为在严格的帧同步的情况下,中途加入游戏从技术上来讲是非常困难的。因为你重新进来之后,你的初始状态和大家不一致,而且你的状态信息都是丢失状态的,比如,你的等级,随机种子,角色的属性信息等。

为了解决这个问题,服务器可保存玩家当场游戏的游戏指令以及状态信息,在玩家断线重连的时候,能够恢复到断线前的状态。不过这个还是无法解决帧同步的问题,因为严格的帧同步,是要等到所有玩家都输入之后,再去通知广播client更新,如果A服务器一直没有输入同步过来,大家是要等着的,那么如何解决这个问题?

采用“定时不等待”的乐观方式,在每次Interval时钟发生时固定将操作广播给所有用户,不依赖具体每个玩家是否有操作更新。如此帧率的时钟由服务器控制,当客户端有操作的时候及时的发送服务器,然后服务端每秒钟20-50次向所有客户端发送更新消息。(如果没有操作, 也要广播空指令来驱动游戏帧前进)

在这种情况下,服务器不会等到搜集完所有用户输入再进行下一帧,而是按照固定频率来同步玩家的输入信息到每一个c端,如果有玩家网络延迟,服务器的帧步进是不会等待的,网速慢的玩家不会卡到快的玩家,只会感觉自己操作延迟而已。

4、技能同步

游戏中有很多是和概率相关的,比如说技能的伤害有一定概率的暴击伤害或者折光被击等。按照帧同步的话,基于相同的输入,每个玩家的client都是独立计算伤害的,那么如何保证所有电脑的暴击伤害一致呢。这个时候就需要用到伪随机了。

大部分编程语言内置库里的随机数都是利用线性同余发生器产生的,如果不指定随机种子(Random Seed),默认以当前系统时间戳作为随机种子。一旦指定了随机种子,那么产生的随机数序列就是确定的。就是说两台电脑采用相同的随机种子,第N次随机的结果是一致的。

所以在游戏开始前,服务器为每个玩家分配一个随机种子,然后同步给client,如此每个client在计算每个角色的技能时候,就能保证伤害是一致的。

帧同步的特性导致客户端的逻辑实现和表现实现必须完全分离。Unity中的一些方法接口(如 Invoke, Update、动画系统等)是不可靠的,所有要自己实现一套物理引擎、数学库,做到逻辑和表现分离。 这样即使Unity的渲染是不同步的,但是逻辑跑出来是同步的。

王者荣耀网络同步方案分享

霸三国(端游):

采用Client-Server模式,服务器做判定,客户端纯表现。

​好处:

  • 安全,因为都是服务器计算,客户端只负责表现层的功能,不会影响各种判断的结果。
  • C端可以预表现,根据S的结果做修正、拉扯,抖动平滑,还允许丢包快速恢复。

缺点:

  • 需要同步的流量非常高(约等于本身的一个录像),后续加入东西需要传的内容会越来越多。(MOBA单位多,同步状态多,流量大)。
  • CS开发关联耦合多,联调、周期长
  • 同等网络条件下,CS配合高频度表现同步困难(客户端表现与服务端判定的完美匹配困难)

王者荣耀:

采用帧同步,对网络要求苛刻,下发的执行序列不允许丢包,如果中间出现丢包,需要等待丢的包重新到达后才能顺序后续执行。

技术要点:
lockstep是一种基于相同的初始状态,相同的输入,相同的处理逻辑,最终有相同的输出的同步方式。每个客户端开始load相同的数据,然后等待同步信号的驱动,每一个step到来之后,才能驱动推进一帧的update,帧间隔(delta)是相同的,如果没收到期望的那一个step驱动则需要挂起逻辑。每个客户端上报自己的输入参数,服务器按固定间隔将输入收集起来,带上step编号广播给所有客户端。

  • 相同的初始状态(不受画质,本地顺序和状态影响)
  • 完全一致的输入驱动内容及顺序
  • ​完全一直的代码执行流程(模块调用顺序,容器顺序)
  • 完全一致的随机数生成规则,一致的调用时机及次数
  • 尽量用整数实现各种游戏系统和基础数学库(浮点数运算有精度问题)
  • 避免本地逻辑(比如获取本地的时钟参数之类的,即要保证使用的参数都是所有客户端都一致的)

整体的网络结构,分三层:服务器、客户端逻辑层、客户端表现层。

服务器主要负责的功能有两部分:一是收集所有玩家上行的输入,把它按定时的间隔打包成输入的序列,投放给所有客户端;二是当客户端出现丢包的时候,服务器进行补发;还有把客户端上行冗余的信息替换掉,比如有新的输入到了,就把老的输入Drop或者替换掉。王者我们的逻辑是66毫秒一次,1秒同步15个包,这是不能少的,因为帧同步不能丢包,数据包必须有严格的执行序列。

客户逻辑层理解为客户端本地的服务,就是所有客户端运行的结果必须强一致,不能有真的随机,不能有本地逻辑,不能有浮点数的运算,拿到相同的输入,产生结果必须一致。

客户端表现层是根据逻辑层的数据去做Copy或者镜像,然后在表现层进行平滑,帧数不一样,但是不会影响最终的运算结果,只影响动画和动作的表现。

TCP技术当外网出现丢包或者抖动的时候,受限于实现方式,比如窗口、慢启动各方面的原因,会发现当出现重联的时候会非常卡,所以PVP没有用TCP,改为了采用udp。如果出现丢包,服务器会在应用层做补发。客户端不需要做反向ACK,因为帧是有序的,如果客户端收到一个1,然后收到一个3,那么肯定2丢失了,然后就请求服务器重发。
上行和下行都会有冗余,对于客户端消息上行的冗余,当客户端放了2个技能,收到回包之后,发现没有消息来抵消刚刚的操作,则会进行补发。对于下行的冗余,每一帧都会带上至少3帧的数据(会根据具体情况浮动),这样子如果最近3帧数据有丢包,则客户端直接可用,不需要再重发,减少延迟。冗余尽量放在同一个mtu里面。udp受限于mtu的大小,大于mtu,会出现分包,可能也会出现整包的丢失。所以我们也会有些比较大的包会在应用层由服务器做分包,中间出现丢包再由服务器补发,把零碎的包拼成整包再做解包。

帧同步的消息比较小,按照理论1秒15个驱动帧来算,20分钟的录像是10M左右。但是我们外网统计,正常的5V5对局20分钟,录像的大小大概是3M左右。服务器会把玩家的操作做纯内存的存储,当出现丢包的时候,服务器会通过编号快速找到缓存信息进行下发(可靠UDP下发)。同时根据丢包的情况,我们会计算给这个人发送冗余量的变化量。最开始发送每个包会冗余前面3帧的信息,如果丢包严重,我们会尝试冗余更多信息再下发。客户端拿到之后会尽量压缩逻辑执行的过程。帧同步有比较麻烦的模式在于,它不像CLIENT-SERVER的模式随进随出,崩溃之后重回必须从一开始运行,中间运算过程不能少掉。

一些尝试最后放弃:

  • 客户端上行之后,不需要服务器定时的间隔去做收集然后下发,而是通过染色帧编号直接下发,这样响应更及时,操作反馈更强、更快。当时我们做出来的结果是,这对手感的提升微乎其微,但是带来的负面问题却很大,因为不再是一秒15个包固定的下发,下发包的数量非常多,完全和这个人的操作习惯有关系,有可能一个人一秒之内产生了十几二十个输入,就需要把这些输入打包之后对客户端下发。客户端因为收包很多,设备也会明显发烫。

  • 传统的帧同步的方式会做延迟投递,这个我们也有尝试过。如果间隔时间内出现丢包,或者出现包下行的时网络波动,可以通过延迟投递这种方式抹平抖动和丢包的情况。我们尝试过这个方案但最终没有这样做的原因在于:《王者荣耀》里面一些英雄体验起来感觉偏动作,对反应要求比较快,延迟投递虽然抗抖动和抗丢包的能力确实不错,但是手感上达不到我们的要求。

  • 做CLIENT-SERVER方式的实现,一般都会有一个套路,客户端提前表现,根据服务器的表现做平滑或者拉扯。这个方案我们也尝试过,但最终还是放弃了,因为这个技术会让角色本身的表现有点发飘。客户端本地动,马上客户端表现就跟着动,但根据服务器的下行,其实会做一些偏移或者修正。当网络抖动出现的时候,角色会有一点发飘,所以这个方案我们放弃掉了。(目前的预表现是大概20帧,一秒多,之后如果没有收到包会卡住不动了)

  • 帧同步方案,所有客户端进行运算,期望产生一致的结果,但如果因为bug或者某个人使用修改器,跑出来的结果会和其他人不一样,当不一样出现,我们的说法是不同步了。我们会定时把一些关键信息提取出来做hash,不同步的人的hash和其他人会不一样。这是时候把这个人踢掉重连。

安全方面:

  • 举手表决:出现不一致时,多数结果者定为最终结果。
  • 对于争议局:客户端记录关键信息,结算时传给服务器,服务器做判定记录,看看哪个异常点多,多者无效。

猜你喜欢

转载自blog.csdn.net/jiange_zh/article/details/78509457