Loading [MathJax]/jax/output/CommonHTML/config.js
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >我用消息队列做了一款联机小游戏

我用消息队列做了一款联机小游戏

作者头像
labuladong
发布于 2022-12-10 08:49:51
发布于 2022-12-10 08:49:51
1.2K00
代码可运行
举报
运行总次数:0
代码可运行

上篇文章 我讲了两种常用的随机算法,本文就把这些算法运用出来,做一个多人在线小游戏。

我小时候特别喜欢在 4399 玩一款叫做 Q 版泡泡堂的游戏:

游戏里玩家可以操控一个机器人放炸弹,炸开障碍物能够获取随机道具,玩家消灭所有其他机器人则闯关成功,如果被其他机器人消灭,则闯关失败。

这个游戏中其他机器人都是电脑控制的,说实话有些蠢,我玩 Hard 难度一个小时就通关了。所以我在想,是否能够把这类炸弹人游戏做成多人在线的游戏,让几个好朋友联机 PK 呢?

分析

我对多人在线游戏的技术点并不了解,但是根据自己的游戏经验简单分析一下,我总结出来以下几个关键点:

1、需要「房间」的概念,在相同房间里的玩家才能一起对战,不同房间之间不能互相影响。

2、多人在线游戏肯定需要有一个后端服务供所有玩家连接,但由于这只是个小游戏,所以希望开发尽可能简单,后端最好不要有代码逻辑,所有逻辑都写在前端(游戏客户端)。

3、炸弹人游戏的初始地图会随机生成一些障碍物以增加游戏的难度和趣味性,但我希望随着游戏的进行,每隔一分钟就能重新生成一个新的随机地图。

4、最重要的,所有玩家的操作必须同步,或者说要保证「一致性」。比如你玩王者荣耀,如果你拆掉一座塔,那么要保证局内所有玩家都知道这座塔被拆了,不能因为某些玩家网络卡顿导致他还能看到这座塔,否则的话玩家们的视图就不同步了。

其实用一个消息队列就可以满足上述要求

我们可以把消息队列的每个 topic 作为一个房间,然后把每个玩家的操作抽象成不同的Event,由游戏客户端作为生产者将Event发到房间的 topic,游戏客户端同时也是消费者,从房间 topic 中读取并执行Event序列。这样一来,游戏房间的概念有了,而且所有游戏客户端展现的事件顺序就是消息队列中消息的顺序,能够保证不同玩家的操作都是同步的。

不过这里还有个问题,怎么做到每隔 1 min 随机生成新的地图呢?这个需求其实有点难办,你可以把生成新地图也抽象成一个Event,但问题是这个Event该由谁发送呢?

显然你不能让每个客户端都持有一个 1 min 的计时器,所以我们可能需要在多个客户端之间进行「选主」的逻辑,保证只有一个 leader 客户端持有更新地图的权限,然后让这个客户端定时发出更新地图的Event

当然,如果这个 leader 客户端下线了,其他客户端应该能感知到,并确定一个新的客户端成为 leader,承担更新地图的任务。

设计思路

首先需要一个游戏框架,我选择了 Go 语言的一款 2D 游戏框架,叫做 Ebitengine,官网如下

https://ebitengine.org/

之所以选择这款 Go 语言的框架,主要是两个原因:

1、比较简单,适合快速上手写 2D 小游戏。

2、支持编译成 WebAssembly,如果需要的话可以直接编译到网页上运行。

这个库的使用原理特别简单,只要你实现这个Game接口的这两个核心方法就可以:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
type Game interface {
    // 在 Update 函数里填写数据更新的逻辑
 Update() error

    // 在 Draw 函数里填写图像渲染的逻辑
 Draw(screen *Image)

    // ...
}

我们知道显示器能够显示动态影像的原理其实就是快速的刷新一帧一帧的图像,肉眼看起来就好像是动态影像了。

在每一帧图像刷新之前,这个游戏框架会先调用Update方法更新游戏数据,再调用Draw方法渲染出每一帧图像,这样就能够制作出简单的 2D 小游戏了。

另外我们还需要一款消息队列作为后端,我选择 Apache Pulsar,官网如下

https://pulsar.apache.org/

我在前文 Apache Pulsar 的架构设计 介绍了 Pulsar 的一些原理,但并没有介绍它的基本用法,本文就来实践一下。

