【javascript小案例】從0開始實現一個俄羅斯方塊

来源:https://www.cnblogs.com/qianbixueyuan/archive/2019/03/19/10560205.html
-Advertisement-
Play Games

寫在前面得話: 這篇文章主要記錄了我是怎麼一步一步寫出俄羅斯方塊,整個代碼用的函數編程,主要是為了讓一些不熟悉es6, 面向對象寫法得 新手能更容易看明白,全部得代碼中都是一些js的基礎知識,很容易理解。要說有點麻煩的,那就是游戲過程中的各種檢測。但是只要你多思考,你就能理解代碼為什麼要那樣寫,你也 ...


寫在前面得話:

這篇文章主要記錄了我是怎麼一步一步寫出俄羅斯方塊,整個代碼用的函數編程,主要是為了讓一些不熟悉es6, 面向對象寫法得 新手能更容易看明白,全部得代碼中都是一些js的基礎知識,很容易理解。要說有點麻煩的,那就是游戲過程中的各種檢測。但是只要你多思考,你就能理解代碼為什麼要那樣寫,你也可以實現這個游戲。(當然也許你有更好的實現方法)。

預覽地址:http://blog.cwlserver.top/demo/Tetris.html 

1,先理清游戲邏輯

  • 游戲場景:場景大小為 10*18,
  • 下落時間:初始方塊每隔1秒,會下落一格。隨著游戲進行時間得增加,方塊下落時間間隔會縮短。
  • 操作方法:方向鍵得 上下左右 分別控制方塊得, 變形,加速下落,左移,右移。
  • 方塊類型:一共7種類型得方塊。每次隨機出現一種, 每種方塊由數個 1*1大小得小方塊組成
  • 方塊下落:當方塊落到底, 或者下一格已經被占,方塊停止下落,然後會有一個新的方塊出現
  • 方塊左右移動:方塊左右移動時,如果左,右是牆或者是已經被占,方塊將不能移動。
  • 方塊變形:方塊逆時針旋轉90°,變形時需要判斷方塊是否可以變形。
  • 游戲會有下一個方塊得提示
  • 消行:當一行被填滿時,這一行將被消除
  • 計分規則: 消1行得2分,2行4分,3行8分,4行16分
  • 游戲結束: 當方塊下落到底,並且方塊超出游戲場景時,判定游戲結束

2,分步實現游戲中得功能

html結構

    <div id="box">
        <canvas id="canvas" width="300" height="540"></canvas>
        <div class="scorebox">
            <p>游戲已進行: <span id="game-time">00:00:00</span></p><br>
            <p>當前得分: <span id="score">0</span></p><br>
            <p>下一個方塊:</p><br>
            <canvas id="next" width="120" height="120"></canvas><br>
            <p class="btns"><button id="pause">暫停</button><button id="restart">重新開始</button></p>
        </div>
    </div>

構建場景

因為場景大小是10x18,所以我決定用一個 10x18得二維數組來模擬場景,這樣方便和方塊做碰撞檢測。

//定義列數
var ROW = 10;
//定義行數
var COL = 18;
//游戲得分
var SCORE = 0;
//游戲場景
var area = new Array(COL);
for(var i=0; i<area.length; i++){
    area[i] = new Array(ROW).fill(0);
}
/*
最終得到得area是這樣得
area = [
    [0,0,0,0....]
    [0,0,0,0....]
    [0,0,0,0....]
    ...
]
*/

構建小方塊

小方塊我同樣使用二維數組來構建

