1. 程式人生 > >HTML5邊玩邊學(8):俄羅斯方塊就是這麼簡單 之 資料模型篇

HTML5邊玩邊學(8):俄羅斯方塊就是這麼簡單 之 資料模型篇

特別提示:
本文中的執行效果需要Chrome瀏覽器或者Firefox瀏覽器。

一、從資料出發還是從介面出發

要寫一個俄羅斯方塊小遊戲,我們先來一塊考慮一下下面幾個問題:

1、用什麼表示方塊

2、怎麼設定或者改變方塊的顏色

3、怎麼移動方塊

4、怎麼消除方塊

請考慮一分鐘後再繼續向下看。。。。。。

如果你對上面幾個問題思考,每一個答案都和介面、控制元件、平臺有關的話,就是說假如你是用 .Net 的,你的每一個答案都是圍繞著如何利用控制元件、如何使用窗體、在控制元件的哪個事件裡面改變哪個屬性等等,那麼說明你被微軟的 RAD 開發環境毒害的不淺,我建議你立刻扔掉 Visual Studio,改用其他輕量級的程式語言和開發平臺,這樣你可以更多的關注問題的本身,而不是控制元件。

記住:程式 = 資料結構 + 演算法

介面只是資料的表象,而資料才是問題的本質。

下面,我們將一步一步建立一個俄羅斯方塊小遊戲的資料模型,當整個模型建立完畢後,我們會發現,雖然沒有介面,仍然不妨礙這是一個功能完整的俄羅斯方塊遊戲,因為發生的每一件事情都很清楚,我們只是沒把它畫而已。當然,後面我們會給出一個操作簡易的介面,等到下一篇,會專門探討介面的問題。

二、“形狀”的資料模型

俄羅斯方塊是一個經久不衰的小遊戲,最常見的版本中一般有七個形狀,分別是:

直線型、S型、Z型、L型、反L型、T型、方形等,如下圖:

那麼我們在程式中如何表示這七個形狀呢?我們發現每一形狀都是四個小方塊組成的,我們完全可以用四個點

表示。

但是問題又來了,四個點的座標分別是什麼呢?我查到的方法是:每個形狀都有一個自己的座標系,比如S型,可以入下圖表示:

這樣,S型的資料模型可以表示為四個點組成的陣列:[ [ 0, -1 ],  [ 0, 0 ],   [ -1, 0 ],  [ -1, 1 ] ] 。

我們可以用同樣的方法建立其他形狀的陣列模型,然後再將這七個形狀的陣列模型合起來組成一個大的陣列。

另外,每個形狀可以是單色,也可以有自己的顏色。增加顏色會增加程式設計的複雜度,但是也增加不了多少,所以我們的模型中也會考慮顏色。

最後,我們最好給每個形狀一個編號,這樣方便在形狀陣列和顏色陣列中應用他們。

完成上面的分析後,我們就可以給出形狀資料模型的程式碼了:

形狀模型的程式碼 //各種形狀的編號,0代表沒有形狀NoShape=0;
ZShape
=1;
SShape
=2;
LineShape
=3;
TShape
=4;
SquareShape
=5;
LShape
=6;
MirroredLShape
=7//各種形狀的顏色Colors=["black","fuchsia","#cff","red","orange","aqua","green","yellow"];

//各種形狀的資料描述Shapes=[
    [ [ 
00 ],   [ 00 ],   [ 00 ],   [ 00 ] ],
    [ [ 
0-1 ],  [ 00 ],   [ -10 ],  [ -11 ] ],
    [ [ 
0-1 ],  [ 00 ],   [ 10 ],   [ 11 ] ],
    [ [ 
0-1 ],  [ 00 ],   [ 01 ],   [ 02 ] ],
    [ [ 
-10 ],  [ 00 ],   [ 10 ],   [ 01 ] ],
    [ [ 
00 ],   [ 10 ],   [ 01 ],   [ 11 ] ],
    [ [ 
-1-1 ], [ 0-1 ],  [ 00 ],   [ 01 ] ],
    [ [ 
1-1 ],  [ 0-1 ],  [ 00 ],   [ 01 ] ]
];

三、定位和旋轉形狀

1、定位

我們上面說到每個形狀都是在自己的座標系裡面描述的,另外還有一個全域性座標系,用來給形狀定位,這樣我們就需要一個方法將形狀的四個點從自身座標系轉換到全域性座標系,從而給形狀定位。

