« 开发笔记 (13) : AOI 服务的设计与实现 | 返回首页 | 开发笔记(15) : 热更新 »

开发笔记(14) : 工作总结及玩家状态广播

总结一下最近的工作,修改 bug 以及调整以前设计的细节是主要的。因为往往只有在实现时,需求和问题才会暴露出来。

pbc 自从开源以来,收到了公司之外的许多朋友的反馈。因此找到了许多 bug ,并一一修复了。一并感谢那些在不稳定阶段就敢拿去用的同学。我相信 bug 会初步趋进于零的,会越来越放心。

之前的 RPC 框架 经历了很大的修改,几乎已经不是一开始的样子了。简化为主。几乎只实现了简单的一问一答的远程调用。但在易用性上做了更多的工作。coroutine 是非常重要的语言基础设施,所以这个很依赖 lua 。主要就是提取出函数定义的元信息,把远程调用描述成简单的函数调用,把函数的参数以及返回值映射为通讯协议中的消息结构。这部分和整个框架结合较紧,本来想做开源,但不太想的明白怎么分拆出来。

skynet 是由蜗牛同学用 erlang 实现的,原来是很依赖每个服务的 lua 虚拟机来跑具体应用。最近一个月,我们把 lua 虚拟机从 skynet 模块中完全剥离出来了。而服务则仅以 so 的形式挂接到框架中。lua 服务做为一个具体应用存在。这样,独立启动 lua 和 luajit 也是可以的了。这个过程中,因为 lua 的链接问题 ,我们再次调整了 makefile 中的链接策略。完整的读过了 dlopen 的文档,把每个参数都熟悉了一遍,要避免插件式的 so 服务工作时不要再出问题。

上面这个工作的起因之一是,在我们这里实习的同学回学校了,把他在做的 sharedb 的部分工作交接到新同事手里。需要把这个模块整合进 skynet ,然后围绕它来实现后面完整的场景服务。在一开始我实现的时候,因为早于 skynet 的实现,我用到了 zeromq 做一些内部通讯,这次想一并拿掉。由于不想在这个 C 写的底层模块上嵌入 lua 来写服务,所以整理了框架的 C 接口。

sharedb 这一系列的工作中还包括了数据格式的定义,解析,持久化。我们也做成了独立的服务。关于持久化,目前暂时用的文本文件,尚未 整合入 redis 。具体数据储存怎样做,是以后的独立工作可以慢慢来。毕竟在运行期都是直接通过共享内存直接访问的。

下面谈谈对于下面工作的一些思路。主要是场景服务以及一些同步策略。

之前的一些想法 差不多废弃了,并没有按照那些想法来实现。

我还是想围绕 agent 来实现功能。为每个 client 部署一个 agent 服务,一对一服务。

agent 在处理完登陆认证,选择角色后(这部分的 client/server 都已经实现完毕),它通知持久化服务加载角色数据。持久化服务从外存中把数据载入 sharedb ,之后,agent 获得了对角色数据的完全读写权。当然我们的读写权是约定的,并不是代码保证的。

之后,这个角色对象,在系统中得到了一个唯一 id 。任何 skynet 上的服务,都可以通过向 sharedb 发起一个 attach 请求,拿到共享对象。任何服务都可以通过共享内存,直接读取角色数据。但是改写数据需要通知 agent 来完成。

场景服务分成两个部分,一是副本管理器,二是地图服务。在角色数据上,记录有角色应该属于的地图。agent 向地图所属的副本管理器查询,得到他所属的地图服务地址。便可以把自己注册到具体地图上。

地图服务管理了所有其中的角色 id ,以及若干 npc 。他的义务在于把让这些 id 对应的 agent 相互了解。但具体逻辑则放在每个 agent 服务上。每个 agent 自己所属进程 attach 其它 id ,可以获取其他对象的状态。

我们会用到刚实现好的 AOI 服务 。每张地图会创建一个(以后会有第 2 个用来驱动 NPC 和怪的行为)AOI 实例用来管理每个 agent 的视野。这个工作是这样完成的:

地图把所有进入的角色 id 加在一个 AOI 实体内,并用固定周期获取 AOI 消息。根据消息里包含的 id 信息,把消息分发给对应的 agent 。agent 获得属于它的 AOI 消息后,分别各自保存一个感兴趣的 id 列表,用来做下面会提到的信息过滤白名单。

我们把 agent 之间通讯用到的消息分为两类。一类是会影响到自身状态修改的消息,比如战斗伤害的计算等等。另一类是外观的变化,比如行走,装备变化等等。后一类消息实际上并不需要 agent 直接处理,agent 并不关心这些外观变化,它需要做的仅仅是把这些消息转发给为之服务的 client ,让玩家机器上看到的对象可以有视觉上的改变就行了。这里涉及到一个流量优化的问题。

