游戏服务端高性能框架:来看《天谕》手游千人团战实例
《天谕》手游设计了许多大规模的跨服团战玩法,人数最多可达到1200人。为了让游戏服务器能支撑如此大规模的团战玩法,我们做了许多优化工作,本文将介绍其中主要的几项重要工作。欢迎大家一同交流!

总体思路
业界已有的一些提升服务器承载人数的做法。一般是将同地图的玩家拆分至不同进程,在边界处需要创建玩家的镜像来解决可见问题。同时,在业务编程上需要通过异步访问的方式来处理不同玩家之间的交互行为,如战斗等。

经过调研后我们认为此类方式不太合适我们游戏,原因是:
1. 益达平台的许多活动中玩家可能会集中在一小片区域进行战斗,当人群过于聚集时,该方案逐渐失去优化效果:
(1)如果地图分块比较大,则等于没分;
(2)如果地图分块太小,则小片区域内玩家虽然都拆分到了不同进程,但彼此都是可能发生战斗的,因此各个进程上都需要创建大部分人的镜像,给镜像同步状态的开销会超过进程拆分带来的收益。
比如下面这种一线天的地形,人群都扎堆在一起,可能每个进程都要创建所有人的镜像。

2. 该方案下同地图的逻辑变为异步编程,降低了后续活动的开发效率,也对已有业务带来了巨大的改动,风险较大。
最后,我们确立了两条总体思路:
尽力挖掘单进程潜力,优化业务与底层机制;
2. 改横向拆分为纵向拆分。横向拆分即刚才介绍的将玩家拆分至不同进程的做法,纵向拆分则是将代码的不同流程拆分到不同进程,尽量保持上层业务逻辑是单线程。

接下来我们展开来介绍一下几个主要的方案:
视野同步优化
视野同步即一个实体(如玩家、怪物、NPC等等)进入一名玩家的视野范围时,需要打包该实体的数据,发送给该玩家的客户端,让其客户端创建这个实体。

当人数很多,尤其是人群又很聚集时,视野内实体同步开始成为一个主要瓶颈。
视野同步时,最大的一项开销是遍历实体上的所有属性进行打包序列化,最好能将这一步拆分到其他线程。由于最终要发到网络层进行传输,由于网络线程一般负载不高,这一步最好能直接拆分到网络线程。

由于需要访问实体上的属性,我们不能直接在网络线程进行这项操作,否则会有多线程数据**问题。
思考发现,要同步的数据即是一个实体的所有广播属性的集合,因此在进行属性变化时,可以在网络线程存储一份副本,需要同步一整个实体时,可以从这个副本上取数据。

在网络线程为每个实体维护一张表格,记录每个需广播的属性的id到一份打包好的属性数据的映射关系。当主线程的实体属性发生变化时,由于广播属性发生变化时,本就要进行打包序列化,发送给网络层用于广播,此时网络线程可以顺便截取下这份打包好的数据,存储进前面所说的表格中。

网络线程有了前面所说的属性表格后,当该实体进入一名玩家的视野中时,主线程只需要将此**通知给网络线程。由网络线程来遍历该实体的所有属性,拼装成实体的完整数据发送给玩家客户端,不再占用主线程的开销。
消息广播
消息广播主要会有两类:一类是面向地图中所有玩家进行消息广播,比如同步实时的排行信息等;另一类是地图中实体所产生的外在行为,比如释放技能等,需要被周围玩家看到,此时需要进行一次消息广播。
基于前面的思路,我们能否将消息广播也交给网络线程?对于**类消息比较好办到,把消息丢给网络线程去广播就好。而对于第二类消息有个问题,网络线程没有AOI,如何得知广播的范围?
为了解决该问题,我们为每个实体在网络线程中维护一个广播列表,记录此实体被哪些玩家所看到,消息要广播给哪些玩家。当实体进出玩家的视野范围时,主线程负责将此**通知给网络线程,网络线程根据这个信息对广播列表中的玩家进行增删。由于实体进出玩家视野范围时,主线程本就需要通知网络线程进行实体同步或者实体删除,因此并不会增加主线程与网络线程的交互频率。

