前陣子發過一個帖子,上傳了自己寫的俄羅斯方塊。但是由於工作比較忙沒時間寫說明,現在補上。
俄羅斯方塊寫過好幾次了,每次的感覺都不一樣,都有新的收獲。就像達芬奇畫雞蛋一樣,雖然都是畫同樣的雞蛋,但是每次都有不同的收獲。
先來看看我們需要的是一個怎麼樣的程序。
首先要有2個大功能:1.開始游戲 2.退出游戲。其中要編程的主要工作都集中在“開始游戲”之後的過程中。俄羅斯方塊的游戲規則相信大家一定都不陌生,也許還有不少人都是骨灰級玩家了,但是在此還是有必要說明一下游戲的規則,因為從編程和玩家的兩個不同角度來觀察游戲規則,還是有微妙的區別的——側重的東西不同。
游戲在一個m*n的矩形框內進行。游戲開始時,矩形框的頂部會隨機出現一個由四個小方塊構成的形狀,每過一個很短的時間(我們稱這個時間為一個tick),它就會下落一格,直到它碰到矩形框的底部,然後再過一個tick它就會固定在矩形框的底部,成為固定塊。接著再過一個tick頂部又會出現下一個隨機形狀,同樣每隔一個tick都會下落,直到接觸到底部或者接觸到下面的固定塊時,再過一個tick它也會成為固定塊,再過一個tick之後會進行檢查,發現有充滿方塊的行則會消除它,同時頂部出現下一個隨機形狀。直到頂部出現的隨機形狀在剛出現時就與固定塊重疊,表示游戲結束。
說的有點羅嗦,但是細細分析一下每隔tick之間發生事情的順序對理清程序思路有好處。
接下來開始分析程序的設計了。
照理應該從大的設計一步一步往下細分的,但由於時間有限,我就反過來從小的往大的講了,請諒解。我會盡量把問題說明白。
顯然俄羅斯方塊是一個簡單的游戲,因為它都是基於矩陣的。既然是面向對象的方法來寫,那麼我們首先應該封裝一個矩陣類,權且叫做CMatrix吧。
class CMatrix //矩陣類,這是一個整形矩陣類型
{
public:
//默認構造函數。
CMatrix();
//根據構造函數參數創建指定大小的矩陣
CMatrix(int width, int height);
//根據構造函數參數創建指定大小的矩陣,並為矩陣的每個元素統一分配一個初始值。
CMatrix(int width, int height, int initValue);
virtual ~CMatrix();
//重新設置矩陣的尺寸。
void ResetSize(int width, int height);
//將左右元素設置為一個值。
void SetAll(int value);
//設置row行col列的元素值為value。
void SetAt(int row, int col, int value);
//獲取矩陣寬度。
int GetWidth() const;
//獲取矩陣高度。
int GetHeight() const;
//獲取row行col列元素的值。
int GetAt(int row, int col) const;
//旋轉矩陣,參數為是否順時針。
bool Rotate(bool clockWise = true);
//獲取第row行指針,調用者可以通過“(CMatrix對象)[行][列]”來獲取或設置元素。
int* Operator[](int row) const;
//重載等號運算符,其實也可以寫拷貝構造函數。
CMatrix& Operator=(CMatrix &srcMat);
protected:
//這些都是私有的成員數據,意義可以從名字上看出。
int *m_pData;
int m_width;
int m_height;
protected:
//這裡都些私有的函數,都是被上面的公共函數所使用的。
//釋放數據資源。
void ReleaseData();
//為數據分配資源。
void InitData(int width, int height);
//設置數據內容。
void SetData(int initValue);
//內存塊拷貝。
static void MemCopy(int *dest, int *src, int len);
};
接下來需要把下落的隨機形狀也封裝成一個類,命名為CShape。
class CShape //該類實際上是把一個CMatrix類的對象封裝了起來,並且組織了一些操作。
{
public:
CShape();
virtual ~CShape();
//創建一個隨機形狀,注意這裡隨機其實是指在俄羅斯方塊中的7種形狀中的某一種。參數是該形狀的左上角坐標,默認(0,0)。
void CreateRandomShape(int posX = 0, int posY = 0);
//設置該形狀的左上角坐標。
void SetPos(int posX, int posY);
//旋轉形狀,眾所周知,俄羅斯方塊游戲中的形狀是可以旋轉的。默認就是順時針的旋轉。
void Rotate();
//取消前次旋轉,這個函數有它的用處,代碼讀下去就會明白的。
void CancelRotate();
//使形狀向下移動一格。
void MoveDown();
//使形狀向上移動一格。
void MoveUp();
//左一格。
void MoveLeft();
//右一格。
void MoveRight();
//獲取當前形狀的類型ID,分別代表7種形狀。(0~6)
int GetShapeType() const;
//獲取當前形狀的左上角坐標。
POINT GetPos() const;
//獲取形狀占用的寬度。
int GetWidth() const;
//..............高度。
int GetHeight() const;
//重載[]操作符。
const int* Operator[](int row) const;
//重載等號,也可以寫一個拷貝構造函數。
CShape& Operator=(CShape& srcShape);
protected:
//被封裝的CMatrix對象。
CMatrix m_mat;
//形狀的類型ID。
int m_type;
//一個比較微妙的值,暫時不解釋,讀代碼會明白的。關系到形狀的旋轉,有些形狀可以旋轉出四種樣子,但有些是由兩種樣子。
bool m_needJump;
//左上角X坐標。
int m_posX;
//......Y....。
int m_posY;
protected:
//這裡都些私有的函數,都是被上面的公共函數所使用的。
//隨機創建一個形狀。
void RandCreate();
//隨機旋轉幾次。
void RandRotate();
//旋轉形狀,參數表示是否順時針。
void RotateShape(bool clockwise);
};
有了形狀類之後,我們還需要一個底盤類,用來表示游戲中除了當前下落的形狀之外的背景部分和已經固定的塊。我們稱這部分為底盤,類名為CBoard。
class CBoard //此類其實也是對一個CMatrix的對象進行了封裝。
{
public:
//構造函數時默認的底盤大小是10*20。
CBoard();
virtual ~CBoard();
//重新設置底盤的大小,並且底盤被清空。這個函數好像沒用到。但是整個編寫過程用的是面向對象的思想,所以在寫這個類的時候我把外面可能會調用的方法都寫成了公共接口。
void ResetSize(int width, int height);
//設置row行col列的值為value。value的取值為兩個宏,R_EMPTY和R_BLOCK,分別表示“空”和“有方塊”狀態。
void SetAt(int row, int col, int value);
//將形狀shape結合到底盤中,成為底盤的一部分。
void UniteShape(const CShape& shape);
//清除所有排滿方塊的行中的方塊,並使這些行上面的方塊們下落下來。
int ClearRows();
//獲取底盤寬度。
int GetWidth() const;
//........高度。
int GetHeight() const;
//獲取row行col列的格子的內容,兩種值:R_EMPTY和R_BLOCK。
int GetAt(int row, int col) const;
//獲取底盤數據到指針的destBuffer所指的空間。該空間至少要有GetWidth()*GetHeight()*sizeof(int)個字節大小。
void GetBoardData(int *destBuffer) const;
//獨立性檢查,檢查形狀shape是否獨立。shape如果和底盤上的固定塊有部分重疊則不獨立返回false,否則獨立返回true。
bool SingleTest(const CShape& shape) const;
//邊界檢查,檢查形狀shape是否在邊界內。在界內返回true,出界則返回false。
bool RangeTest(const CShape& shape) const;
//重載[]運算符。
const int* Operator[](int index) const;
protected:
//被封裝的矩陣對象。
CMatrix m_mat;
protected:
//這裡都些私有的函數,都是被上面的公共函數所使用的。
//檢查第index行是否全滿。
bool IsRowFull(int index) const;
//檢查第index行是否全空。
bool IsRowEmpty(int index) const;
//檢查第index行是否全X。當full為trye時X=滿,否則X=空。此函數為上面兩個函數服務。
bool IsRowInStatus(int index, bool full) const;
//清除第index行中的所有方塊。
void ClearRow(int index);
//使所有漂浮在半空的固定塊下落。
void FallDown();
//拷貝第srcRow行到第destRow行。
void CopyRow(int destRow, int srcRow);
//狀態檢測,服務於SingleTest函數和RangeTest函數。
bool ShapeTest(const CShape& shape, bool coverTest) const;
};
現在有了形狀類和底盤類,應該可以做游戲的邏輯部分了。我們把整個游戲邏輯封裝在一個叫做CRussia的類中。
class CRussia
{
public:
CRussia();
virtual ~CRussia();
//初始化游戲,默認的底盤大小是10*20。
void InitGame(int width = 10, int height = 20);
//將當前形狀向左移動一格,返回是否成功。
bool MoveShapeLeft();
//............右......................。
bool MoveShapeRight();
//時間流逝函數,表示經過一個tick的時間,一般返回true,返回false表示gameover了。
bool PassTick();
//旋轉當前形狀,返回是否成功。
bool RotateShape();
//獲取底盤數據,填充到指針buffer中,buffer的大小至少要在(底盤寬度*底盤高度*sizeof(int))以上。數據內容為R_EMPTY或R_BLOCK。
void GetBoardInfo(int *buffer) const;
//獲取底盤上第row行col列的數據,內容為R_EMPTY或R_BLOCK。
int GetBoardInfo(int row, int col) const;
//獲取當前下落的形狀的信息,返回值是一個ShaoeInfo結構類型,該數據結構的構成稍後說明。
ShapeInfo GetCurrentShape() const;
//獲取下一個形狀的信息,返回值也是ShapeInfo類型。
ShapeInfo GetNextShape() const;
//獲取當前得分。
int GetScore() const;
//獲取當前游戲狀態,值:RS_NORMAL、RS_UNITE、RS_CLEAR或RS_GAMEOVER,表示四個游戲狀態。Normal是普通狀態,Unite表示當前
的tick發生了下落塊結合到底盤固定塊的事件,Clear表示當前tick發生了滿行清除的事件,Gameover不用多說了。
int GetStatus() const;
protected:
//游戲的底盤。
CBoard m_board;
//當前的形狀。
CShape m_currentShape;
//下一個形狀。
CShape m_nextShape;
//當前形狀是否存在。
bool m_hasShape;
//當前得分。
int m_score;
//當前游戲狀態。
int m_status;
protected:
//這裡都些私有的函數,都是被上面的公共函數所使用的。
//初始化當前下落快的位置。
void InitShapePos();
//嘗試將出界的形狀拽進界內,返回是否成功。
bool DragInRange();
//使當前形狀下落一格,如果已經到底了就與底盤結合。
void ShapeDown();
//用下一個形狀作為當前形狀,並重新產生隨機的下一個形狀。
bool NextShape();
//獲取形狀shape的信息返回一個ShapeInfo結構。
ShapeInfo GetShapeInfo(const CShape &shape) const;
};
最後是ShapeInfo,這個結構表示一個形狀的信息。
struct ShapeInfo
{
//這個布爾型值表示該數據結構是否有效,在一些函數返回一個ShapeInfo的時候如果這個值是false則表示此時不存在這個形狀。
bool isValidated;
//type是這個形狀的類型ID,對應俄羅斯方塊中的7種形狀,取值0~6。
int type;
//每個形狀一定是由四個小方塊構成,下面這個POINT[]型的blocks數組裡面存儲了四個小方塊各自的坐標。
POINT blocks[4];
//構造。
ShapeInfo()
{
isValidated = false;
type = 0;
}
};
現在有了這些類之後,只需要套到MFC的界面框架裡,然後編寫鍵盤事件及繪制事件等函數就可以完成游戲了,這個過程不是我發布這篇短文的重點,有興趣的看客可以去源碼裡看。在MFC裡創建一個CRussia類的實例就可以通過調用它的接口函數來完成游戲了。
這次俄羅斯方塊的編寫,我主要是想提升一下自己面向對象的感覺,但是寫下來還是覺得有些不舒服的地方。不過還是希望能夠對新手有些幫助,更希望得到高人指點。一直都是通過網絡的幫助來學習的,所以發這篇短文希望也能幫助別人。