canvas+js從0開始擼一個俄羅斯方塊

NO IMAGE

最近工作之餘看了一個俄羅斯方塊小遊戲實現的視頻教程,覺得這個小遊戲的算法挺有意思,打算整理出來學習參考一下。

雖然標題標著canvas,其實這個小遊戲用到的canvas知識點甚少,其核心還是js算法問題。教程會比較枯燥,可能對實際工作中也沒太大用處(不過可以用俄羅斯方塊做一個營銷小活動),寫出來目的主要是為了學習一下這款小遊戲的算法思想,以及鍛鍊解決問題的能力。

前言

本篇文章將分為幾個模塊逐步講解:

  1. 遊戲玩法介紹
  2. 算法思想
  3. 創建遊戲地圖
  4. 方塊的創建
  5. 生成不同形狀方塊
  6. 方塊下落
  7. 方塊碰撞
  8. 方塊左右移動
  9. 方塊變形
  10. 方塊向下加速
  11. 消除整行
  12. 遊戲結束

遊戲雖小,功能還挺複雜,需要的就是思維和耐心。下面直接進入正題。

遊戲玩法介紹

俄羅斯方塊想必大家都玩過,大體玩法簡單說一下,在一個格子地圖上隨機下落不同形狀的方塊,通過←→方向鍵去控制方塊的移動,以及↑鍵改變方塊的形狀,↓鍵加速其下落,當方塊落下後滿一行時消除該行。

來看一下我們最終實現的效果圖:

canvas+js從0開始擼一個俄羅斯方塊

算法思想

我們可以把俄羅斯方塊一分為二的看,地圖+方塊。我們把地圖看成一個二維的數組:

[
[0,0,0,0,0,0...],
[0,0,0,0,0,0...],
[0,0,0,0,0,0...],
[0,0,0,0,0,0...],
...
]

方塊也看作一個二維數組

如Z形狀的方塊可看作為:

[
[1,1,0],
[0,1,1]
]

倒T形狀的方塊可看作為:

[
[0,1,0],
[1,1,1]
]

等等
然後兩個數組結合起來:

[
[0,0,1,1,0,0,0,0],
[0,0,0,1,1,0,0,0],
[0,0,0,0,0,0,0,0],
[0,0,0,0,0,0,0,0],
[0,0,0,0,0,0,0,0],
[0,0,0,0,0,0,0,0],
]

上面是一個Z字形方塊在第一行。

從上面數組可以一目瞭然的看到,我們渲染canvas的時候將數組值為0的元素渲染成地圖的顏色,而元素值為1的渲染成方塊的顏色即可。
那麼方塊的移動又是怎麼做呢,很簡單就是去改變方塊左右的元素從0改為1,再將本來的位置重置為0即可,在視覺上造成方塊的移動。

如下圖一個Z字方塊的移動:

canvas+js從0開始擼一個俄羅斯方塊

知道其核心思想之後,可以碼代碼開發了。

創建遊戲地圖

首先我們先準備一個canvas畫布:

index.html

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>canvas</title>
<style>
#myCanvas {
position: absolute;
left: 0;
top: 0;
right: 0;
bottom: 0;
margin: auto;
background: #000;
}
</style>
</head>
<body>
<canvas id="myCanvas" height="500" width="500"></canvas>
<button id="stop">停止</button>
<button id="score">得分: 0</button>
<script src="/js/tetris/index.js"></script>
</body>
</html>

首先我們需要了解一下canvas的簡單操作,在畫布上畫一個小方塊:

index.js

let Tetris = {
init(){
//初始化
const canvas = document.getElementById('myCanvas');
this.gc = canvas.getContext('2d');
this.render();
},
//渲染畫布
render(){
//在座標為100,100的位置畫一個40*40的紅色方塊
this.gc.fillStyle = 'red';
this.gc.fillRect(100,100,40,40);
}
};
Tetris.init();

如圖:

canvas+js從0開始擼一個俄羅斯方塊

