目录
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 来复现此操作,这与我们自己召唤怪兽几乎无异,只是放置对象是对方场上的卡槽,也就是战场界面的上半部分。
针对如上述例子所说的不同功能场景,我们会编辑不同种类的 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');
}
}
}
每次更新完某一方墓地后会刷新一次副面板显示,让玩家获悉墓地的变化。
到这里一个完整的客户端消息发送接收机制就搭建完毕了!把各种功能与需求分类为几个明确的类型,再针对每个类型定制相关消息与函数就是这个系统实现的核心。再加之服务端的消息识别,转发功能,我们已经完整地建立了一套可用的联机交互系统。
下一章我们把部分已经介绍过的函数拿出来,讨论一下具体是哪些功能的哪些操作需要我们编辑并发送相关消息指示对方进行同步。