这篇专栏目前打算写两个部分,第一个部分是弱网络解决方案以及帧同步的总结;第二个部分主要讲基于ECS架构下的状态同步解决方案。
如果有时间的话应该还会写几篇关于UE4的网络同步原理的文章。
废话不多说,那些网络游戏的前世今生以及远古网络游戏的同步技术这里就不逼逼了,Let's get into the point!
弱网络下为什么用UDP而不用TCP
弱网络下其丢包、乱序的情况比较常见,延迟也相对有线网络来说更加大,所以手游在弱网络下无法直接照搬端游的那套解决方案。所以手游必须的同步方案必须要另辟蹊径,手游中不用TCP的原因主要有两个:
1. 了解TCP协议的同学应该知道TCP有一个机制叫做拥塞控制,这个机制之所以存在的原因是因为考虑到网络阻塞带宽不够的情况下,发送方要减少发包的速度来缓解网络拥塞,但是这个东西放在游戏上就是个极其傻逼的机制,不管是游戏服务器还是客户端,一旦出现丢包的情况,应当立即重新发包,不然的话就会带来玩家体验上的问题,比如延迟过大,拉扯严重。
TCP的Nagle算法,简单来讲TCP为了避免网络拥塞,减少小包在网络中的数量,通过合包的机制来提高数据利用率,但是这种傻逼机制对于游戏来说也显然会加大延迟,不过好在这个机制可以用过TCP_NODELAY这个标志来禁用,但是就算不延迟发包也无法拯救TCP在手游上糟糕的表现。
为什么用UDP呢?因为UDP够快,包头小,但是UDP不保证按序到达,没有超时重传机制,所以为了保证可靠性可以在上层逻辑中来对收到的UDP包进行一定的处理来保证可靠性。
在手游中大量的使用UDP主要是因为UDP可以通过冗余包这个杀手锏来抵抗网络抖动。为了保证UDP包的按序到达,接收端收到乱序的包必须直接丢弃,发送端发包之后注册Timer,一旦超时则进行重传。为了减缓网络抖动带来的影响,有人发明了冗余包这么个东西,它实际上是一种空间换时间的做法,每次发包自动带上前面几个逻辑帧的包,这样的话,就算某个包在传输的过程中丢失,也会有很大的可能在后续的包中收到它的冗余包,这样就能极大的缓解网络抖动带来的影响,它的缺点就是流量开销,耗电量和发热量增大。
帧同步的手游有很多,代表性的比如王者荣耀。帧同步的机制比较简单,开发起来每个客户端基本都是差不多的套路,服务器仅仅是简单的把客户端操作挨个转发的所有其他的客户端上,每个客户端都必须要严格的进行全量模拟,这个也是为什么帧同步很难做大世界大战场游戏的原因。
帧同步必须要保证每一个客户端在相同的状态收到相同的输入一定要产生相同的输出,这是帧同步最大的特点也是帧同步难点之一。为了保持每个客户端的步调严格一致,一般来说不能使用物理引擎,就算有也要自己实现消除浮点误差的数学库,另外在游戏开始之前每个客户端要有相同的随机种子,一般在服务器上生成,统一下发到每个客户端。
我们知道,本地客户端要领先服务器至少半个RTT(Round-trip-time)的时间,为什么说至少呢,因为服务器每次的处理周期是一个逻辑帧间隔,对于中途收到的包必须要等当前逻辑帧结束才能处理,所以实际上是半个RTT~半个RTT + 一个逻辑帧间隔时间。而服务器又要领先other至少半个RTT时间,所以对于MOBA游戏来说,在世界时间轴上保持客户端的一致性是比较重要的,所以本地客户端必须要有一个输入延迟,来等待其他客户端收到服务器replicate过去的命令帧之后才能执行,这个输入延迟一般是动态变化的来适应不同网络条件下不同的RTT时间。
比如在游戏里面ClientA锤了ClientB一下,那ClientA就会先把这个命令帧发送给服务器,服务器收到之后转发给ClientB,这个时候ClientB计算出自己被锤了,于是ClientB就播放受击动画,与此同时ClientA输入延迟了一个RTT时间,于是这个时候ClientA就和ClientB就在世界时间上保持了一致性,也就是说你在任何客户端上看到的表现都是一致的。
在帧同步中,客户端是受到服务器发过来的命令帧驱动的,也就是说服务器发的包如果没有办法及时到达客户端(弱网络下相当常见),那客户端就没有帧可以处理,它将freeze在原地,表现就是卡顿。传统的帧同步在解决卡顿的问题上引入了Buffer机制,你可以理解为看视频时候的缓冲,如果卡的话我先暂停缓冲一波,再播放就不卡了。帧同步的buffer机制也是类似的道理,如果buffer的size为4的话,我不收到第五帧我是不会去处理第一帧的。但是buffer机制的缺点显而易见就是延迟可以巨他妈大,所以对于MOBA游戏比如LOL王者荣耀来说,这种延迟下你玩个蛇皮。在王者荣耀的帧同步方案中实际上是取消了buffer的,因为实时性太重要了,不能延迟,那么卡顿的解决方案就是客户端预表现+平滑插值,收到逻辑帧之后再将所有角色通过平滑插值的方式拟合过去。
帧同步的客户端预表现方式应该和状态同步other的预表现类似,具体可以了解一下DeadReckoning。
帧同步的回放是比较好做的,因为每个客户端本地可以接受到所有其他客户端的操作序列,那么回放实际上就是把所有操作序列缓存下来再重新模拟一遍就可以了。
帧同步的断线重连比较蛋疼,因为一旦掉线,比如游戏崩溃,那么所有运行时的状态都没了,你本地客户端连上来怎么才能直到现在世界跑到什么地方去了,其他客户端的状态是怎么样子的。所以帧同步的回放一般是在服务器上缓存全量数据,如果掉线的客户端发送重连请求,那么服务器会发送全量数据给该客户端,该客户端重头模拟一遍来赶上世界当前状态,这就是为什么帧同步的游戏断线重连的loading时间比较长的原因。
帧同步由于所有的逻辑都是在本地计算的,而客户端代码由于是发布出去的,对于玩家来说基本是可见的。所以反作弊的压力会相当大,因为外挂开发者水平太高,只要钱给到位没有破解不了得游戏。
客户端在作弊检测方面可以采用对关键数据进行hash然后上报服务器,服务器通过收集得到不同客户端的hash值比较,如果发现hash不同的客户端就把他踢出局让他断线重连来重新模拟得到正确的数据。但是有一个特殊情况就是1V1的SOLO局,如果有一方作弊那服务器显然不能根据客户端的Hash值来找出作弊的一方,这种情况下王者荣耀的解决办法是服务器额外跑一份逻辑来做仲裁。
个人感觉网络同步问题的定位和debug非常困难和烧脑,一般要通过大量的log+录像回放来的定位到不同步的逻辑帧,再进行debug。但是好像王者荣耀有一些高级一点的工具能对关键代码进行标记跟踪来定位不同步的问题,感兴趣的鹅厂同学可以去Q-Learning上看一下相关讲座录像,具体这里就不细说了。