首先,Pulsar 支持多租户、多命名空间的企业级特性,也就是说一个 topic 的全名实际上是 tenant/namespace/topic。不过我们不用管这些,如果我们不指定租户名称和 namespace 名称创建一个名为room1的 topic,则会使用默认的租户名 public 和默认 namespace 名 default,创建一个全名是public/default/room1的 topic。

另外,我们说每个游戏客户端同时是生产者和消费者,Pulsar 的生产者只需要指定 topic 名字即可。但 Pulsar 的消费者这边抽象了一个 Subscription 的概念,有点类似 Kafka 的消费者组,但是更加灵活。

具体来说,Subscription 有三种模式,分别是Shared, Exclusive, Failover, Key_Shared,下面贴一张官网的图:

Shared模式的 Subscription 可以被任意数量的 consumer 订阅,对应 topic 的消息会被负载均衡算法分发给多个 consumer。

Key_Shared模式类似Shared模式,区别是能够根据消息的 key 进行负载均衡。

Exclusive模式的 Subscription 只允许一个 consumer 独占连接,其他试图连接该 Subscription 的 consumer 将会被拒绝。

Failover模式有些类似Exclusive模式,也是只能有第一个 consumer 能独占该 Subscription,但是后续试图连接该 Subscription 的 consumer 会作为备用,如果独占的 consumer 挂了,备用 consumer 能立刻补上。

在我们这个游戏的场景中,可以把玩家名称作为 Subscription 的名字,且把这个 Subscription 设置为Exclusive模式,这样如果有两个玩家用了同一个昵称,可以报错提示玩家重新设置:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
roomName := inputRoomName()
playerName := inputPlayerName()

// 创建 pulsar client
client, err := pulsar.NewClient(pulsar.ClientOptions{
    URL:            "your-pulsar-cluster-url",
})
// topic 名称就是房间名
topicName := roomName + "-topic"
// 玩家的名称就是 subscription 名
subscriptionName := playerName + "-sub"

// 创建 pulsar consumer
consumer, err := client.Subscribe(pulsar.ConsumerOptions{
    Topic:            topicName,
    SubscriptionName: subscriptionName,
    // 使用 Exclusive 模式订阅
    Type:             pulsar.Exclusive,
    // 保证玩家第一次登录时,从最新的消息开始消费
    SubscriptionInitialPosition: pulsar.SubscriptionPositionLatest,
})

if err != nil {
    log.Fatal("玩家昵称" + playerName + "已经被其他人用过了,请换一个")
}

// 保证玩家再次登录时,从最新的消息开始读
err = consumer.SeekByTime(time.Now())

代码中的SubscriptionInitialPosition用来设置创建这个 Subscription 时开始消费消息的位置,我们设置为Latest的意思是忽略之前的消息,从最新的消息开始消费。

但为什么需要调用SeekByTime方法呢,这需要解释一下 Pulsar 中 Subscription 的机制。

在 Pulsar 中,一个 Subscription 就好像是一个指向某个消息的命名指针,一旦创建之后就会持久化在 broker 端。也就是说这个SubscriptionPositionLatest只能设置 Subscription 创建时指向最新的消息,如果再次使用这个 Subscription 的话,并不能保证指向最新的消息。

具体到我们的游戏中是以下场景:

1、玩家首次使用用昵称player1进入房间room1,此时相当于在 Pulsar 中新建了一个名为room1-topic的 topic,然后新建了一个名为player1-sub的 Subscription 去消费room1-topic中的消息。由于设置SubscriptionInitialPositionSubscriptionPositionLatest,所以player1-sub指向room1-topic中的最新消息。

2、玩家player1退出游戏,consumer 断开和 Pulsar 的连接,但此时 Pulsar 中已经保存了名为player1-sub的 Subscription,指向player1退出时最后消费的那条消息,我们假设是msg-X

3、虽然玩家player1退出了,但房间room1中还有其他玩家在向room1-topic发送事件消息。

4、过了一段时间,玩家再次使用昵称player1进入房间room1,此时 Pulsar broker 发现room1-topic中已经有名为player1-sub的 Subscription,且该 Subscription 将从msg-X开始消费,所以player1将会看到类似放电影的场景:自己下线后所有其他玩家的操作都会重放一遍。

所以为了避免这种「放电影」的情景出现,我们需要手动调用SeekByTime方法,让重新登录的玩家也从最新的消息开始消费,投入战斗。

PS:回想一下,我们在玩 MOBA 游戏时,如果由于网络原因短暂卡顿重连,也会出现类似放快速放电影的情况。所以我猜测真实的多人在线游戏可能真的是通过类似消息队列的机制来保证玩家之间同步的。

当然这里有一个潜在的 bug:对于一个分布式消息系统来说,考虑到网络延迟、系统时钟的差异,时间戳的语义是不明确的,我们其实不应该依赖消息的时间戳

所以更好的一个方式是在玩家退出时调用Unsubscribe方法,相当于手动删除存储在 Pulsar broker 里的 Subscription:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
func Close() {
    consumer.Unsubscribe()
 consumer.Close()
    // ...
}

再考虑随机生成地图的功能,如何在地图中随机生成障碍物可以使用前文 水塘抽样算法 来实现。关键是我们需要在多个游戏客户端之间进行类似「选主」的操作,可以利用一个Exclusive模式的 Subscription 来达到目的:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
// 这个函数每分钟调用一次,试图向后端发送更新地图的事件
func trySendUpdateEvent(client pulsar.Client) {
    // 所有房间内的玩家都有相同的房间名,所以他们的 mapSubscriptionName 都相同
    mapTopicName := inputRoomName() + "-map-topic"
    mapSubscriptionName := inputRoomName() + "-map-sub"
    // 抢占这个 Subscription,抢到的那个客户端才能发起更新地图的请求
    _, err := client.Subscribe(pulsar.ConsumerOptions{
        Topic:            mapTopicName,
        // Exclusive 模式下只有第一个 consumer 能连接成功
        Type:             pulsar.Exclusive,
        SubscriptionName: mapSubscriptionName,
    })
    
    if err != nil {
        // 已经有别的 consumer 抢到这个 Subscription 了
        // 让他们更新地图吧
        return
    }
    // 我抢到了这个 Subscription,我负责来更新随机地图
    producer, err := c.client.CreateProducer(pulsar.ProducerOptions{
        Topic:           mapTopicName,
    })
    // 生成新的随机地图 event,发送到 pulsar 中
    payload := getUpdateMapEventData()
    producer.Send(context.Background(), &pulsar.ProducerMessage{Payload: payload})
}

假设游戏房间名称是room1,那么玩家的动作将会发送到名为room1-topic的 topic 中,而地图的更新操作将会发送到名为room1-map-topic的 topic 中。

有的读者可能好奇,为什么要给地图更新单独建立一个 topic 呢?直接把更新地图的Event也直接发到room1-topic里面不行吗?其实是不行的。

根据我们前面的代码,玩家登录后会从最新的消息开始消费,那么玩家大概率收不到这个更新地图的Event,也就无法初始化地图,只下一次更新地图的时才能完成地图的初始化。

而如果把地图的更新事件放在另一个专用的 topic 中,玩家登录后只需从这个 topic 读取最新的消息,就可以得到初始化地图了。

想要读取 topic 中最新的那条消息,可以用 Pulsar 提供的Reader接口:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
reader, err := c.client.CreateReader(pulsar.ReaderOptions{
    Topic: mapTopicName,
    // 指向最新的那一条消息
    StartMessageID: pulsar.LatestMessageID(),
    StartMessageIDInclusive: true,
})

if reader.HasNext() {
    // 读取最新的地图消息,初始化地图
    msg, err := reader.Next(context.Background())
    updateMap(msg)
}

Pulsar 的Reader接口就好比一个迭代器,可以通过HasNextNext方法一条一条读取消息,不过在这里我们仅仅使用它来读取地图 topic 中最新的消息,其他时候还是用 consumer 读取消息。

上述代码演示了使用 Pulsar 实现多人游戏的核心逻辑,下面再介绍一些关键的代码实现

关键代码实现

根据前文的内容,每个游戏客户端需要持有一个 producer,用来把玩家的操作事件发送到操作事件对应的 topic 中,其中有一个客户端需要一个额外的 producer,定期将新的随机地图发到地图更新事件的 topic 中。另外,每个游戏客户端需要持有两个 consumer,分别订阅操作事件的 topic 和地图更新的 topic。

所以我就使用 go 语言的 channel 来处理所有事件的输入和输出:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
type Game struct {
    // 从 pulsar 中接收事件
 receiveCh chan Event
 // 向 pulsar 中发送事件
 sendCh chan Event

    // 地图、其他玩家的坐标、炸弹坐标等等游戏数据
    // ...
}

