在这段代码中,Game 中的 MasterClient 对象注册了多人对战的自定义事件,当玩家 A 发送 play 事件给 MasterClient 时,这个事件会被触发。我们在这个事件中使用了 Game 的转发事件方法 forwardToTheRests(),这个方法第一个参数是原始的事件,第二个参数是原始事件的 eventData 数据处理,我们将原始的 eventData 数据,也就是玩家 A 发来的 {index},修改为空数据 {},这样当玩家 B 收到事件后无法获知玩家 A 的详细动作。
你的第一个 Client Engine 小游戏 · Node.js
该文档帮助您快速上手,通过 Client Engine 实现一个剪刀石头布的猜拳小游戏。完成本文档教程后,您会对 Client Engine 的基础使用流程有初步的理解。
准备初始项目
这个小游戏分为服务端和客户端两部分,其中服务端使用 Client Engine 来实现,客户端则是一个简单的 Web 页面。在这个教程中我们着重教您一步一步写 Client Engine 中的代码,客户端的代码请您查看示例项目。
Client Engine 项目
请先阅读 Client Engine 快速入门:运行及部署项目 获得初始项目,了解如何本地运行及部署项目。
./src
中主要源文件及用途如下:该项目中的
Game
及GameManager
使用的是 Client Engine SDK 的功能,关于 SDK 的详细用法请参考 Client Engine 开发指南。您可以从
index.ts
文件入手来了解整个项目,该文件是项目启动的入口,它通过 express 框架定义了名为/reservation
的 Web API,供客户端快速开始时为客户端下发新的房间名称。reception.ts
及rps-game.ts
里面有本教程的全部代码。您可以选择备份这两个文件,清空这两个文件后根据本文档撰写自己的代码,同时也可以查看已经写好的代码以做对比。客户端项目
点击下载客户端项目。打开
./src
中的config.ts
,将 appId 和 appKey 修改为自己应用的信息,按照 README 启动项目后观察界面的变化。游戏相关的逻辑位于./src/components
下的文件中,在有需要的时候您可以打开这里的文件查看代码。核心流程
在多人对战服务中,房间的创建者为 MasterClient,因此在这个小游戏中,每一个房间都是由 Client Engine 管理的 MasterClient 调用在线对战服务相关的接口来创建的。Client Engine 中会有多个 MasterClient,每一个 MasterClient 管理着自己房间内的游戏逻辑。
这个小游戏的核心逻辑为:Client Engine 中的 MasterClient 及客户端玩家 Client 加入到同一个房间,在通信过程中由 MasterClient 控制游戏内的逻辑。具体拆解步骤如下:
/reservation
接口请求快速开始游戏。代码开发
自定义 Game
我们的目标是让 MasterClient 和客户端 Client 进入同一个房间,第一步在 Client Engine 中我们先准备好房间。在 Client Engine SDK 中,每一个房间都对应一个
Game
对象,每一个Game
对象都对应一个自己的 MasterClient。接下来我们创建一个继承Game
的子类RPSGame
,在RPSGame
中撰写猜拳小游戏的房间内逻辑。在
rpg-game.ts
文件中初始化自定义的RPSGame
:管理 Game
Client Engine SDK 中,
GameManger
负责Game
的创建及销毁,具体的原理及结构介绍请参考 Client Engine 开发指南。在这篇文档中,我们通过简单的配置就可以使用GameMnager
的管理功能。自定义 GameManager
首先创建一个子类
Reception
继承自GameManager
,在这个子类中我们就可以使用GameManager
提供的方法来帮我们撰写自己的逻辑。在
reception.ts
文件中初始化自定义的Reception
:这个自定义的类
Reception
用于管理 T 类型的Game
对象,在实际游戏中会是您自定义的Game
类型的实例。接下来,我们在reception
中使用GameManager
的方法来实现自己的自定义逻辑:快速开始。实现逻辑:「快速开始」
这里我们要实现的快速开始的逻辑是:随便找一个有空位的房间返回给客户端,如果当前的 Client Engine 实例没有可用的房间,那么就创建一个房间返回给客户端。我们在
Reception
类中撰写名为makeReservation()
的自定义方法来实现这个逻辑并供入口 API 调用。在这段代码中,我们调用了
GameManager
的getAvailableGames()
来获得当前 Client Engine 实例管理的Game
:Game
,使用GameManager
的reserveSeats()
方法为玩家占位并返回 roomName。Game
房间都已经满员了,使用GameManager
的createGame()
方法创建一个新的房间并返回 roomName。实现逻辑:「创建新游戏」
如果您希望自己先创建房间,再邀请朋友加入该房间,可以在
reception
中写一个创建新游戏的方法供供入口 API 调用。同样我们自定义的createGameAndGetName()
方法内用到的createGame()
是由 SDK 中的GameManager
提供的。绑定 GameManager 及 Game
当
GameManager
的子类Reception
及Game
的子类RPSGame
都准备好后,我们要在整个项目入口把RPSGame
给到Reception
,由Reception
来管理RPSGame
。在
index.ts
文件创建Reception
对象的方法中可以看到,第一个参数已经传入了RPSGame
,如果您的自定义Game
使用的是其他的名字,可以将RPSGame
换成您自定义的Game
类。在这里配置完成后,
reception
会在合适的时机创建并管理PRSGame
和对应的 MasterClient。配置负载均衡
由于
GameManager
中的逻辑会直接被外部请求所调用,因此需要为在入口处为GameManager
配置负载均衡。关于负载均衡详细的介绍可以参考Client Engine 开发指南,在这里我们先简单的查看index.ts
文件中的这些代码,了解如何配置即可:到这里,管理
RPSGame
的reception
我们已经准备完成,接下来开始撰写具体的房间内游戏逻辑。设定房间内玩家数量
在这个猜拳小游戏中,我们设定只允许两个玩家玩,满两个玩家后就不允许新的玩家再进入房间,可以这样设置
Game
的静态属性defaultSeatCount
:在这里配置完成后,Client Engine 初始项目每次请求多人对战服务创建房间时,都会根据这里的值限定房间内的玩家数量。
对设置房间内玩家数量的详细讲解请参考 Client Engine 开发指南。
MasterClient 及客户端进入同一房间
在完成 Game 的基础配置之后,MasterClient 和客户端就可以准备加入同一个房间了。
入口 API:快速开始
index.ts
文件的入口 API/reservation
,当客户端调用这个接口时,该接口会调用Reception
自定义的makeReservation()
方法来帮助客户端快速开始游戏。客户端可以调用这个 API 来快速开始,用该接口的示例代码如下 (非 Client Engine 代码):
当客户端调用
/reservation
并加入房间成功后,意味着客户端 Client 及 MasterClient 进入了同一个房间内,当房间人数足够时,就可以开始游戏了。客户端项目中已经帮您写好了调用
/reservation
的代码,无需您自己再写代码,您可以在/src/components/Lobby.vue
中查看相关代码。入口 API:创建新游戏
该入口 API 撰写方式与「快速开始」相同,不再重复说明,可以参考 index.ts 文件中的
/game
方法。宣布游戏开始
在这个小游戏中,人满后我们就可以开始游戏了。我们可以在 Game 的人满事件中宣布游戏开始:
在这段代码中,
watchRoomFull
装饰器在人满时会使Game
抛出AutomaticGameEvent.ROOM_FULL
事件,在这个事件中我们选择调用自定义的start
方法。在start
方法中我们将房间打开,然后向所有客户端广播游戏开始。到了这一步,您可以启动当前 Client Engine 项目,启动客户端并开启两个客户端 Web 页面,在界面上点击「快速开始」,可以观察到第一个点击「快速开始」的界面显示出了日志:
xxxx 加入了房间
。猜拳逻辑
接下来我们开始开发具体的游戏中逻辑。具体步骤分为以下几步:
这三者之间的交互可以用这张图来表示:
接下来我们对每一步进行拆解并撰写代码:
玩家 A 选择手势,发送出拳事件给 MasterClient
这一部分代码是客户端的,不需要您写在 Client Engine 中,您可以在客户端项目中
./src/components/Game.vue
找到相关代码。MasterClient 收到事件,转发事件给玩家 B
这部分代码写在 Client Engine 中,您可以根据下方的示例代码写在自己的
RPSGame
中。我们在start
方法中注册自定义事件,并在收到play
事件后,将玩家 A 的动作内容抹去,转发给玩家 B。在这段代码中,Game 中的 MasterClient 对象注册了多人对战的自定义事件,当玩家 A 发送
play
事件给 MasterClient 时,这个事件会被触发。我们在这个事件中使用了Game
的转发事件方法forwardToTheRests()
,这个方法第一个参数是原始的事件,第二个参数是原始事件的 eventData 数据处理,我们将原始的 eventData 数据,也就是玩家 A 发来的{index}
,修改为空数据{}
,这样当玩家 B 收到事件后无法获知玩家 A 的详细动作。玩家 B 收到 MasterClient 转发来的事件,界面展示:对方已选择
这部分代码是客户端的,不需要您写在 Client Engine 中,您可以在客户端项目中
./src/components/Game.vue
找到相关代码。玩家 B 选择手势,发送出拳事件给 MasterClient
这部分逻辑和上文「玩家 A 选择手势,发送出拳事件给 MasterClient」相同,使用的也是相同部分的代码,您可以在客户端项目中
./src/components/Game.vue
找到相关代码。MasterClient 收到事件,转发事件给玩家 A
这部分逻辑和上文「MasterClient 收到事件,转发事件给玩家 B」相同,使用的也是相同部分的代码,不需要再额外在 Client Engine 中写代码。
玩家 A 收到 MasterClient 转发来的事件,界面展示:对方已选择
这部分逻辑和上文「玩家 A 收到 MasterClient 转发来的事件,界面展示:对方已选择」相同,使用的也是相同部分的代码,您可以在客户端项目中
./src/components/Game.vue
找到相关代码。到这一步时,您可以运行项目,打开两个界面猜拳,观察双方的动作同步到各自的界面中,但各自分别不知道对方选了什么。
MasterClient 发现双方都已经出拳,判断结果,公布答案并宣布游戏结束
每次 MasterClient 收到玩家选择事件时,我们要把玩家的选择存起来,并判断两位玩家是不是都已经做出选择了:
在上面这段代码中,我们构造了一个 Array 类型的 choice 来存储玩家的选择,当收到出拳事件时会将用户的选择存储起来,接下来我们判断两个玩家是不是都做出选择了,如果做出选择了则广播游戏结果,并广播游戏结束:
在上面的代码中,使用了
getWinner()
方法来获取游戏结果,这个是我们自定义的判断胜负的方法,您可以直接复制粘贴下方的代码到自己的RPSGame
文件中:客户端在收到 MasterClient 广播结束事件后在界面上做相应的结果展示。到这里基础逻辑都已经开发完成,您可以运行项目,打开两个页面,愉快的开始自己和自己的对战了。
离开房间
当所有客户端离开房间后,
GameManager
会帮助我们把空房间销毁,因此在我们这个小游戏的代码中不需要再额外写这部分的代码。RxJS
当您查看示例 Demo 时,会发现代码和本文档中的代码相比更精简一些,原因是示例 Demo 中使用了 RxJS。如果您感兴趣,可以自行研究 RxJS 及相关接口的 API 文档。
开发指南
当您按照本文档的说明一步一步开发出猜拳小游戏后,对 Client Engine SDK 和初始项目一定有了初步的感受,接下来您可以参考 Client Engine 开发指南更深入的了解整体结构及用法。