做俄羅斯方塊所需的canvas知識點你只需要知道fillStyle,fillRect即可,沒有那麼複雜。

接下來,我們要畫出N*N的格子地圖了。

先生成一個N*N的二維數組,值都為0:

function map(r,c){
let data = [];
for(let i = 0; i < c; i++){
data.push([]);
//每行長度為 r
data[i].length = r;
//所有元素默認值 0
data[i].fill(0);
}
return data;
}
console.table(map(20,10));

canvas+js從0開始擼一個俄羅斯方塊

根據上面的方塊的渲染方式和地圖數組的生成結合,來渲染出一個20*20的地圖:

完整代碼如下:

index.js

let Tetris = {
init(){
//初始化
const canvas = document.getElementById('myCanvas');
this.gc = canvas.getContext('2d');
//20*20的格子
let data = this.map(20,20);
this.render(data);
},
map(r,c){
let data = [];
for(let i = 0; i < c; i++){
data.push([]);
//每行長度為 r
data[i].length = r;
//所有元素默認值 0
data[i].fill(0);
}
return data;
},
render(data){
//計算每個方塊的寬高 方塊之間間隔為4
let w = 500/20 - 4;
let h = 500/20 - 4;
//計算方塊的行列
let r = data.length;
let c = data[0].length;
for(let i = 0; i < r; i ++){
for (let j = 0; j < c; j++){
//判斷數組裡的值 若為1 則渲染為紅色 0 則渲染為白色
this.gc.fillStyle = data[i][j] === 0 ? 'white' : 'red';
this.gc.fillRect(
(w+4)*j+2,
(h+4)*i+2,
w,
h
);
}
}
}
};
Tetris.init();

分析一下上面稍微難以理解的是每個方塊x,y軸值的計算。

詳細分析如下:

畫布寬高500 * 500 要平均分配20*20個方塊,
那麼方塊的最大寬度為 500/20 = 25 
但是這樣的話方塊會擠在一塊 因此我們再給每個方塊之間增加4個單位的間距
於是 單個方塊的 w/h = 500/20 - 4 = 21;
座標的算法:
20個方塊之間有19個間隙 剩下一個4單位的距離 分配到小方塊距離左右兩側的間隙
於是可以列出:
n列   x座標
0     w*0 + 2
1     w*1 + 2 + 4*1
2     w*2 + 2 + 4*2
3     w*3 + 2 + 4*3
...
n     w*n + 2 + 4*n 
所以第j列的x座標可以歸納為 (w+4)*j + 2
y座標亦然

執行完代碼看效果如下:

canvas+js從0開始擼一個俄羅斯方塊

長征已經成功走出第一步,接下來就是方塊的創建了。

方塊的創建

方塊有多種類型,如一字型,L字型,Z字型,倒T字型,田字型等等,根據上面算法中提到的,每種類型可以用一個二維數組描述出來:

blockType: [
[[1,1,1,1]],
[[1,1],[1,1]],
[[1,1,0],[0,1,1]],
[[0,1,1],[1,1,0]],
[[0,1,0],[1,1,1]],
[[1,0,0],[1,1,1]],
[[0,0,1],[1,1,1]]
]

那怎麼把一個方塊再地圖上顯示出來呢?

也很簡單,只需要把方塊的二維數組插入到地圖的數組中就可以了。

簡單實現如下:

//更新data數組
draw(block){
/*
* 假如block為Z字型的方塊 [[1,1,0],[0,1,1]]
*/
for (let i = 0; i < block.length; i++){
for (let j = 0; j < block[0].length; j++){
this.data[i][j + this.y] = block[i][j];
}
}
console.table(this.data);
//再次調用render方法更新畫布
this.render(this.data);
}

生成不同形狀方塊

要隨機生成一個不同形狀的方塊,調用Math.random()即可。

貼完整的代碼:

index.js