//定義各種方塊得數組, 一共7種不同得方塊,數組中的1,2,3,4..這些數字主要是為了每個方塊設置不同的顏色
var data = {
    'o':[
        [1, 1],
        [1, 1]
    ],
    's':[
        [2, 0, 0],
        [2, 2, 0],
        [0, 2, 0]
    ],
    '5':[
        [0, 0, 3],
        [0, 3, 3],
        [0, 3, 0]
    ],            
    'l':[
        [4, 0, 0],
        [4, 0, 0],
        [4, 4, 0]
    ],
    't':[
        [5, 5, 5],
        [0, 5, 0],
        [0, 0, 0]
    ],
    'j':[
        [0, 0, 6],
        [0, 0, 6],
        [0, 6, 6]
    ],
    '|':[
        [0, 7, 0, 0],
        [0, 7, 0, 0],
        [0, 7, 0, 0],
        [0, 7, 0, 0]
    ]
};
//定義方塊得顏色,每個數字對應一種顏色
var aColor = ['', '#fff', '#0000FF', '#00FF00', '#CC00FF', '#CCFFFF','#FFFF33','#99FFFF'];
//將data中得key放到一個字元串中 方便隨機調用
var sKey = 'os5ltj|';
//定義當前方塊, 當前方塊預設null;
var cur = null;
//因為游戲中會有下一個方塊得提示, 所以這裡要提前聲明一下
var next = null;
//定義一個生成方塊得函數
function createBox(){
    //首先創建提示方塊
    if(!next){
        //從skey中隨機取出一個鍵名
        var rnd = Math.floor(Math.random()*sKey.length);
        //根據key取得方塊數組
        var box = data[sKey[rnd]];
        //每一個方塊都有,x, y, box 這三個屬性
        next = {
            //方塊初始在場景中間位置,方塊左移 x--, 右移 x++;
            x: Math.floor((ROW-box[0].length)/2), 
            //方塊在垂直方向得位置,剛好在場景外, y++ 方塊下落
            y: -box[0].length,
            //方塊得數組
            box: box
        };
    }
    //當前方塊不存在時, 創建當前方塊
    if(!cur){
        //直接下一個方塊變成當前這個
        cur = next;
        //然後再重新生成下一個
        next = {
            x: Math.floor((ROW-box[0].length)/2),
            y: -box[0].length,
            box: data[sKey[Math.floor(Math.random()*sKey.length)]]
        }
    }
}

現在想一個問題,有了場景和方塊的數據之後,如何把他們聯繫起來?

我的處理方式是這樣的,在方塊下落的過程中,方塊和場景是分開的,方塊的位置和場景是分開刷新的。在下落的過程中我會 檢測方塊和場景是否發生碰撞,如果發生了碰撞,將當前方塊的數組合併到場景的數組中,使方塊變成場景的一部分,同時生成一個新的方塊。看下代碼如何實現

//將當前方塊合併到場景
function mergeBoxArea(){
    //迴圈當前方塊
    for(var i=0; i<cur.box.length; i++){
        //這裡的判斷是為了當方塊的一部分在場景外的時候,將那一部分跳過,只計算在場景中的部分
        if(i+cur.y>=0){
            for(var j=0; j<cur.box[i].length){
                //將當前方塊數組中不為0的項,和 場景中當前位置為0的項合併
                if(cur.box[i][j] !== 0 && area[i+cur.y][j+cur.x] == 0){
                    //合併的結果, 將場景中當前位置的值設置為方塊對應位置的值
                    area[i+cur.y][j+cur.x] = cur.box[i][j];
                }
            }
        }
    }
    //將方塊合併入場景的同時要嘗試 消行
    var arr = isRemove(area);
    if(arr.length !== 0){
        for(var i=0; i<arr.length; i++){
            area.splice(arr[i], 1)
            area.unshift(new Array(ROW).fill(0))
        }
        //更新得分
        SCORE+=Math.pow(2, arr.length)
        scoreEle.innerHTML = SCORE;
    };
}
//碰撞檢測
//垂直方向的碰撞檢測, 需要接受當前方塊做為參數,
//作用:檢測方塊下落一格之後和場景的碰撞情況,如果會碰撞返回true,否則返回false;
function collide(cur){
    var box = cur.box;
    var len = box.length;
    var x = box.x;
    //因為是檢測下一個位置,所以要+1;
    var y = box.y + 1;
    for(var i=0; i<len; i++){
        //做碰撞檢測同樣需要將場景外的方塊部分排除掉
        if(i+y>=0){
            //方塊的數組都是n*n的所以都用len
            for(var j=0; j<len; j++){
                //將方塊為0的項不檢測
                if(box[i][j] !== 0){
                    //第一種碰撞情況:當i+y大於等於場景的高度時,說明方塊出界
                    //第二種碰撞情況:方塊沒有出界,但是場景中的這個位置,被占用了
                    if(i+y>=area.length || (i+y<area.length && area[i+y][j+x] !== 0)){
                        //碰撞了返回 true
                        return true;
                    }
                }
            }
        }
    }
    //代碼執行到這裡時說明沒有碰撞,返回false;
    return false;
}
//水平方向的移動限制
//當用鍵盤控制方塊左右移動的時候,需要檢測左右是否是牆,或者方塊,這裡檢測的也是下一個位置的碰撞情況
//如果沒有牆或者方塊(不碰撞),返回true
//如果碰撞, 返回 false;
//接受參數: 當前方塊:cur, 移動方向: dir -1|0|1
function bMove(cur, dir){
    //當前位置加上方向 就是 下一個位置
    var x = cur.x+dir;
    for(var i=0; i<cur.box.length; i++){
        for(var j=0; j<cur.box[i].length; j++){
            if(cur[i][j] !== 0){
                //這裡發生碰撞的情況有3中
                //1.方塊在左邊出界了, 這時 j+x<0
                //2.方塊在右邊出界了, j+x>= ROW
                //3.方塊沒有出界,但是場景中的這個位置被占用 area[i+cur.y][j+x]!==0
                // 加上 i+cur.y>=0 && j+x>=0 && area[i+cur.y] 是為了防止報錯
                if(j+x<0 || j+x==ROW || ( i+cur.y>=0 && j+x>=0 && area[i+cur.y] && area[i+cur.y][j+x]!==0)){
                    return false;
                }
            }
        }
    }
    return true;
}

