HTML+JS+websocket 實例,聯機“游戲王”對戰(十一)- 客戶端消息的收發

目錄

HTML+JS+websocket 實例,聯機“游戲王”對戰 1
HTML+JS+websocket 實例,聯機“游戲王”對戰 2 - 聯機模式
HTML+JS+websocket 實例,聯機“游戲王”對戰 3 - 界面布局
HTML+JS+websocket 實例,聯機“游戲王”對戰 4 - 卡組系統
HTML+JS+websocket 實例,聯機“游戲王”對戰 5 - 卡片選中系統
HTML+JS+websocket 實例,聯機“游戲王”對戰 6 - 卡片放置,戰場更新
HTML+JS+websocket 實例,聯機“游戲王”對戰 7 - 墓地,副控制面板
HTML+JS+websocket 實例,聯機“游戲王”對戰 8 - 返回手卡,卡組
HTML+JS+websocket 實例,聯機“游戲王”對戰 9 - 實現簡單 websocket 通信
HTML+JS+websocket 實例,聯機“游戲王”對戰 10 - 搭建游戲服務端
HTML+JS+websocket 實例,聯機“游戲王”對戰 11 - 客戶端消息的收發
HTML+JS+websocket 實例,聯機“游戲王”對戰 12 - 消息發送具體場景
HTML+JS+websocket 實例,聯機“游戲王”對戰 13 - 實機演示

客戶端消息的發送與接收

上一章我們介紹了服務端的搭建,用于接收并轉發消息,這章來實現客戶端的聯機機制。客戶端的聯機分為發送和接收,當玩家執行某些操作時會發送一條 message 通知另一位玩家(由服務端做中介轉發),同樣,客戶端也可以接收另一位玩家的 message,執行某些操作。

在聯機之前我們需要兩個基本變量:

var playerID = "player1";  //獨立玩家ID
var ws = new WebSocket("ws://localhost:9999/");

玩家ID以及服務端的地址。這里由于方便測試寫的本地地址,如果服務端在其他設備上運行可以改成那臺設備的ip地址。

之后我們定義一個消息發送函數 wsSend

function wsSend(content) {  //由于傳輸的message類型多樣,由各函數自行編碼后傳遞
    if (ws.readyState === WebSocket.OPEN) {
        ws.send(content);
        console.log("message sent");
    }
}

該函數只負責將其他函數傳來的消息內容原封不動發給服務端。

然后在客戶端與服務端初次建立連接時,會將自己的基本信息率先發過去讓服務端保存:

//初次與服務端建立連接時觸發
ws.onopen = function() {
    /*初次與服務端建立連接時告知玩家的pid讓服務器存下來 */
    var message = JSON.stringify({
        "type": "connection",  //向服務器告知的消息類型
        "pid": playerID,  //向服務器告知的本玩家ID
    });
    wsSend(message);  
}

type 設為 “connection” 告知服務端這是初次連接,pid 是客戶端的基本信息,如果有需要我們還可以設置更多信息內容。message 編輯好后由 wsSend 函數發出。理論上這應該是建立連接后客戶端發送的第一條消息,接下來客戶端就可以發送常規 message 了。

在前面第二章聯機模式里我們提到過,客戶端接收對方消息后一般就是把對方執行的操作復現一遍,同步到我們自己的界面中來。比如對方通常召喚一只怪獸時,會向戰場上放置一張怪獸卡片,同時對方的 message 中會告知我們對方放置了什么樣的卡片(圖片url),以及放置在哪里(卡槽id)。我們將根據 message 的內容調用戰場更新函數 updateField 來復現此操作,這與我們自己召喚怪獸幾乎無異,只是放置對象是對方場上的卡槽,也就是戰場界面的上半部分。

game_ui_areas.png

針對如上述例子所說的不同功能場景,我們會編輯不同種類的 message。首先所有 message 都必帶 type,pid 與 msgtype 三個變量,前兩者用于服務端的傳輸識別,而 msgtype 則是告知對方需要執行那種類型的操作,目前我們共有三種類型:updateHand(更新手牌),updateField(更新戰場),updateTomb(更新墓地),下面來逐一介紹。

消息發送

1. 手牌更新:

/**
 * 編碼令對方更新手卡的 message并發送
 * @param {string} updateType - updating type
 * @param {*} handNo - hand slot number
 */
function messageHand(updateType, handNo) {
    var message_hand = JSON.stringify({
        "type": "message",  //向服務器告知的消息類型
        "pid": playerID,  //向服務器告知的本玩家ID
        "msgtype": "updateHand",  //向對方玩家告知的更新類型
        "updateType": updateType,  //向對方玩家告知增/減手卡
        "handNo": handNo  //向對方玩家告知被更新的卡槽
    });
    wsSend(message_hand);
}

這是一個手牌更新的 message,在我方抽卡或從手牌打出卡片等手牌有變動的場景時會調用。除了前三個必帶的變量外,后面的變量根據具體的功能特性來設置。

變量 值 & 含義
updateType add / reduce - 告知是增加還是減少手卡
handNo 手牌卡槽序號 - 告知是哪個卡槽有增減卡片

2. 戰場更新:

/**
 * 編碼令對方更新(我方/對方)戰場的 message并發送
 * @param {string} state - card state
 * @param {string} fieldID - updated card slot ID
 * @param {string} cardsrc - card img src
 */
function messageField(state, fieldID, cardsrc) {
    var message_field = JSON.stringify({
        "type": "message",  //向服務器告知的消息類型
        "pid": playerID,  //向服務器告知的本玩家ID
        "msgtype": "updateField",  //向對方玩家告知的更新類型
        "state": state,  //卡片放置狀態
        "fieldID": fieldID,  //需更新的卡槽ID
        "cardsrc": cardsrc  //放置的卡片src
    });
    wsSend(message_field);
}

這是戰場更新的 message,將在戰場有變化的場景中調用,比如召喚,放置蓋覆卡等。同樣前三個是必帶變量,大家都統一。

變量 值 & 含義
state attk / defen / back / on / off / change-on / change-off / change-back - 告知卡片更新后的狀態
fieldID 戰場卡槽 id - 告知是哪一個戰場卡槽需要更新
cardsrc 需要更新的圖片 url

上表的變量中,state 的 change-on, change-off, change-back 為特殊狀態,用于表示卡片通過“更變形式”功能而變化成的“打開”,“蓋覆”,“被蓋召喚”狀態,與我們常規從手牌向場上放置卡片的時的 on,off,back 狀態有所區別(因為觸發的音效不一樣)。

事實上這三個變量在被對方客戶端接收后將原封不動的喂給戰場更新函數 updateField,關于這三個變量將如何被使用完全可以參考 updateField 函數中的內容。

3. 墓地更新 :

前面的章節有提到過我們有專門用于存放我方墓地卡片的數組:

var P1Tomb = [];  //我方墓地(卡片src)

事實上,我們還有用于存放對方墓地卡片的數組:

var P2Tomb = [];  //對方墓地

無論是我方還是對方墓地發生變化,我們都會及時同步。任何時刻雙方客戶端的墓地數組都是相互同步的(當然名字是反的,我方 P1Tomb 中的內容到了對方客戶端就存儲在 P2Tomb 中,我方的 P2Tomb 中也保存著對方的 P1Tomb)。

/**
 * 編碼令對方更新(我方/對方)墓地 message并發送
 * @param {string} updateType - updating type (add/reduce) 
 * @param {string} ply - indicated player
 * @param {*} cardNo - card number in tomb
 * @param {string} cardsrc - card img src
 */
function messageTomb(updateType, ply, cardNo, cardsrc) {
    var message_tomb = JSON.stringify({
        "type": "message",  //向服務器告知的消息類型
        "pid": playerID,  //向服務器告知的本玩家ID
        "msgtype": "updateTomb",  //向對方玩家告知的更新類型
        "updateType": updateType,  //向對方玩家告知增/減墓地卡片
        "ply": ply,  //定義誰的墓地需被更新, player1表示你的對手,player2表示你自己
        "cardNo": cardNo,  //卡片序號,剔出墓地卡片時需要用到
        "cardsrc": cardsrc  //卡片的src,新增墓地卡片時需要用到
    });
    wsSend(message_tomb);
}

這個墓地更新的 message,由于雙方玩家既可以操作自己的墓地也可以操作對方的墓地,墓地更新的 message 必須指明此次操作是更新哪一方的墓地卡片,以便正確同步。

變量 值 & 含義
updateType add / reduce - 告知是增加還是減少墓地的卡
ply player1 / player2 - 告知本次操作的是哪一方的墓地
cardNo 卡片序號 - 告知操作的是墓地中的哪一張卡(從墓地拿出某張卡片時)
cardsrc 卡片圖片 url(向墓地送入某張卡片時)

需要說明一下的是,這四個變量中,cardNo 與 cardsrc 并不是每次發送信息的時候都會被使用,需要分情況討論。當我們從自己或對方墓地剔出某張卡片時,由于這張卡片已經存在于墓地數組中,我們只需告知其在數組中的位置便可定位并操作該卡片,這時候我們只傳送 cardNo 即可,cardsrc 可以留空或賦值一個“null”(便于識別,避免奇怪 bug)。當我們向墓地丟入新卡片時,我們則需要提供該卡的圖片信息,即 cardsrc,這時候 cardNo 是不需要的變量。

消息接收

消息編輯,發送之后,下一步就是接收它。當客戶端接收到服務端轉發來的消息時會觸發 ws.onmessage 函數,函數內容如下:

//接收服務器消息后觸發
ws.onmessage = function(message) {
    var msg = JSON.parse(message.data);
    var msgtype = msg.msgtype;

    switch(msgtype) {
        case 'updateHand':
            var handNo = msg.handNo;
            var updateType = msg.updateType;
            updateP2Hand(handNo, updateType);
            break;
        case 'updateField':
            var fieldID = msg.fieldID;
            var state = msg.state;
            var cardsrc = msg.cardsrc;
            updateField(fieldID, state, cardsrc);
            break;
        case 'updateTomb':
            var updateType = msg.updateType;
            var ply = msg.ply;
            var cardNo = msg.cardNo;
            var cardsrc = msg.cardsrc;
            updateTomb(updateType, ply, cardNo, cardsrc);
            break;
        default:
            alert("error message!");
            break;
    }
}