假如S型在自身座標系中四個點的座標為:[ [ 0, -1 ],  [ 0, 0 ],   [ -1, 0 ],  [ -1, 1 ] ]

它當前在全域性座標系位置為:[12,8]

則,四個點轉換為全域性座標系的座標為:[ [ 0+12, -1+8 ],  [ 0+12, 0+8 ],   [ -1+12, 0+8 ],  [ -1+12, 1+8 ] ]

這樣,我們就完成了 S型 的全域性座標轉換。

這裡需要注意一個問題,形狀自身座標系是用 (x,y) 描述的,而全域性座標系為了邏輯上更直觀,是用 (row,col) 描述的,所以我們在實際程式設計中並不是向上面那樣轉換的,而是:

[ [ -1+12, 0+8 ],  [ 0+12, 0+8 ],   [ 0+12, -1+8 ],  [ 1+12, -1+8 ] ]

即:先將 x 變為 col ,y 變為 row ,再轉換為全域性座標系。

2、旋轉

旋轉是在形狀的自身座標系中,並圍繞形狀的原點完成的,公式很簡單,每個點旋轉後的座標與旋轉前座標的關係如下(向右旋轉):

x' = y

y' = -x 

注意:方塊形狀不發生旋轉。

有了上面的分析,我們就可以給出兩個全域性方法,他們用來對形狀進行全域性定位和旋轉:

全域性定位和旋轉的程式碼 //將形狀自身的座標系轉換為  Map 的座標系,row col 為當前形狀原點在 Map 中的位置function translate(data,row,col){
    var copy
=[];
    
for(var i=0;i<4;i++){
        var temp
={};
        temp.row
=data[i][1]+row;
        temp.col
=data[i][0]+col;
        copy.push(temp);
    }
    
return copy;
}

//向右旋轉一個形狀:x'=y, y'=-xfunction rotate(data){
    var copy
=[[],[],[],[]];
    
for(var i=0;i<4;i++){
        copy[i][
0]=data[i][1];
        copy[i][
1]=-data[i][0];
    }
    
return copy;
}

四、移動空間

前面我們說過,形狀是由四個點組成的,而形狀的移動空間也是由 m * n 個點組成的一個二維陣列。

這裡為了更直觀的描述,我將 n 個點組成一條線 Line,再將 m 條 Line 組成形狀的移動空間,我把它叫做 Map 。

我們有了這 m * n 個點有什麼用呢?用處很簡單,就是儲存形狀的編號,如果一個點沒有被形狀佔用,則編號為 NoShape。這就是前面給出形狀編號的用處,同時也是為什麼要有一個 NoShape 編號的原因。

Map 應該具有什麼功能呢?下面我列舉了一些:

1、建構函式:這不用說了,n 個點組成一行 Line, m 行 Line 組成Map,每個點初始化成 NoShape

2、newLine:生成新的一行。為什麼需要這個方法呢,因為除了建構函式中,遊戲執行過程中我們也需要用到它,當一行或者幾行被消除以後,我們需要在頂部假如一行或者幾行新的Line

3、isFullLine(row):這個方法用來判斷第 row 行是否滿了,每次一個形狀落地後,就需要對每一行進行這個判斷,滿了當然是消除了。

4、isCollide(data): data 是一個定位後的形狀資料,這樣我們就可以檢查這些資料是否超出移動空間的上下左右邊界,另外還檢查資料的四個點是否已經被佔用,這就是碰撞檢測。

5、appendShape(shape_id,data):當一個形狀落地以後,我們就應該將執行空間中某些點的值改變為這個形狀的編號,我把這稱為佔用。

6、消除操作:這個功能沒有單獨列為一個方法,我把它放在 appendShape 方法中了。消除操作也很簡單,發現某一行 isFullLine 了以後,在 lines 陣列中移除這一行,並在 lines 陣列的頂部加入一個空行即可。

有了上面的分析,我們就可以給出移動空間的程式碼了:

移動空間的程式碼 /*
 * 說明:由 m 行 Line 組成的格子陣
 
*/
function  Map(w,h){
    
//遊戲區域的長度和寬度this.width=w;
    
this.height=h;
    
//生成 height 個 line 物件,每個 line 寬度為 widththis.lines=[];
    
for(var row=0;row<h;row++)
        
this.lines[row]=this.newLine();
}

//說明:間由 n 個格子組成的一行Map.prototype.newLine=function(){
    var shapes