如何處理方塊旋轉?

方塊的旋轉比較容易處理,就把二維數組旋轉一下就可以了。但是要註意方塊旋轉的時候也是需要檢測 旋轉的合理性, 可以想象一下,一個長條下落的過程中,如果他的左右兩邊都是方塊,這種情況肯定是不能旋轉的(其它方塊同理)。還有一種情況就是,方塊靠牆下落的時候,旋轉一下之後,有一部分轉到牆裡面去了,這種也是不合理的,但是玩游戲的時候,這種情況也能旋轉,所以出現這種情況的時候,我們需要修正一下方塊的位置。 下麵看代碼怎麼寫

//此函數用於檢測方塊是否能夠旋轉
/*
參數: 當前方塊 cur
返回值: true        //方塊可以直接旋轉
        false        //方塊不能旋轉,即使是在嘗試修正位置之後,就是上面說到的左右都是方塊的情況
        cur.x        //當返回 一個數值的時候,說明 將方塊水平移動到這個位置後,可以旋轉, 即上面說的修正位置
*/
function bRotate(cur){
    //在這裡複製一個旋轉後的方塊出來,用於檢測
    var _cur = {x: cur.x, y:cur.y, box: rotateBox(cur.box)};
    //檢測方塊旋轉之後,水平和垂直方向的碰撞情況, 如果在任意方向會發生碰撞
    if( collide(_cur) === true || bMove(_cur, 0) === false ){
        //嘗試水平移動方塊,移動方向是分別向左,向右移動2格
        for(var i=0; i<2; i++){
            //方塊靠近左邊的時候,嘗試向右移動,並且檢測移動的合理性
            if(_cur.x<4 && bMove(_cur, 1)){
                _cur.x++;
            }
            //靠近右側的時候,向左移動,並且檢測移動的合理性
            if(_cur.x>6 && bMove(_cur, -1)){
                _cur.x--;
            }
            //移動之後再檢測是否碰撞, 如果不會發生碰撞, 返回移動後的位置
            if(collide(_cur) === false && bMove(_cur, 0)){
                return _cur.x;
            }
        }
        //代碼執行到這裡的時候說明,移動了之後仍會碰撞
        return false;
    //如果旋轉之後不會發生碰撞,直接返回true;
    }else{
        return true;
    }
}
//旋轉數組的函數
function rotateBox(arr){
    var res = [];
    for(var i=0; i<arr.length; i++){
        res.push([]);
    }
    //旋轉
    for(var i=0; i<arr.length; i++){
        for(var j=0; j<arr[i].length; j++){
            res[arr.length-1-y][x] = arr[x][y];
        }
    }
    return res;
}

現在開始處理游戲的刷新, 計算游戲的時間

