教你利用工具将单机改造成对战网游(详细教程)
游戏配置
Demo运行之前需要去Matchvs 官网配置游戏相关信息,以获取Demo运行所需要的GameID、AppKey、SecretID。如图:
获取到相关游戏信息之后,运行Demo,即可进入房间,准备开始游戏,如图所示:
初始化SDK
在引入SDK之后,在初始化前需要先调用Matchvs.MatchvsEngine.getInstance()获取一个Matchvs引擎对象实例:
var engine = Matchvs.MatchvsEngine.getInstance();
另外我们需要定义一个对象,该对象定义一些回调方法,用于获取游戏中玩家加入、离开房间、数据收发的信息,这些方法在特定的时刻会被SDK调用。
var response = { // 可以现在定义一些回调方法,也可以过后再定义。 };
为方便使用,我们把engine和reponse放到单独的文件Mvs.js中,使用module.exports将它们作为全局变量使用:
var engine = Matchvs.MatchvsEngine.getInstance(); var response = {}; module.exports = { engine: engine, response: engine }; // 文件路径:assets\scripts\Mvs.js
其他文件可以用require函数引入engine和reponse:
var mvs = require("Mvs"); // 引擎实例:mvs.engine // 引擎回调实现:mvs.response
完成以上步骤后,我们可以调用初始化接口建立相关资源。
mvs.engine.init(response, channel, platform, gameId); // 文件路径:assets\scripts\Lobby.js
注意 在整个应用全局,开发者只需要对引擎做一次初始化。
建立连接
接下来,我们就可以从Matchvs获取一个合法的用户ID,通过该ID连接至Matchvs服务端。
获取用户ID:
cc.Class({ onLoad: function() { mvs.response.registerUserResponse = this.registerUserResponse.bind(this); mvs.engine.registerUser(); }, registerUserResponse: function(userInfo) { // 注册成功,userInfo包含相关用户信息 }, // ... }) // 文件路径:assets\scripts\Lobby.js
用户信息需要保存起来,我们使用一个类型为对象的全局变量GLB来存储:
GLB.userInfo = userInfo;
登录:
cc.Class({ onLoad: function() { // ... mvs.engine.login(userInfo.id, userInfo.token, gameId, gameVersion, appKey, secret, deviceId, gatewayId); // ... }, loginResponse: function(loginRsp) { // 登录成功,loginRsp包含登录相关信息 }, // ... }) // 文件路径:assets\scripts\Lobby.js
加入房间
成功连接至Matchvs后,立即随机匹配加入一个房间进行游戏。
代码如下:
cc.Class({ loginResponse: function() { // ... mvs.response.joinRoomResponse = this.joinRoomResponse.bind(this); mvs.engine.joinRandomRoom(maxPlayer, userProfile); // ... }, joinRoomResponse: function(status, userInfoList, roomInfo) { // 加入房间成功,status表示结果,roomUserInfoList为房间用户列表,roomInfo为房间信息 // ... }, // ... }) // 文件路径:assets\scripts\Lobby.js
停止加入
我们设定如果有3个玩家匹配成功则满足开始条件且游戏设计中不提供中途加入,此时需告诉Matchvs不要再向房间里加人。
代码如下:
cc.Class({ joinRoomResponse: function(status, userInfoList, roomInfo) { // 加入房间成功,status表示结果,roomUserInfoList为房间用户列表,roomInfo为房间信息 // ... if (userIds.length >= GLB.MAX_PLAYER_COUNT) { mvs.response.joinOverResponse = this.joinOverResponse.bind(this); // 关闭房间之后的回调 var result = mvs.engine.joinOver(""); this.labelLog("发出关闭房间的通知"); if (result !== 0) { this.labelLog("关闭房间失败,错误码:", result); } GLB.playerUserIds = userIds; } }, joinOverResponse: function(joinOverRsp) { if (joinOverRsp.status === 200) { this.labelLog("关闭房间成功"); // ... } else { this.labelLog("关闭房间失败,回调通知错误码:", joinOverRsp.status); } }, }) // 文件路径:assets\scripts\Lobby.js
在这里需要记下房间的用户列表,记入到全局变量GLB.playerUserIds中,后面要使用到。
发出游戏开始通知
如果收到服务端的房间关闭成功的消息,就可以通知游戏开始了。
cc.Class({ // ... joinOverResponse: function(joinOverRsp) { if (joinOverRsp.status === 200) { this.labelLog("关闭房间成功"); this.notifyGameStart(); } else { this.labelLog("关闭房间失败,回调通知错误码:", joinOverRsp.status); } }, notifyGameStart: function () { GLB.isRoomOwner = true; var event = { action: GLB.GAME_START_EVENT, userIds: GLB.playerUserIds } mvs.response.sendEventResponse = this.sendEventResponse.bind(this); // 设置事件发射之后的回调 mvs.response.sendEventNotify = this.sendEventNotify.bind(this); // 设置事件接收的回调 var result = mvs.engine.sendEvent(JSON.stringify(event)); // ... // 发送的事件要缓存起来,收到异步回调时用于判断是哪个事件发送成功 GLB.events[result.sequence] = event; }, sendEventResponse: function (info) { // ... 输入校验 var event = GLB.events[info.sequence] if (event && event.action === GLB.GAME_START_EVENT) { delete GLB.events[info.sequence] this.startGame() } }, sendEventNotify: function (info) { if (info && info.cpProto && info.cpProto.indexOf(GLB.GAME_START_EVENT) >= 0) { GLB.playerUserIds = [GLB.userInfo.id] // 通过游戏开始的玩家会把userIds传过来,这里找出所有除本玩家之外的用户ID, // 添加到全局变量playerUserIds中 JSON.parse(info.cpProto).userIds.forEach(function(userId) { if (userId !== GLB.userInfo.id) GLB.playerUserIds.push(userId) }); this.startGame() } }, startGame: function () { this.labelLog('游戏即将开始') cc.director.loadScene('game') }, }) // 文件路径:assets\scripts\Lobby.js
游戏数据传输
游戏进行中在创建星星、玩家进行向左、向右操作时,我们将这些操作广播给房间内其他玩家。界面上同步展示各个玩家的状态变化。
其中星星是房主创建和展示,然后通知其他玩家,其他玩家收到消息后展示,相关的代码如下:
cc.Class({ onLoad: function() { mvs.response.sendEventNotify = this.sendEventNotify.bind(this); // ... }, sendEventNotify: function (info) { // ... if (info.cpProto.indexOf(GLB.NEW_START_EVENT) >= 0) { // 收到创建星星的消息通知,则根据消息给的坐标创建星星 this.createStarNode(JSON.parse(info.cpProto).position) } /* 其他else if条件 */ }, // 根据坐标位置创建渲染星星节点 createStarNode: function (position) { // ... }, // 发送创建星星事件 spawnNewStar: function () { if (!GLB.isRoomOwner) return; // 只有房主可创建星星 var event = { action: GLB.NEW_START_EVENT, position: this.getNewStarPosition() } var result = mvs.engine.sendEvent(JSON.stringify(event)) if (!result || result.result !== 0) return console.error('创建星星事件发送失败'); this.createStarNode(event.position); }, // 随机返回'新的星星'的位置 getNewStarPosition: function () { // ... }, // ... }) // 文件路径:assets\scripts\Game.js
玩家进行向左、向右操作时,这些消息会发送给其他玩家:
cc.Class({ setInputControl: function () { var self = this; cc.eventManager.addListener({ event: cc.EventListener.KEYBOARD, onKeyPressed: function (keyCode, event) { var msg = { action: GLB.PLAYER_MOVE_EVENT }; switch (keyCode) { case cc.KEY.a: case cc.KEY.left: msg.accLeft = true; msg.accRight = false; break; case cc.KEY.d: case cc.KEY.right: msg.accLeft = false; msg.accRight = true; break; default: return; } var result = mvs.engine.sendEvent(JSON.stringify(msg)); if (result.result !== 0) return console.error("移动事件发送失败"); self.accLeft = msg.accLeft; self.accRight = msg.accRight; }, onKeyReleased: function (keyCode, event) { var msg = { action: GLB.PLAYER_MOVE_EVENT }; switch (keyCode) { case cc.KEY.a: msg.accLeft = false; break; case cc.KEY.d: msg.accRight = false; break; default: return; } var result = mvs.engine.sendEvent(JSON.stringify(msg)); if (result.result !== 0) return console.error("停止移动事件发送失败"); if (msg.accLeft !== undefined) self.accLeft = false; if (msg.accRight !== undefined) self.accRight = false; } }, self.node); }, onLoad: function () { // ... this.setInputControl(); } // ... }) // 文件路径:assets\scripts\Player1.js cc.Class({ sendEventNotify: function (info) { if (/* ... */) { // ... } else if (info.cpProto.indexOf(GLB.PLAYER_MOVE_EVENT) >= 0) { // 收到其他玩家移动的消息,根据消息信息修改加速度 this.updatePlayerMoveDirection(info.srcUserId, JSON.parse(info.cpProto)) } /* 更多else if条件*/ }, // 更新每个玩家的移动方向 updatePlayerMoveDirection: function (userId, event) { // ... }, // ... }) // 文件路径:assets\scripts\Game.js
考虑到数据同步会有延迟,不同客户端收到的数据的延迟也会有差异,如果只在同步玩家左右移动的操作数据,那么过一段时间之后,不同客户端的小怪物位置可能会不一样,因此每隔一段时间还是需要再同步一次小怪物的位置、速度和加速度数据:
cc.Class({ onLoad: function () { // ... setInterval(() => { mvs.engine.sendEvent(JSON.stringify({ action: GLB.PLAYER_POSITION_EVENT, x: this.node.x, xSpeed: this.xSpeed, accLeft: this.accLeft, accRight: this.accRight, ts: new Date().getTime() })); }, 200); // .. } // ... }) // 文件路径:assets\scripts\Player1.js cc.Class({ sendEventNotify: function (info) { if (/* ... */) { // ... } else if (info.cpProto.indexOf(GLB.PLAYER_POSITION_EVENT) >= 0) { // 收到其他玩家的位置速度加速度信息,根据消息中的值更新状态 this.receiveCountValue++; this.receiveCount.string = "receive msg count: " + this.receiveCountValue; var cpProto = JSON.parse(info.cpProto); var player = this.getPlayerByUserId(info.srcUserId); if (player) { player.node.x = cpProto.x; player.xSpeed = cpProto.xSpeed; player.accLeft = cpProto.accLeft; player.accRight = cpProto.accRight; } // ... } /* 更多else if条件 */ }, // ... }) // 文件路径:assets\scripts\Game.js
最终效果如下:
搞定。