关于后端:用-Pulsar-开发多人小游戏二纯消息队列作为游戏后端

4次阅读

共计 3387 个字符,预计需要花费 9 分钟才能阅读完成。

本文是《用 Pulsar 开发多人在线小游戏》的第三篇,配套源码和全副文档参见我的 GitHub 仓库 play-with-pulsar 以及我的文章列表。

之前说了,每个游戏客户端蕴含一个 Pulsar 生产者和一个 Pulsar 消费者。

游戏中所有玩家动作都会被形象成一个事件,游戏客户端会监听本地键盘的动作并生成对应的事件,由生产者发送到 Pulsar 音讯队列里;同时每个游戏客户端的消费者会一直从 Pulsar 中拉取事件并把事件利用到本地,从而保障所有玩家之间的视图是同步的。

但玩家间的同步只是一个多人游戏最根本的要求,我之前列出了诸如房间、计分板等很多性能,上面咱们看看如何仅仅利用 Pulsar 的各种 feature 来实现。

如何实现游戏房间

咱们的游戏须要「房间」的概念,在雷同房间里的玩家能力一起对战,不同房间之间不能相互影响。

这个需要能够用 Pulsar 的 topic 来实现。一个游戏房间就是一个 topic,雷同房间的玩家会连贯到雷同的 topic 中,所有事件的生产和生产都会在雷同的 topic 中进行,从而做到不同房间的隔离。

如何实现推炸弹

为了晋升游戏的操作难度和趣味性,咱们容许玩家推炸弹。

这其实就是容许让炸弹挪动,和玩家挪动是一样的,咱们也能够把炸弹的挪动形象成一个事件:

// 炸弹挪动的事件
type BombMoveEvent struct {
    bombName string
    pos      Position
}

当玩家碰到炸弹的时候,向音讯队列继续发送炸弹挪动的事件即可。

如何定时更新房间的地图

地图中的障碍物是随机生成的,障碍物分为可捣毁的和不可捣毁的两种类型。思考到可捣毁的障碍物会被玩家炸掉,咱们须要给每个房间定时更新新的地图。

这个性能略微有点难办。可能你会说,也能够把更新地图的动作形象成一个事件(事实上我也是这样做的):

type UpdateMapEvent struct {
    // 这个列表存储所有障碍物的坐标
    Obstacles []Position}

但这有两个问题:

1、由谁来发送这个更新地图的事件

要晓得咱们的后端只有 Pulsar 音讯队列,你无奈在后端写代码实现一个定时器定期给 topic 中发送音讯的。

PS:实际上 Pulsar 也能提供一些简略的计算性能,也就是 Pulsar Function,我会在前面介绍。

那么咱们只能把更新地图的逻辑写在前端(游戏客户端),但这里还有问题。假如有 3 个在线客户端,每个客户端都每隔 3 分钟发送一次更新地图的命令,那么实际上就是每 1 分钟更新一次地图了,这显然是不合理的。

所以咱们须要在多个客户端之间进行相似「 选主 」的逻辑,保障只有一个 leader 客户端持有更新地图的权限,只有这个客户端会定时收回更新地图的 Event。而且如果这个客户端下线了,得有其余客户端接替 leader 的地位定时更新地图。

2、如何保障新退出的玩家可能正确初始化地图

因为新玩家创立的消费者须要从 topic 中最新的音讯开始生产,所以如果把更新地图的事件和其余事件混在一起,新退出的玩家无奈从历史音讯中找到最近一次更新地图的音讯,从而无奈初始化地图:

当然,Pulsar 除了提供 Producer, Consumer 接口之外,还提供了 Reader 接口,能够从某个地位开始按程序读取音讯。

Reader 还是不能解决这个问题,因为咱们不晓得最近一次地图更新事件的具体位置,除非咱们从头开始遍历一遍所有事件,这显然是很低效的。

其实咱们稍作变通就能解决下面两个问题。

首先,除了记录玩家操作事件的 event topic,咱们能够创立另一个 map topic 专门存储更新地图的相干音讯,这样最新的地图更新事件就是最初一条音讯,能够利用 Reader 读取进去给新玩家初始化地图:

另外,Pulsar 创立 producer 时有一个 AccessMode 的参数,只有设置成 WaitForExclusive 就能够保障只有一个 producer 胜利连贯到对应 topic,其余的 producer 会排队作为备用。