=[];
    
for(var col=0;col<this.width;col++)
        shapes[col]
=NoShape;
    
return shapes;
}

//判斷一行是否全部被佔用
//如果有一個格子為 NoShape 則返回 falseMap.prototype.isFullLine=function(row){
    var line
=this.lines[row];
    
for(var col=0;col<this.width;col++)
        
if(line[col]==NoShape)
            
returnfalsereturntrue;
}
/*
 * 預先移動或者旋轉形狀,然後分析形狀中的四個點是否有碰撞情況:
 *      1:col<0 || col>this.width 超出左右邊界
 *      2:row==this.height ,說明形狀已經到最底部
 *      3:任意一點的 shape_id 不為 NoShape ,則發生碰撞
 *  如果發生碰撞則放棄移動或者旋轉
 
*/
Map.prototype.isCollide
=function(data){
    
for(var i=0;i<4;i++){
        var row
=data[i].row;
        var col
=data[i].col;
        
if(col<0|| col==this.width) returntrue;
        
if(row==this.height) returntrue;
        
if(row<0continue;
        
elseif(this.lines[row][col]!=NoShape)
                
returntrue;
    }
    
returnfalse;
}

//形狀在向下移動過程中發生碰撞,則將形狀加入到 Map 中Map.prototype.appendShape=function(shape_id,data){
    
//對於形狀的四個點:for(var i=0;i<4;i++){
        var row
=data[i].row;
        var col
=data[i].col;
        
//找到所在的格子,將格子的顏色改為形狀的顏色this.lines[row][col]=shape_id;
    }
    
//========================================
    
//形狀被加入到 Map 中後,要進行逐行檢測,發現滿行則消除for(var row=0;row<this.height;row++){
        
if(this.isFullLine(row)){
            
//將滿的那一行替換成新的空,這一步主要是為了顯示效果,可以不要!
            
//this.lines[row]=null;
            
//重繪 Map 消除效果
            
//onClearLine(row);
            
//將滿行刪除this.lines.splice(row,1);
            
//第一行新增新的一行this.lines.unshift(this.newLine());
            
//重繪 Map 整行下落效果            onDraw(this.lines);
        }
    }
}

五、遊戲模型

我們有了遊戲的資料模型,我們就可以讀寫他們了。所謂讀好理解,所謂寫就是改變他們,改變的方法當然是使用者的操作了。

下面給出 GameModel 類,他維護三個主要的資料:

1、一個形狀的編號,就是使用者可以操作移動的那個形狀

2、形狀的全域性位置,用 row col 表示

3、一個 Map,用它完成碰撞檢測,新增等操作

另外,還抽象出幾個使用者的操作動作:

1、left:左移。將形狀的全域性座標 col  減少 1 。請思考一下,這樣就可以了嗎?當然不行,我們還需要進行碰撞檢測,如果已經在最左邊,則放棄處理。

2、right:右移。同上。

3、rotate:旋轉。同上。

4、down:下落。同上。下落過程中的碰撞檢測有所不同,一旦發生碰撞,我們不能再放棄處理了,而是要將當前形狀加入到空間中。

5、GameOver:下落過程中還需要進行一個檢測就是遊戲是否結束。如果當前形狀在出生地點剛一下落就發生碰撞,說明已經到頂部了,則遊戲結束。

有了上面的分析,我們就可以給出 GameModel 的程式碼:

GameModel 程式碼 /*
 * 說明:GameModel 類
 
*/
function GameModel(w,h){
    
this.map=new Map(w,h);
    
this.born();
}

//出生一個新的形狀GameModel.prototype.born=function(){
    
//隨機選擇一個形狀this.shape_id=Math.floor(Math.random()*7)+1;
    
this.data=Shapes[this.shape_id];
    
//重置形狀的位置為出生地點this.row=1;
    
this.col=Math.floor(this.map.width/2);
    
//通知繪製移動效果,傳回資料為形狀的四個點在 Map 中的位置    onMove(this.shape_id,this.map,translate(this.data,this.row,this.col));
}

//向左移動GameModel.prototype.left=function(){
    
this.col--;
    var temp
=translate(this.data,this.row,this.col);
    
if(this.map.isCollide(temp))
    
//發生碰撞則放棄移動this.col++;
    
else//通知繪製移動效果,傳回資料為形狀的四個點在 Map 中的位置        onMove(this.shape_id,this.map,temp);
}

//向右移動GameModel.prototype.right=function(){
    