有了广播列表之后,有广播消息需要发送时,只需要将数据直接推送给网络线程,由网络线程来遍历广播列表进行发送,主线程不需要关心发送范围。稍微要留意的是,为了保证时序性,仅发送给自己的消息,也需要如此操作。经过这个修改后,广播消息对主线程的CPU占用也大幅降低。

属性同步优化
对于实体外在表现有影响的属性发生改变时,需要将新的属性值广播给所有看得到该实体的玩家客户端。这个行为类似于消息广播,可以同样使用网络线程的广播列表来进行优化。主线程属性发生变化时,仅打包属性值发送给网络线程,由网络线程遍历广播列表进行发送,主线程不需要关心发送范围。

此外,有些属性一帧之内会变化多次,且只关心最终值,比如血量,可以将中间变化省去,合并为一次属性同步,减少打包次数。

也有的属性比较复杂,当其发生变化时,整体进行打包序列化的开销比较大。为了解决这个问题,我们通过监听每一层复合属性变更的方式,实现了一套属性增量同步机制。

以该数据结构为例。**层是一个字典类型,有5个预先定义好的成员,对于预先定义好的成员,我们使用编号来代替名字。第二层是一个Map类型,key是字符串,第三层也是字典类型,第四层是数组,数组的第三个数字发生了变更,最终我们仅需向客户端发送路径信息:{1001, 4, key2, 1, 2} 和 变更的值:6。与原先的方式相比,打包的耗时和数据量都有了大幅下降。
写库优化
玩家的数据需要定时写库。在写库时,我们需要遍历玩家身上的属性进行打包序列化,也是一项比较大的开销。
借助前面提到的属性增量更新的机制,我们可以将玩家属性同步到另一个进程维护一份副本,由那个进程进行写库操作,从而降低本进程的开销。该做法还有另外一个好处就是,在本进程发生崩溃时,可以由另一个进程维护的副本进行恢复,达到容灾的效果。

益达平台技能同步优化
在我们的原技能方案中,技能的中间过程的同步,尤其是技能中的子弹(比如打出的法球等)的同步占据了最大的开销。
为降低中间过程的同步,我们借鉴了一些帧同步的思路,采取了一套状态同步+帧同步的方案:
技能的中间过程使用帧同步,只同步操作指令,中间过程在服务端和客户端共同执行,保证双方执行的表现结果是一致的;
为了避免作弊,对于最终的结算结果由服务端计算并进行状态同步。
为了能实现帧同步的效果,益达平台将技能流程抽象为播放一段时间轴上的**,策划向时间轴上添加**组装出一个完整技能。在使用技能后,服务器与客户端共同播放该时间轴上的**,只要保证两端施法的起点与目标点一致,则执行的中间过程也会一致。

在技能过程中,两端会在相同帧、向相同的目标点、释放相同的子弹,因此这些子弹的运动轨迹也会一致。
两端的子弹各自移动,且各自结算,服务器端结算伤害、buff等实质效果,客户端则结算决定子弹是否消失,或者产生命中的表现效果。

经过这项优化,技能的中间过程不再需要同步,战斗开销大幅下降。
优化后的效果
在经历众多的优化之后,我们的团战性能有了大幅提升,以线上的某次决胜云境活动为例,活动过程中同地图最高同时进入了1150+人,整个进程CPU60%-80%,其中主线程在40%-50%波动,网络线程在20%-30%波动,理论上还可以承载更多人数的团战。
未来我们希望更多发挥多线程的能力,将更多流程拆分至主线程之外,比如定时器、AOI等。
如果你有任何建议意见,欢迎一起交流!
如果您对本站有任何建议,欢迎您提出来!本站部分信息来源于网络,如果侵犯了您权益,请联系我们删除!
微信客服