开放大世界千人同屏网络设计

        项目要求同服最多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 组合在一起,提供一种类似于面向对象的外观模式。

【Unity】Entities 1.0 学习(一):Aspect_unity entities-CSDN博客文章浏览阅读5.3k次,点赞4次,收藏16次。Unity在 2022年下半年(我印象是9月份左右)推出了 Entities 1.0 ,可以在 2022.2.0b8 以上的版本使用。当时我粗略地看了一下,但是没有深入学习。最近空闲时间稍多,就认真来学习一下 Entities 1.0有啥新的东西。这篇文章算一个起头,以及对Aspect的一些学习总结_unity entitieshttps://blog.csdn.net/cyf649669121/article/details/127814468        例如,单位的基本信息由单位信息和移动信息组成:

        显然,单位的Guid、类型与移动信息这些不可能放在一个Data里,也不会放在一个System里处理。但是在通信时,这一块数据总是会同时下发,因此协议里就是针对这个Aspect进行通信。


        Aspect 最好只在表现层使用,逻辑层使用只能只读。因为 Aspect 中包含了很多数据,操作不好很容易出现多个System写入同一个Data 。


        客户端在与服务器通信时,如果考虑到网络通信压力,可以在发送时携带 FrameID,表示客户端已经获取到的数据是在第几帧拿到的。

        服务器可以与当前 Aspect 作个简单比对,如果 FrameID 相同,则返回一个空消息,表示客户端保存的就是最新的。如果 FrameID 不同,则将新的信息以 Aspect 为单位下发下来。

6、关于客户端的拉取频率问题

        如果所有数据,客户端都每帧实时拉取,显然也是不太优雅的(虽然性能上不会有很大问题)。不同消息(Aspect)是应该有一个不同的频率进行拉取,例如 Buff 信息可能就拉取频繁些,外观信息甚至可以都不需要进行拉取。

        如果需要针对某一个消息动态调整拉取频率,使用固定算法可能难以满意。个人建议使用动态规划法,由服务器统计一个各个 Aspect 的变化频率,再下发给客户端。客户端依此数据为参考,确定实际的拉取频率。

参考文章

  • 20
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值