游戲用 requestAnimationFrame 更新

var timer = null;
//記錄一個舊的時間,這裡用於輔助計算, 每次刷新的間隔時間
var oldTime = Date.now();
//n 用於累加 raf 的間隔時間
var n = 0;
//游戲運行時間 單位 毫秒
var gameTime = 0;
//方塊下落的間隔時間
var step = 1000;
//游戲是否暫停
var bPause = false;
//獲取dom元素
//主場景canvas
var canvas = document.getElementById('canvas');
var ctx = canvas.getContext('2d');
//提示下一個方塊 canvas
var nextCanvas = document.getElementById('next');
var nextctx = nextCanvas.getContext('2d');
//游戲得分
var scoreEle = document.getElementById('score');
//暫停按鈕
var pauseEle = document.getElementById('pause');
//重新開始按鈕
var restartEle = document.getElementById('restart')
//顯示游戲時間
var gameTimeEle = document.getElementById('game-time');
//開啟主迴圈
timer = requestAnimationFrame(animate);
//主迴圈函數
function animate(){
    //累加 raf 的間隔時間
    n+=Date.now()-oldTime;
    //累加游戲運行時間
    gameTime+=Date.now()-oldTime;
    oldTime = Date.now();
    //方塊要開始下落了
    if(n>=step){
        n = 0;
        //每秒鐘更新一次游戲時間
        updateGameTime()
        //根據游戲進行時間提高游戲難度
        changeDifficulty();
        //方塊下落之前要先檢測是否會發生碰撞
        //會發生碰撞
        if(collide(cur)){
            //會碰撞,並且此時,如果方塊有一部分在外面,說明游戲結束
            if(cur.y<0){
                gameover()
            //正常的碰撞
            }else{
                //將方塊合併入游戲場景
                mergeBoxArea()
                //並將cur 設置為null
                cur = null;
                //產生一個新的方塊
                createBox()
            }
        //不會碰撞
        }else{
            cur.y++;
        }
    }
    //更新游戲場景
    drawArea();
    //畫提示方塊
    drawNextBox();    
    timer = requestAnimationFrame(animate);
}
//更新游戲場景
function drawArea(){
    ctx.clearRect(0, 0, 300, 540)
    ctx.save()
    ctx.scale(30, 30)
    //ctx.fillStyle = '#fff';
    //畫游戲場景
    drawcube(ctx, area)    
    //畫當前方塊
    drawcube(ctx, cur.box, cur.x, cur.y)                
    ctx.restore();
} 
//更新提示
function drawNextBox(){
    nextctx.clearRect(0, 0, 120, 120)
    nextctx.save()
    nextctx.scale(30, 30)
    //畫下一個方塊
    next&&drawcube(nextctx, next.box)    
    nextctx.restore();        
}
//畫方塊,接受一個ctx對象,一個數組, 數組的偏移值
function drawcube(ctx, arr, x, y){
    x = x || 0;
    y = y || 0;
    for(var i=0; i<arr.length; i++){
        for(var j=0; j<arr[i].length; j++){
            if(arr[i][j] !== 0){
                //設置方塊的顏色
                ctx.fillStyle = aColor[arr[i][j]];
                ctx.fillRect(j+x, i+y, 1, 1)
            }
        }
    }    
}

監聽鍵盤事件,移動方塊

//監聽鍵盤事件
document.addEventListener('keydown', function(ev){
    if(bPause || !cur){
        return false;
    }
    var keycode = ev.keyCode;
    switch(keycode){
        //左
        case 37:
            //是否能向左移動
            if(bMove(cur, -1)){
                cur.x--;
            }
        break;
        //右
        case 39:
            //是否能向右移動
            if(bMove(cur, 1)){
                cur.x++;
            }
        break;
        //下
        case 40:
            //如果觸底或者落到其它方塊上面
            if(collide(cur)){
                if(cur.y<0){
                    gameover()
                }else{
                    mergeBoxArea()
                    cur = null;
                    createBox()
                }
            }else{
                cur.y++;
            }
        break;
        //上
        case 38:
            //是否能旋轉 當n為true時可以直接旋轉,當n為數值時需要將方塊x位置移動到此處才能旋轉
            var rotateRes = bRotate(cur);
            //可以直接旋轉
            if(rotateRes === true){
                cur.box = rotateBox(cur.box);
            //不能旋轉
            }else if(rotateRes === false){
                console.log('不能旋轉')
            //需要移動之後才能旋轉
            }else{
                cur.x = rotateRes;
                cur.box = rotateBox(cur.box);
            }
        break;
    }
})

