大家好啊,我是码财同行。游戏服务器里一个比较重要的话题就是无缝世界的特性。一些知名的游戏如魔兽世界、天谕、明日之后等都支持无缝世界。
之前也大致了解无缝世界是怎么处理,边缘处用的是 Real、Ghost机制。最近又系统学习和整理了一些这方面的资料,特分享出来。
大世界需求面临的问题
无缝大世界
通常指的是一种虚拟世界或者游戏中的理念,其目标是创造一个无缝连接的虚拟环境,让玩家能够在其中自由流动而不受到任何中断或加载的影响。这种理念旨在提供更加沉浸式和连贯的体验,使玩家感觉仿佛置身于一个真实而连续的世界中。
这是相对于经典MMO游戏中,地图传送来说的。
在无缝大世界中,玩家可以自由地探索、移动和互动,而无需经历传统游戏中常见的加载屏幕或过渡场景。这种设计能够增强玩家的沉浸感,使他们更容易沉浸于游戏世界之中,而不会被中断所打断。这种理念在许多现代游戏和虚拟现实体验中得到了广泛的应用。
具体到实现上,有一些特定需求的案例:
- 单人大世界(如原神) ,这类玩法没有多人聚集的需求,优点: space分块按需加载,没有大小限制 缺点: 只适合单人或者组队联机玩法(不需要AOI)
- 分线或者副本,这类游戏实际是需求上的妥协,牺牲了用户体验,毕竟是给玩家做shard强制分割,互相之间是看不见的。优点是相对简单。
经典的无缝地图玩法要求是玩家互相之间能看见,并做交互,不会牺牲体验。但是,大家都知道,服务器的资源是有限的,单个服务器不可能承载这种无缝地图,上面的玩家数量太大,几乎肯定会过载。
很显然,这就要求 用一个服务器集群去承载这种无缝地图。
解决办法
大地图不能在一台机器上的解法:
- 垂直拆分:把物理、碰撞、AI等拆出去;
- 水平拆分:把地图做切割,分块管理
重点在于水平拆分的方式。一张完整的地图被划分成多个区域,每个区域由一个server管理,如下:
业界有比较知名的方案:bigworld。
bigworld 方案
BigWorld 游戏引擎是由澳大利亚的 Pty Ltd 公司开发,其架构以及和经典服务器架构的对应关系如下:
基于BigWorld引擎的代表作有: 中国:《天下贰》《天下叁》等等数十款,网易对BigWorld的实用化贡献很大。 国际:《魔兽世界》早期版本,《坦克世界》,《战争雷霆》
地图切割
切割与管理
一个无缝的 space 被划分成多个 cell,每个cell 可以分配给特定的 server。
所有的cell 用 bsptree 管理。
负载均衡
考虑到玩家是会在多个cell之间移动、聚集的,因此根据负载,定时做 cell 的拆分或合并。
边界处理:Real-Ghost方案
那么,怎么能让不同cell(位于不同server)的玩家互相看见呢?
大致的思想就是为处于边缘的玩家互相做数据副本。那什么范围内的玩家需要做副本呢?
可以为每个cell 增加一部分边界,比 AOI 大一些,这样能保证客户端的体验更平滑,在副本进入AOI之前服务器就已经有相关数据了。
如果看整个地图的划分,可能如下:
交互与传送
处于地图边界(border)的实体需要像非跨服玩家一样直接交互,比如战斗
用 Real、Ghost 机制模拟一个攻击扣血的例子:
一个 A(real) 对应两个 Ghost(A1、A2,都只读)。
如果B攻击A2,因为有相关数据,可以算出来扣多少血,可以把这个攻击和扣血消息转发到实际的A,A执行实际的修改操作之后再同步给A1、A2。
这个过程其实是非常复杂的,涉及到异步,有的复杂技能会有多个来回。上下文很可能发生变化。
如果一个 Entity 实体跨边界移动呢?那就涉及到数据在多个 server 之间传送的问题,但是因为之前已经有了副本 Ghost,对于传送,需要做的事情其实就是把 Ghost 提升为 Real,这样就不用重新创建一个新的实体;而原来服务器上的 Real 降级为 Ghost。
此外,一个好的 GUI形式的 debug tool 很重要,比如在客户端界面显示地图边界及server标号等。这样当出现数据问题时,可以在客户端很直观的定位到问题出现在什么地方:
开源实现
有一个go实现bigworld方案的开源项目:
bigworld 方案的问题
对于极端情况下,玩家都聚集在一个cell,但是这个时候,我们的cell又不可能无限分割下去,比如cell已经接近AOI的大小,继续分下去是没有意义的。
改进方案
【新改进方案】
新方案必须做到位置解耦,一个 cell 坐标点的 Entity 不再由单一的 server 来服务:
比如A和B的位置都很接近,自然也可以交互。这个时候,如果A、B位于不同的cellapp(server),当他们需要交互的时候,需要把原本不在一个server的双方动态调度到同一个 cellapp 来执行交互逻辑(如战斗)。
这个过程可能存在并发冲突,需要解决(比如B同时要和C交互,等A调度到B所在的cellapp时,B又被调度到C所在的cellapp)。
如果不想做跨进程调度数据,可以用共享内存(跨机器才调度)。
另外如果想在跨机器调度时候降低开销,可以按照交互类型(操作不同的数据component)按需调度:
比如战斗就调度技能数据,其他交互调度对应模块的数据等。
如果用共享内存,那可能也会有同时修改数据的情况(多个cellapp都操作共享内存中的数据),需要加锁。如果加锁,就容易出现死锁。(这里的a、b、c代表上面不同模块的component)
具体来说可以把取数和加锁的逻辑调整一下:
抢锁失败的情况可以重试来解决(需要对业务无感)
好了,基于位置解耦的改进方案就是这些。
那么问题来了:处于多个cellapp管理的相邻的entity,在客户端要能互相看见,保证AOI的逻辑正确,怎么办?
有项目的方案是让客户端拉取并合并多个cellapp的信息。服务器的AOI逻辑没有那么严谨。
自己的理解与设想
搞到最后,是不是有点类似分布式环境同步化操作数据?其实我们现在项目中,分布式修改数据用的是 协程 + 协程锁 + redis读写的解法,有点类似前面说的共享内存方案,只不过redis的方案更通用,但是操作消耗肯定比共享内存大很多的。而用协程就能很方便的编写同步化的代码。
用cell来分片,再在cell集中的地方分线,数据放redis里,可以交互,内存态可以用缓存,做展示,保证最终一致性。
能想到这种方案的一些关键点:
- 内存缓存:将Redis中的共享数据在应用程序中进行一层缓存,可以提高数据的访问速度和降低对Redis的频繁访问。
- 数据修改加锁:在进行数据修改时,您可以引入锁机制来确保数据的一致性和避免并发修改问题。这可以通过分布式锁或者Redis的事务和WATCH命令来实现。
- 更新机制:在成功修改Redis中的数据后,您可以通过某种机制来通知各个server中的缓存,让缓存数据与Redis中的数据保持同步。这可以通过发布/订阅模式、回调函数、定时任务或其他方式来实现。
另外,加锁也可以用乐观锁?当然只是初步想法,还没有详细设计过。