开发笔记(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
Posted by: pppzzz | (9) April 3, 2012 02:04 AM
Posted by: june | (8) March 26, 2012 11:24 AM
Posted by: rnd | (7) March 26, 2012 10:54 AM
Posted by: eva | (6) March 24, 2012 04:08 PM
Posted by: Anonymous | (5) March 23, 2012 04:20 PM
Posted by: suerey | (4) March 23, 2012 01:13 AM
Posted by: jsz | (3) March 22, 2012 09:35 PM
Posted by: nciaer | (2) March 22, 2012 05:09 PM
Posted by: 小自行车 | (1) March 22, 2012 04:07 PM