项目要求同服最多5千人,但可能会出现大量玩家都集中在热点区域的情况。在客户端与服务器设计架构时,需要直接按照千人同屏的性能要求进行设计。这对我们的网络设计做出了很高的要求,在参考了很多文章(尤其是云风大佬的文章)之后,我们做出了满足要求的网络架构设计。
1、总体设计简介
同步要点如下:
-
客户端(逻辑层)服务器都采用ECS架构(只有ECS能满足设计需求)。
-
总体上还是使用状态同步,各个客户端看到的表现不一致。
-
玩家主要关心周边的单位,而不是全部的AOI块内的单位。
-
同步时,只同步移动(路径)信息,其他信息一概不同步(包括血量)
-
服务器提供一个协议,供客户端直接拉取某个玩家的具体信息(客户端主动请求,避开主服务器的同步,类似于灯塔)。客户端根据相机范围来主动向服务器拉取(只读)。这个请求需要做到能无代价,即,只要性能可以支撑,客户端可以无限制地拉取任意数量、任意次数的单位详细信息,而不会引起(服务器的)性能问题。
-
玩家的操作命令、状态变化只同步给相关玩家,服务器维护一个关注列表,以战斗、组队等条件进行维护。
-
操作命令都是过程,而不是状态(比如移动的路径,攻击也是类似于玩家A将在X帧对玩家B释放技能X)
-
所有命令加上固定延迟,都是至少1帧之后生效,之前会有本地前摇(仅表现,其他玩家不播放此前摇,直接开始执行命令)。
1.1、网络同步的三层筛选
我们项目因为单位多,且对操作有一定要求,所以不能直接照搬成熟的MMO或者SLG框架。大量单位的逻辑和渲染、网络同步都有一定压力,但是好在一般情况下,玩家所需要关心的单位是有限的。
以视野范围距离,我们假设玩家只需要关心视野范围内的船只(详细信息),距离相机较远的船只玩家并不在意,而相机的范围是有限的。设在一个AOI Block 块中有5千单位,其估算结果如下:
所以客户端只需要保证最内圈的27个单位的显示、同步的流畅与正确,至于外围的几千个单位,只需要保证最低限度的同步即可。而中间圈层的75个单位属于缓冲层,除了最内圈外,其他只是会拉取详细数据但是并不需要进行渲染,同步的优先级居中。
1.2、单位同步流程
世界单位同步采用状态同步的方式;玩家的操作体验、客户端的预测与回滚,采用帧同步的方式(类似于快照+帧同步+预测与回滚)实现。
大世界单位同步的基本流程如下:
关于玩家关心范围:这个范围是可以客户端动态设定,考虑到多种应用场景。一般来讲会比视野范围略大,业务上倾向于 可能对玩家造成影响 的范围。例如和玩家距离近的、玩家的队友、玩家正在攻击/被攻击的对象等。
2、示例:单位数据的同步
单位数据同步流程如下(这里是船只单位(Ship)):
-
由于显示(上屏)范围是按照用户相机来进行判定的,所以表现层需要的数据是可能会有延迟或缺省的,这个需要特别注意。
-
逻辑层如果遍历压力大,可以将这部分放在 JobSystem 中。逻辑层需要尽可能不大量遍历。
-
需要同步的请求需要作为命令Buffer,统一请求(可以考虑去重)。
-
同步频率根据需求和数据类型、性能压力、使用场景动态确定。
-
详细信息的同步范围、频率可以由多种因素综合决定:机型、使用情景、性能压力、视野范围以及策划需求等。
-
同步的数据未必都是单位信息,可能是全局使用的信息(例如联盟信息),这种就客户端自己优化处理即可。
因为是需要严格按照ECS的架构来写,所以逻辑层没有“玩家自己”这个概念,所有的船只同步都是走的同一个流程。但是对于玩家自己,其很多数据可以不通过服务器,而可以通过本地数据进行拼接。
3、示例:玩家操作命令同步
对于操作命令,建议只对玩家自己的操作进行预测。(后面如果出现需要服务转发队友或者敌人的操作命令,会在数据结构上做一些修改,但是总体流程是一样的)
这里针对玩家自己的操作,梳理一下整个客户端的运作流程:
先要明确几个要点:
-
玩家的操作会直接发给服务器,同时同步给逻辑层。
-
玩家的操作命令会指明命令在未来的第几帧(至少为当前帧+1)开始执行。
-
命令一经发出便不能反悔。
这里举例一个实际应用场景的情况进行说明:
① 第1帧:玩家发出命令1(假设在第2帧开始执行)
② 第2帧:玩家发出命令2,客户端逻辑开始执行命令1;
③ 中间玩家又发出命令3,客户端逻辑执行命令2;
④ 在第N(N≥2)帧:服务下发,或者玩家主动拉取得到服务器的第2帧的数据(命令1已经被服务器接受,但服务器可能执行可能没有执行)
⑤ 客户端将服务器的数据覆盖本地(预测与回滚的逻辑,之后会详细说明),之后将命令3(在第2帧已经开始执行)移除。
根据上述的流程,预测与回滚就出现在客户端已经预测,但是还没有从服务器获取到正确数据的区间(一般是因为网络延迟等情况造成的)。
关于回滚的说明:
因为目前框架的设计,客户端是无法做到仅仅依赖命令就能实现完全同步的(与帧同步不同)。因此,定时覆盖逻辑层的数据是必要的。也就是无论客户端预测成功与否,都会按照服务器的数据重新计算命令。
当然这种操作确实有操作隐患,可以在后续实现时再做优化。
(其实上述流程可以看到,服务器和客户端是可以跑不同的帧数的,比如客户端始终落后于服务器1帧)
4、客户端预测与回滚逻辑
注意,这里的说的预测回滚操作是基于逻辑层严格按照ECS书写的前提。
在ECS架构中,当某一帧客户端与服务器特定的数据同步之后,在有效时间(比如技能的总生命周期)内,双端跑同样的 System 逻辑,就能保证两边同步,且有效时间内不需要再同步。
这里对逻辑有以下硬性要求:
-
对任意 CompoenetData 的写入操作只能在唯一的一个 System 中执行,其他的 System 只能对其只读!
-
各个 System 逻辑需要设计成无时序相关(这也符合 ECS 的基本思想)。
4.1、为什么会出现预测和回滚
无论是客户端还是服务器,时间都是会固定流逝的。
因此,当客户端从服务器拉下某一帧的状态时,就已经落后了(时间又向前流逝了一帧)。因此就需要客户端进行预测,而一旦做了预测就可能预测失败,此时就需要根据最新状态进行回滚。
有一种特殊情况,当网络特别好,网络来回通信的时间甚至小于单帧的时长;此时客户端总能立刻拿到服务器最新的数据,此时就是不需要再预测了。
其实,无论玩家是否在操作,都是在预测(预测其他玩家没有任何操作)。因此只要网络延迟存在,就一直在预测。
4.2、如何进行预测与回滚
客户端每次和服务器同步一次,就会得到一个确定的状态(假定是第N帧)。
当客户端跑到 N+1 帧的时候,还没有收到服务器的消息。此时就根据客户端第N帧的状态,再加上客户端本地发的一条命令(C1),自行计算逻辑得到第N+1帧新的状态。(N+C1=N+1)
之后客户端可能又发布了多个命令(C2,C3),但是也是走相同的逻辑。
当 N+X帧的时候,因为网络延迟,客户端终于收到了服务器服务下发的第N+1帧的数据。
此时,如果按照ECS的思路,客户端是可以不必理会命令C1是否执行过,直接覆盖数据即可。但一般还是会进行判断,以方便表现和其他处理。
之后客户端将N+1帧的数据,设置为已经同步的数据,将需要在N+1帧开始执行的命令(C1)移除。再根据客户端后续尚未同步的命令(C2、C3)合成新的状态:
同步前状态 = N + C1 + C2 + C3
同步后状态 =(N+1) + C2 + C3
之后将新的状态交由表层,由表现层重新处理上屏效果。
4.3、表现层的处理
表现层需要根据表现层数据与最新逻辑层数据的差异进行比对,从而做出不同的表现。从逻辑流程上来讲非常简单:
当然图上只是举一个例子,真实情况对于各种情况的处理是会比较复杂的,需要充分考虑各种情况。
表现层不能像逻辑层那样只单独考虑一个Data,而是要讲船只视为一个整体来进行考量。例如某艘船只突然短时间大量位移,单独看移动轨迹肯定是比较奇怪的;但是此时船只是有个技能,类似于冲刺,配合动画和特效,整体表现就合理了。
一般情况下,只要表现层能在下一帧逻辑开始前追到当前逻辑帧结束的状态(逻辑帧会瞬时算好),就不会有太多问题。
这里对于表现层有个难点:
因为逻辑层的数据并不会从0开始模拟到当前时间,而是直接给出一个当前状态。所以要求表现层能够原地启动:例如某个技能放到50%,则需要直接表现出50%时的状态。
5、通信命令的设计
这里需要提供一个概念:Aspect。
这个是 Unity 在 entities 1.0 引入的新功能,主要功能是将多个 ComponenetData 组合在一起,提供一种类似于面向对象的外观模式。
显然,单位的Guid、类型与移动信息这些不可能放在一个Data里,也不会放在一个System里处理。但是在通信时,这一块数据总是会同时下发,因此协议里就是针对这个Aspect进行通信。
Aspect 最好只在表现层使用,逻辑层使用只能只读。因为 Aspect 中包含了很多数据,操作不好很容易出现多个System写入同一个Data 。
客户端在与服务器通信时,如果考虑到网络通信压力,可以在发送时携带 FrameID,表示客户端已经获取到的数据是在第几帧拿到的。
服务器可以与当前 Aspect 作个简单比对,如果 FrameID 相同,则返回一个空消息,表示客户端保存的就是最新的。如果 FrameID 不同,则将新的信息以 Aspect 为单位下发下来。
6、关于客户端的拉取频率问题
如果所有数据,客户端都每帧实时拉取,显然也是不太优雅的(虽然性能上不会有很大问题)。不同消息(Aspect)是应该有一个不同的频率进行拉取,例如 Buff 信息可能就拉取频繁些,外观信息甚至可以都不需要进行拉取。
如果需要针对某一个消息动态调整拉取频率,使用固定算法可能难以满意。个人建议使用动态规划法,由服务器统计一个各个 Aspect 的变化频率,再下发给客户端。客户端依此数据为参考,确定实际的拉取频率。
参考文章
- 帧同步联机战斗(预测,快照,回滚)_预测回滚-CSDN博客
- 再谈网游同步技术 - Skywind Inside
- 探讨:为什么在游戏开发中不使用MVC?
- 云风的 BLOG: 浅谈《守望先锋》中的 ECS 构架
- A guide to understanding netcode - Overwatch - GameReplays.org
- 网络游戏同步:状态同步核心原理剖析【1】
- 云风的 BLOG: MMORPG 的同步设计
- 云风的 BLOG: MMORPG 客户端的网络消息框架
- 云风的 BLOG: 继续谈网络游戏的同步问题
- 云风的 BLOG: 如何只基于请求回应模式实现 MMO 级别的场景服务
-
《Unity3D高级编程之进阶主程》第六章,网络层(六) - 网络同步解决方案 - 技术人生 - 编程技术 - JESSE人生
- 3D游戏的万人同屏技术详解(1)
- 3D游戏的万人同屏技术详解(2)
- Unity3D游戏GC优化总结---protobuf-net无GC版本优化实践
- 游戏网络 UDP+FEC+KCP_kcp fec-CSDN博客
- 流媒体弱网优化之路(FEC)——FEC原理简介-CSDN博客
- MMO技能系统的同步机制分析 - GameRes游资网
- 如何在高丢包率的链路上建立低延迟连接? - Skywind Inside
- 网络游戏同步法则 - Skywind Inside
- 技能模块的防外挂机制和同步机制优化
- MMO中基于3D空间下战斗系统的实现细节
- 【转】一款已上市MMO手游地图同步方案总结
- 《守望先锋》回放技术:阵亡镜头、全场最佳和亮眼表现
- 《王者荣耀》技术总监复盘回炉历程:没跨过这三座大山,就是另一款MOBA霸占市场了 – 游戏葡萄