8

我正在开发一款回合制休闲MMORPG游戏服务器。

处理网络、多线程、定时器、服务器间通信、主游戏循环等的低级引擎(不是我们编写的)是用 C++ 编写的。高级游戏逻辑由 Python 编写。

我的问题是关于我们游戏中的数据模型设计。

起初,我们只是尝试在客户端登录时将播放器的所有数据加载到 RAM 和共享数据缓存服务器中,并安排一个计时器定期将数据刷新到数据缓存服务器,数据缓存服务器将数据持久化到数据库中。

但是我们发现这种方法存在一些问题

1)一些数据需要立即保存或检查,例如任务进度,升级,物品和金钱收益等。

2)根据游戏逻辑,有时我们需要查询一些离线玩家的数据。

3)一些全局游戏世界数据需要在不同的游戏实例之间共享,这些实例可能运行在不同的主机上,也可能是同一主机上的不同进程。这是我们需要一个位于游戏逻辑服务器和数据库之间的数据缓存服务器的主要原因。

4)玩家需要在游戏实例之间自由切换。

下面是我们过去遇到的困难:

1)所有的数据访问操作都应该是异步的,避免网络I/O阻塞主游戏逻辑线程。我们必须向数据库或缓存服务器发送消息,然后在回调函数中处理数据回复消息并继续进行游戏逻辑。编写一些中度复杂的游戏逻辑,需要与db多次对话,并且游戏逻辑分散在许多回调函数中,难以理解和维护,很快就会变得痛苦。

2) ad-hoc 数据缓存服务器使事情变得更加复杂,我们很难保持数据的一致性和有效地更新/加载/刷新数据。

3) 游戏内数据查询效率低下且繁琐,游戏逻辑需要查询库存、物品信息、头像状态等很多信息。还需要一些事务机制,例如如果一个步骤失败则整个操作回滚. 我们尝试在 RAM 中设计一个好的数据模型系统,构建大量复杂的索引以简化大量信息查询,添加事务支持等。很快我意识到我们正在构建的是内存数据库系统,我们正在重新发明轮子。 ..

最后我转向stackless python,我们移除了缓存服务器。所有数据都保存在数据库中。游戏逻辑服务器直接查询数据库。借助stackless python的micro tasklet和channel,我们可以同步编写游戏逻辑。它更容易编写和理解,生产力也大大提高。

事实上,底层的 DB 访问也是异步的:一个客户端 tasklet 向另一个专用 DB I/O 工作线程发出请求,该 tasklet 在一个通道上被阻塞,但整个主游戏逻辑没有被阻塞,其他客户端的 tasklet 将被调度并自由奔跑。当 DB 数据回复时,阻塞的 tasklet 将被唤醒并继续在“断点”上运行(继续?)。

通过上述设计,我有一些问题:

1)DB访问会比以前的缓存解决方案更频繁,DB可以支持高频率的查询/更新操作吗?近期是否需要一些成熟的缓存方案如redis、memcached?

2) 我的设计中是否存在严重的缺陷?你们能给我一些更好的建议吗,尤其是关于游戏内数据管理模式。

任何建议将不胜感激,谢谢。

4

2 回答 2

6

我曾使用过一个以某种类似方式运行的 MMO 引擎。然而,它是用 Java 编写的,而不是 Python。

关于您的第一组要点:

1) async db access我们实际上走的是另一条路,避免“主游戏逻辑线程”。所有游戏逻辑任务都作为新线程生成。与 I/O 相比,线程创建和销毁的开销在本底噪声中完全消失了。这也保留了将每个“任务”作为一个相当简单的方法的语义,而不是一个令人抓狂的回调链,否则最终会得到(尽管仍然存在这种情况。)这也意味着所有游戏代码都必须是并发,我们越来越依赖带有时间戳的不可变数据对象。