處理游戲結束, 游戲暫停, 游戲重新開始, 消行, 更新游戲得分, 更新游戲運行時間等等

//點擊暫停按鈕
pauseEle.addEventListener('click', function(){
    var html = this.innerHTML;
    if(html === '暫停'){
        pause();
        this.innerHTML = '繼續';
    }else{
        start();
        this.innerHTML = '暫停';
    }
})
//點擊重新開始
restartEle.addEventListener('click', function(){
    restart();
})
//暫停游戲
function pause(){
    cancelAnimationFrame(timer);
    bPause = true;
}
//繼續
function start(){
    timer = requestAnimationFrame(animate);
    bPause = false;
}
//重新開始
function restart(){
    //重置場景
    for(var i=0; i<area.length; i++){
        for(var j=0; j<area[i].length; j++){
            area[i][j] = 0;
        }
    }
    cancelAnimationFrame(timer);
    timer = requestAnimationFrame(animate);
    bPause = false;
    pauseEle.innerHTML = '暫停';
    //重置游戲時間
    gameTime = 0;
    //更新游戲時間
    updateGameTime();
    cur = null;
    //創建第一個方塊
    createBox();
}
//游戲結束
function gameover(){    
    cancelAnimationFrame(timer);
    alert('游戲結束, 您一共獲得:'

您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 安裝 卸載 1. ,卸載pod命令。 2. ,查看所有安裝的cocoapods組件,依次刪除, 或者執行以下腳本 Xcode工程里的cocoapods卸載與重裝 ...
  • 常規需求: 外層RecyclerView嵌套內層RecyclerView , 在上下滑動的時候會出現item數據以及view的顯示異常。 解決辦法: 1、重寫 getItemViewType 方法 2、因為是RecyclerView的復用機制導致的問題,可以暴力解決,禁止RecyclerView的復 ...
  • NSString * totalAssetString =@"1161000.00"; NSDecimalNumber *totalAssetNumber = [NSDecimalNumber decimalNumberWithString:totalAssetString]; NSDecimalN ...
  • RadioButton為單選按鈕,他需要與RadioGroup配合使用 對應的佈局代碼: Java代碼: 在上述代碼中,利用setCheckedChangeListener()監聽RadioGroup控制項狀態,獲取監聽結果輸出到TextView控制項里顯示 ...
  • 學CSS很好的一個方法大概是先用純CSS來實現一個自己的框架,然後便可以在之後的使用中對一開始可能很粗糙的框架做細緻的優化與改進,刪除些冗餘,添加些功能之類的。 當然,為了避免一開始寫框架時候的時候手足無措,對一個css框架應該實現些什麼功能不大清楚,便可以參考已有的框架自己重寫一遍,寫的過程中這些 ...
  • 01-jquery簡介1)功能: ·html元素選取 ·Html元素操作 ·Css操作 ·Html事件函數 ·JavaScript特效和動畫 ·DOM的遍歷及修改 ·AJAX ·Utilities ·插件2)版本支持 ·jquery2 及以上不支持IE6,7,8 ·使用註釋: · · ·... ...
  • 解構賦值 Destructuring Assignment ES6中可以通過一定的模式將數組或對象中的值直接賦值給外部變數,稱為解構 對象的解構賦值 對象解構過程中,外部變數名也可以與對象內部變數名不同,但此時不可以使用對象的縮寫形式 對象解構過程中,外部變數名也可以與對象內部變數名不同,但此時不可 ...
  • 什麼是Vue? Vue (讀音 /vjuː/,類似於view) 是一套用於構建用戶界面的漸進式框架。與其它大型框架不同的是,Vue 被設計為可以自底向上逐層應用。Vue 的核心庫只關註視圖層,不僅易於上手,還便於與第三方庫或既有項目整合。另一方面,當與現代化的工具鏈以及各種支持類庫結合使用時,Vue ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...