H5 遊戲 俄羅斯方塊 雙人互動遊戲
最近在慕課網上看到了一個課程是關於俄羅斯方塊的。用到了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