实现接口功能

最近更新时间:2022-01-10 15:46:23

注意:

因产品策略调整,游戏联机对战引擎后续将与云开发 CloudBase 整合为新产品形态,现将该产品保持维护状态,不再接收新用户使用申请,老用户仍可正常使用。

操作场景

本文档指导您如何实现,从匹配加房到开始帧同步、结束帧同步的流程。

前提条件

已创建 MainView.js、RoomView.js 、GameView.js 文件。

操作步骤

实现快速加房

快速加房操作在 MainView 主页完成。交互逻辑是玩家单击快速加房后,SDK 发起房间匹配请求,请求成功后玩家将加入房间,此时需跳转至 RoomView 房间页。

  1. 在 MainView.js 的 onInit 方法中为 button 绑定点击事件。
  2. 为 MainView 类添加 matchRoom 方法。
  3. 编译代码后,单击快速加房,模拟器页面将跳转到 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 主页。

  1. 准备绑定点击事件。在 RoomView.js 的 onInit 中为 button1 添加事件。示例代码如下:

        const button1 = new Component.Button(20, 20, "准备");
        // 点击后切换状态
        button1.onClick(() => this.changeCustomPlayerStatus(this.customPlayerStatus === 1 ? 0 : 1));
    
        this.button = button1;    

  2. 0和1表示玩家“未准备”、“已准备”两种状态。customPlayerStatus 为 RoomView 类的属性,用来保存玩家状态。示例代码如下:

    export default class RoomView extends BaseView {
    
      button;
      msgBox;
    
      customPlayerStatus;
    
      // ...

  3. 为 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 方法检查房间属性并更新页面。

  1. 为 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);
        }
    }

  2. 在 onInit 追加代码,绑定 onRoomUpdate。示例代码如下:

    onInit() {
        // ...
        Global.room.onUpdate = this.onRoomUpdate.bind(this);
        // 刷新canvas
        this.onRoomUpdate();
    }

  3. 在 onDestroy 中解除绑定 onRoomUpdate。示例代码如下:

    onDestroy() {
        Global.room.onUpdate = null;
    }

  4. 编译代码,玩家单击快速加房进入 RoomView 房间页后,单击准备,按钮和文本框都将更新。如果房间内的玩家都是已准备状态,页面将跳转到 GameView 游戏页。
    房间信息更新

处理其他玩家加房

  1. 当其他玩家进入房间时,SDK 会触发 onJoinRoom 回调。因此可以在进入页面时为房间绑定 onJoinRoom 广播回调函数。修改 RoomView.js 中的 onInit 方法。示例代码如下所示:
    onInit() {
        // ...
    
        // 设置广播回调
        Global.room.onJoinRoom = this.onJoinRoom.bind(this);
    }
  2. 添加 onJoinRoom 方法,这里做一个 toast 提醒玩家即可。示例代码如下所示:
    // 加房广播
    onJoinRoom(event) {
        this.toast("新玩家加入");
    }
注意

Global.room.onJoinRoom 指定了一个回调函数,当页面跳转到其他页面时,这个回调函数引用还在。如果该函数依赖于页面脚本的上下文,有可能在触发广播时报错。

  1. 离开页面时置空 Global.room.onJoinRoom。示例代码如下所示:
    onDestroy() {
        Global.room.onUpdate = null;
        Global.room.onJoinRoom = null;
    }
  2. 使用两个设备进行“快速加房”操作(如模拟器+手机预览),两个玩家有可能匹配在一起,这时页面上将弹出“新玩家加入”提示。
注意

手机预览时打开调试,跳过域名检查。

模拟器界面效果图如下所示:
模拟器界面效果
手机预览效果图如下所示:
手机预览效果

实现退房功能

  1. 在 RoomView 房间页中为 button2 绑定点击事件。示例代码如下:
      onInit() {
    
        // ...
    
        const button2 = new Component.Button(150, 20, "退出房间");
        button2.onClick(() => this.leaveRoom());
    
        // ...
      }
  2. 在 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;

  1. 编译代码后,在模拟器中进行加房操作进入 RoomView 房间页后,单击退出房间,即可跳转到 MainView 主页。

实现开始帧同步

在 RoomView 房间页中,全部玩家单击准备后,页面自动跳转到 GameView 游戏页。此时需要在 GameView 游戏页中检查房间帧同步状态,如果房间未开启帧同步,则调用开始帧同步接口。之后继续将玩家状态改为未准备,避免出现回到 RoomView 房间页中又自动跳转 GameView 游戏页的情况。

  1. 修改 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);
    }

  2. 在 GameView 类中添加 startFrameSync 方法。示例代码如下:

    // 开始帧同步
    startFrameSync() {
        const func = () => Global.room.startFrameSync({}, event => {
            if (event.code !== Global.ErrCode.EC_OK) {
                return this.dialog("操作失败,是否重试?", () => func());
            }
        });
    
        func();
    }

  3. 在 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 回调函数,接收并处理每一帧消息。

  1. 在 GameView 类的 onInit 中为 room 对象绑定帧消息广播回调,并实现 onFrame 方法。示例代码如下:
    onInit() {
            // ...
            // 设置广播回调
        Global.room.onRecvFrame = this.onRecvFrame.bind(this);
    }
    // ...
    onRecvFrame(event) {
        // 在这里处理帧广播
    }
  2. 在 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);
            }
    }
  3. 在 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);
    }
  4. 编译项目,在模拟器上依次单击快速加房准备后,进入 GameView 游戏页,页面将自动更新 MsgBox,并显示玩家消息。如下图所示:
    GameView效果

实现停止帧同步

为 button 绑定点击事件监听,实现停止帧同步。

  1. 在 onInit 中添加代码。示例代码如下:

    onInit() {
        const button = new Component.Button(20, 20, "结束帧同步");
        button.onClick(() => this.stopFrameSync());
    
        // ...
    }

  2. 使用 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();
    }

  3. 帧同步停止后,需要跳转到 RoomView 房间页。检查房间帧同步状态有两种方法:

    • 监听 onStartFrameSync、onStopFrameSync 广播。
    • 在 room.onUpdate 中检查 room.roomInfo.frameStatus 的值。
  4. 利用 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);
        }
    }

  5. 在 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();
    }

  6. 在 onDestroy 中清除 onUpdate、onFrame 回调。示例代码如下:

    onDestroy() {
        Global.room.onUpdate = null;
        Global.room.onFrame = null;
    }

  7. 编译代码,在模拟器中进入 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 示例结束,单击 这里 下载完整代码。

目录