最近在慕课网上看到了一个课程是关于俄罗斯方块的。用到了socket.io 做双屏互动的游戏。正好最近在看websocket所以就把整个课程看完了,感觉很有意思,这里用一篇文章仔细的分析下这个游戏的制作思路。
实际在操作的时候,对方游戏区域会同步对方的操作。
js部分先进行逻辑的分析
先不讲socket.io 同步部分,先分析游戏实现的逻辑。由于这部分js代码量比较大,所以我们先组织好开发的架构
根据作者提供的结构图。可以看出。我们使用了
script.js
这部分是初始化远程和本地游戏的一个入口,将来也是和服务器代码进行交互的一个入口。
local.js
这个文件主要处理的是本地,单机版的游戏时,操作的逻辑
本地操作包括:
启动游戏
点击不同的键盘的触发事件,旋转、左、右、下、下落
启动之后出现自动下落的方块
游戏结束的方法
离开游戏的方法
游戏成功,失败的方法
remote.js
在双人版是 负责监听 服务器传送过来的事件。用于同步数据。
game.js
这里处理的是游戏的事件执行逻辑
在local.js 中点击键盘触发的事件逻辑都是在game.js 中实现的
square.js
这是方块的设定。包括当前界面和next界面。
随机出现的方块的样子,是否还可以再移动,移动是否到了边界等。
squareFactory.js
这里使用了原型链的方式实现了对square.js 的继承
分别构造出了7中方块的不同形态变化。
以上就是整个的架构模式。
现在分析一下整个架构之间的通信
1、游戏刚开始
先加载了script.js 这个文件,在这个文件中我们初始化 local 和 remote
那多对象后调用start方法。游戏开始了。
这里可以吧local对象理解为一个类,在这个类里面封装了本地需要的操作。暴露出去需要在外部调用的接口。这样可以保护私有变量。
var Local = function(socket) {
//游戏对象
var game;
//方块下落的时间间隔
var INTERVAL = 200;
var timer = null;
//时间次数
var timeCount = 0;
//游戏了多久
var time = 0;
//绑定键盘事件
var bindKeyEvent = function() {
document.onkeydown = function(e) {
}
}
......
//开始方法
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'),
gameoverDiv: 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);
}
........
//导出API 这个方法可以在外部访问到。上面的都是私有的不可被访问到。
this.start = start;
}
这算是单利的模式。
我们看到了这里初始化了game对象
game.js 是同样的模式
var Game = function() {
//dom 元素
var gameDiv;
var nextDiv;
var timeDiv;
var scoreDiv;
var gameoverDiv;
//保留得分
var score = 0;
//游戏矩阵
// 10*20
var gameData = [
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
];
//当前方块
var cur;
//下一个方块
var next;
//divs
var nextDivs = [];
var gameDivs = [];
//初始化
var init = function(doms, type, dir) {
gameDiv = doms.gameDiv;
nextDiv = doms.nextDiv;
timeDiv = doms.timeDiv;
scoreDiv = doms.scoreDiv;
gameoverDiv = doms.gameoverDiv;
// 这里要修改为随机的
// cur = SquareFactory.prototype.make(0,0);
next = SquareFactory.prototype.make(type, dir);
initDiv(gameDiv, gameData, gameDivs);
initDiv(nextDiv, next.data, nextDivs);
// setData();
// console.log('gameData', gameData);
// refreshDiv(gameData, gameDivs);
refreshDiv(next.data, nextDivs);
}
//导出API
this.init = init;
this.down = down;
this.left = left;
this.right = right;
this.rotate = rotate;
this.fall = function() {
while (down());
}
this.fixed = fixed;
this.performNext = performNext;
this.checkClear = checkClear;
this.checkGameOver = checkGameOver;
this.setTime = setTime;
this.addScore = addScore;
this.gameover = gameover;
this.addTailLines = addTailLines;
}
在game.js中导出了这么多的方法。在其他的文件中都是可以调用的。只要我们 new 一个game对象就可以了。这样就可以实现两个 对象之间的通信了。
Square.js
这个对象的作用是抽出所的方块都需要的变量和方法。然后在实例化单个对象,每个对象都继承自这个父对象。
这里的处理方法就是构造函数和原型链的结合
var Square = function() {
//方块数据
this.data = [
[0, 0, 0, 0],
[0, 0, 0, 0],
[0, 0, 0, 0],
[0, 0, 0, 0]
];
//原点
this.origin = {
x: 0,
y: 0
}
//旋转的方向,也就是旋转数组中的索引
this.dir = 0;
}
//是否还可以旋转
Square.prototype.canRotate = function(isValid) {
var d = (this.dir + 1)%4;
var test = [
[0, 0, 0, 0],
[0, 0, 0, 0],
[0, 0, 0, 0],
[0, 0, 0, 0]
];
for (var i = 0; i < this.data.length; i++) {
for (var j = 0; j < this.data[0].length; j++) {
test[i][j] = this.rotates[d][i][j];
}
}
return isValid(this.origin, test);
}
Square.prototype.rotate = function(num) {
if(!num){
num = 1;
}
this.dir = (this.dir + num)%4;
for (var i = 0; i < this.data.length; i++) {
for (var j = 0; j < this.data[0].length; j++) {
this.data[i][j] = this.rotates[this.dir][i][j];
}
}
}
//是否还可以下降
Square.prototype.canDown = function(isValid) {
var test = {};
test.x = this.origin.x + 1;
test.y = this.origin.y;
return isValid(test, this.data);
}
Square.prototype.down = function() {
this.origin.x = this.origin.x + 1;
console.log(this.origin.x);
}
//是否还可以左移
Square.prototype.canLeft = function(isValid) {
var test = {};
test.x = this.origin.x;
test.y = this.origin.y - 1;
return isValid(test, this.data);
}
Square.prototype.left = function() {
this.origin.y = this.origin.y - 1;
console.log(this.origin.x);
}
//是否还可以右移
Square.prototype.canRight = function(isValid) {
var test = {};
test.x = this.origin.x;
test.y = this.origin.y + 1;
return isValid(test, this.data);
}
Square.prototype.right = function() {
this.origin.y = this.origin.y + 1;
}
这个对象中,对Squrae的方法都绑定到了原型链上。这样子对象,就可以通过原型链找到这个公共的方法。
SquareFactory.js
这是个初始化不同类型的方块的工程,根据随机的方块样式 和 方块的旋转方向来初始化不同的方块。
var Square1 = function() {
Square.call(this);
//旋转数组后,枚举出来的值
this.rotates = [
[
[0, 2, 0, 0],
[0, 2, 0, 0],
[0, 2, 0, 0],
[0, 2, 0, 0]
],
[
[0, 0, 0, 0],
[2, 2, 2, 2],
[0, 0, 0, 0],
[0, 0, 0, 0]
],
[
[0, 2, 0, 0],
[0, 2, 0, 0],
[0, 2, 0, 0],
[0, 2, 0, 0]
],
[
[0, 0, 0, 0],
[2, 2, 2, 2],
[0, 0, 0, 0],
[0, 0, 0, 0]
]
]
}
Square1.prototype = Square.prototype;
这里初始化第一个方块类型;
使用的是构造函数和原型链方法
Square.call(this); 这个方法就扩展了this的范围。使得Square1 获取了Square的变量。
在这个之后在声明Square1 自己的变量;
Square1.prototype = Square.prototype ;
把父对象的原型链赋值给了Square1. 这样就完成了继承。
var SquareFactory = function() {};
SquareFactory.prototype.make = function(index, dir) {
var s;
index = index + 1;
switch (index) {
case 1:
s = new Square1();
break;
case 2:
s = new Square2();
break;
case 3:
s = new Square3();
break;
case 4:
s = new Square4();
break;
case 5:
s = new Square5();
break;
case 6:
s = new Square6();
break;
case 7:
s = new Square7();
break;
default:
break;
}
s.origin.x = 0;
s.origin.y = 3;
s.rotate(dir);
return s;
}
这里的SquareFactory 创建的对象,在其他文件是可访问到的。
这里涉及到了很多知识。原型链,继承等,这部分就是面向对象的程序设计的思想。
下一个知识点 socket.io
https://socket.io/docs/
先下载socket.io。
同时还要引入socket.io.js 文件
https://socket.io/blog/
然后我们创建服务端
创建wsWerver.js 文件
var app = require('http').createServer();
var io = require('socket.io')(app);
var PORT = 3000;
app.listen(PORT);
//客户端的计数
var clientCount = 0;
//用来存储客户端的socket
var socketMap = {};
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('connection', function(socket) {
clientCount = clientCount + 1;
// 把clientCount 存储在socket中
socket.clientNum = clientCount;
socketMap[clientCount] = socket;
if (clientCount % 2 == 1) {
socket.emit('waiting', 'waiting for another persion');
} else {
//配对的socket
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');
bindListener(socket, 'bottomLines');
bindListener(socket, 'addTailLines');
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]);
});
})
console.log('websocket listening on port' + PORT);
服务器的作用是为了监听 客户端发送的需要同步到对方区域的数据。
socket.on() 监听事件 socket.emit() 发送事件
本地与服务器的链接在script.js 中
var socket = io('ws://localhost:3000');
var local = new Local(socket);
var remote = new Remote(socket);
这样就可以吧链接的socket 传递到local和remote中了。
比如服务器检测到有两个玩家了,发去了可以开始游戏的指令“start”。
这时在local 中就检测到了 “start”.然后就可以出发start()方法了。
socket.on('start', function() {
document.getElementById('waiting').innerHTML = "";
start();
});
在local中 触发了初始化游戏的方法,同时也要同步到对方游戏区域。这是就发送给服务器一个消息
告诉他我的游戏初始化了,并传递过去初始化的参数
game.init(doms, type, dir);
socket.emit(‘init’, { type: type, dir: dir });
游戏的整体整体结构就是这样了,具体的游戏实现细节就不用讲了,感兴趣的可以去看视频。
我也把代码上传到了我的github上了,需要node环境。执行命令是 node wsServer.js
https://github.com/zhouyujuan/games/tree/master