在正文開始之前還要囉嗦一下,標題中所謂自給自足,是在沒有參考任何設計思路的前提下去開發這遊戲的,你可能會不解,如果參考優秀的思路,豈不是事半功倍,當然,參考與不參考都有利,我只說不參考的利,當我煞費苦心、歷經數十個BUG修改,終於完成一件作品的時候,我可以很自豪地對別人說:「看,我開發的遊戲! ,回過頭來再看一看別人的思路,有時會拍案而起,“這個我當初怎麼就沒想到呢?”,“原來這個問題可以這樣解決”,“這個設計思路比我的思路好多了! ”,諸如此類總比開始就直接看別人的思路而阻塞自己的思考要強得多,對吧?
好叻,正文開始~
想先看效果的,先跳轉試試看吧!
俄羅斯方塊,主遊戲介面應該由一個一個的方塊組成,如下圖,當然成品裡面這些網格是看不到的,這裡只是助於理解,主介面尺寸為400× 500,設定每塊磚(網格)的尺寸為20×20,則每行有20個磚塊,每列有25個磚塊。 相關程式碼:
brickWidth = 20, //砖块大小 width = 400, height = 500; //画布宽高,20X25
提到主介面的網格,就要提到一個非常重要的變數了,它就是BOARD,一個二維數組,形象化地說其尺寸是20×26,儲存的值為0或1,0表示該位置沒有磚塊,1表示該位置有磚塊,在接下來的一些判定中有重要作用,遊戲細心的同學可能發現,為什麼是20×26,而不是對應主界面網格的20×25,我在一開始的時候也是設定為20×25的,後來注意到如果加一行而且這一行的值都為1就可以很容易判斷磚塊是否到觸及主界面底部了。 相關程式碼:
// 初始化BOARD,注意纵向有26个,最后一排用来判断是否触底 for(i=0;i<20;i++){ BOARD[i] = []; for(j=0;j<26;j++) { if(j==25) { BOARD[i][j] = 1 } else { BOARD[i][j] = 0; } } }
#接下來看由4個磚塊組成的“形狀”,有五種,為了好描述,我把它們為別命名,Tian(田),Chu(鋤頭),Tu(凸起來),Thunder(閃電),Line(一橫),哈哈有趣的名字,原諒我沒找到它們的英文名字吧。
先定義一個磚頭類別Brick:
function Brick() { }
其下有幾個原型變數和方法:
Brick.prototype.embattle = null; //砖块的布局(需重载) Brick.prototype.isOverturn = 0; //是否翻转 Brick.prototype.originX = 9; //砖头的绘制起点X Brick.prototype.originY = -3; //砖头的绘制起点Y Brick.prototype.direction = 0; //砖头朝向 Brick.prototype.autoMoveTimer = null; //自动移动计时器 Brick.prototype.draw = function() { …… } //画砖块的方法 Brick.prototype.move = function(moveX, moveY) { …… } //移动的方法 Brick.prototype.autoMove = function() { …… } //自动移动的方法 Brick.prototype.change = function() { …… } //变换砖头朝向
Brick的子類別有:Tian,Chu,Tu,Thunder ,Line五個,每個子類中都重載Brick的embattle變量,embattle是什麼,英譯中的意思是布陣,這個陣是個什麼陣呢?首先,同學們要理解我的思路,用Tu的embattle來舉例,其代碼如下:
this.embattle = [ [ [0,4,5,8], [1,4,5,6], [1,4,5,9], [0,1,2,5] ], //布局表为4X4表格,数字为砖头位置 [ [0,4,5,8], [1,4,5,6], [1,4,5,9], [0,1,2,5] ] //次行为翻转的情况];
embattle是一個三維數組,第一維是是否翻轉isOverturn(形象來說就像圖片的水平翻轉),第二維是方向direction(上左下右),第三維是形狀的4個磚塊分佈情況,我把每個新形狀物件定義在一個4 ×4的陣中,例如,Tu的this.embattle[0][0]為[0,4,5,8],數字即該磚塊的所在位置,如下圖:
所以要確定一個形狀的位置和樣子,需要isOverturn確定是否翻轉,需要direction確定其方向,需要originX和originY確定「陣」的位置。
接下來,分別解釋Brick的4個原型方法。
Brick.prototype.draw
#ctx.fillStyle = 'rgb('+Math.floor(Math.random()*256)+','+Math.floor(Math.random()*256)+', '+Math.floor(Math.random()*256)+')'; for(i=0;i<4;i++) { tmp = this.embattle[this.isOverturn][this.direction][i]; ctx.fillRect((this.originX+tmp%4)*brickWidth, (this.originY+Math.floor(tmp/4))*brickWidth, brickWidth, brickWidth); ctx.strokeRect((this.originX+tmp%4)*brickWidth+1, (this.originY+Math.floor(tmp/4))*brickWidth+1, brickWidth-2, brickWidth-2); //注意+1和减2 }
有上面说的确定形状的位置和样子的方法,之后就是纯粹canvas画图,4个砖块一个一个地画,不要看代码很长其实就是那么一点点,originX、originY和砖块在阵中的位置就可以确定画砖块的起点了。注意到代码的注释了没有,画边框的时候,它是从起点向外面画的,就像我把一个塑料袋套在另一个塑料袋的外面,为了以后的清除的方便且不影响其他的砖块,把边框画进fillRect的领土,就像我现在把这个塑料袋不套在外面而是放进这另一个塑料袋里面一样,就这个意思。
Brick.prototype.move
这是最长的一个了,移动的时候,moveX和moveY表示横纵的增量,没有同时非0的情况(这是人为的设定,要么横向移动要么纵向移动),当然要判断即将移动到的位置是否违规:
横向:
如果阵贴靠主界面左侧则不能向左移即moveX不能为-1
(this.originX==0 && moveX==-1)
判断右边时比较麻烦,因为不能直接用阵来判断是否贴靠右侧(看前面的图就知道阵的右边和下边可能没有砖块的),这时要一个个地判断4个砖块是否有至少有一个在最右,这时不能向右移动
|| (this.originX+tmp[0]%4==19 && moveX==1) || (this.originX+tmp[1]%4==19 && moveX==1) || (this.originX+tmp[2]%4==19 && moveX==1) || (this.originX+tmp[3]%4==19 && moveX==1)
最后还要判断即将到达的位置是否已经有砖块了。
|| (BOARD[this.originX+tmp[0]%4+moveX][this.originY+Math.floor(tmp[0]/4)]==1) || (BOARD[this.originX+tmp[1]%4+moveX][this.originY+Math.floor(tmp[1]/4)]==1) || (BOARD[this.originX+tmp[2]%4+moveX][this.originY+Math.floor(tmp[2]/4)]==1) || (BOARD[this.originX+tmp[3]%4+moveX][this.originY+Math.floor(tmp[3]/4)]==1)
纵向:
即将到达的位置是否已经有砖块了,注意到下面的代码的&& moveX==0,原来是没有的,后来发现每次砖块怎么刚刚靠上下面堆着的砖块就不能再移动了,原来横向移动的时候也进行了这个判断,即刚刚靠上下面的砖块,如果这时想左右移动,但下方有砖块,但是问题来了,下面有没有砖块跟我左右移动有什么关系呢?是吧。
if((as==1 || bs==1 || cs==1 || ds==1) && moveX==0) { …… }
纵向终止判断里面主要做了几件事:清除autoMoveTimer,设置BOARD在该形状当前位置的值为1,有可以消除的整行就消除,加分改分,判断胜利/失败,删除当前对象,召唤下一个形状。
横纵都没违规时:
这时,把该形状前一个位置的砖块清除,更新originX和originY,再画出来。
for(i=0;i<4;i++) { tmp = this.embattle[this.isOverturn][this.direction][i]; ctx.clearRect((this.originX+tmp%4)*brickWidth, (this.originY+Math.floor(tmp/4))*brickWidth, brickWidth, brickWidth); } this.originX += moveX; this.originY += moveY; this.draw();
Brick.prototype.autoMove
只做一件事,设置计时器,定时向下移动。
var status, self = this;this.autoMoveTimer = setInterval(function() { status = self.move(0,1); },speed);
Brick.prototype.change
改变形状的朝向,很好办啊,不是有embattle数组了吗?当然没有那么简单,不只是换个数组这么简单。要考虑改变方向之后占用的位置是否已经有砖块了,如果形状是贴着主界面右边界就更糟糕了,比如原来是竖着的Line,改变其方向变为横,占用阵的0、1、2、3,如果Line贴着右边界,originX为19,变为横向,占用阵的0、1、2、3,后面三个砖块已经溢出了主界面。
解决方案是:如果有越界的砖块就把阵往左挪一挪,直到不再越界。
while(ox+tmp[0]%4 > 19 || ox+tmp[1]%4 > 19 || ox+tmp[2]%4 > 19 || ox+tmp[3]%4 > 19) { ox -= 1; }
最后,如果都没事,就可以清除原位置,画出改变方向之后的形状了。
并不是太完美,因为有些卡位的情况没考虑进来,什么是卡位,看下图,你知道Line实例调用change方法的结果是什么了吗?事实上,它不应该成功改变方向的,对吧?还有其他一些卡位的情况。
Brick的4個原型方法就介紹到這裡了。現在如果我要在右邊的信息界面顯示下一個的形狀,最直接的方法就是,通過該形狀的構造函數實例化一個對象,為防止其自動調用autoMove,為構造函數添加了isModel來判斷是不是供提示用的。
還有按鍵事件監聽、NextBrick函數和deleteObj自己看看吧,很容易看懂,遊戲的入口就是NextBrick函數。
還有就是,我無法確定deleteObj是否真的成功讓GC把物件回收了。
還有就是,我本來想增加關卡功能,因為可以自由設定速度(speed變數),就把這功能放一放了。
以上是動手打造html5俄羅斯方塊的(圖文)的詳細內容。更多資訊請關注PHP中文網其他相關文章!