纯前端如何利用帧同步做一款联机游戏?

一、游戏帧同步

1.简介

·现代多人游戏中,多个客户端之间的通讯大多以同步多方状态为主要目标,为了实现这一目标,主要有两个技术方向:状态同步、帧同步。

·状态同步的思想中不同玩家屏幕上的一致性的表现并不是重要指标,只要每次操作的结果相同即可。所以状态同步对网络延迟的要求并不高。

·帧同步主要依赖客户端的能力,服务器仅仅是做一个转发,甚至客户端可以无需服务器,通过P2P方式来转发数据。由于只是转发游戏的行为,所以广播的数据量比状态同步要小很多。

本文将以帧同步技术为主来介绍如何实现一款联机游戏。

2.小游戏案例

·本次我们在《街霸小游戏》中利用腾讯云的游戏联机对战引擎实现了玩家之间的PVP玩法。

感兴趣的同学可以扫码体验:

二、游戏联机对战引擎:Mgobe

1.引擎简介

·Mgobe是由腾讯云提供的游戏联机对战引擎,可以为游戏提供房间管理、在线匹配、帧同步、状态同步等网络通信服务,帮助开发者快速搭建多人交互游戏。

·Mgobe可以让我们在没有后台开发人力的情况下也能实现游戏的帧同步。

Unity Editor也嵌入了MGOBE,在Unity Editor 2019.1.9及以上版本,各位开发者可以通过服务面板,一键开通腾讯云服务MGOBE。

Cocos Creator嵌入了MGOBE,在v2.3.4及以上版本,各位开发者可以通过Cocos Service服务面板,一键开通腾讯云服务MGOBE。

·官网:https://cloud.tencent.com/product/mgobe

2.开发语言

·Mgobe支持使用 JavaScript 或 TypeScript 来进行前端开发。

3.支持平台

·Mgobe目前支持:微信小游戏、QQ小游戏、百度小游戏、OPPO小游戏、vivo小游戏、字节小游戏;H5小游戏和手游。

三、纯前端打造帧同步实现联机对战

·接下来会从前端的角度来一步一步讲解使用Mgobe的方法,借助Mgobe我们可以不用知晓后台和运维知识,就可以构建起一套性能优越的帧同步游戏。

1.控制台配置

·首先我们需要在Mgobe的控制台中创建游戏实例,以获取游戏ID、游戏Key和域名等信息,我们会在初始化SDK时使用到游戏ID和游戏Key。

·出于安全考虑,微信小游戏会限制请求域名,所有的 HTTPS、WebSocket、上传、下载请求域名都需要在微信公众平台进行配置。因此,在正式接入游戏联机对战引擎 SDK 前,还需要开发者在微信公众平台配置合法域名。 ·需要配置的域名包含一条 request 域名和两条 socket 域名记录,配置如下:

// request 域名

report.wxlagame.com

// socket 域名

xxx.wxlagame.com

xxx.wxlagame.com:5443

2.SDK

2.1.下载

·SDK下载地址:https://cloud.tencent.com/document/product/1038/33406

2.2.引入SDK

·SDK文件包含 MGOBE.js 和 MGOBE.d.ts,即源代码文件和定义文件。在 MGOBE.js 中,SDK接口被全局注入到 window 对象下。因此,只需要在使用SDK接口之前执行 MGOBE.js 文件即可。

·以微信为例,只需将 MGOBE.js 放到项目下任意位置,在 game.js 中 import SDK 文件后即可使用 MGOBE 的方法。当然也可以使用 import/from、require 语法显式导入 MGOBE 模块。

2.3.直接使用密钥进行初始化

·用这种方式可以快速初始化SDK,可以最快的速度使用引擎的帧同步功能,但这种方式会在前端暴露游戏Key。

var gameInfo = {

openId: 'xxxxxx', //玩家的openID

gameId: "xxxxxx", //游戏id,在控制台中的“游戏ID”中获取

secretKey: 'xxxxxx' //游戏密钥,在控制台中的“游戏key”获取

};

var config = {

url: 'xxx.wxlagame.com',//游戏域名,在控制台中的“域名”获取

reconnectMaxTimes: 5, //重连接次数

reconnectInterval: 1000, //重连接时间间隔

resendInterval: 1000, //消息重发时间间隔

resendTimeout: 10000 //消息重发超时时间

};

Listener.init(gameInfo, config, function() {

if (event.code === 0) {

// 初始化成功

}

});

·Listener 对象为 MGOBE 的子属性,该对象方法全为静态方法,不需要实例化。Listener对象主要用于给 Room 对象的实例绑定广播事件监听。