let Tetris = {
//初始化
init(){
const canvas = document.getElementById('myCanvas');
this.gc = canvas.getContext('2d');
//20*20的格子
this.data = this.map(20,20);
//X軸的偏移量 之所以保存為變量 是以後我們做左右移動的是需要通過改變這個值來實現 
this.x = 7;
//隨機生成一個方塊
this._block = this.block();
this.draw(this._block);
},
//地圖數據
map(r,c){
let data = [];
for(let i = 0; i < c; i++){
data.push([]);
//每行長度為 r
data[i].length = r;
//所有元素默認值 0
data[i].fill(0);
}
return data;
},
//隨機生成一個類型的方塊
block(){
let index = Math.floor(Math.random()*7);
return this.blockType[index];
},
//方塊的類型
blockType: [
[[1,1,1,1]],
[[1,1],[1,1]],
[[1,1,0],[0,1,1]],
[[0,1,1],[1,1,0]],
[[0,1,0],[1,1,1]],
[[1,0,0],[1,1,1]],
[[0,0,1],[1,1,1]]
],
//重繪畫布
draw(block){
for (let i = 0; i < block.length; i++){
for (let j = 0; j < block[0].length; j++){
//要向x軸偏移 需要為j加一個偏移量即可
this.data[i][j + this.x] = block[i][j];
}
}
console.table(this.data);
this.render(this.data);
},
//渲染
render(data){
//計算每個方塊的寬高 方塊之間間隔為4
let w = 500/20 - 4;
let h = 500/20 - 4;
//計算方塊的行列
let r = data.length;
let c = data[0].length;
for(let i = 0; i < r; i ++){
for (let j = 0; j < c; j++){
//判斷數組裡的值 若為1 則渲染為紅色 0 則渲染為白色
this.gc.fillStyle = data[i][j] === 0 ? 'white' : 'red';
/*
* 座標算法
* 畫布寬度500 小方格寬度21 個數20 則 留下的空隙寬度為 500 - 21*20 = 80 其中 20個小方塊可分4單位的間隙
* 20個方塊之間有19個間隙 剩下一個4單位的距離 分配到小方塊距離左右兩側的間隙
* 總結一下規律
* n行     x座標
* 0       w*0 + 2
* 1       w*1 + 2 + 4
* 2       w*2 + 2 + 4*2
* 3       w*3 + 2 + 4*3
* ...
* n       w*n + 2 + 4*n
* 所以第j列的x座標可以歸納為 (w+4)*j + 2
* y座標亦然
*/
this.gc.fillRect(
(w+4)*j+2,
(h+4)*i+2,
w,
h
);
}
}
}
};
Tetris.init();

刷新頁面會出現隨機的方塊如下圖:

canvas+js從0開始擼一個俄羅斯方塊

方塊下落

方塊下落,其實就是要改變方塊在畫布上的Y軸位置,怎麼持續下落呢,很顯然需要開一個定時器。

[未完待續。。。]

方塊碰撞

[未完待續。。。]

方塊左右移動

[未完待續。。。]

方塊變形

[未完待續。。。]

方塊向下加速

[未完待續。。。]

消除整行

[未完待續。。。]

遊戲結束

[未完待續。。。]

後言

最近時間比較緊,剩下模塊慢慢更新。大體思想了解之後,其他問題都可以迎刃而解,無非就是需要耐心和不斷的調試。

喜歡就贊一波,給點更新的動力😆

參考文獻

俄羅斯方塊遊戲實戰

人人貸大前端技術博客中心

最後廣而告之。
歡迎訪問人人貸大前端技術博客中心

裡面有關nodejs react reactNative 小程序 前端工程化等相關的技術文章陸續更新中,歡迎訪問和吐槽~

上一篇: 微信小程序打怪之定時發送模板消息(node版)

相關文章

當Kotlin愛上React,會發生什麼反應

Gin(九):生成restful接口

JSI小試牛刀——Native同步調用JS代碼

iOSDeepLink調研與實踐