2) ad-hoc cache我们使用了很多 WeakReference 对象(我相信 Python 也有类似的概念?),并且还利用了数据对象之间的拆分,例如“播放器”和“加载器”(实际上是数据库访问)方法)例如“PlayerSQLLoader;” 实例保存一个指向它们的加载器的指针,加载器由一个全局“工厂”类调用,该类将处理缓存查找与网络或 SQL 加载。数据类中的每个“Setter”方法都会调用该方法changed,这是一个继承的样板文件myLoader.changed (this);

为了处理来自其他活动服务器的加载对象,我们使用了“代理”对象,这些对象使用相同的数据类(再次说,“播放器”),但我们关联的 Loader 类是一个网络代理,它将(同步,但超过千兆本地网络)在另一台服务器上更新该对象的“主”副本;反过来,“主”副本会调用changed自己。

我们的 SQLUPDATE逻辑有一个计时器。如果后端数据库在最后 ($n) 秒内收到了一个UPDATE对象(我们通常保持在 5 秒左右),它会将对象添加到“脏列表”中。后台计时器任务将定期唤醒并尝试将仍在“脏列表”上的任何对象异步刷新到数据库后端。

由于全局工厂维护了对所有核心对象的 WeakReferences,并且会在任何实时服务器上查找给定游戏对象的单个实例化副本,因此我们永远不会尝试实例化由单个 DB 记录支持的游戏对象的第二个副本,因此游戏的内存状态可能与它的 SQL 映像不同,每次最多 5 或 10 秒这一事实是无关紧要的。

我们的整个 SQL 系统在 RAM(是的,很多RAM)中运行,作为另一个服务器的镜像,该服务器勇敢地尝试写入磁盘。(由于“老化”,那台糟糕的机器平均每 3-4 个月烧毁一次 RAID 驱动器。RAID 很好。)

值得注意的是,当从缓存中删除对象时,必须将对象刷新到数据库,例如由于超出缓存 RAM 限额。

3)内存数据库……我没有遇到过这种精确的情况。我们确实有“类似事务”的逻辑,但这一切都发生在 Java getter/setter 级别。

而且,关于你的后一点:

1) 是的,PostgreSQL 和 MySQL 尤其能很好地处理这个问题,特别是当您使用数据库的 RAMdisk 镜像来尝试最小化实际的 HDD 磨损时。然而,根据我的经验, MMO确实倾向于对数据库进行超出严格要求的操作。我们的“5 秒规则”* 是专门为避免“正确”解决问题而制定的。我们的每个二传手都会打电话changed. 在我们的使用模式中,我们发现一个对象通常要么更改了 1 个字段,然后一段时间内没有活动,要么发生更新“风暴”,其中许多字段连续更改。构建适当的事务(例如,通知对象它即将接受许多写入,并且应该在将自己保存到数据库之前等待片刻)将涉及更多的计划、逻辑和系统的主要重写;所以,相反,我们绕过了这种情况。

2)嗯,上面是我的设计:-)

事实上,我目前正在开发的 MMO 引擎更加依赖内存中的 SQL 数据库,并且(我希望)会做得更好。但是,该系统是使用实体-组件-系统模型构建的,而不是我上面描述的 OOP 模型。

如果您已经基于 OOP 模型,那么转向 ECS 是一个相当大的范式转变,如果您可以使 OOP 为您的目的工作,那么坚持您的团队已经知道的可能会更好。

*- “5 秒法则”是美国口语“民间信仰”,即食物掉在地上后,5 秒内捡起来还是可以吃的。

于 2011-12-28T20:33:23.400 回答
2

如果不对软件有更深入的了解,就很难评论整个设计/数据模型,但听起来您的应用程序可以从内存数据库中受益。*将此类数据库备份到磁盘(相对而言)是一种廉价的操作。我发现通常更快:

A) 创建一个内存数据库,创建一个表,在给定的表中插入一百万行**,然后将整个数据库备份到磁盘

B) 将一百万行插入到磁盘绑定数据库的表中。

显然,单条记录的插入/更新/删除也在内存中运行得更快。我已经成功地将 JavaDB/Apache Derby 用于内存数据库。

*请注意,数据库不需要嵌入到您的游戏服务器中。**一百万可能不是此示例的理想大小。

于 2011-12-28T20:23:15.223 回答