这样,就能够完满解决定时更新地图的需要了。

如何实现房间计分板

每个游戏房间要有一个房间计分板,显示房间内每个玩家的得分状况。

这个需要看起来简略,但实现起来略有些简单,须要借助 Pulsar FunctionPulsar tableview 的能力,我会在前面的章节中具体 Pulsar Function 的开发,这里就简略过一下。

Pulsar Function 容许你编写函数对 topic 中的数据进行一些解决,函数的输出就是一个或多个 topic 中的音讯,函数的返回值能够发送到其余 topic 中。

Pulsar 官网的一张图就能看明确了:

Pulsar Function 反对 Stateful Storage,比方官网给了一个单词计数器的例子:

在咱们的炸弹人游戏中,玩家的死亡也会被形象成事件发送到 topic 中:

type UserDeadEvent struct {
    // 被炸死的玩家名
    playerName string
    // 杀手玩家名
    killerName string
}

相似单词计数器,咱们这里也能够实现一个 Pulsar Function,专门过滤玩家死亡的 UserDeadEvent 事件,而后统计 killerName 呈现的次数,就能够作为该玩家的分数了。

当然,咱们须要实时更新房间内玩家的分数,所以每个游戏房间除了 event topic 和 map topic 之外,咱们还须要一个 score topic,让 Pulsar Function 把分数更新事件输入到 score topic,并且利用 Pulsar client 的 tableview 性能做一个比拟好的展示。

无关 Pulsar Function 和 tableview 的具体用法这里临时跳过,前面再具体解说。

如何实现全局计分板

除了以后游戏房间中的分数状况,咱们还须要有一个全局计分板,能够对所有玩家在不同房间的总得分进行排名。

既然曾经能够实现房间内的计分板了,那么实现全局计分板必定能够有多种不同的方法。

之前咱们用 Pulsar Function 统计进去的每个房间内的玩家分数其实就是 playerName -> score 的键值对,那么咱们只有遍历存储在 Pulsar Function 中的所有键值对,不就能够累加出某个 playerName 的总分了吗?但遗憾的是,Pulsar Function 并没有提供一个接口来遍历所有键值对,所以咱们必须想其余方法。

其实说到排行榜之类的需要,我首先想到的就是 Redis,是否能够把玩家分数相干的统计数据导出到 Redis 中呢?这也不便当前对这些数据做更多样化的解决。

必定是能够的,方才说了 Pulsar Function 能够把多个 topic 里的音讯作为输出,那么我只有在 Pulsar Function 里面包一个 Redis 客户端,当然能够把数据写到 Redis 外面。

不过,往 Redis 外面导数据的 Function 代码齐全不必咱们亲自去写,Pulsar 提供了现成的工具,也就是 Pulsar Connector

顾名思义,connector 就是 Pulsar 和其余数据系统之间的连接器,能够把其余数据系统中的数据导入到 Pulsar 里,也能够把 Pulsar 外面的数据导入到其余数据系统中。

咱们只须要下载 Redis 的 connector,做一些简略的配置就能够投入使用了。数据导到 Redis 中,做一些聚合和排序的工作就很简略了,前面的章节我介绍 Pulsar Connector 时再具体解说。

如何实现游戏回放

假如咱们会举办重要赛事,须要反对游戏「录制」,以便观看游戏回放。

因为咱们把玩家产生的所有事件都存储在 topic 中,而且从雷同的初始状态开始重演这些事件失去的最终状态都雷同,所以只有从 event topic 头部开始向后读取所有音讯,就能够重演整个游戏过程,相当于是游戏回放。

当然,Pulsar 默认会启用一些数据过期删除的策略把 topic 中比拟老的数据删掉,咱们能够敞开这个性能,或者利用 Pulsar Offloader 把比拟旧的数据卸载到其余存储介质上。

Pulsar Offloader 的理论应用场景是升高海量数据存储的老本,把老旧的数据卸载到读写效率更低但老本也更低的存储介质上,把高性能的存储介质让给新数据应用。

前面咱们也会体验一波 Offload 的应用,这里跳过不提。

更多高质量干货文章,请关注我的微信公众号 labuladong 和算法博客 labuladong 的算法秘籍

正文完
 0