共计 8592 个字符,预计需要花费 22 分钟才能阅读完成。
一、游戏帧同步
1. 简介
·古代多人游戏中,多个客户端之间的通信大多以同步多方状态为次要指标,为了实现这一指标,次要有两个技术方向:状态同步、帧同步。
·状态同步的思维中不同玩家屏幕上的一致性的体现并不是重要指标,只有每次操作的后果雷同即可。所以状态同步对网络提早的要求并不高。
·帧同步次要依赖客户端的能力,服务器仅仅是做一个转发,甚至客户端能够无需服务器,通过 P2P 形式来转发数据。因为只是转发游戏的行为,所以播送的数据量比状态同步要小很多。
本文将以帧同步技术为主来介绍如何实现一款联机游戏。
2. 小游戏案例
·本次咱们在《街霸小游戏》中利用腾讯云的游戏联机对战引擎实现了玩家之间的 PVP 玩法。
感兴趣的同学能够扫码体验:
二、游戏联机对战引擎:Mgobe
1. 引擎简介
·Mgobe 是由腾讯云提供的游戏联机对战引擎,能够为游戏提供房间治理、在线匹配、帧同步、状态同步等网络通信服务,帮忙开发者疾速搭建多人交互游戏。
·Mgobe 能够让咱们在没有后盾开发人力的状况下也能实现游戏的帧同步。
Cocos Creator 嵌入了 MGOBE,在 v2.3.4 及以上版本,各位开发者能够通过 Cocos Service 服务面板,一键开明腾讯云服务 MGOBE。
Unity Editor 也嵌入了 MGOBE,在 Unity Editor 2019.1.9 及以上版本,各位开发者能够通过服务面板,一键开明腾讯云服务 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 也反对在后盾编写自定义匹配逻辑来实现更加丰盛的帧同步,感兴趣的同学可自行查阅官网文档。
或者关注公众号: