目錄
HTML+JS+websocket 實(shí)例尝江,聯(lián)機(jī)“游戲王”對戰(zhàn) 1
HTML+JS+websocket 實(shí)例,聯(lián)機(jī)“游戲王”對戰(zhàn) 2 - 聯(lián)機(jī)模式
HTML+JS+websocket 實(shí)例英上,聯(lián)機(jī)“游戲王”對戰(zhàn) 3 - 界面布局
HTML+JS+websocket 實(shí)例炭序,聯(lián)機(jī)“游戲王”對戰(zhàn) 4 - 卡組系統(tǒng)
HTML+JS+websocket 實(shí)例,聯(lián)機(jī)“游戲王”對戰(zhàn) 5 - 卡片選中系統(tǒng)
HTML+JS+websocket 實(shí)例苍日,聯(lián)機(jī)“游戲王”對戰(zhàn) 6 - 卡片放置惭聂,戰(zhàn)場更新
HTML+JS+websocket 實(shí)例,聯(lián)機(jī)“游戲王”對戰(zhàn) 7 - 墓地相恃,副控制面板
HTML+JS+websocket 實(shí)例辜纲,聯(lián)機(jī)“游戲王”對戰(zhàn) 8 - 返回手卡,卡組
HTML+JS+websocket 實(shí)例拦耐,聯(lián)機(jī)“游戲王”對戰(zhàn) 9 - 實(shí)現(xiàn)簡單 websocket 通信
HTML+JS+websocket 實(shí)例耕腾,聯(lián)機(jī)“游戲王”對戰(zhàn) 10 - 搭建游戲服務(wù)端
HTML+JS+websocket 實(shí)例,聯(lián)機(jī)“游戲王”對戰(zhàn) 11 - 客戶端消息的收發(fā)
HTML+JS+websocket 實(shí)例杀糯,聯(lián)機(jī)“游戲王”對戰(zhàn) 12 - 消息發(fā)送具體場景
HTML+JS+websocket 實(shí)例幽邓,聯(lián)機(jī)“游戲王”對戰(zhàn) 13 - 實(shí)機(jī)演示
客戶端消息的發(fā)送與接收
上一章我們介紹了服務(wù)端的搭建,用于接收并轉(zhuǎn)發(fā)消息火脉,這章來實(shí)現(xiàn)客戶端的聯(lián)機(jī)機(jī)制牵舵。客戶端的聯(lián)機(jī)分為發(fā)送和接收倦挂,當(dāng)玩家執(zhí)行某些操作時(shí)會發(fā)送一條 message 通知另一位玩家(由服務(wù)端做中介轉(zhuǎn)發(fā))畸颅,同樣,客戶端也可以接收另一位玩家的 message方援,執(zhí)行某些操作没炒。
在聯(lián)機(jī)之前我們需要兩個基本變量:
var playerID = "player1"; //獨(dú)立玩家ID
var ws = new WebSocket("ws://localhost:9999/");
玩家ID以及服務(wù)端的地址。這里由于方便測試寫的本地地址犯戏,如果服務(wù)端在其他設(shè)備上運(yùn)行可以改成那臺設(shè)備的ip地址送火。
之后我們定義一個消息發(fā)送函數(shù) wsSend:
function wsSend(content) { //由于傳輸?shù)膍essage類型多樣拳话,由各函數(shù)自行編碼后傳遞
if (ws.readyState === WebSocket.OPEN) {
ws.send(content);
console.log("message sent");
}
}
該函數(shù)只負(fù)責(zé)將其他函數(shù)傳來的消息內(nèi)容原封不動發(fā)給服務(wù)端。
然后在客戶端與服務(wù)端初次建立連接時(shí)种吸,會將自己的基本信息率先發(fā)過去讓服務(wù)端保存:
//初次與服務(wù)端建立連接時(shí)觸發(fā)
ws.onopen = function() {
/*初次與服務(wù)端建立連接時(shí)告知玩家的pid讓服務(wù)器存下來 */
var message = JSON.stringify({
"type": "connection", //向服務(wù)器告知的消息類型
"pid": playerID, //向服務(wù)器告知的本玩家ID
});
wsSend(message);
}
type 設(shè)為 “connection” 告知服務(wù)端這是初次連接弃衍,pid 是客戶端的基本信息,如果有需要我們還可以設(shè)置更多信息內(nèi)容坚俗。message 編輯好后由 wsSend 函數(shù)發(fā)出镜盯。理論上這應(yīng)該是建立連接后客戶端發(fā)送的第一條消息,接下來客戶端就可以發(fā)送常規(guī) message 了猖败。
在前面第二章聯(lián)機(jī)模式里我們提到過速缆,客戶端接收對方消息后一般就是把對方執(zhí)行的操作復(fù)現(xiàn)一遍,同步到我們自己的界面中來恩闻。比如對方通常召喚一只怪獸時(shí)艺糜,會向戰(zhàn)場上放置一張怪獸卡片,同時(shí)對方的 message 中會告知我們對方放置了什么樣的卡片(圖片url)幢尚,以及放置在哪里(卡槽id)倦踢。我們將根據(jù) message 的內(nèi)容調(diào)用戰(zhàn)場更新函數(shù) updateField 來復(fù)現(xiàn)此操作,這與我們自己召喚怪獸幾乎無異侠草,只是放置對象是對方場上的卡槽辱挥,也就是戰(zhàn)場界面的上半部分。
針對如上述例子所說的不同功能場景边涕,我們會編輯不同種類的 message晤碘。首先所有 message 都必帶 type,pid 與 msgtype 三個變量功蜓,前兩者用于服務(wù)端的傳輸識別园爷,而 msgtype 則是告知對方需要執(zhí)行那種類型的操作,目前我們共有三種類型:updateHand(更新手牌)式撼,updateField(更新戰(zhàn)場)童社,updateTomb(更新墓地),下面來逐一介紹著隆。
消息發(fā)送
1. 手牌更新:
/**
* 編碼令對方更新手卡的 message并發(fā)送
* @param {string} updateType - updating type
* @param {*} handNo - hand slot number
*/
function messageHand(updateType, handNo) {
var message_hand = JSON.stringify({
"type": "message", //向服務(wù)器告知的消息類型
"pid": playerID, //向服務(wù)器告知的本玩家ID
"msgtype": "updateHand", //向?qū)Ψ酵婕腋嬷母骂愋? "updateType": updateType, //向?qū)Ψ酵婕腋嬷?減手卡
"handNo": handNo //向?qū)Ψ酵婕腋嬷桓碌目ú? });
wsSend(message_hand);
}
這是一個手牌更新的 message扰楼,在我方抽卡或從手牌打出卡片等手牌有變動的場景時(shí)會調(diào)用。除了前三個必帶的變量外美浦,后面的變量根據(jù)具體的功能特性來設(shè)置弦赖。
變量 | 值 & 含義 |
---|---|
updateType | add / reduce - 告知是增加還是減少手卡 |
handNo | 手牌卡槽序號 - 告知是哪個卡槽有增減卡片 |
2. 戰(zhàn)場更新:
/**
* 編碼令對方更新(我方/對方)戰(zhàn)場的 message并發(fā)送
* @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", //向服務(wù)器告知的消息類型
"pid": playerID, //向服務(wù)器告知的本玩家ID
"msgtype": "updateField", //向?qū)Ψ酵婕腋嬷母骂愋? "state": state, //卡片放置狀態(tài)
"fieldID": fieldID, //需更新的卡槽ID
"cardsrc": cardsrc //放置的卡片src
});
wsSend(message_field);
}
這是戰(zhàn)場更新的 message,將在戰(zhàn)場有變化的場景中調(diào)用浦辨,比如召喚蹬竖,放置蓋覆卡等。同樣前三個是必帶變量,大家都統(tǒng)一币厕。
變量 | 值 & 含義 |
---|---|
state | attk / defen / back / on / off / change-on / change-off / change-back - 告知卡片更新后的狀態(tài) |
fieldID | 戰(zhàn)場卡槽 id - 告知是哪一個戰(zhàn)場卡槽需要更新 |
cardsrc | 需要更新的圖片 url |
上表的變量中列另,state 的 change-on, change-off, change-back 為特殊狀態(tài),用于表示卡片通過“更變形式”功能而變化成的“打開”旦装,“蓋覆”页衙,“被蓋召喚”狀態(tài),與我們常規(guī)從手牌向場上放置卡片的時(shí)的 on同辣,off拷姿,back 狀態(tài)有所區(qū)別(因?yàn)橛|發(fā)的音效不一樣)惭载。
事實(shí)上這三個變量在被對方客戶端接收后將原封不動的喂給戰(zhàn)場更新函數(shù) updateField旱函,關(guān)于這三個變量將如何被使用完全可以參考 updateField 函數(shù)中的內(nèi)容。
3. 墓地更新 :
前面的章節(jié)有提到過我們有專門用于存放我方墓地卡片的數(shù)組:
var P1Tomb = []; //我方墓地(卡片src)
事實(shí)上描滔,我們還有用于存放對方墓地卡片的數(shù)組:
var P2Tomb = []; //對方墓地
無論是我方還是對方墓地發(fā)生變化棒妨,我們都會及時(shí)同步。任何時(shí)刻雙方客戶端的墓地?cái)?shù)組都是相互同步的(當(dāng)然名字是反的含长,我方 P1Tomb 中的內(nèi)容到了對方客戶端就存儲在 P2Tomb 中券腔,我方的 P2Tomb 中也保存著對方的 P1Tomb)。
/**
* 編碼令對方更新(我方/對方)墓地 message并發(fā)送
* @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", //向服務(wù)器告知的消息類型
"pid": playerID, //向服務(wù)器告知的本玩家ID
"msgtype": "updateTomb", //向?qū)Ψ酵婕腋嬷母骂愋? "updateType": updateType, //向?qū)Ψ酵婕腋嬷?減墓地卡片
"ply": ply, //定義誰的墓地需被更新, player1表示你的對手拘泞,player2表示你自己
"cardNo": cardNo, //卡片序號纷纫,剔出墓地卡片時(shí)需要用到
"cardsrc": cardsrc //卡片的src,新增墓地卡片時(shí)需要用到
});
wsSend(message_tomb);
}
這個墓地更新的 message陪腌,由于雙方玩家既可以操作自己的墓地也可以操作對方的墓地辱魁,墓地更新的 message 必須指明此次操作是更新哪一方的墓地卡片,以便正確同步诗鸭。
變量 | 值 & 含義 |
---|---|
updateType | add / reduce - 告知是增加還是減少墓地的卡 |
ply | player1 / player2 - 告知本次操作的是哪一方的墓地 |
cardNo | 卡片序號 - 告知操作的是墓地中的哪一張卡(從墓地拿出某張卡片時(shí)) |
cardsrc | 卡片圖片 url(向墓地送入某張卡片時(shí)) |
需要說明一下的是染簇,這四個變量中,cardNo 與 cardsrc 并不是每次發(fā)送信息的時(shí)候都會被使用强岸,需要分情況討論锻弓。當(dāng)我們從自己或?qū)Ψ侥沟靥蕹瞿硰埧ㄆ瑫r(shí),由于這張卡片已經(jīng)存在于墓地?cái)?shù)組中蝌箍,我們只需告知其在數(shù)組中的位置便可定位并操作該卡片青灼,這時(shí)候我們只傳送 cardNo 即可,cardsrc 可以留空或賦值一個“null”(便于識別妓盲,避免奇怪 bug)聚至。當(dāng)我們向墓地丟入新卡片時(shí),我們則需要提供該卡的圖片信息本橙,即 cardsrc扳躬,這時(shí)候 cardNo 是不需要的變量。
消息接收
消息編輯,發(fā)送之后贷币,下一步就是接收它击胜。當(dāng)客戶端接收到服務(wù)端轉(zhuǎn)發(fā)來的消息時(shí)會觸發(fā) ws.onmessage 函數(shù),函數(shù)內(nèi)容如下:
//接收服務(wù)器消息后觸發(fā)
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;
}
}
函數(shù)解碼傳來的 JSON 消息后役纹,首先獲取 msgtype偶摔,確認(rèn)更新類型。之前介紹的每種類型的 message 中促脉,前三個必帶變量用于了服務(wù)端的識別與這里的更新類型判定辰斋,而后面那些自定義變量則是在確認(rèn)了具體的更新類型后,作為參數(shù)被原封不動的傳入相關(guān)函數(shù)中瘸味。
更新手牌會調(diào)用 updateP2Hand 函數(shù)宫仗,用于同步對方手牌區(qū)域的顯示情況:
/**
* 更新對方手牌區(qū)域
* 對方手牌均為卡片背面圖片
* @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);
/*執(zhí)行增或減手牌 */
if(updateType == "add") {
element.src = CardBackSrc;
} else {
element.src = "";
}
}
更新戰(zhàn)場會調(diào)用 updateField 函數(shù),此函數(shù)我們已經(jīng)介紹過旁仿,是專門用于更新整個戰(zhàn)場狀態(tài)的函數(shù):
/**
* 戰(zhàn)場狀態(tài)更新藕夫,單獨(dú)更新某一個卡槽
* @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函數(shù)可知對于我方來說,即使卡片是背面圖片仍可以顯示卡片信息
* 由于音效種類問題修改分類了多種情況
*/
switch (cardstate) {
case 'off':
case 'back':
element.src = CardBackSrc;
stateclass = "card-" + cardstate;
/*觸發(fā)背蓋或蓋卡音效 */
var snd = new Audio("sound/activate.wav");
snd.play();
break;
case 'on': //正常發(fā)動卡片
element.src = cardsrc;
stateclass = "card-" + cardstate;
/*觸發(fā)發(fā)動卡片音效 */
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': //通過更變形式實(shí)現(xiàn)的打開蓋卡
/*觸發(fā)打開蓋卡音效 */
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;
/*觸發(fā)發(fā)召喚怪獸音效 */
var snd = new Audio("sound/summon.wav");
snd.play();
} else { //更變形式
stateclass = "card-" + cardstate.replace("change-", "");
}
break;
}
element.setAttribute("class", stateclass); //更新對應(yīng)img容器的class
}
更新墓地會調(diào)用 updateTomb 函數(shù)枯冈,同步雙方墓地的狀態(tài):
/**
* 更新我方/對方墓地
* @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 { //對方拿走對方墓地卡片毅贮,我方執(zhí)行同步
P2Tomb.splice(cardNo, 1);
sf_buttons('p2tomb');
}
}
}
每次更新完某一方墓地后會刷新一次副面板顯示,讓玩家獲悉墓地的變化尘奏。
到這里一個完整的客戶端消息發(fā)送接收機(jī)制就搭建完畢了滩褥!把各種功能與需求分類為幾個明確的類型,再針對每個類型定制相關(guān)消息與函數(shù)就是這個系統(tǒng)實(shí)現(xiàn)的核心炫加。再加之服務(wù)端的消息識別瑰煎,轉(zhuǎn)發(fā)功能,我們已經(jīng)完整地建立了一套可用的聯(lián)機(jī)交互系統(tǒng)琢感。
下一章我們把部分已經(jīng)介紹過的函數(shù)拿出來丢间,討論一下具體是哪些功能的哪些操作需要我們編輯并發(fā)送相關(guān)消息指示對方進(jìn)行同步。