所有需要发往 Pulsar 的事件只要塞到sendCh,Pulsar 的 producer 实例就会把事件发往对应的 topic;其他玩家产生的操作事件都会被发到receiveCh中,只要渲染这些事件即可在当前玩家的屏幕上显示出其他玩家的操作。

这个Event是我自己实现的一个接口,该接口声明了一个handle方法:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
type Event interface {
    // 传入 Game 结构,可以修改游戏数据
 handle(game *Game)
}

这样,只要我们把用户的操作抽象成不同的Event,然后实现对应的handle方法即可。比如我列举几个关键的事件:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
// 放置炸弹的事件
type SetBombEvent struct {
 bombName string
 pos      Position
}

func (e *SetBombEvent) handle(game *Game) {
    // ...
    go func() {
        // 3 秒后炸弹爆炸
        explodeTimer := time.NewTimer(3 * time.Second)
        <-explodeTimer.C
        // 发送炸弹爆炸的事件
        game.sendSync(&ExplodeEvent{
            bombName: bombName,
        })
    }()
}


// 炸弹爆炸的事件
type ExplodeEvent struct {
 bombName string
 pos      Position
}

func (e *ExplodeEvent) handle(game *Game) {
 // ...
    go func() {
        // 2 秒后炸弹爆炸的火焰消失
        undoTimer := time.NewTimer(2 * time.Second)
        <-undoTimer.C
        // 发送爆炸结束的事件
        game.sendSync(&UndoExplodeEvent{
            pos: bomb.pos,
        })
    }()
}


// 爆炸结束的事件
type UndoExplodeEvent struct {
 pos Position
}

func (e *UndoExplodeEvent) handle(game *Game) {
 // ...
}


// 玩家移动的事件
type UserMoveEvent struct {
    // ...
}

func (a *UserMoveEvent) handle(g *Game) {
 // ...
}

有了这个Event接口,结合 Ebitengine 游戏框架的使用,我们可以这样实现关键的UpdateDraw方法:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
// 这个函数会在每一帧显示前调用,用于更新游戏数据
func (g *Game) Update() error {
 // 1、非阻塞地接收并处理一个事件,更新游戏数据
 select {
 case event := <-g.eventCh:
  event.handle(g)
 default:
 }

    // 2、监听玩家的键盘操作,发给后端的 pulsar
 var event = listenPlayerKeyboardEvent()
    g.sendSync(event)
    // ...

    return nil
}

// 这个函数会 Update 后调用,用于显示游戏界面
func (g *Game) Draw(screen *ebiten.Image) {
    // 画出地图和障碍物
 for pos, _ := range g.obstacleMap {
  ebitenutil.DrawRect(screen, float64(pos.X*gridSize), float64(pos.Y*gridSize), gridSize, gridSize, obstacleColor)
 }

    // 画出炸弹
 for pos, _ := range g.posToBombs {
  ebitenutil.DrawRect(screen, float64(pos.X*gridSize), float64(pos.Y*gridSize), gridSize, gridSize, bombColor)
 }

    // 画出每个玩家的位置
 for _, player := range g.nameToPlayers {
  // ...
 }

    // 画出炸弹爆炸后的火焰
 for pos, val := range g.flameMap {
  // ...
 }
}

当然,本文中的代码是大幅简化过的,省略了诸如错误处理的细节,不过现在整个游戏的关键逻辑应该已经理清了

我们还可以给游戏添加有趣的新特性,比如道具系统、爆炸效果不同的炸弹、允许玩家推动炸弹、计分系统等,目前我实现了一部分新特性。

运行游戏

首先,我们需要一个 Pulsar 集群作为后端系统,且需要你和你的朋友连接同一套 Pulsar 集群才能一起游戏。

你可以在 Apache Pulsar 的官网查看文档自己搭建服务器部署一套:

https://pulsar.apache.org/

也可以在 StreamNative Cloud 平台上建立一个免费 Pulsar 集群:

https://console.streamnative.cloud/

首先,集群需要用 OAuth 的方式连接认证,所以需要先在 Service Account 中新建一个秘钥,然后把秘钥文件下载到本地:

然后新建一个免费的集群(注意免费集群拥有的资源很少,且一段时间后会自动回收集群资源):

