學習C++的最好方式,就是使用C++來做項目。然而,我手中并沒有需要使用C++的工作,咋辦?只好自己寫個小游戲練練手了。
我選擇的游戲是俄羅斯方塊,之所以選擇它,是因為它簡單,簡單到對于很多高手來講,實現這樣一個游戲,他們幾乎只需要一個.cpp源文件就夠了。然而我認為,優雅的代碼來自于完善的設計。下面,我把自己設計該游戲的過程寫下來,歡迎大家探討。
首先,是選擇用戶界面。我選擇的是一個基于對話框的MFC項目。對話框在這里有兩個作用,一個是作為顯示游戲畫面的畫布,另一個就是用來接受用戶的鍵盤輸入。MFC的CDialog類對鍵盤輸入做了一些預處理,因此在OnKeyDown中根本捕獲不到上下左右四個方向鍵的按鍵動作,因此,必須重寫PreTranslateMessage函數,如下:
BOOL?CRussiaBlockDlg::PreTranslateMessage(MSG*?pMsg)
{
????//return?CDialog::PreTranslateMessage(pMsg);
????return?FALSE;
}
其次,是考慮游戲運行的方式。在這里,我使用了一個隊列和兩個輔助線程。隊列是干什么的呢?是用來保存游戲動作的。在用戶按下按鍵之后,對話框的OnKeyDown將用戶的按鍵翻譯為游戲動作,然后依次添加到隊列的尾部,然后,在另外的線程中,通過讀取該隊列的頭部,就知道接下來游戲該如何進行了。兩個輔助線程,其中一個就是游戲線程,該線程從隊列中讀取游戲動作,并進行游戲,另外一個輔助線程是干嘛的呢?很簡單,就是定時往隊列中投遞DOWN動作。
先定義一個枚舉類型,如下:
enum?GameAction{LEFT,RIGHT,ROTATE,DOWN,RESTART};
然后,在對話框類中添加一個用于保存游戲動作的隊列,如下:
deque<GameAction>?m_GameActionQue;這時,對話框類的OnKeyDown函數看起來是這樣的:
void?CRussiaBlockDlg::OnKeyDown(UINT?nChar,?UINT?nRepCnt,?UINT?nFlags)
{
????if((nFlags?&?0x7F)?==?57)?//空格鍵開始
????{
????????if(m_isRunning?==?true)
????????{
????????????m_GameActionQue.push_back(RESTART);
????????}else
????????{
????????????m_isRunning?=?true;
????????????this->Invalidate();
????????????AfxBeginThread(GameThreadFunc,NULL);
????????????AfxBeginThread(TimerThreadFunc,NULL);
????????}
????????
????}else?if((nFlags?&?0x7F)?==?75?&&?m_isRunning?==?true)?//左方向鍵
????{
????????m_GameActionQue.push_back(LEFT);
????}else?if((nFlags?&?0x7F)?==?72?&&?m_isRunning?==?true)?//上方向鍵
????{
????????m_GameActionQue.push_back(ROTATE);
????}else?if((nFlags?&?0x7F)?==?77?&&?m_isRunning?==?true)?//右方向鍵
????{
????????m_GameActionQue.push_back(RIGHT);
????}else?if((nFlags?&?0x7F)?==?80?&&?m_isRunning?==?true)?//下方向鍵
????{
????????m_GameActionQue.push_back(DOWN);
????}
????CDialog::OnKeyDown(nChar,?nRepCnt,?nFlags);
}
這時,對話框類基本上已經完成了自己的使命,剩下的工作只是在OnPaint函數中適當的繪制一些提示信息而已。工作的重點進入到線程函數GameThreadFunc中。在該線程中,游戲的進行和幾個類密切相關,它們分別是GameBoard類和Sprite類及Sprite的派生類。
先來看Sprite類及其派生類,它們分別代表了游戲中可以移動和旋轉的各種類型的方塊,如下圖:

Sprite類的定義如下:
class?Sprite
{
public:
????Sprite(void);
????~Sprite(void);
????int?x;
????int?y;
????int?tiles[4][4];
????void?left(void);
????void?right(void);
????void?down(void);
????virtual?void?rotate(void);
};
可以看到,我使用x,y來代表Sprite左上角的坐標,使用一個二維數組來表示Sprite的形狀,這Sprite的派生類中,只有該二維數組的值不同,rotate函數的實現不同,所以基類的rotate函數是虛函數。
為了創建Sprite對象,還用到了Factory模式,我使用了類SpriteFactory,其GetSprite函數如下:
Sprite*?SpriteFactory::GetSprite(void)
{
????switch(std::rand()?%?7)
????{
????case?0:
????????return?new?ISprite();
????case?1:
????????return?new?LSprite();
????case?2:
????????return?new?SSprite();
????case?3:
????????return?new?ZSprite();
????case?4:
????????return?new?PSprite();
????case?5:
????????return?new?OSprite();
????case?6:
????????return?new?TSprite();
????}
????return?NULL;
}
另外一個類GameBoard,代表的是游戲的主界面,其主要結構是個10*20的網格,如下圖:

這設計的時候,我將這個網格設計為12*22的,周圍的這一圈,在顯示的時候是不可見的,之所以要這一圈存在,是為了方便碰撞檢測,不讓Sprite跑出界。GameBoard類的定義如下:
class?GameBoard
{
public:
????GameBoard(void);
????~GameBoard(void);
????int?tiles[22][12];
????int?m_score;
????int?m_speed;
????Sprite*?m_pSprite;
????void?sprite_left(void);
????void?sprite_right(void);
????void?sprite_rotate(void);
????void?sprite_down(void);
????void?clear(void);
????void?combine(void);
????bool?collide(Sprite*?sprite);
????void?show(void);
};
其中的m_pSprite指針保存了一個Sprite對象,分別提供四個函數指揮這個這個Sprite對象向上向下向右移動和旋轉,在每次移動和旋轉之前,都需要進行碰撞檢測,函數collide提供碰撞檢測的功能。如果Sprite對象下降到底部之后,就應該調用combine函數將這個Sprite對象的tiles數組的數據加入到GameBoard的tiles數組中,并創建另外一個Sprite對象。而clear函數的作用,主要是在一局游戲Game Over后開始下一局時,清空GameBoard。
有了這幾個類,游戲線程看起來就是這樣的了:
UINT?GameThreadFunc(LPVOID?pParam)
{
????CRussiaBlockDlg*?pDlg?=?dynamic_cast<CRussiaBlockDlg*>(::AfxGetApp()->GetMainWnd());
????CClientDC?dc(pDlg);
????if(pDlg->m_isGameOver?==?true)
????{
????????pDlg->m_isGameOver?=?false;
????????pDlg->m_GameBoard.clear();
????}
????while(pDlg->m_isRunning)
????{
????????pDlg->m_GameBoard.show();
????????if(pDlg->m_GameActionQue.empty())
????????{
????????????::Sleep(100);
????????}
????????else
????????{
????????????switch(pDlg->m_GameActionQue.front()){
????????????????case?RESTART:
????????????????????pDlg->m_isRunning?=?false;
????????????????????pDlg->Invalidate();
????????????????????break;
????????????????case?LEFT:
????????????????????pDlg->m_GameBoard.sprite_left();
????????????????????break;
????????????????case?RIGHT:
????????????????????pDlg->m_GameBoard.sprite_right();
????????????????????break;
????????????????case?DOWN:
????????????????????pDlg->m_GameBoard.sprite_down();
????????????????????break;
????????????????case?ROTATE:
????????????????????pDlg->m_GameBoard.sprite_rotate();
????????????????????break;
????????????}
????????????pDlg->m_GameActionQue.pop_front();
????????}
????}
????return?0;
}
剩下的事情,就是怎樣去操作數組,怎么樣去調用GDI在窗口上畫圖了。最終游戲效果如下圖:
這里是我的源代碼,使用VS 2008可以直接打開。歡迎大家探討。