好久沒使用canvas了,于是通過寫小游戲“俄羅斯方塊”再次熟悉下canvas,如果有一定的canvas基礎(chǔ),要實現(xiàn)還是不難的。
原理詳解
看游戲最終界面,可知需要實現(xiàn)以下關(guān)鍵功能:
- 游戲面板,也就是12 * 20的方格,以及是否填充了方塊信息;
- 運動方塊,方塊需要實現(xiàn)移動,變形的功能。
![](/d/20211016/54d53efe8160e7a5d3ccf3bb400c5b1c.gif)
界面的實現(xiàn)
整個面板就是以左上角(0,0)為原點的坐標系,右上角(12,0)左下角(0,20)右下角(12,20),每個點的坐標位置都可以確定。是否已經(jīng)填充方塊,我們可以將每個方格看成一個數(shù)組元素,0表示沒有,1表示已經(jīng)填充。12 * 20 的面板使用兩層數(shù)組,即用20個長度為12的數(shù)組實現(xiàn)。
var maps = [[0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,1,0,1,0], ...];
畫出面板的代碼,用最基礎(chǔ)的canvas的api就能實現(xiàn)
//格子
for(var i=0;i<12;i++){
for(var j=0;j<20;j++){
ctx.fillRect(i*40,j*40,40,40);
ctx.strokeRect(i*40,j*40,40,40);
if(this.maps[j][i]==1){//方格已經(jīng)有填充內(nèi)容
ctx.save();
ctx.lineWidth=4;
ctx.fillStyle='hsla(200,100%,50%,.5)';
ctx.strokeStyle='hsla(200,100%,50%,.9)';
ctx.fillRect(i*40,j*40,40,40);
ctx.strokeRect(i*40+2,j*40+2,38,38);
ctx.restore();
}
}
}
//邊框
ctx.lineWidth=4;
ctx.strokeStyle='hsla(0,100%,0%,.3)';
ctx.moveTo(0,0);
ctx.lineTo(0,20*40);
ctx.lineTo(12*40,20*40);
ctx.lineTo(12*40,0);
ctx.stroke();
ctx.restore();
方塊的實現(xiàn)
游戲中用到以下 7 種圖形
![](/d/20211016/c3c6a5cc1a8babd6105a859f3bfcdf4a.gif)
結(jié)合上面介紹的坐標系,數(shù)組 [x1, y1, x2, y2, x3, y3, x4, y4] 就是上面圖形中4個點坐標的數(shù)據(jù)表現(xiàn)形式,7 種圖形的坐標分別如下:
var Arr = [[4,0,4,1,5,1,6,1],[4,1,5,1,6,1,6,0],[4,0,5,0,5,1,6,1],[4,1,5,0,5,1,6,0],
[5,0,4,1,5,1,6,1],[4,0,5,0,6,0,7,0],[5,0,6,0,5,1,6,1]];
方塊的移動,遍歷整個數(shù)組,加上位移向量就行,非常簡單
class Shape {
constructor(m){
this.m = Object.assign([],m);
}
move(x,y){ // 位移
var m = this.m,
l = m.length;
y = y||0;
for (var i=0;i<l;i=i+2){
m[i]+=x;
m[i+1]+=y;
}
return this;
}
方塊的旋轉(zhuǎn),俄羅斯方塊里面方塊除了左右和上下運動,還會旋轉(zhuǎn),不是嗎?稍微思考下就知道,這不過就是矩陣變換而已,也就是每次圖形繞中心點旋轉(zhuǎn)90度。我這里用數(shù)組第三個點作為圖形變換的中心點,當然這樣處理不夠完善。
class Shape {
transform(){//二維矩陣變換
var m =this.m,
l = m.length,
c = Math.ceil(l/2),
x = m[c],
y = m[c+1],
cos = Math.cos(Math.PI/180 * 90),
sin = Math.sin(Math.PI/180 * 90);
for (var i=0;i<l;i=i+2){
if(i == c) continue;
var mx = m[i]- x,
my = m[i+1] - y,
nx = mx*cos - my*sin,
ny = my*cos + mx*sin;
m[i]=x+nx;
m[i+1]=y+ny;
}
return this;
}
邊界條件
主要包括如下三個方面
- 方塊位置不能超出界面的判斷;
- 方塊到達底部或放置完成的判斷;
- 游戲結(jié)束的判斷。
遍歷數(shù)組 (1)任意一個點y坐標為19時表示到達了底部;(2)獲取該坐標的y+1位置在maps的信息,如果為1表示已經(jīng)填充。這兩種情況下,運動方塊的周期結(jié)束,將該方塊的坐標填充到maps對應(yīng)的數(shù)組里面即可。
如果坐標的y+1已經(jīng)有填充,同時當前坐標小于1,即已經(jīng)在界面的頂部了,那么表示游戲結(jié)束。
var isEnd = false,isOver=false,x,y;
for(var i=0,sl=that.shape.m.length;i<sl;i=i+2){
x=that.shape.m[i];
y=that.shape.m[i+1];
if(y >= 19){ // 到了底部
isEnd = true;break;
}
if(that.maps[y+1][x]==1){ // y+1位置已經(jīng)填充
isEnd = true;
if(y <= 1){isOver=true;} // 游戲結(jié)束
break;
}
}
方塊運動周期結(jié)束時檢測每一層是否滿格,以及滿格后的處理。某項數(shù)組全部元素都為1則表示已經(jīng)滿格,那么刪除該項數(shù)組,同時列表頭再壓入一項每個元素都為0的數(shù)組即可。
checkPoint(){
var that = this,
maps = that.maps;
for(var i=0,l=maps.length;i<l;i++){
if(Math.min.apply(null,maps[i]) == 1){// 表示該層已經(jīng)滿格
that.maps.splice(i,1);
that.score+=10; // 增加分數(shù)
that.maps.unshift([0,0,0,0,0,0,0,0,0,0,0,0]);
}
}
return this;
}
綁定事件
主要就是綁定keydown事件,要注意的是左移和右移事件包括了邊界判斷
bindEvent(){
var that = this;
document.addEventListener('keydown',function(e){
switch(e.keyCode){
case 13: //enter
cancelAnimationFrame(that.timer);
that.init().update();
break;
case 80: //p
that.pause = !that.pause;
break;
case 40: //down
that.d = 0.5;
break;
case 37: //left
var over = false,
maps = that.maps,
shape = that.shape,
m = shape.m;
for(var i=0,l=m.length;i<l;i=i+2){
if(m[i]<=0 || maps[m[i+1]][m[i]-1] == 1){
over = true;break;
}
}
if(!over) shape.move(-1,0);
break;
case 39: //right
var over = false,
shape = that.shape,
maps = that.maps,
m = shape.m;
for(var i=0,l=m.length;i<l;i=i+2){
if(m[i]>=11 || maps[m[i+1]][m[i]+1] == 1){
over = true;break;
}
}
if(!over) shape.move(1,0);
break;
case 32: //space
that.shape.transform();
break;
}
},false);
}
總結(jié)
這里面實現(xiàn)了俄羅斯方塊的最基本功能,還有關(guān)卡等功能點并沒有實現(xiàn),同時該demo仍然有不完善的地方需要修正。
以上就是本文的全部內(nèi)容,希望對大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。