手把手教你將單機遊戲改造成對戰網遊(附詳細教程)

Matchvs發表於2019-03-02
前言:本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複製程式碼

最終效果如下:

搞定。

相關文章