一开始我想到的是一个消息队列以及主动查询的策略。就是反其道而行之。角色并不向其他人广播这条消息,而是记录在自己的状态队列里。状态队列是有限固定大小的,每条消息有版本号,并且可以冲刷掉老消息。

这个状态消息队列是用 sharedb 共享出去,其他玩家可以来查询。这样,每个 agent 可以根据其它 agent 的距离远近来设置频率主动查询其它 agent 最近的状态改变。如果太久没有查询而丢失掉一些线索(根据版本),则把角色的全部状态信息全部发送给 client ,而不仅仅是根据消息发送差异。其实全部状态也并不会很多,无非就是位置坐标,动作,以及身上的装备和 buff 而已。假设队列设置的足够大,查询频率不算太低,则比较难丢失线索。

改主动发送为主动查询,好处在于可以减少玩家密集时造成的峰值数据包交换数量。每个 agent 的运行都是公平时间片的。agent 可以根据周围玩家数量决定单个时间片内的查询次数,调节网络流量,也可以根据业务逻辑,适当的忽视一些不太重要的消息。玩家可能会觉得周围的景象变化不太灵敏,但几乎不会有卡的感受。

不过这个方案在考虑具体实现时,我发现了许多实现上的复杂度,暂时放弃了。

现在的做法是利用 agent 的 AOI 对象做一个白名单过滤。每个 agent 发出一个状态消息时,它其实是广播给地图上的所有其他 agent 的。我们在 skynet 层提供了一个高性能的组播服务 。由于 agent 和地图一定存在于一台物理机上,skynet 用 erlang 可以高效的实现组播,甚至没有内存拷贝,只是将数据包的引用传给了每个 agent 。agent 根据包头就可以识别出这是一个状态信息。它检查白名单,发现消息来源不在白名单内(距离很远),就直接丢弃这个包。否则,把这个包转发给 client 。这个过程是不需要解开数据包的。

地图服务的另一项工作是管理所有的 NPC 和怪。我原本希望把单个 NPC 和怪都放到独立的进程(非 os 进程)中,以 agent 的独立地位存在。做过一番估算后,觉得内存开销方面不太现实。所以还是打算由地图统一管理它们。初步的计划是讲怪物的巡逻路径单独管理,把 AI 的状态机则独立出来。怪物是不作为观察者进入 AOI 的。让 agent 在观察到怪后,主动向怪发送消息激活怪的 AI 模块。这样可以在没有人活动的场景上,大大减少怪物 AI 轮询造成的 CPU 压力。这部分的实现以后由新入职的 mike 同学负责实现。这几天我们讨论了好长时间的相关方案。在实现过程中一定还会有许多实现者自己的想法。不过我相信 mike 同学多年的 MMO 制作经验可以很好的解决掉问题。

Comments

水平有限,云里雾里啊,大大

玩家消息类型分为两种,一种是向自己,一种是向他人,向他人的消息定制广播级别,重要、单一和可合并。
地图做九宫格或者7宫格,格子注册灯塔,灯塔负责向照亮区域同步玩家数据,灯塔注册消息队列,在轮询期间发送固定数量的消息。
消息队列采用消息池,池消息定长,消息分为消息头和数据区,头包括消息类型和广播级别。
轮询到的时候判断消息池数量,若超长,则后向前合并消息和丢弃单一消息。

这种做法很简单粗暴,但悲剧的是,我们在18个格子里注册了800个机器人来回跑动,没有发现超长,再加人的时候,网络扛不住了。

看了所有的blog后,感觉你并不是很充分的了解mmo的游戏性概念。blog很多东西对我帮助也很大,so忍住的说下:
1.“我们把 agent 之间通讯用到的消息分为两类。” --这个通常信息是一起下发的,客户顿收到的时候变更逻辑和刷新表现。
2.“AI 模块”--将它独立与另一个服务很难很难很难,还有不管有没有人,服务器cpu逻辑都要跑

前面的文章还有好多好多呢就不说了。

关于怪物NPC AOI的实现这一块,我认为以玩家为驱动确实可以减少轮询次数,之前我们的游戏就是这样的,后来改为了以怪物自身AI驱动,增加了不少轮询,不过貌似也没发现什么性能问题,优劣一时很难判断

果然一句也看不懂。。。什么时候可以说中文呢。。。

我怎么觉得从sharedb可以截取得到很多信息。

要避免插件式的 so 服务工作时不要再出问题??

大牛,你的博客用的啥语言做的?

技术性总结!

Post a comment

非这个主题相关的留言请到:留言本