函數解碼傳來的 JSON 消息后,首先獲取 msgtype,確認更新類型。之前介紹的每種類型的 message 中,前三個必帶變量用于了服務端的識別與這里的更新類型判定,而后面那些自定義變量則是在確認了具體的更新類型后,作為參數被原封不動的傳入相關函數中。

更新手牌會調用 updateP2Hand 函數,用于同步對方手牌區域的顯示情況:

/**
 * 更新對方手牌區域
 * 對方手牌均為卡片背面圖片
 * @param {string} handNo - updated hand slot number 
 * @param {string} updateType - updating type (add/reduce)
 */
function updateP2Hand(handNo, updateType) {
    var handID = 'p2-hand' + handNo;
    element = document.getElementById(handID);

    /*執行增或減手牌 */
    if(updateType == "add") {  
        element.src = CardBackSrc;
    } else {
        element.src = "";
    }
}


更新戰場會調用 updateField 函數,此函數我們已經介紹過,是專門用于更新整個戰場狀態的函數:

/**
 * 戰場狀態更新,單獨更新某一個卡槽
 * @param {string} fieldID - field img container id 
 * @param {string} cardstate - state of card (attk/defen/back/on/off)
 * @param {string} cardsrc - card source url
 */
function updateField(fieldID, cardstate, cardsrc) { 
    var stateclass;
    element = document.getElementById(fieldID);

    /**
     * 如果是蓋卡或背蓋召喚直接顯示卡片背面
     * 檢查showCardInfo函數可知對于我方來說,即使卡片是背面圖片仍可以顯示卡片信息
     * 由于音效種類問題修改分類了多種情況
     */
    switch (cardstate) {
        case 'off':
        case 'back':
            element.src = CardBackSrc;
            stateclass = "card-" + cardstate;
            /*觸發背蓋或蓋卡音效 */
            var snd = new Audio("sound/activate.wav");
            snd.play();
            break;
        case 'on':  //正常發動卡片
            element.src = cardsrc;
            stateclass = "card-" + cardstate;
            /*觸發發動卡片音效 */
            var snd = new Audio("sound/activate.wav");
            snd.play();
            break;
        case 'change-off':  //通過更變形式覆蓋卡片
            element.src = CardBackSrc;
            stateclass = "card-" + cardstate.replace("change-", "");
            break;
        case 'change-back':  //通過更變形式背蓋召喚卡片
            element.src = CardBackSrc
            stateclass = "card-" + cardstate.replace("change-", "");
            break;
        case 'change-on':  //通過更變形式實現的打開蓋卡
            /*觸發打開蓋卡音效 */
            element.src = cardsrc;
            stateclass = "card-" + cardstate.replace("change-", "");
            var snd = new Audio("sound/open.wav");
            snd.play();
            break;
        case 'null':
            stateclass = "card";
            element.src = cardsrc;
            break;
        default:
            element.src = cardsrc;
            if (cardstate.search("change-") == -1) {  //正常召喚
                stateclass = "card-" + cardstate;
                /*觸發發召喚怪獸音效 */
                var snd = new Audio("sound/summon.wav");
                snd.play();
            } else {                                  //更變形式
                stateclass = "card-" + cardstate.replace("change-", "");
            }
            break;
    }

    element.setAttribute("class", stateclass);  //更新對應img容器的class
}


更新墓地會調用 updateTomb 函數,同步雙方墓地的狀態:

/**
 * 更新我方/對方墓地
 * @param {string} updateType - updating type (add/reduce) 
 * @param {string} ply - indicated player
 * @param {*} cardNo - card number in tomb
 * @param {string} cardsrc - card img src
 */
function updateTomb(updateType, ply, cardNo, cardsrc) {
    /*向墓地增卡一定是對方將卡牌送入對方墓地(對方無法將卡牌放入我方墓地) */
    if (updateType == 'add') {  
        P2Tomb.push(cardsrc);
        sf_buttons('p2tomb');

    /*向墓地剔出卡片則分情況 */
    } else if (updateType == 'reduce') {  
        if (ply == 'player1') {  //對方拿走我方墓地卡片
            P1Tomb.splice(cardNo, 1);
            sf_buttons('p1tomb');  //刷新副面板顯示
        } else {  //對方拿走對方墓地卡片,我方執行同步
            P2Tomb.splice(cardNo, 1);
            sf_buttons('p2tomb');
        }
    }
}

每次更新完某一方墓地后會刷新一次副面板顯示,讓玩家獲悉墓地的變化。

到這里一個完整的客戶端消息發送接收機制就搭建完畢了!把各種功能與需求分類為幾個明確的類型,再針對每個類型定制相關消息與函數就是這個系統實現的核心。再加之服務端的消息識別,轉發功能,我們已經完整地建立了一套可用的聯機交互系統。

下一章我們把部分已經介紹過的函數拿出來,討論一下具體是哪些功能的哪些操作需要我們編輯并發送相關消息指示對方進行同步。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容