新建了 Instance 之后可以查看 Pulsar Cluster 的信息,包括连接集群的地址:

现在 Pulsar 集群就建好了,可以从我的仓库 clone 游戏代码:

https://github.com/labuladong/pulsar-bomb-game

下载依赖后修改main.go文件中的privateKeyPath为秘钥文件的路径,修改pulsarUrl为 Pulsar 集群的地址,最后运行程序go run *.go即可启动游戏。

多个玩家只要连接同一个集群并且输入相同的房间号,即可一起游戏:

我让地图里随机生成炸弹以提高难度,但如果玩家被炸死,还可以按 R 键复活继续游戏。

详细的代码实现可以看我的代码仓库,本文就到这里,主要带大家实操一下 Apache Pulsar 的使用,后续我还会分享更多消息系统相关的技术,敬请期待。

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2022-10-18,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 labuladong 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
暂无评论
推荐阅读
编辑精选文章
换一批
《玩游戏,学技术》第二篇,用消息队列实现所有游戏功能
书接前文,《玩游戏,学技术》第一讲:需求分析 提出了最关键的一个设计,即每个游戏客户端包含一个 Pulsar 生产者和一个 Pulsar 消费者:
labuladong
2023/03/02
7220
《玩游戏,学技术》第二篇,用消息队列实现所有游戏功能
简单几步,教你搭建一款联机游戏
联机游戏的社交属性强,玩家粘性高,但是相对单机游戏,联机游戏开发周期长、成本高,因此很多开发者选择开发单机游戏,然而投入大量开发时间和资源,单机游戏活跃度不温不火,玩家数量持续流失。本文利用两款小游戏案例介绍如何快速搭建联机玩法,帮助开发者短期低成本实现一款联机游戏。
腾讯游戏云
2021/01/04
7.5K1
简单几步,教你搭建一款联机游戏
如何利用状态同步开发一款联机游戏
目前市场上单机游戏占比高,因为相对联机游戏开发周期短、成本低,但联机游戏的社交属性强,玩家粘性高。总体来说,开发联机游戏有一定的技术门槛。
腾讯游戏云
2020/12/24
4.2K0
如何利用状态同步开发一款联机游戏
《玩游戏,学技术》第一讲:画饼
我之前写过一篇文章 我用消息队列做了个联机游戏 用 Pulsar 这款消息队列实现了一个比较简陋的炸弹人游戏
labuladong
2023/03/02
4660
《玩游戏,学技术》第一讲:画饼
腾讯云大学大咖分享 | 小游戏联机对战引擎实践
腾讯云大学本期直播课程邀请到了腾讯云Web前端工程师通过两个小游戏demo,讲解了小游戏联机对战引擎中帧同步和状态同步两种应用场景。「腾讯云大学」联合「云加社区」为大家整理了课程精彩干货!
可可爱爱没有脑袋
2019/09/11
4.6K0
腾讯云大学大咖分享 | 小游戏联机对战引擎实践
纯前端如何利用帧同步做一款联机游戏?
·现代多人游戏中,多个客户端之间的通讯大多以同步多方状态为主要目标,为了实现这一目标,主要有两个技术方向:状态同步、帧同步。
腾讯游戏云
2020/11/13
2.8K0
别在纠结“后端”开发了,联机小游戏还可以这样做!
本篇文章要感谢「银笑的尤里」从 9月28日腾讯云深圳「游戏开发的超“音”“速”」沙龙发来了重磅消息,下面 Shawn 重点介绍对个人开发者惊喜的“MGOBE” 联机对战引擎。
张晓衡
2019/10/14
2.7K0
别在纠结“后端”开发了,联机小游戏还可以这样做!
【干货】看看我司消息队列用啥,全网最接地气pulsar教程(含业务解耦demo源码)
通过简单代码demo进行讲解,pulsar在java中如何使用?如何通过pulsar进行异步解耦?......等
JavaDog程序狗
2024/09/19
4120
【干货】看看我司消息队列用啥,全网最接地气pulsar教程(含业务解耦demo源码)
消息队列基本概念与pulsar学习
Pub-sub架构(发布/订阅),异步的服务间通信方式,适用于无服务器和微服务。发布到主题的任何消息都会立即被主题的所有订阅者接收。
千灵域
2022/06/17
4700
cs1.6开服教程
服务器的最低硬件配置大概在PIII500、内存在128M以上,要是内存较低的话,那你就要常常忍受超时的痛苦了。
用户8112612
2021/12/11
2.3K0
ColyseusJS 轻量级多人游戏服务器开发框架 - 中文手册(中)
快速上手多人游戏服务器开发。后续会基于 Google Agones,更新相关 K8S 运维、大规模快速扩展专用游戏服务器的文章。拥抱☁️原生? Cloud-Native! 系列 ColyseusJS
为少
2021/05/27
2.1K0
用Python写了一个水果忍者小游戏
今天小五就用python简单的模拟一下这个游戏。在这个简单的项目中,我们用鼠标选择水果来切割,同时炸弹也会隐藏在水果中,如果切开了三次炸弹,玩家就会失败。
用户8544541
2022/01/27
7280
用Python写了一个水果忍者小游戏
Python游戏开发,pygame模块,Python实现扫雷小游戏
游戏界面左上角的数字代表所有方格中埋有雷的数目,右上角是一个计时器。你要做的就是根据提示找出方格中所有的雷。
玖柒的小窝
2021/12/14
2.3K0
Python游戏开发,pygame模块,Python实现扫雷小游戏
仅有两名前端开发,联机小游戏一周内上线,如何做到?
它在上线四小时内用户数激增60倍,获得新华社力荐,开发过程中仅投入2个前端开发+1个美术+1个策划,这款小游戏里,单机玩法、邀请好友对战、在线匹配对战、排行榜、背景音乐音效等功能一应俱全。
腾讯云开发TCB
2020/03/31
4.4K0
仅有两名前端开发,联机小游戏一周内上线,如何做到?
13个Python小游戏,今天上班摸鱼玩了一天
玩法:这让我想起了魂斗罗那第几关的boss,有点类似,不过魂斗罗那个难度肯定高点。
用户8544541
2022/03/24
5870
13个Python小游戏,今天上班摸鱼玩了一天
在 TKE 使用 KEDA 实现基于 Apache Pulsar 消息队列的弹性伸缩
KEDA 的触发器支持 Apache Pulsar,即根据 Pulsar 消息队列中的未消费的消息数量进行水平伸缩,用法参考 KEDA Scalers: Apache Pulsar。
imroc
2024/04/29
2460
在 TKE 使用 KEDA 实现基于 Apache Pulsar 消息队列的弹性伸缩
仅有两名前端开发,联机小游戏一周内上线,如何做到?
它在上线四小时内用户数激增60倍,获得新华社力荐,开发过程中仅投入2个前端开发+1个美术+1个策划,这款小游戏里,单机玩法、邀请好友对战、在线匹配对战、排行榜、背景音乐音效等功能一应俱全。
腾讯云开发TCB
2023/07/14
1.1K0
仅有两名前端开发,联机小游戏一周内上线,如何做到?
SpringBoot整合分布式消息平台Pulsar
作为优秀的消息流平台,Pulsar 的使用越来越多,这篇文章讲解 Pulsar 的 Java 客户端。
jinjunzhu
2022/09/23
7830
SpringBoot整合分布式消息平台Pulsar
使用pygame开发合金弹头(5)
Python的强大超出你的认知,Python的功能不止于可以做网络爬虫,数据分析,Python完全可以进行后端开发,AI,Python也可进行游戏开发,本文将会详细介绍Python使用pygame模块来开发一个名为“合金弹头”的游戏
疯狂软件李刚
2020/06/24
1.4K0
使用pygame开发合金弹头(5)
用 Node.js 写一个多人游戏服务器引擎 [每日前端夜话0x31]
听说过文字冒险游戏吗? 如果你的年龄足够大的话(就像我一样),那么你可能听说过、甚至玩过“back in zhe day”。在本文中,我将向你展示编写的整个过程。这不仅仅是一个文本冒险游戏,而是一个能让你和你的朋友们一起玩的,可以进行任何剧情的文本冒险游戏引擎。 没错,我们将通过在添加多人游戏功能来增加它的趣味性。
疯狂的技术宅
2019/03/27
2.5K0
用 Node.js 写一个多人游戏服务器引擎 [每日前端夜话0x31]
推荐阅读
相关推荐
《玩游戏,学技术》第二篇,用消息队列实现所有游戏功能
更多 >
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档
本文部分代码块支持一键运行,欢迎体验
本文部分代码块支持一键运行,欢迎体验