HTML5游戏之Websocket俄罗斯方块终极版(三)
本文是 HTML5 小游戏俄罗斯方块案例的最后一篇,在这篇文章我们会实现这个案例的所有效果,在前两篇《HTML5 游戏之 Websocket 俄罗斯方块基础版(一)》和《HTML5 游戏之 Websocket 俄罗斯方块进阶版(二)》我们分别介绍了基础和进阶版知识,在基础中主要介绍了 Websocket 的基础知识和一些使用技巧,在进阶版我们主要是对游戏的一些实现逻辑进行一个详细的介绍。在本章将会增加服务器端逻辑,还有 Websocket 一些通讯机制,将一个单机版的俄罗斯方块游戏升级成一个真正的双人版火拼游戏,如果说你没有看过之前的两篇文章内容直接来看本篇可能会有点吃力,所以建议先去看之前的两篇文章,打好基础再过来。但是如果你看了之前的基础版和进阶版内容,再看本章内容,将会很轻松的学习到,所以强烈建议大家看之前的两篇文章,好了,废话不多说直接奔主题吧。
通过上图我们看到,在我的游戏区域内已经可以正常运行一个俄罗斯方块游戏了,在对方游戏区域内我们可以根据按钮对方块进行一个简单操作,现在哦我们要做的就是不再需要页面按钮去控制对方游戏区域俄罗斯方块的运作,而是通过 websocket 发送过来的数据来驱动对方游戏俄罗斯方块的运动。
案例及代码回顾
看一下我们之前写的代码,在 js 文件夹中共有 6 个文件,首先是 square.js 和 aquareFactory.js 这两个文件主要是封装方块的逻辑,然后是我们的 game.js 文件,这个文件是我们整个俄罗斯方块核心,再代码最下方导出了很多 API;接着是我们的 local.js,这个主要是控制我的游戏区域的逻辑,还有 remote.js,这个是控制对方游戏区域逻辑,当然之前是通过按钮控制的。之后是我们的 script.js,这个主要是调用 local 和 remote 去实现整个案例的逻辑。由于欧文们接下来要使用 websocket,我们将使用 socketio 来实现对 websocket 通讯,关于 socketio 在基础版里介绍过了,它需要在服务端和客户端同时引入 socketio 模块,如果你之前看了第一节基础版内容,并且安装过 socketio,那么你可以直接复制一份即可,不需要重复安装。
waiting 消息的逻辑及处理
首先我们来实现一下服务器端 js 文件,在根目录下新建一个 wsServer.js,里面的 diamante 可以从之前的基础版复制过来,这里我带大家重写一遍。当一个客户端连接进来的时候,因为我们这个双人的俄罗斯游戏呢需要客户端两两配对,所以需要我们再定义一个变量 clientCount,初始值为 0,用来表示客户端计数;在定义一个 socketMap,用来存储客户端 socket。
//客户端计数 var clientCount = 0; //用来存储客户端 socket var socketMap = {};
当我们 socket 连接出来的时候,首先 clientCount 加 1
clientCount = clientCount + 1;
接着把 clientCount 存入 socket 里面
socket.clientNum = clientCount;
然后我们需要把 socket 存入到 socketMap 里,将 clientCount 作为 key,socket 作为 value 把它存进来
socketMap[clientCount] = socket;
这里我们需要判断一下,假如 clientCount 是基数,也就是第一个进来的,他需要等待第二个进来和他进行一个配对,这个时候我们就给他发送一个 waiting 消息,调用 socket.emit 输出一段话,其他的情况,当第二个进来和第一个进行了配对,他们两个就可以都开始进行游戏,首先我们要给这个 socket 发送一个 start 消息,同时给他配对的也发送一个 start 消息,和他配对的这个消息我们去 socketMap 里面取,他的 key 就是(clientCount-1),然后给他 emit 发送 start 消息,这样 waiting 和 start 消息就写完了。
if (clientCount % 2 == 1) { socket.emit('waiting', 'waiting for another person'); } else { socket.emit('start'); socketMap[(clientCount - 1)] }
完整的 wsServer.js 代码:
var app = require('http').createServer(); var io = require('socket.io')(app); var PORT = 3000; //客户端计数 var clientCount = 0; //用来存储客户端 socket var socketMap = {}; app.listen(PORT); io.on('connection', function (socket) { clientCount = clientCount + 1; socket.clientNum = clientCount; socketMap[clientCount] = socket; if (clientCount % 2 == 1) { socket.emit('waiting', 'waiting for another person'); } else { socket.emit('start'); socketMap[(clientCount - 1)].emit('start'); } socket.on('disconnect', function () { }); }); console.log('websocket listening on port ' + PORT);
接着我们在 index 里面新添加一个 id 为 waiting 的 div,表示等待状态。记得引入 sockit.io.js
<div>请用方向键和空格键进行操作:上->旋转,左->左移,右->右移,下->下移,空格->坠落</div> <div id="waiting"></div> <div class="square" id="local"> <div class="title">我的游戏区域</div> <div class="game" id="local_game"></div> <div class="next" id="local_next"></div> <div class="info"> <div>已用时:<span id="local_time">0</span>s</div> <div>已得分:<span id="local_score">0</span>分</div> <div id="local_gameover"></div> </div> </div>
接下来我们在 script.js 里面创建 socket,然后传递到 local 和 remote 对象里面,到时候会用到,这样在它们的构造函数中也需要将 socket 作为参数传入
var socket = io('ws://localhost:3000'); var local = new Local(socket); var remote = new Remote(socket);
然后在 script.js 中,当我们收到 waiting 消息的时候做一个处理
socket.on('waiting', function (str) { document.getElementById('waiting').innerHTML = str; });
通过命令行 node wsServer.js 运行
页面上显示效果:
通过上图可以看到,“waiting for another person”已经显示出来了,说明我们收到了一个 waiting 这样的一个消息。但是在打开一个标签的话,就不会有 waiting 在显示了,因为它是第二个进来的,所以它收到一个 start 消息,代码如下:
游戏开始的处理
接着我们实现 start 消息背后的逻辑,我们先找到 local.js,最后的导出 API 就不需要了,我们就直接删除即可。我们通过 socket.on 去监听 start 消息,拿到 waiting 元素,设置它的 innerHTML 为空,把它的状态清理一下,然后调用 start 方法。调用 start 方法之后,这个游戏就开始了
socket.on('start', function () { document.getElementById('waiting').innerHTML = ''; start(); })
但是对方的游戏区域未运行,看下面代码
//开始 var start = function () { var doms = { gameDiv: document.getElementById('local_game'), nextDiv: document.getElementById('local_next'), timeDiv: document.getElementById('local_time'), scoreDiv: document.getElementById('local_score'), resultDiv: document.getElementById('local_gameover') } game = new Game(); game.init(doms, generateType(), generateDir()); bindKeyEvent(); game.performNext(generateType(), generateDir()); timer = setInterval(move, INTERVAL); }
start 之后我们看一下有哪些消息要发送出去,首先是 gime.init,里面有三个参数,第一个是 dome 元素,这个我们不用管它,后面的 generateType 参数是表示随机生成的种类,generateDir 参数表示随机生成方块的方向,这两个参数我们需要通过 websocket 传送给相对应的另外一个客户端,随意这两个参数我们先把他缓存下来。我们先定义一个变量 type 等于 generateType(),变量 dir 等于 generateDir(),init 之后我们发送一个消息告诉我们的 wsServer,这里面传入的参数就是我们刚刚定义的两个变量需要传递的数据变成一个对象传过去。改造后的代码如下
//开始 var start = function () { var doms = { gameDiv: document.getElementById('local_game'), nextDiv: document.getElementById('local_next'), timeDiv: document.getElementById('local_time'), scoreDiv: document.getElementById('local_score'), resultDiv: document.getElementById('local_gameover') } game = new Game(); var type = generateType(); var dir = generateDir(); game.init(doms, type, dir); socket.emit('init', {type: type, dir: dir}); bindKeyEvent(); game.performNext(generateType(), generateDir()); timer = setInterval(move, INTERVAL); }
然后在 wsServer.js 中,我们就可以接收到这个消息,发送给与这个 socket 相匹配的另外一个 socket,所以需要我们判断一下,如果(socket.clientNum%2==0)也就是是偶数的时候,比如说它是 2,就需要发送给 clientNum 为 1 的 socket。我们通过 socketMap 把对应的 socket 取出来 socketMap[socket.clientNum-1],那么让它如给他的客户端发送一个消息;else 的话就是一个基数,就与另外一个匹配的 socket 加 1 即可
socket.on('init', function (data) { if (socket.clientNum % 2 == 0) { socketMap[socket.clientNum - 1].emit('init', data); } else { socketMap[socket.clientNum + 1].emit('init', data); } });
那么他的客户端怎么接收消息呢?其实就是在 remote.js 中,在 bindEvents 事件中接收这个 init,然后调用一下这个 start 方法,这个 start 方法里面的参数就是通过 data 传递过来的
//绑定按钮事件 var bindEvents = function () { socket.on('init', function (data) { start(data.type, data.dir); }); }
在 local.js 中的 start 方法内还调用了 game.performNext,那么这个消息同样需要传给另外一个客户端,然后调用 socket 发送 next 消息
//开始 var start = function () { var doms = { gameDiv: document.getElementById('local_game'), nextDiv: document.getElementById('local_next'), timeDiv: document.getElementById('local_time'), scoreDiv: document.getElementById('local_score'), resultDiv: document.getElementById('local_gameover') } game = new Game(); var type = generateType(); var dir = generateDir(); game.init(doms, type, dir); socket.emit('init', {type: type, dir: dir}); bindKeyEvent(); var t = generateType(); var d = generateDir(); game.performNext(t, d); socket.emit('next',{type: t, dir: d}); timer = setInterval(move, INTERVAL); }
然后在 wsServer.js 中进行接收 next 消息,收到消息后,发送到对应的客户端
socket.on('next', function (data) { if (socket.clientNum % 2 == 0) { socketMap[socket.clientNum - 1].emit('next', data); } else { socketMap[socket.clientNum + 1].emit('next', data); } });
那么在对方的游戏区域中,在它接收到 next 消息后调用 game.performNext,这样就相当于驱动对方游戏区域中也去调用 game.performNext,这样一来,当游戏开始的时候,对方游戏区域也会有一些东西,我们来看一下
//绑定按钮事件 var bindEvents = function () { socket.on('init', function (data) { start(data.type, data.dir); }); socket.on('next', function (data) { game.performNext(data.type, data.dir) }); }
基本操作处理
在 wsServer.js 有两段代码它的冗余度是非常高的,如下:
socket.on('init', function (data) { if (socket.clientNum % 2 == 0) { socketMap[socket.clientNum - 1].emit('init', data); } else { socketMap[socket.clientNum + 1].emit('init', data); } }); socket.on('next', function (data) { if (socket.clientNum % 2 == 0) { socketMap[socket.clientNum - 1].emit('next', data); } else { socketMap[socket.clientNum + 1].emit('next', data); } });
所以我们可以抽出来写一个公共的函数,然后在 io.on 里面直接调用函数方法,这个函数改写如下:
var bindListener = function (socket, event) { socket.on(event, function (data) { if (socket.clientNum % 2 == 0) { if (socketMap[socket.clientNum - 1]) { socketMap[socket.clientNum - 1].emit(event, data); } } else { if (socketMap[socket.clientNum + 1]) { socketMap[socket.clientNum + 1].emit(event, data); } } }); }
在 io.on 调用方式
bindListener(socket, 'init'); bindListener(socket, 'next');
然后在 local.js 中看一下那些消息需要转发
//绑定键盘事件 var bindKeyEvent = function () { document.onkeydown = function (e) { if (e.keyCode == 38) { //up game.rotate(); socket.emit('rotate'); } else if (e.keyCode == 39) { //right game.right(); socket.emit('right'); } else if (e.keyCode == 40) { //down game.down(); socket.emit('down'); } else if (e.keyCode == 37) { //left game.left(); socket.emit('left'); } else if (e.keyCode == 32) { //space game.fall(); socket.emit('fall'); } } }
接着在 move 方法中
//移动 var move= function() { timeFunc(); if (!game.down()) { game.fixed(); socket.emit('fixed'); var line = game.checkClear(); if (line) { game.addScore(line); socket.emit('line', line); } var gameOver = game.ckeckGameOver(); if (gameOver) { game.gameover(false); stop(); }else{ var t = generateType(); var d = generateDir(); game.performNext(t, d); socket.emit('next',{type: t, dir: d}); } } else{ socket.emit('down'); } }
然后在 wsServer.js 中将这些消息传入
io.on('connection', function (socket) { clientCount = clientCount + 1; socket.clientNum = clientCount; socketMap[clientCount] = socket; if (clientCount % 2 == 1) { socket.emit('waiting', 'waiting for another person'); } else { if (socketMap[(clientCount - 1)]) { socket.emit('start'); socketMap[(clientCount - 1)].emit('start'); } else { socket.emit('leave'); } } bindListener(socket, 'init'); bindListener(socket, 'next'); bindListener(socket, 'rotate'); bindListener(socket, 'right'); bindListener(socket, 'down'); bindListener(socket, 'left'); bindListener(socket, 'fall'); bindListener(socket, 'fixed'); bindListener(socket, 'line'); bindListener(socket, 'time'); bindListener(socket, 'lose'); });
然后在 remote.js 接收
//绑定按钮事件 var bindEvents = function () { socket.on('init', function (data) { start(data.type, data.dir); }); socket.on('next', function (data) { game.performNext(data.type, data.dir) }); socket.on('rotate', function (data) { game.rotate(); }); socket.on('right', function (data) { game.right(); }); socket.on('down', function (data) { game.down(); }); socket.on('left', function (data) { game.left(); }); socket.on('fall', function (data) { game.fall(); }); socket.on('fixed', function (data) { game.fixed(); }); socket.on('line', function (data) { game.checkClear(); game.addScore(line); }); }
细节处理一
接下来我们看看还有什么消息需要转发,在 local.js 中 timeFunc 函数中去掉之前写的在底部添加一些干扰行,然后在发送一个消息 time
//计时函数 var timeFunc = function () { timeCount = timeCount + 1; if (timeCount == 5) { timeCount = 0; time = time + 1; game.setTime(time); socket.emit('time', rime) } }
然后在 wsServer 端进行转换。
bindListener(socket, 'time');
在 remote.js 中我们接收到 time
socket.on('time', function (data) { game.setTime(data); });
这样我们的时间就同步过来了。接下来处理一下输赢的问题,我们在 local.js 中找到 move 函数的 game.gameover(),这里我们发送一个“lose”
var gameOver = game.ckeckGameOver(); if (gameOver) { game.gameover(false); socket.emit('lose'); stop(); }else{ var t = generateType(); var d = generateDir(); game.performNext(t, d); socket.emit('next',{type: t, dir: d}); }
同样,在 wsServer.js 中进行转换
bindListener(socket, 'lose');
然后在 remote.js 中接收 lose 时,我们调用 gameover
socket.on('lose', function (data) { game.gameover(false); });
最后,在 local.js 中监听 lose,这里要注意,我们收到的 lose 一定是对方发过来的
socket.on('lose', function () { game.gameover(true); stop(); })
讲到这儿,我们在测试的时候发现。不管输赢最后提示消息只在我方显示,而对方没有,我们接着对对方区域消息的改造,首先去掉 index 里面按钮,因为这些已经没有用了
<div>请用方向键和空格键进行操作:上->旋转,左->左移,右->右移,下->下移,空格->坠落</div> <div id="waiting"></div> <div class="square" id="local"> <div class="title">我的游戏区域</div> <div class="game" id="local_game"></div> <div class="next" id="local_next"></div> <div class="info"> <div>已用时:<span id="local_time">0</span>s</div> <div>已得分:<span id="local_score">0</span>分</div> <div id="local_gameover"></div> </div> </div> <div class="square" id="remote"> <div class="title">对方的游戏区域</div> <div class="game" id="remote_game"></div> <div class="next" id="remote_next"></div> <div class="info"> <div>已用时:<span id="remote_time">0</span>s</div> <div>已得分:<span id="remote_score">0</span>分</div> <div id="remote_gameover"></div> </div> </div>
然后在 local.js 中找到 lose 的地方(move 函数方法中)获取显示消息的 ID
document.getElementById('remote_gameover').innerHTML = '你赢了';
这样我们的输赢就处理完了。
细节处理二
接着我们处理一下 disconnect,这个逻辑和我们的 bindListener 方法逻辑是一样的,所以我法如法炮制,然后我们将删除 socketMap 相应的 socket
socket.on('disconnect', function () { if (socket.clientNum % 2 == 0) { if (socketMap[socket.clientNum - 1]) { socketMap[socket.clientNum - 1].emit('leave'); } } else { if (socketMap[socket.clientNum + 1]) { socketMap[socket.clientNum + 1].emit('leave'); } } delete(socketMap[socket.clientNum]); });
然后在 local.js 中,让我们收到 leave 消息的时候,注意这个消息一定是对方发过来的,说明对方掉线了
socket.on('leave', function () { document.getElementById('local_gameover').inneiHTML = '对方掉线'; document.getElementById('remote_gameover').innerHTML = '已掉线'; stop(); });
这个掉线出来就完毕了。
细节处理三
接下来我们处理一下消行,找到 loacl.js 中的 move 方法判断一下,如果 line 大于 1 的话,那说明他一次最少消了两行,这个时候我们要给对方增加一个底部干扰,调用 generataBottomLine 方法,产生的干扰我们用 bottomLines 接收一下,产生了消息后给对方发送一个消息到 server
if (line) { game.addScore(line); socket.emit('line', line); if (line > 1) { var bottomLines = generataBottomLine(line); socket.emit('bottomLines', bottomLines); } }
在 server 这边进行一个转发
bindListener(socket, 'bottomLines');
转发之后在 loacl.js 进行监听,调用 addTailLines 方法增加干扰,传入数据 data,然后给对方同步一下
socket.on('bottomLines', function (data) { game.addTailLines(data); socket.emit('addTailLines', data); });
然后在 server 端进行转化
bindListener(socket, 'addTailLines');
转化了之后在 remote.js 进行监听
socket.on('addTailLines', function (data) { game.addTailLines(data); });
完!
码云笔记 » HTML5游戏之Websocket俄罗斯方块终极版(三)
作者可以发一下源码吗?
我帮你找一下
干货满满,支持一下
感谢认可,多多交流