Skip to content

Latest commit

 

History

History
730 lines (574 loc) · 21.3 KB

File metadata and controls

730 lines (574 loc) · 21.3 KB

!由于产品逻辑已无法满足游戏行业技术发展,游戏联机对战引擎 MGOBE 将于2022年6月1日下线,请您在2022年5月31日前完成服务迁移。

操作场景

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

前提条件

已创建 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;    
  1. 0和1表示玩家“未准备”、“已准备”两种状态。customPlayerStatus 为 RoomView 类的属性,用来保存玩家状态。示例代码如下:
export default class RoomView extends BaseView {

  button;
  msgBox;
  
  customPlayerStatus;

  // ...
  1. 为 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);
    }
}
  1. 在 onInit 追加代码,绑定 onRoomUpdate。示例代码如下:
onInit() {
    // ...
    Global.room.onUpdate = this.onRoomUpdate.bind(this);
    // 刷新canvas
    this.onRoomUpdate();
}
  1. 在 onDestroy 中解除绑定 onRoomUpdate。示例代码如下:
onDestroy() {
    Global.room.onUpdate = null;
}
  1. 编译代码,玩家单击快速加房进入 RoomView 房间页后,单击准备,按钮和文本框都将更新。如果房间内的玩家都是已准备状态,页面将跳转到 GameView 游戏页。 房间信息更新

处理其他玩家加房

  1. 当其他玩家进入房间时,SDK 会触发 onJoinRoom 回调。因此可以在进入页面时为房间绑定 onJoinRoom 广播回调函数。修改 RoomView.js 中的 onInit 方法。示例代码如下所示:
onInit() {
    // ...

    // 设置广播回调
    Global.room.onJoinRoom = this.onJoinRoom.bind(this);
}
  1. 添加 onJoinRoom 方法,这里做一个 toast 提醒玩家即可。示例代码如下所示:
// 加房广播
onJoinRoom(event) {
    this.toast("新玩家加入");
}
Global.room.onJoinRoom 指定了一个回调函数,当页面跳转到其他页面时,这个回调函数引用还在。如果该函数依赖于页面脚本的上下文,有可能在触发广播时报错。
  1. 离开页面时置空 Global.room.onJoinRoom。示例代码如下所示:
onDestroy() {
    Global.room.onUpdate = null;
    Global.room.onJoinRoom = null;
}
  1. 使用两个设备进行“快速加房”操作(如模拟器+手机预览),两个玩家有可能匹配在一起,这时页面上将弹出“新玩家加入”提示。
手机预览时打开调试,跳过域名检查。

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

实现退房功能

  1. 在 RoomView 房间页中为 button2 绑定点击事件。示例代码如下:
  onInit() {

    // ...

    const button2 = new Component.Button(150, 20, "退出房间");
    button2.onClick(() => this.leaveRoom());

    // ...
  }
  1. 在 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);
}
  1. 在 GameView 类中添加 startFrameSync 方法。示例代码如下:
// 开始帧同步
startFrameSync() {
    const func = () => Global.room.startFrameSync({}, event => {
        if (event.code !== Global.ErrCode.EC_OK) {
            return this.dialog("操作失败,是否重试?", () => func());
        }
    });

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

    // ...
}
  1. 使用 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();
}
  1. 帧同步停止后,需要跳转到 RoomView 房间页。检查房间帧同步状态有两种方法:
  • 监听 onStartFrameSync、onStopFrameSync 广播。
  • 在 room.onUpdate 中检查 room.roomInfo.frameStatus 的值。
  1. 利用 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);
    }
}
  1. 在 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();
    }
    
  2. 在 onDestroy 中清除 onUpdate、onFrame 回调。示例代码如下:
onDestroy() {
    Global.room.onUpdate = null;
    Global.room.onFrame = null;
}
  1. 编译代码,在模拟器中进入 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 示例结束,单击 这里 下载完整代码。