this.col++;
    var temp
=translate(this.data,this.row,this.col);
    
if(this.map.isCollide(temp))
        
this.col--;
    
else
        onMove(
this.shape_id,this.map,temp);
}

//旋轉GameModel.prototype.rotate=function(){
    
//正方形不旋轉if(this.shape_id==SquareShape) return;
    
//獲得旋轉後的資料    var copy=rotate(this.data);
    
//轉換座標系    var temp=translate(copy,this.row,this.col);
    
//發生碰撞則放棄旋轉if(this.map.isCollide(temp))
        
return;
    
//將旋轉後的資料設為當前資料this.data=copy;
    
//通知繪製移動效果,傳回資料為形狀的四個點在 Map 中的位置    onMove(this.shape_id,this.map,translate(this.data,this.row,this.col));
}

//下落GameModel.prototype.down=function(){
    var old
=translate(this.data,this.row,this.col);
    
this.row++;
    var temp
=translate(this.data,this.row,this.col);
    
if(this.map.isCollide(temp)){
        
//發生碰撞則放棄下落this.row--;
        
//如果在 1 也無法下落,說明遊戲結束if(this.row==1) {
            
//通知遊戲結束
            
//onGameOver();            alert("Game Over")
            
return;
        }
        
//無法下落則將當前形狀加入到 Map 中this.map.appendShape(this.shape_id,old);
        
//出生一個新的形狀this.born();
    }
    
else//通知繪製移動效果,傳回資料為形狀的四個點在 Map 中的位置        onMove(this.shape_id,this.map,temp);
}

六、一個簡單的操作介面

雖然到現在為止,我們沒有給出一行和介面有關的程式碼,但是整個遊戲在邏輯上已經完全可以執行起來了,只是我們沒有把他畫出來而已,要想把他畫出來也很簡單。

注意上面給出的程式碼中很多地方呼叫了兩個全域性函式:onDraw 和 onMove ,這兩個函式就是用來進行繪製的。

繪製的程式碼其實只佔很少的一部分,其中一些繪圖函式我為了方便對 HTML5 的 2D 函式進行了簡單的封裝,您完全可以用原生的 HTML5 函式,或者用您自己平臺的繪圖函式,因為他們本身不是太複雜。

另外有一個全域性變數 Spacing ,他表示一個格子的寬度。

下面給出操作介面的程式碼:

介面操作程式碼 //每一格的間距,也即一個小方塊的尺寸Spacing=20;

//在記憶體中繪製一個小方塊function drawRect(color){
    var temp
=new Surface(Spacing,Spacing,"rgba(255,255,255,0.2)");//背景色    temp.fillRect(11, Spacing-2, Spacing-2, color);//前景色return temp;
}

var display
= Display.attach(document.getElementById("html5_09_1"));
var model 
=new GameModel(display.width/Spacing,display.height/Spacing);


function onDraw(map){
    
//清屏    display.clear();
    var lines
=map.lines;
    
//依次繪製每一個非空的格子for(var row=0;row<map.height;row++)
        
for(var col=0;col<map.width;col++){
            var shape_id
=lines[row][col];
            
if(shape_id!=NoShape){
                var rect 
= drawRect(Colors[shape_id]);
                var y
=row * Spacing;
                var x
=col * Spacing;
                display.draw(rect, x, y);
            }
    }
}

function onMove(shape_id,map,data){
    onDraw(map);
    
//繪製當前的形狀for(var i=0;i<4;i++){
        var y
=data[i].row * Spacing;
        var x
=data[i].col * Spacing;
        var rect 
= drawRect(Colors[shape_id]);
        display.draw(rect, x, y);
    }
}

function down(){
    model.down();
}

function left(){
    model.left();
}

function right(){
    model.right();
}

function rotate_click(){
    model.rotate();
}

HTML 程式碼很簡單,也給出來吧,就一塊畫布和四個按鈕,如下:

HTML 程式碼 <canvas id="html5_09_1" width="260" height="400" style=" background-color: black ">
    你的瀏覽器不支援 Canvas 標籤,請使用 Chrome 瀏覽器 或者 FireFox 瀏覽器
</canvas><p/><input type="button" value="向下" onclick="down()"/><input type="button" value="向左" onclick="left()"/><input type="button" value="向右" onclick="right()"/><input type="button" value="旋轉" onclick="rotate_click()"/>

七、執行效果

{{{{{{

}}}}}}