解决不同步问题首先考虑什么是不同步的问题,为什么会出现不同步的情况。
那么什么是不同步的问题呢?帧同步是由客户端计算,发送命令给服务器,服务器转发命令达到两边客户端显示一致。服务器是不知道具体逻辑的,所以如果两边客户端计算的某些变量出现不一致的时候,就会出现不同步的情况,随着时间的推进,一个小小的不同步也会造成滚雪球效应,最后的结果可能就南辕北辙了。
所以为什么会出现不同步的情况,也很好解释了,两边客户端无法保证输入的一致性,计算的一致性,就无法保证唯一的输出。
首先,一个战斗系统的可玩性很大的一点是战斗的伤害具有随机性,幅度会在一定范围内随机显示,然后还有几率出现暴击;而帧同步的特性就是要消灭这样的随机性,两边客户端在同一时间计算出来的伤害一定是一个数字。那么如何控制这个随机性呢?通过服务器在帧同步开始的时候下发同一个随机数种子,通过自定义的随机数算法保证这个数字看上去是随机的,但是实际上是必然的。
其次是计算过程中的精度损失,最为关键的就是浮点数导致的精度损失,在不同的硬件环境下,相同的浮点数可能会造成不同的运算结果,虽然这个结果在这一帧内影响有限,但是由于滚雪球效应,最终的战斗结果可能就会出现非常明显的不同步问题了。对于这个问题,采用定点数去代替浮点数做计算,使用一个万分比的参数去转换定点数和浮点数,这样就能避免规避掉浮点数的问题。
然后是逻辑顺序执行不一致,这个一般是由于使用到了一些插件,而这些插件的Update并不能由帧同步去控制,导致两边客户端可能执行某个计算片段的时间并不是在同一逻辑帧,所以解决这个问题的方案就是,不要使用这些插件,尽量去使用开源插件或者自己写,保证Update函数掌控在自己的逻辑中,去避免出现不同步的问题。例如物理系统、动画系统、AI等这些插件,就尽量去使用自己写的,保证一致性。
然后是网络接收数据的先后顺序不一致的不同步问题,通常的帧同步为了保证网络的流程,会丢弃掉成熟的TCP,采用UDP去自行实现可靠的网络收发,但是如果一旦不考虑清楚网络的复杂性,就会出现网络波动的情况下,后发的消息可能会比先发的消息先发送到接收端,如果这时候接收端不进行处理的话,就会出现接收端先处理后一条消息,再处理前一条消息的情况,这样输入的顺序不一致,也是会造成最终的不同步的。解决方法也很简单,对每一个发出的消息做一个自增的编号,根据编号的连续性确定消息的顺序,就算先收到后面的消息,也可以等待前面的消息收到之后进行顺序传入游戏逻辑中。
总结来说,就是保证客户端的输入一致,计算一致,就能解决帧同步的不同步问题。
最后,一个优秀的帧同步系统,首先应该尽量去避免不同步的问题,其次就是在出现不同步问题的时候,能快速查找出问题的根源,然后去解决问题。
2019年9月10日新增
最近半年一直在做帧同步战斗这块,对于解决不同步问题又有了一些新的感悟,因为项目人员变多,上一个项目在小公司,做战斗就一两个,非常好控制,现在换到了腾讯,战斗差不多十个人在做,几乎每天都能遇到各种各样的不同步问题,在这里做一个阶段性的总结。
首先是之前说的随机数种子,项目demo阶段为了快速开发,多人协作,表现层有时候会调用到不少逻辑API,而这些逻辑API里面可能会调用到随机数生成算法,这样就可能造成A客户端随机数调用了3次,B客户端调用了4次,最后真正逻辑去调用的时候,两边生成的随机数就不一致了。我们称之为随机数污染,解决方法就是讲表现层和逻辑层彻底分离,表现层不回调逻辑层任何代码,当需要用到逻辑层的接口的时候,一定要注意这个接口只是提供const返回,而不修改任何逻辑层数据。
然后还有一个是在逻辑层杜绝Dictionary的使用,Dictionary内存放的数据是无序的,如果需要遍历的话,就会造成AB客户端遍历的顺序不一致,最后导致不同步,如果想要使用字典的话,需要自行实现一个确定顺序的。同样的情况还出现在List.Sort上,Sort的原理是通过快排去实现有序排列的,而快排是不稳定的,所以也需要自行实现排序算法。
还有一点出现在断线重连追帧重回,断线重连的时候为了快速重连,一般都是通过快照的方式实现的,比如每隔1000帧上传一份客户端的快照,但是随着客户端的逻辑修改,快照很多时候都会漏数据,这时候就需要有一个强大的框架去实现快照功能,避免开发的时候因为需求变动导致断线重连不同步的bug。
同样是快照,还会造成另外一些问题,有些程序在代码中会使用一些静态变量,而快照不是从第0帧开始计算的,这时候这些静态变量的值可能就没有正确的计算,导致最后重连回来不同步。比如说我在某个类里面定义了一个静态变量count,这个变量的含义就是在游戏中调用了函数function多少次,而A客户端没有断线重连,正常执行下去,count=10,而B客户端通过快照重回,count=5,这时候两个客户端就不一致了。
还有一点是数据、代码版本一致,一定要在进入战斗之前确定好进入帧同步战斗的所有客户端数据、代码版本完全一致。
最后非常值得注意的一点是,表现层的报错千万不能影响到逻辑层,最好的方法就是表现层update和逻辑层的update分离,先执行完逻辑层的update再执行表现层的update。
10月19日补充
今天出现了一种不同步,由于我们新开发的功能没有做好表现层和逻辑层分离,逻辑层直接调用了表现层的接口,然后A手机切后台回来,执行了一些逻辑,导致调用进去表现层抛出异常了,B手机无此异常,此时AB逻辑层不同步。这个问题由于表现层和逻辑层没有彻底分离导致的,解决方法就是逻辑层不直接调用表现层的数据,而是通过Command的方式去通知表现层,表现层通过表现层单独的Update去轮询Command,然后去处理。