·初始化 Listener 成功后才能继续调用 Mgobe 引擎的其他接口。

2.4.利用签名来进行初始化(在前端隐藏游戏Key)

·用2.3的方法初始化 SDK 时,会在前端暴露游戏的密钥,为了避免在客户端泄露游戏的密钥,我们也可以使用签名的方式来初始化 SDK。

·在开发者服务器通过游戏 ID、游戏 Key、玩家 openId 等信息计算出游戏签名,然后再下发给客户端。客户端在初始化 SDK 时,需要实现一个 createSignature 签名函数,从服务端获取签名信息然后回调给 SDK。也就是在 gameInfo 中,将2.3中 的 secretKey 字段改为 createSignature 字段。

//这里仅列出与2.3不同的gameInfo, config和Listener.init与2.3一致,不再赘述。 var gameInfo = {

gameId: "xxxxx", //游戏id,在控制台中的“游戏ID”中获取

openId: "xxxxxx", //玩家的openID

// 实现签名函数

createSignature: callback => { //假设https://example.com/sign就是我们后台计算签名的接口

fetch("https://example.com/sign").then(rsp => rsp.json()).then(json => {

const sign = json.sign;

const nonce = json.nonce;

const timestamp = json.timestamp;

return callback({ sign, nonce, timestamp });

});

},

};

·签名过程详见:https://cloud.tencent.com/document/product/1038/38863

3.房间

·在开发游戏的过程中,大部分接口都位于 Room 对象中。由于每个玩家只能加入一个房间,在游戏生命周期中可以只实例化一个 Room 对象来进行接口的调用。

3.1.实例化Room

