目录
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 - 实机演示
搭建游戏 WebSocket 服务端
第二章联机模式的那篇博文里我们提到过,这个游戏双方玩家的通信方法是通过服务端做中介来相互传递指令。当一方玩家执行某个操作时,会将该操作的相关指令发给服务端,服务端接收后简单处理一下传给另外一位玩家,之后另外一位玩家接收并重现该指令,这样就在自己的界面里同步了对方玩家所执行的操作,比如抽牌,召唤,放置等等。
现在轮到我们来具体实现一下这个做中介的服务端:
这个服务端的消息传递机制很像上一章csdn大佬博文中的聊天室案例。我们首先在服务端构建一个空的用户数组 players,当每有玩家初次连接服务端的时候,我们会获取并保存该玩家的基本信息(玩家连接的时候顺便发过来),包括玩家的通信地址以及玩家id。
当所有玩家均建立连接后,如果某一位玩家执行操作并发送指令,我们首先会检查该指令所携带的玩家信息,比如玩家id。当我们通过id确认这是已连接的玩家发送的信息后,我们就通过循环的方法将他的指令发给除他之外的所有已连接的玩家。像聊天室一样,某位用户发送的消息将被服务端群发给其他用户,确保每个用户的屏幕上都出现该消息,而发送人自己的屏幕一般通过本地的方法展示发送的内容,无需经过服务器之手(一般情况下)。
这样的传递方法对于这个只有两位玩家的应用来说貌似鸡肋了一些,但扩展性好,如果想扩展成游戏平台之类的这种机制是需要的。
在传递消息前,我们需要规定一下消息格式以便服务端做统一处理:
var message_hand = JSON.stringify({
"type": "message", //向服务器告知的消息类型
"pid": playerID, //向服务器告知的本玩家ID
"msgtype": "updateHand", //向对方玩家告知的更新类型
"updateType": updateType, //向对方玩家告知增/减手卡
"handNo": handNo //向对方玩家告知被更新的卡槽
});
这里是游戏中的一个例子(在客户端里),一个手牌操作相关的消息,本质上是js对象,通过 JSON.stringify 编码成JSON字符串,方便传递(通信接口规定的格式)。我们暂且只看消息中的最上面两行变量,这是我们游戏中每个类型的消息都必带的信息:
变量 | 含义 |
---|---|
type | 发送的消息类型 |
pid | Player ID,即玩家id |
前文说到,我们的服务端靠玩家id来识别接收消息的源头,避免转发消息时发回给发送者本人,玩家id在这里就起到一个识别码的作用。注:玩家id必须是独一无二的,这里本来想动态生成uuid,但貌似相关函数失效了,暂时用自定的字符串代替。
type 代表这条消息的类型,这个变量的存在与我们服务端的消息传递机制有关。因为客户端与服务端的通信分为初次连接与常规通信,不同情况服务端执行的功能不一样。初次连接时服务端需要记录玩家相关信息并保存,常规通信时只需转发消息。
接下来我们来看下客户端与服务端之间的详细通信机制:
首先客户端初次与服务端建立连接的时候,客户端会触发 ws.onopen 函数,我们在此函数中设置向服务端发送一则消息,消息内容只包含 type 与 pid:
//初次与服务端建立连接时触发
ws.onopen = function() {
/*初次与服务端建立连接时告知玩家的pid让服务器存下来 */
var message = JSON.stringify({
"type": "connection", //向服务器告知的消息类型
"pid": playerID, //向服务器告知的本玩家ID
});
wsSend(message);
}
接下来看服务端:
//每一个客户端和服务端建立连接时触发
wss.on('connection', function (ws) {
players.push({"pid": "null", "ws": ws}); //记录连接上的玩家信息,客户端发送的pid不可相同(待优化)
playerIndex += 1;
//收到客户端发送的消息时触发
ws.on('message', function (message) {
var msg = JSON.parse(message);
var type = msg.type;
var pid = msg.pid; //客户端每次需发送自己的pid
switch(type) {
case "connection": //如果有玩家首次建立连接
players[playerIndex].pid = pid;
console.log("client [%s] has connected", pid);
break;
case "message":
wsSend(pid, msg); //将一位玩家发来的消息原封不动发给另一位玩家
console.log("client [%s] send message [%s]", pid, msg.msgtype);
break;
}
});
//SIGINT这个信号是系统默认信号,代表信号中断,就是ctrl+c
process.on('SIGINT', function () {
console.log("Closing things");
process.exit();
});
});
服务端接收消息后会首先将作为消息载体的JSON字符串通过 JSON.parse 函数解析成js对象,也就是还原本来样貌。
之后服务端将通过 type 判断消息的类型是属于初次连接(connection)还是常规消息(message)。初次连接意味着该客户端首次与服务端通信,之前没有保存它的任何信息。
如果是常规消息,说明该客户端已经连接上了,我们将该客户端的id与消息内容传入消息转发函数 wsSend,id用做识别码。wsSend 遍历所有已保存的玩家信息,将消息内容发给除发送者之外的玩家:
function wsSend(pid, content) {
for (var i=0; i < players.length; i++) {
if (players[i].pid != pid) { //若pid不匹配则发送,即发给另一位玩家而非自己
var clientSocket = players[i].ws;
if (clientSocket.readyState == WebSocket.OPEN) {
clientSocket.send(JSON.stringify(content));
}
}
}
}
最后贴一下服务端 ygo-server.js 完整代码:
//引入ws模块,初始化websocket服务端
var WebSocket = require('ws');
var WebSocketServer = WebSocket.Server,
wss = new WebSocketServer({port: 9999});
//客户端数组,储存客户端基本信息
var players = [];
var playerIndex = -1;
function wsSend(pid, content) {
for (var i=0; i < players.length; i++) {
if (players[i].pid != pid) { //若pid不匹配则发送,即发给另一位玩家而非自己
var clientSocket = players[i].ws;
if (clientSocket.readyState == WebSocket.OPEN) {
clientSocket.send(JSON.stringify(content));
}
}
}
}
//每一个客户端和服务端建立连接时触发
wss.on('connection', function (ws) {
players.push({"pid": "null", "ws": ws}); //记录连接上的玩家信息,客户端发送的pid不可相同(待优化)
playerIndex += 1;
//收到客户端发送的消息时触发
ws.on('message', function (message) {
var msg = JSON.parse(message);
var type = msg.type;
var pid = msg.pid; //客户端每次需发送自己的pid
switch(type) {
case "connection": //如果有玩家首次建立连接
players[playerIndex].pid = pid;
console.log("client [%s] has connected", pid);
break;
case "message":
wsSend(pid, msg); //将一位玩家发来的消息原封不动发给另一位玩家
console.log("client [%s] send message [%s]", pid, msg.msgtype);
break;
}
});
//SIGINT这个信号是系统默认信号,代表信号中断,就是ctrl+c
process.on('SIGINT', function () {
console.log("Closing things");
process.exit();
});
});
下一章我们来介绍客户端的联机功能,实现完整的消息发送,接收机制。