!由于产品逻辑已无法满足游戏行业技术发展,游戏联机对战引擎 MGOBE 将于2022年6月1日下线,请您在2022年5月31日前完成服务迁移。
本文档指导您如何实现,从匹配加房到开始帧同步、结束帧同步的流程。
已创建 MainView.js、RoomView.js 、GameView.js 文件。
快速加房操作在 MainView 主页完成。交互逻辑是玩家单击快速加房后,SDK 发起房间匹配请求,请求成功后玩家将加入房间,此时需跳转至 RoomView 房间页。
- 在 MainView.js 的 onInit 方法中为 button 绑定点击事件。
- 为 MainView 类添加 matchRoom 方法。
- 编译代码后,单击快速加房,模拟器页面将跳转到 RoomView 房间页。
MainView 主页示例代码如下:
// 游戏主页
import * as Util from "../Util.js";
import BaseView from "./BaseView.js";
import Component from "../component/index.js";
const Global = Util.Global;
export default class MainView extends BaseView {
constructor() {
super();
}
onInit() {
const button = new Component.Button(20, 20, "快速加房");
// 绑定点击事件
button.onClick(() => this.matchRoom());
const msgBox = new Component.MsgBox(20, 100, "");
this.addComponent(button);
this.addComponent(msgBox);
}
// 房间匹配方法
matchRoom() {
this.loading('匹配中...');
const playerInfo = {
name: Util.mockName(),
};
Global.name = playerInfo.name;
const matchRoomPara = {
playerInfo,
maxPlayers: 2,
roomType: "1",
};
// 调用房间匹配接口实现快速加房
Global.room.matchRoom(matchRoomPara, event => {
wx.hideLoading();
if (event.code === Global.ErrCode.EC_OK) {
// 接口调用成功,跳转到 RoomView
return this.open(Global.RoomView);
}
return this.toast("匹配失败" + event.code + " " + event.msg);
});
}
onUpdate() {}
onDestroy() {}
}
Global.MainView = MainView;
RoomView 房间页有两个按钮,分别是准备、退出房间。交互逻辑是单击准备将修改玩家自定义状态,当房间内全部玩家都准备好后,可以开始帧同步进行游戏。单击退出房间,玩家将发起退房请求,成功后进入 MainView 主页。
- 为准备绑定点击事件。在 RoomView.js 的 onInit 中为 button1 添加事件。示例代码如下:
const button1 = new Component.Button(20, 20, "准备");
// 点击后切换状态
button1.onClick(() => this.changeCustomPlayerStatus(this.customPlayerStatus === 1 ? 0 : 1));
this.button = button1;
- 0和1表示玩家“未准备”、“已准备”两种状态。customPlayerStatus 为 RoomView 类的属性,用来保存玩家状态。示例代码如下:
export default class RoomView extends BaseView {
button;
msgBox;
customPlayerStatus;
// ...
- 为 RoomView 类添加 changeCustomPlayerStatus 方法。示例代码如下:
// 修改用户自定义状态
changeCustomPlayerStatus(customPlayerStatus) {
const changeCustomPlayerStatusPara = {
customPlayerStatus
};
Global.room.changeCustomPlayerStatus(changeCustomPlayerStatusPara, event => {
if (event.code !== Global.ErrCode.EC_OK) {
return this.toast("操作失败" + event.code);
}
return this.customPlayerStatus = customPlayerStatus;
});
}
MGOBE SDK 中的 Room 对象会管理所有房间变化,任何玩家进行加房、退房、修改玩家状态等会改变房间信息的操作后,Room 对象会自动更新房间信息。 因此,玩家在 RoomView 房间页修改状态后,只需要访问 Room 实例的 roomInfo 属性就能获得最新房间信息,页面也可以同步更新。可以直接使用 Room 实例的 onUpdate 方法检查房间属性并更新页面。
- 为 RoomView 类添加 onRoomUpdate 方法检查房间最新属性,更新按钮和文本框,并当全部玩家处于已准备状态时跳转到 GameView 游戏页。示例代码如下:
onRoomUpdate() {
// 更新 按钮
const playerId = MGOBE.Player.id;
const player = Global.room.roomInfo.playerList.find(player => player.id === playerId);
if (player) {
this.customPlayerStatus = player.customPlayerStatus;
// 更新 按钮 文字
if (player.customPlayerStatus === 0) {
this.button.setText("准备");
} else {
this.button.setText("取消准备");
}
}
// 更新 MsgBox
let msg = "房间ID:\n" + Global.room.roomInfo.id + "\n玩家列表:\n";
Global.room.roomInfo.playerList.forEach((player, i) => {
msg += i + ":" + player.id + (player.customPlayerStatus === 1 ? " 已准备" : " 未准备") + "\n";
});
this.msgBox.setText(msg);
if (Global.room.isInRoom() && !Global.room.roomInfo.playerList.find(player => player.customPlayerStatus !== 1)) {
// 全部玩家准备好就跳转
setTimeout(() => this.open(Global.GameView), 1000);
}
}
- 在 onInit 追加代码,绑定 onRoomUpdate。示例代码如下:
onInit() {
// ...
Global.room.onUpdate = this.onRoomUpdate.bind(this);
// 刷新canvas
this.onRoomUpdate();
}
- 在 onDestroy 中解除绑定 onRoomUpdate。示例代码如下:
onDestroy() {
Global.room.onUpdate = null;
}
- 当其他玩家进入房间时,SDK 会触发 onJoinRoom 回调。因此可以在进入页面时为房间绑定 onJoinRoom 广播回调函数。修改 RoomView.js 中的 onInit 方法。示例代码如下所示:
onInit() {
// ...
// 设置广播回调
Global.room.onJoinRoom = this.onJoinRoom.bind(this);
}
- 添加 onJoinRoom 方法,这里做一个 toast 提醒玩家即可。示例代码如下所示:
// 加房广播
onJoinRoom(event) {
this.toast("新玩家加入");
}
- 离开页面时置空 Global.room.onJoinRoom。示例代码如下所示:
onDestroy() {
Global.room.onUpdate = null;
Global.room.onJoinRoom = null;
}
- 使用两个设备进行“快速加房”操作(如模拟器+手机预览),两个玩家有可能匹配在一起,这时页面上将弹出“新玩家加入”提示。
- 在 RoomView 房间页中为 button2 绑定点击事件。示例代码如下:
onInit() {
// ...
const button2 = new Component.Button(150, 20, "退出房间");
button2.onClick(() => this.leaveRoom());
// ...
}
- 在 RoomView 类中实现 leaveRoom 方法。示例代码如下:
// 退出房间
leaveRoom() {
Global.room.leaveRoom({}, event => {
if (event.code !== Global.ErrCode.EC_OK && event.code !== Global.ErrCode.EC_ROOM_PLAYER_NOT_IN_ROOM) {
return this.toast("操作失败" + event.code);
}
return this.open(Global.MainView);
});
}
RoomView 房间页最终示例代码如下:
// 房间页
import * as Util from "../Util.js";
import BaseView from "./BaseView.js";
import Component from "../component/index.js";
const Global = Util.Global;
export default class RoomView extends BaseView {
button;
msgBox;
// 自定义玩家状态:“0和1”表示玩家“未准备”、“已准备”
customPlayerStatus;
constructor() {
super();
}
onInit() {
const button1 = new Component.Button(20, 20, "准备");
// 点击后切换状态
button1.onClick(() => this.changeCustomPlayerStatus(this.customPlayerStatus === 1 ? 0 : 1));
this.button = button1;
const button2 = new Component.Button(150, 20, "退出房间");
button2.onClick(() => this.leaveRoom());
const msgBox = new Component.MsgBox(20, 100, "");
this.msgBox = msgBox;
this.addComponent(button1);
this.addComponent(button2);
this.addComponent(msgBox);
Global.room.onUpdate = this.onRoomUpdate.bind(this);
// 刷新canvas
this.onRoomUpdate();
// 设置广播回调
Global.room.onJoinRoom = this.onJoinRoom.bind(this);
}
// 修改用户自定义状态
changeCustomPlayerStatus(customPlayerStatus) {
const changeCustomPlayerStatusPara = {
customPlayerStatus
};
Global.room.changeCustomPlayerStatus(changeCustomPlayerStatusPara, event => {
if (event.code !== Global.ErrCode.EC_OK) {
return this.toast("操作失败" + event.code);
}
return this.customPlayerStatus = customPlayerStatus;
});
}
onRoomUpdate() {
// 更新 按钮
const playerId = MGOBE.Player.id;
const player = Global.room.roomInfo.playerList.find(player => player.id === playerId);
if (player) {
this.customPlayerStatus = player.customPlayerStatus;
// 更新 按钮 文字
if (player.customPlayerStatus === 0) {
this.button.setText("准备");
} else {
this.button.setText("取消准备");
}
}
// 更新 MsgBox
let msg = "房间ID:\n" + Global.room.roomInfo.id + "\n玩家列表:\n";
Global.room.roomInfo.playerList.forEach((player, i) => {
msg += i + ":" + player.id + (player.customPlayerStatus === 1 ? " 已准备" : " 未准备") + "\n";
});
this.msgBox.setText(msg);
if (Global.room.isInRoom() && !Global.room.roomInfo.playerList.find(player => player.customPlayerStatus !== 1)) {
// 全部玩家准备好就跳转
setTimeout(() => this.open(Global.GameView), 1000);
}
}
// 加房广播
onJoinRoom(event) {
this.toast("新玩家加入");
}
// 退出房间
leaveRoom() {
Global.room.leaveRoom({}, event => {
if (event.code !== Global.ErrCode.EC_OK && event.code !== Global.ErrCode.EC_ROOM_PLAYER_NOT_IN_ROOM) {
return this.toast("操作失败" + event.code);
}
return this.open(Global.MainView);
});
}
onUpdate() { }
onDestroy() {
Global.room.onUpdate = null;
Global.room.onJoinRoom = null;
}
}
Global.RoomView = RoomView;
- 编译代码后,在模拟器中进行加房操作进入 RoomView 房间页后,单击退出房间,即可跳转到 MainView 主页。
在 RoomView 房间页中,全部玩家单击准备后,页面自动跳转到 GameView 游戏页。此时需要在 GameView 游戏页中检查房间帧同步状态,如果房间未开启帧同步,则调用开始帧同步接口。之后继续将玩家状态改为未准备,避免出现回到 RoomView 房间页中又自动跳转 GameView 游戏页的情况。
- 修改 GameView.js 的 onInit 方法。示例代码如下:
onInit() {
const button = new Component.Button(20, 20, "结束帧同步");
const msgBox = new Component.MsgBox(20, 100, "");
this.msgBox = msgBox;
this.addComponent(button);
this.addComponent(msgBox);
if (Global.room.roomInfo.frameSyncState !== Global.ENUM.FrameSyncState.START) {
// 调用 startFrameSync 接口
this.startFrameSync();
}
// 修改玩家状态
this.changeCustomPlayerStatus(0);
}
- 在 GameView 类中添加 startFrameSync 方法。示例代码如下:
// 开始帧同步
startFrameSync() {
const func = () => Global.room.startFrameSync({}, event => {
if (event.code !== Global.ErrCode.EC_OK) {
return this.dialog("操作失败,是否重试?", () => func());
}
});
func();
}
- 在 GameView 类中添加 changeCustomPlayerStatus 方法。示例代码如下:
// 修改用户状态
changeCustomPlayerStatus(customPlayerStatus) {
const changeCustomPlayerStatusPara = { customPlayerStatus };
const func = () => Global.room.changeCustomPlayerStatus(changeCustomPlayerStatusPara, event => {
if (event.code !== Global.ErrCode.EC_OK) {
this.dialog("操作失败,是否重试?", () => func());
}
});
func();
}
玩家在游戏过程中发送的帧消息都是由一些指令组成,例如“跳跃”、“发射子弹”等操作。这里实现一个简单的指令,每个玩家发送一个随机数出去。在 GameView 类中添加一个 sendFrame 方法。示例代码如下:
// 玩家发送帧消息
sendFrame() {
const data = {
name: Global.name,
action: "random",
number: Math.ceil(Math.random() * 100),
}
Global.room.sendFrame({ data });
}
开始帧同步后,SDK 会收到服务器推送的帧广播消息。您需要绑定 onFrame 回调函数,接收并处理每一帧消息。
- 在 GameView 类的 onInit 中为 room 对象绑定帧消息广播回调,并实现 onFrame 方法。示例代码如下:
onInit() { // ... // 设置广播回调 Global.room.onRecvFrame = this.onRecvFrame.bind(this); } // ... onRecvFrame(event) { // 在这里处理帧广播 }
- 在 onFrame 中添加代码,实现记录帧广播消息,并定时发送帧消息。示例代码如下:
frameId = 0; frameItems = []; onRecvFrame(event) { // 在这里处理帧广播 const frameId = event.data.frame.id; // 每隔 15 帧发送一次帧消息 if (frameId > this.frameId + 15) { this.frameId = frameId; this.sendFrame(); } // 记录帧广播消息 if (event.data.frame.items) { this.frameItems = this.frameItems.concat(event.data.frame.items); } }
- 在 onUpdate 中将帧消息渲染到页面上。示例代码如下:
onUpdate() { // 渲染层不断更新页面 this.drawFrameItems(); } // ... drawFrameItems() { // 只显示5行 const max = 5; if (this.frameItems.length > max) this.frameItems = this.frameItems.slice(this.frameItems.length - max); let msg = ""; this.frameItems.forEach(item => { msg += item.data.name + " : " + item.data.number + "\n"; }); this.msgBox.setText(msg); }
- 编译项目,在模拟器上依次单击快速加房、准备后,进入 GameView 游戏页,页面将自动更新 MsgBox,并显示玩家消息。如下图所示:
为 button 绑定点击事件监听,实现停止帧同步。
- 在 onInit 中添加代码。示例代码如下:
onInit() {
const button = new Component.Button(20, 20, "结束帧同步");
button.onClick(() => this.stopFrameSync());
// ...
}
- 使用 stopFrameSync 方法实现。示例代码如下:
// 停止帧同步
stopFrameSync() {
if (Global.room.frameSyncState === Global.ENUM.FrameSyncState.STOP) {
return;
}
const func = () => Global.room.stopFrameSync({}, event => {
if (event.code !== Global.ErrCode.EC_OK) {
this.dialog("操作失败,是否重试?", () => func());
}
});
func();
}
- 帧同步停止后,需要跳转到 RoomView 房间页。检查房间帧同步状态有两种方法:
- 监听 onStartFrameSync、onStopFrameSync 广播。
- 在 room.onUpdate 中检查 room.roomInfo.frameStatus 的值。
- 利用 room.onUpdate 回调同时检查帧同步状态和房间成员状态,如果房间停止帧同步并且房间成员状态全部为 0,就跳转到 RoomView 房间页。示例代码如下:
onRoomUpdate() {
if (Global.room.roomInfo.frameSyncState === Global.ENUM.FrameSyncState.STOP &&
!Global.room.roomInfo.playerList.find(player => player.customPlayerStatus === 1)) {
return this.open(Global.RoomView);
}
}
- 在 onInit 方法、startFrameSync 的回调中绑定 onRoomUpdate。示例代码如下:
onInit() { // ... if (Global.room.roomInfo.frameSyncState !== Global.ENUM.FrameSyncState.START) { // 调用 startFrameSync 接口 this.startFrameSync(); } else { Global.room.onUpdate = this.onRoomUpdate.bind(this); } // ... } // ... // 开始帧同步 startFrameSync() { const func = () => Global.room.startFrameSync({}, event => { if (event.code !== Global.ErrCode.EC_OK) { return this.dialog("操作失败,是否重试?", () => func()); } Global.room.onUpdate = this.onRoomUpdate.bind(this); }); func(); }
- 在 onDestroy 中清除 onUpdate、onFrame 回调。示例代码如下:
onDestroy() {
Global.room.onUpdate = null;
Global.room.onFrame = null;
}
-
编译代码,在模拟器中进入 GameView 游戏页开始帧同步,然后单击结束帧同步,界面将跳转到 RoomView 房间页,并且玩家状态为未准备。
GameView 游戏页最终示例代码如下:
// 帧同步游戏页面 import * as Util from "../Util.js"; import BaseView from "./BaseView.js"; import Component from "../component/index.js"; const Global = Util.Global; export default class GameView extends BaseView { msgBox; constructor() { super(); } onInit() { const button = new Component.Button(20, 20, "结束帧同步"); button.onClick(() => this.stopFrameSync()); const msgBox = new Component.MsgBox(20, 100, ""); this.msgBox = msgBox; this.addComponent(button); this.addComponent(msgBox); if (Global.room.roomInfo.frameSyncState !== Global.ENUM.FrameSyncState.START) { // 调用 startFrameSync 接口 this.startFrameSync(); } else { Global.room.onUpdate = this.onRoomUpdate.bind(this); } // 修改玩家状态 this.changeCustomPlayerStatus(0); // 设置广播回调 Global.room.onRecvFrame = this.onRecvFrame.bind(this); } // 开始帧同步 startFrameSync() { const func = () => Global.room.startFrameSync({}, event => { if (event.code !== Global.ErrCode.EC_OK) { return this.dialog("操作失败,是否重试?", () => func()); } Global.room.onUpdate = this.onRoomUpdate.bind(this); }); func(); } // 停止帧同步 stopFrameSync() { if (Global.room.frameSyncState === Global.ENUM.FrameSyncState.STOP) { return; } const func = () => Global.room.stopFrameSync({}, event => { if (event.code !== Global.ErrCode.EC_OK) { this.dialog("操作失败,是否重试?", () => func()); } }); func(); } onRoomUpdate() { if (Global.room.roomInfo.frameSyncState === Global.ENUM.FrameSyncState.STOP && !Global.room.roomInfo.playerList.find(player => player.customPlayerStatus === 1)) { return this.open(Global.RoomView); } } // 修改用户状态 changeCustomPlayerStatus(customPlayerStatus) { const changeCustomPlayerStatusPara = { customPlayerStatus }; const func = () => Global.room.changeCustomPlayerStatus(changeCustomPlayerStatusPara, event => { if (event.code !== Global.ErrCode.EC_OK) { this.dialog("操作失败,是否重试?", () => func()); } }); func(); } // 玩家发送帧消息 sendFrame() { const data = { name: Global.name, action: "random", number: Math.ceil(Math.random() * 100), } Global.room.sendFrame({ data }); } frameId = 0; frameItems = []; onRecvFrame(event) { // 在这里处理帧广播 const frameId = event.data.frame.id; // 每隔 15 帧发送一次帧消息 if (frameId > this.frameId + 15) { this.frameId = frameId; this.sendFrame(); } // 记录帧广播消息 if (event.data.frame.items) { this.frameItems = this.frameItems.concat(event.data.frame.items); } } drawFrameItems() { // 只显示5行 const max = 5; if (this.frameItems.length > max) this.frameItems = this.frameItems.slice(this.frameItems.length - max); let msg = ""; this.frameItems.forEach(item => { msg += item.data.name + " : " + item.data.number + "\n"; }); this.msgBox.setText(msg); } onUpdate() { // 渲染层不断更新页面 this.drawFrameItems(); } onDestroy() { Global.room.onUpdate = null; Global.room.onFrame = null; } } Global.GameView = GameView;
至此,整个 HelloWorld 示例结束,单击 这里 下载完整代码。