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 存儲在socketsocket.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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章