思来想去还是在开篇写一下自己相对比较熟悉的网络模块,因为它对于刚入门做网游的同学来说,是一个必须要攻克的知识点,可以作为联网Gameplay开发学习的入门着手点。
我这里想以比较容易理解的问答方式阐述下我对UE网络的理解:
1.什么是网络同步?
多人游戏里面需要把某个玩家操作的结果通知给其玩家,这个通知的过程就是同步,再放到局域网或者广域网中进行,就是网络同步了。
2.点对点还是中介转发?
点对点(P2P)就是N台机器相互直连,每台机器需与剩下N-1个机器建立连接,复杂度为O(N^2),而且增删机器也非常麻烦,对于大型网游来说这个方案基本不考虑。
中介转发就是我们常说的客户端/服务器模式(C/S),添加一个服务器作为中介节点,每台客户端只与服务器建立连接,客户端与客户端彼此独立,服务器负责转发消息。这种架构的复杂度仅为O(N),维护起来也方便,所以网游几乎清一色的采用这套架构。
3.帧同步还是状态同步?
帧同步是服务器将某个玩家的输入直接转发给其他玩家,自己不做处理。理论上所有客户端都以相同的初始状态开始,只要收到的输入相同,那么每时每刻的状态都会是相同的。
状态同步是服务器只同步影响游戏功能的某些重要状态变量,并且这些重要变量是在服务器运算出来的或者至少校验过的,客户端拿到这些状态变量后自行做本地的表现。
一般来说帧同步在实时性、节省流量方面比较好,状态同步则在安全性角度来说更胜一筹。具体选用哪种方案由具体游戏类型来决定,UE是在射击游戏基础上发展而来的,它默认的网络同步方案是状态同步,把决策权放在服务器上做,可以有效减少外挂,对于中途加入/断线重连也能天然支持。
4.TCP还是UDP?
TCP连接的优点是可靠稳定,但速度慢这个缺点导致它并不适合网游。但UE也并未照搬UDP连接方案,而是在这个基础上融合了TCP的优点,例如加入了乱序处理,以及对reliable的包丢失重传。可谓是各取所优,既保证了连接速度,也保证了可靠性。
5.UE如何同步重要状态变量?
保守的方法是把这些重要状态变量每时每刻都对所有客户端进行广播,但这样会造成流量的问题,也会带来客户端本地的计算复杂性。对于UE来说,需要同步的重要状态变量都存在于Actor这个容器里。UE网络同步的设计方案有以下两个特点:
(1)尽可能节省带宽:
相关性计算:对于像吃鸡这种大世界玩法,不需要每帧都同步一百个玩家的状态,对于A玩家而言,只需要同步他可以看到的或者会受到影响的那部分玩家就可以了,这就是相关性的计算。
优先级计算:判断相关后还会进行优先级计算,优先同步高优先级的状态变量,如带宽饱合则当帧不会同步低优先级的状态变量。
成员变量计算:需要同步的Actor里面会包含很多成员变量,但不是每个成员变量都需要同步,只有开发者打上网络同步标记的变量才会被同步,而且这个变量只有在服务器端发生变化了才会同步给相关的客户端,甚至还有额外标记控制同步,比如bNetInit只在刚建立同步通道时同步。
合包:每个包里都会有额外的包头信息,如果包拆得越细那么占用带宽就会越多,UE里面将多个Bunch(最小的同步单位)合入到一个Packet里面进行发送,再在收端分拆回Bunch进行逐个处理。
(2)尽可能可靠:
丢失重传:reliable的包在发端会保存一个备份,只有收到收端返回的Ack包确认后才会清掉,若收到的Ack包跳序则会触发重传。
Packet乱序整理:因网络链路复杂性,到达包顺序可能与发端不一致,这时会进行整理,对于分包导致的不完整的部分(Partial)包也会等待重组。
包头校验:包头存有内容块的大小,如果不符合,则会丢弃这个包,防止坏包带来的后续数据处理异常。
6.成员变量同步还是远程函数(RPC)?
成员变量同步的持久性较好,RPC则是瞬发的,玩家断线重连后采用变量同步的方式仍是正确的,但RPC同步的东西却不会再重放。
成员变量同步仅发生在服务器到客户端,RPC则是双向的,如果想客户端同步给服务器什么东西,那没得选只能RPC。
RPC的实时性比成员变量好,因为RPC调用是瞬时发到远端执行的,而成员变量同步还要等packet满了之后才发车,不过这个因素其实不那么重要,两者相差很小。
7.如何正确又优雅地使用UE网络同步功能?
提三点建议:
其一:一开始做功能先别管网络,优先单机,单机表现不正常剩下的都是白谈。
其二:大概看下UE官网对于网络同步的使用,知道有哪些关键字(不多很简单),再结合自身项目代码或者ue4的sample工程(如shooter game),看下别人如何使用的,理解后学以致用,写好后在编辑器里面选2人勾DedicatedServer跑下是否正常。值得一说的是,UE4新加了component的同步,要善用这个模块化的利器,不必像UE3一样把同步变量都堆在Actor上。
其三:知其然更要知其所以然,使用起来方便但背后设计却非常复杂,要静下心下跟跟代码,看看前面几位大神的文章,对网络实现细节深入理解。
8.大世界同步方案ReplicationGraph
上面介绍了下UE经典网络同步方案,适用于绝大多数射击游戏同步场景,但眼下吃鸡玩法正兴,Fortnite大世界有多达50000+个同步对象,按照传统的同步方案会给服务器的CPU带来巨大的压力。
专用服务器(Dedicated Server,简称DS)CPU的消耗位于网络同步的发送端,因此我们先回顾下经典网络同步方案中的发端算法。
举个图例,DS每帧会遍历游戏世界里的所有对象,这里假定是A1,A2,A3,排除掉不需要进行同步或者不相关的A1,再根据A2和A3的同步属性划规到不同的连接通道Connection里面,这里A2与所有客户端连接都相关,而A3仅与Connection2相关。因同步带宽有限,只能保证高优先级的Actor最先同步,所以需要进行排序。排序后就是每个Actor内部的同步属性比较,以及将变化属性的地址和变化量写入包中并发送。
注意经典发端算法存在的几个问题:
(1)每帧遍历所有Actor,还要逐个计算每一个Actor与每一个客户端连接的相关性,即使这些相关性是确定的(如同队玩家)。
(2)一些场景中固定不动的同步对象,比如拾取物Pickup,房屋门窗(因有破坏性所以需同步)都会在每帧计算相关性,而客户端玩家在有限时间内不太可能有较大范围的空间移动。
以上,如能利用起大世界的空间相关性,就可以进一步优化网络同步的发端算法。
一种想法是将大世界网格化,每一个需要同步的Actor都会落在其中一个格子里,那么只要规划好每个格子的大小,就只需要同步玩家周围的九个格子里面的Actor即可,如下图所示橙色区域所示,还要包含玩家蓝色太阳自身所在的格子,形成九宫格:
这样对于静态物体,比如拾取物pickup,就不再需要每帧计算相关性了,只要有玩家当前所在位置,周围九宫格内(含自身)的Actor都是需要同步的。
对于动态物体,比如其他玩家,他们的位置每帧都可能在变化,仍可空间化到具体的栅格中来,只需要每帧更新玩家所在的栅格即可。
所以对一个客户端玩家来言,每帧需要做两件同步相关的事情:
(1)根据位置拉取它周围九宫格内的同步对象
(2)自身位置变换时需更新所在栅格,要保证其他客户端连接能及时地同步自己。
上述算法已经考虑到了空间相关性,但每个格子划分多大也是要讲究的,划分太大则同步对象太多,CPU负担降不下来,划分太小又可能因为同步不及时带来体验上的问题(eg:被一个看不见的敌人杀掉)。
对于Pickup来说,因为大多集中在室内,所以同步距离可以少一些,但对于玩家来说,同步距离则要大到视距,这样格子划分的大小很难统一。保守做法就是统一按最大视距来划分格子,但这样优化量就很少。另一种做法是分门别类,根据类型划规不同大小的栅格,然后分开处理不同类型对象的同步,缺点就是存在多套栅格系统,是一种以空间换时间的做法。
UE4的ReplicatonGraph解决了上面的问题,相较于上述将一个对象放置于一个栅格中,转而设置这个对象的多个影响栅格,具体如下图示:
每个对象设置了一个同步相关的CullDistance,
将之栅格化:
橙色区域为其影响的栅格:
只要客户端玩家走进了橙色栅格中的任意一个,都会收到这个太阳对象的同步信息。以下示例:
这个方案的优点是可以根据实际需要设置不同的CullDistance,同时保证了栅格自身大小可以固定不变。而且一般地,相同类型对象的CullDistance是相同的,即只要把CullDistance这个变量配置到类Class身上即可。
这样Class配置会很多吗?当父类Class与子类Class的关键同步属性相同时,只会考虑父类Class,所以不必担心。
同样分析下静态/动态物体的空间化同步,对于静态物体,位置总是固定的,因此不需要实时计算影响栅格,对于动态物体,因为空间相关性,短时间内栅格变化的概率也比较低,即使有变化,也只是少数几个,不必要每帧都新算一遍影响栅格,大家可以研究下UE4是如何增量计算栅格变化的。
以上是需要空间化的Class,还有一些诸如总是同步的对象,仅与某个客户端连接相关(或小队客户端连接相关)的对象,UE4都是分开处理的,具体大家可以参考官方示例ShooterGame里面的ReplicationGraph配置。
总结一下,采用ReplicationGraph充分利用了大世界Actor虽然多但却具有空间相关性的特性,并且考虑不同类型Actor的特性(总是相关的,与特定连接相关的,以及空间化相关的),有效减少了不必要的相关性计算,从而有效节省服务器CPU的负载。
专栏地址:https://www.zhihu.com/people/jerry-3-96/posts