在上一篇文章中
Jerry:UE4网络同步思考(一)---经典同步方案zhuanlan.zhihu.com介绍了下UE经典网络同步方案,适用于绝大多数射击游戏同步场景,但眼下吃鸡玩法正兴,Fortnite大世界有多达50000+个同步对象,按照传统的同步方案会给服务器的CPU带来巨大的压力。
专用服务器(Dedicated Server,简称DS)CPU的消耗位于网络同步的发送端,因此我们先回顾下经典网络同步方案中的发端算法。
![475a7bef21e1b55ef85692265bfc68a3.png](https://i-blog.csdnimg.cn/blog_migrate/16d151e51b3d10487675250f5f443995.jpeg)
举个图例,DS每帧会遍历游戏世界里的所有对象,这里假定是A1,A2,A3,排除掉不需要进行同步或者不相关的A1,再根据A2和A3的同步属性划规到不同的连接通道Connection里面,这里A2与所有客户端连接都相关,而A3仅与Connection2相关。因同步带宽有限,只能保证高优先级的Actor最先同步,所以需要进行排序。排序后就是每个Actor内部的同步属性比较,以及将变化属性的地址和变化量写入包中并发送。
![5748b8b29b4fafbb7ac877e34d390036.png](https://i-blog.csdnimg.cn/blog_migrate/c0cf8e4c1957e3baab4266bff06665e9.jpeg)
注意经典发端算法存在的几个问题:
(1)每帧遍历所有Actor,还要逐个计算每一个Actor与每一个客户端连接的相关性,即使这些相关性是确定的(如同队玩家)。
(2)一些场景中固定不动的同步对象,比如拾取物Pickup,房屋门窗(因有破坏性所以需同步)都会在每帧计算相关性,而客户端玩家在有限时间内不太可能有较大范围的空间移动。
以上,如能利用起大世界的空间相关性,就可以进一步优化网络同步的发端算法。
一种想法是将大世界网格化,每一个需要同步的Actor都会落在其中一个格子里,那么只要规划好每个格子的大小,就只需要同步玩家周围的九个格子里面的Actor即可,如下图所示橙色区域所示,还要包含玩家蓝色太阳自身所在的格子,形成九宫格:
![6a03c3ce2aae5d762bc2ad37ec36f73f.png](https://i-blog.csdnimg.cn/blog_migrate/93dfe21240674953e21825fea31bb42e.jpeg)
这样对于静态物体,比如拾取物pickup,就不再需要每帧计算相关性了,只要有玩家当前所在位置,周围九宫格内(含自身)的Actor都是需要同步的。
对于动态物体,比如其他玩家,他们的位置每帧都可能在变化,仍可空间化到具体的栅格中来,只需要每帧更新玩家所在的栅格即可。
所以对一个客户端玩家来言,每帧需要做两件同步相关的事情:
(1)根据位置拉取它周围九宫格内的同步对象
(2)自身位置变换时需更新所在栅格,要保证其他客户端连接能及时地同步自己。
上述算法已经考虑到了空间相关性,但每个格子划分多大也是要讲究的,划分太大则同步对象太多,CPU负担降不下来,划分太小又可能因为同步不及时带来体验上的问题(eg:被一个看不见的敌人杀掉)。
对于Pickup来说,因为大多集中在室内,所以同步距离可以少一些,但对于玩家来说,同步距离则要大到视距,这样格子划分的大小很难统一。保守做法就是统一按最大视距来划分格子,但这样优化量就很少。另一种做法是分门别类,根据类型划规不同大小的栅格,然后分开处理不同类型对象的同步,缺点就是存在多套栅格系统,是一种以空间换时间的做法。
UE4的ReplicatonGraph解决了上面的问题,相较于上述将一个对象放置于一个栅格中,转而设置这个对象的多个影响栅格,具体如下图示:
每个对象设置了一个同步相关的CullDistance,
![69c4cfbc70046bcb80cabe687e3da70e.png](https://i-blog.csdnimg.cn/blog_migrate/2bfbc19896013ef9de4003f8e12eaf1f.jpeg)
将之栅格化:
![956514666d8a1ffd1d625b808dbdd32e.png](https://i-blog.csdnimg.cn/blog_migrate/f40d19d85e7610216aff3aac1ccd8495.jpeg)
橙色区域为其影响的栅格:
![8b698410b21204d19cc67cbdbed40dd1.png](https://i-blog.csdnimg.cn/blog_migrate/3299f7e41b018ab050c7e1f4c150ef6e.jpeg)
只要客户端玩家走进了橙色栅格中的任意一个,都会收到这个太阳对象的同步信息。以下示例:
![28aaaf96dcfde802b9f605efcaffd298.png](https://i-blog.csdnimg.cn/blog_migrate/59e3263d04874b7f927d2c9453ad4422.jpeg)
![99ac16bb328a350c734d6dd8bbb6d259.png](https://i-blog.csdnimg.cn/blog_migrate/18b85b80b8d3005e569c760199f884f9.jpeg)
这个方案的优点是可以根据实际需要设置不同的CullDistance,同时保证了栅格自身大小可以固定不变。而且一般地,相同类型对象的CullDistance是相同的,即只要把CullDistance这个变量配置到类Class身上即可。
这样Class配置会很多吗?当父类Class与子类Class的关键同步属性相同时,只会考虑父类Class,所以不必担心。
同样分析下静态/动态物体的空间化同步,对于静态物体,位置总是固定的,因此不需要实时计算影响栅格,对于动态物体,因为空间相关性,短时间内栅格变化的概率也比较低,即使有变化,也只是少数几个,不必要每帧都新算一遍影响栅格,大家可以研究下UE4是如何增量计算栅格变化的。
以上是需要空间化的Class,还有一些诸如总是同步的对象,仅与某个客户端连接相关(或小队客户端连接相关)的对象,UE4都是分开处理的,具体大家可以参考官方示例ShooterGame里面的ReplicationGraph配置。
总结一下,采用ReplicationGraph充分利用了大世界Actor虽然多但却具有空间相关性的特性,并且考虑不同类型Actor的特性(总是相关的,与特定连接相关的,以及空间化相关的),有效减少了不必要的相关性计算,从而有效节省服务器CPU的负载。