var roomInfo = { id: "xxx" //房间ID }; var room = new MGOBE.Room(roomInfo);

  创建房间、加入房间、匹配等接口调用直接使用 room 实例即可。但有3个接口例外:getMyRoom、getRoomList、getRoomByRoomId 接口是 Room 对象的静态方法,需要使用 Room.getMyRoom、Room.getRoomList、Room.getRoomByRoomId 来调用。

3.2.几个常用属性

3.2.1.roomInfo 属性

·roomInfo 为 Room 实例的属性,保存房间的相关信息,调用 Room 相关的接口会导致该属性发生变化。可以从 roomInfo 中获得房间的id、名称和玩家列表等。

3.2.2.networkState 属性

·用于获取客户端本地 SDK 的网络状态。注意 networkState 的网络状态与玩家信息 Player 中的网络状态概念不同,room.networkState 表示本地 socket 的状态,而 Player.commonNetworkState 和 Player.relayNetworkState 表示玩家在 Mgobe 后台中的状态。 ·networkState 网络状态发生变化时,room.onUpdate 将被触发。

room.onUpdate = function() {

console.log("房间信息更新:", room.roomInfo);

};

3.3.初始化Room

room.initRoom();

·通过 room.initRoom 方法可以初始化一个房间,同时更新房间信息 roomInfo 。初始化可以更新 WebSocket 连接,这样才能及时收到房间的广播。此外,如果要加入指定ID的房间,也需要先对房间进行初始化,否则将无法使用 room.joinRoom 加入指定ID的房间。

3.4.为Room添加广播侦听

MGOBE.Listener.add(room);

·一个房间对象会有很多广播事件与其相关,例如该房间有新成员加入、房间属性变化、房间开始对战等广播。Room 实例需要在 Listener 中注册广播监听,之后可以通过 room.xxx 回调函数的形式来使用广播侦听,详见下文。

3.5.创建房间

·通过使用 room 实例的 createRoom 可以创建一个房间,创建成功后创建者会自动进入该房间。

var playerData = {

name:nickname, //玩家昵称

customPlayerStatus:playerStatus, //自定义玩家状态

customProfile:figureURL //自定义玩家信息

};//玩家信息

var createRoomData = {

roomName:"roomName", //房间名称

roomType:"1v1", //房间类型

maxPlayers:2, //房间最大玩家数量

isPrivate:true, //是否为私有房间,属性为 true 表示该房间为私有房间,不能被 matchRoom 接口匹配到

customProperties:roomStatus, //自定义房间属性

playerInfo:playerData //房主信息

};//房间信息

room.createRoom(createRoomData, function(e)

{

if(e.code === 0)

{

//创建房间成功

}

});

·注意:创建房间的结果是通过回调异步返回的,而非派发事件。

3.6.加入房间

·通过使用 room 实例的 joinRoom 可以加入一个已经存在的房间。

var playerData =

{

name:nickname, //玩家昵称

customPlayerStatus:playerStatus, //自定义玩家状态

customProfile:figureURL //自定义玩家信息

};//玩家信息

var joinRoomInfo =

{

playerInfo:playerData

};//加入房间的信息

room.initRoom({ id: "xxx" });//加入房间前需要先初始化room实例

room.joinRoom(joinRoomInfo, function(e)

{

if(e.code === 0)

{

console.log("加入房间成功");

}

});

·注意:加入房间的结果也是通过回调异步返回的,而非派发事件。加入房间前必须先初始化房间实例。

·对于已经存在于房间中的其他人,可以通过 room.onJoinRoom 来侦听新玩家的加入。

room.onJoinRoom = function(e)

{

console.log("新玩家加入,ID为:", e.data.joinPlayerId);

};

3.7.离开房间

·使用 room.leaveRoom 就可以退出房间。

room.leaveRoom({}, function(e)

{

if(e.code === 0)

{

console.log("离开房间成功");

}

});

·对于房间中的其他人,可以通过 room.onLeaveRoom 来侦听玩家的离开。

room.onLeaveRoom = function(e)

{

console.log("离开房间的玩家的ID:", e.data.leavePlayerId);

};

4.匹配

4.1.匹配规则

·要进行房间匹配,需要先在控制台创建匹配规则,匹配规则既可以满足按人数匹配、按队伍匹配,也可以按段位等特殊方式来匹配。成功创建规则后会获得一个匹配code,匹配code将会用于匹配的相关接口,表示用这个规则来匹配符合条件的玩家。

规则创建之后还需要将规则绑定到服务器中才能生效,在“新建匹配”中选择上一步创建的匹配集即可。

4.2.匹配玩家

·有了匹配code后我们就可以在前端进行玩家匹配了,只要是符合规则中定义的条件的玩家,就会被匹配进同一个房间中。

var matchPlayersData = {

playerInfo:playerData, //发起匹配的玩家的信息,playerData在上文已多次出现,这里不再赘述

matchCode:matchCode //匹配code,在4.1中获得

};//玩家匹配信息

room.matchPlayers(matchPlayersData, function(e)

{

if(e.code === 0)

{

console.log("匹配请求成功");

}

});

4.3.匹配房间

·matchPlayers 配合匹配code可以用来匹配玩家,那么通过使用 room.matchRoom 则可以进行房间的匹配。房间匹配是指按照传入的参数搜索现存的房间,如果存在,则将玩家加入该房间,如果不存在,则为玩家创建并加入一个新房间。

·matchRoom 不需要使用匹配code。

var playerInfo = {

name: "Tom",

customPlayerStatus: 1,

customProfile: "https://xxx.com/icon.png",

};//发起匹配者的信息

const matchRoomPara = {

playerInfo,

maxPlayers: 5,

roomType: "1",

};//房间匹配信息

room.matchRoom(matchRoomPara, function(e) {

if (event.code === 0) {

console.log("匹配成功");

}

});

·matchRoom 与 matchPlayers 最大的不同就是:matchRoom 一定会让匹配发起人进入一个房间,但 matchPlayers 则不一定,如果当前没有符合匹配规则的玩家,则 matchPlayers 会返回失败。

5.帧同步

·终于来到这一步了,如果玩家已经成功加入房间,就可以通过帧同步功能进行游戏对战。

5.1.开启帧同步

·使用 room.startFrameSync 接口就可以开启帧广播。房间内任意一个玩家成功调用该接口都将导致全部玩家开始接收帧广播。

room.startFrameSync({}, function(e)

{

if(e.code === 0)

{

console.log("开始帧同步成功");

}

});

·调用成功后房间内全部成员都将收到 onStartFrameSync 广播。该接口会修改房间帧同步状态为“已开始帧同步”。

room.onStartFrameSync = function()

{

//收到此广播后将持续收到 onRecvFrame 广播 //注意,这里还不是玩家之间相互进行帧同步的信息内容,onRecvFrame 中才是我们拿到帧同步内容的地方,见下文

};

5.2.发送帧消息

·玩家收到帧同步开始广播后,才可以发送帧消息,后台会将每个玩家的帧消息组合后再广播给每个玩家。

·帧数据内容 data 类型为普通 Object,由开发者自定义,目前支持最大长度不超过1k。后台将集合全部玩家的帧数据,并以一定时间间隔(由房间帧率定义,可以在控制台配置)通过 onRecvFrame 广播给各客户端。调用结果将在 callback 中异步返回。

var frame = { cmd: "xxxxxxxx", id: "xxxxxxxx" };//一帧的内容,由开发者自定义

var sendFramePara = { data: frame };//发送给Mgobe的帧内容

room.sendFrame(sendFramePara, function(e) { console.log("发送帧同步数据"); });

5.3.接收帧广播

·开发者可设置 room.onRecvFrame 广播回调函数来获得帧广播数据。onRecvFrame 广播表示收到一个帧 frame,frame 的内容由多个 MGOBE.types.FrameItem 组成,即一帧时间内房间内所有玩家向服务器发送帧消息的集合。

room.onRecvFrame = function()

{

console.log("收到帧同步消息=", e.data.frame); //我们就是从 e.data.frame.items 这个数组的每个元素的 data 属性来拿到我们在5.2中发送给Mgobe的帧内容的。 //5.2的帧内容:var frame = {cmd: "xxxxxxxx", id:"xxxxxxxx"}

};

5.4.停止帧同步

·使用 room.stopFrameSync 接口可以停止帧广播。房间内任意一个玩家成功调用该接口将导致全部玩家停止接收帧广播。

room.stopFrameSync({}, function(e)

{

if(e.code === 0)

{

console.log("停止帧同步成功");

}

});

·调用成功后房间内全部成员将收到 onStopFrameSync 广播。该接口会修改房间帧同步状态为“已停止帧同步”。

room.onStopFrameSync = function()

{

//收到该广播后将不再收到 onRecvFrame 广播

};

·至此,利用Mgobe来进行帧同步开发的相关主要接口就介绍完毕了。下面将讲一些关于玩家信息的内容。

6.玩家信息

6.1.玩家ID

·玩家信息 Player 对象为 MGOBE 的子属性,用于访问玩家的基本信息,例如玩家 ID、openId 等。该对象记录了玩家的基本信息,默认全部为空。成功初始化 Listener 后,ID、openId 属性才生效。

·Player 中的 玩家 ID 是 MGOBE 后台生成的 ID,而 openId 是开发者初始化时候使用的 ID。需要注意,openId 只有初始化 Listener 的时候才使用,后续其它接口提到的“玩家 ID”均指后台生成的 ID,也就是 Player.id 属性,它不是 openId,切记!

·玩家进入房间后,Player 对象中的属性与 roomInfo.playerList 中的玩家信息是一致,通过两者任何一个都可以获得正确的玩家信息。

6.2.几个常用事件

·这里提两个经常用到的玩家事件:网络状态变化、玩家状态变化。

·在Mgobe中,玩家的网络状态分以下4种,但玩家的网络状态发生变化时均会触发。

room.onChangePlayerNetworkState = function(e)

{

if(e.data.networkState === MGOBE.ENUM.NetworkState.COMMON_OFFLINE)

{

console.log("房间中玩家掉线");

}

else if(e.data.networkState === MGOBE.ENUM.NetworkState.COMMON_ONLINE)

{

console.log("房间中玩家在线");

}

else if(e.data.networkState === MGOBE.ENUM.NetworkState.RELAY_OFFLINE)

{

console.log("帧同步中玩家掉线");

}

else if(e.data.networkState === MGOBE.ENUM.NetworkState.RELAY_ONLINE)

{

console.log("帧同步中玩家在线");

}

//通过 e.data.changePlayerId 可以知道是哪个玩家的网络状态发生了变化

};

·如果修改了玩家的自定义信息(由开发者自定义的,也即上文多次提到的 playerInfo 中的 customPlayerStatus),则以下事件会被触发:

room.onChangeCustomPlayerStatus = function()

{
//房间内 ID 为 changePlayerId 的玩家状态发生变化。玩家状态由开发者自定义。

console.log("玩家自定义状态变化=", e.data.changePlayerId);
console.log("自定义数据=", e.data.customPlayerStatus);

};

7.错误处理

·最后,如果在使用Mgobe的过程中如果发生客户端错误、系统逻辑错误、用户信息错误、房间错误、匹配错误、帧同步错误、参数错误、队伍团队错误时,均会发出错误码,可以通过以下文档查阅相关错误码对应的描述信息,以便排除和解决错误。

·错误码说明文档详见:https://cloud.tencent.com/document/product/1038/33317

四、结尾

· 本文仅从前端角度出发,介绍了利用 Mgobe 进行纯前端的帧同步开发,但 Mgobe 的功能远不止这些,Mgobe 也支持在后台编写自定义匹配逻辑来实现更加丰富的帧同步,感兴趣的同学可自行查阅官方文档。也可关注公众号,关注持续技术分享。