手把手教你将单机游戏改造成对战网游(附详细教程)

 阅读约 21 分钟

前言:本Demo原来是Cocos Creator官方的一个Demo,本文章利用了第三方联网插件工具Matchvs将其改造成了一个三人对战的Demo,(在线体验地址)。

注意事项

1.游戏满三人才可以开启,匹配成功后,玩家通过键盘AD键操纵小怪物向左向右移动抢摘星星。

2.下载Demo源码后,需用Cocos Creator打开工程(建议使用1.7.0及以上版本)。
游戏配置

信息配置

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

};
// 文件路径:assetsscriptsMvs.js
其他文件可以用require函数引入engine和reponse:

var mvs = require("Mvs");
// 引擎实例:mvs.engine
// 引擎回调实现:mvs.response
完成以上步骤后,我们可以调用初始化接口建立相关资源。

mvs.engine.init(response, channel, platform, gameId);
// 文件路径:assetsscriptsLobby.js
注意 在整个应用全局,开发者只需要对引擎做一次初始化。

建立连接

接下来,我们就可以从Matchvs获取一个合法的用户ID,通过该ID连接至Matchvs服务端。

获取用户ID:

cc.Class({

onLoad: function() {
    mvs.response.registerUserResponse = this.registerUserResponse.bind(this);
    mvs.engine.registerUser();
},
registerUserResponse: function(userInfo) {
    // 注册成功,userInfo包含相关用户信息
},
// ...

})
// 文件路径:assetsscriptsLobby.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包含登录相关信息
},
// ...

})
// 文件路径:assetsscriptsLobby.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为房间信息
    // ...
},
// ...

})
// 文件路径:assetsscriptsLobby.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);
    }
},

})
// 文件路径:assetsscriptsLobby.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')
},

})
// 文件路径:assetsscriptsLobby.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 () {
    // ...
},
// ...

})
// 文件路径:assetsscriptsGame.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();
}
// ...

})
// 文件路径:assetsscriptsPlayer1.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) {
    // ... 
},
// ...

})
// 文件路径:assetsscriptsGame.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);

    // ..
}
// ...

})
// 文件路径:assetsscriptsPlayer1.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条件 */
},
// ...

})
// 文件路径:assetsscriptsGame.js
最终效果如下:

图片描述

搞定。

阅读 2.4k更新于 2018-08-14

推荐阅读
联网游戏开发
用户专栏

专注于联网游戏相关技术内容的,不定期为广大开发者分享联网游戏开发实战案例。

5 人关注
9 篇文章
专栏主页
目录