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

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來(lái)代表Sprite左上角的坐標(biāo),使用一個(gè)二維數(shù)組來(lái)表示Sprite的形狀,這Sprite的派生類中,只有該二維數(shù)組的值不同,rotate函數(shù)的實(shí)現(xiàn)不同,所以基類的rotate函數(shù)是虛函數(shù)。
為了創(chuàng)建Sprite對(duì)象,還用到了Factory模式,我使用了類SpriteFactory,其GetSprite函數(shù)如下:
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;
}
另外一個(gè)類GameBoard,代表的是游戲的主界面,其主要結(jié)構(gòu)是個(gè)10*20的網(wǎng)格,如下圖:

這設(shè)計(jì)的時(shí)候,我將這個(gè)網(wǎng)格設(shè)計(jì)為12*22的,周圍的這一圈,在顯示的時(shí)候是不可見(jiàn)的,之所以要這一圈存在,是為了方便碰撞檢測(cè),不讓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指針保存了一個(gè)Sprite對(duì)象,分別提供四個(gè)函數(shù)指揮這個(gè)這個(gè)Sprite對(duì)象向上向下向右移動(dòng)和旋轉(zhuǎn),在每次移動(dòng)和旋轉(zhuǎn)之前,都需要進(jìn)行碰撞檢測(cè),函數(shù)collide提供碰撞檢測(cè)的功能。如果Sprite對(duì)象下降到底部之后,就應(yīng)該調(diào)用combine函數(shù)將這個(gè)Sprite對(duì)象的tiles數(shù)組的數(shù)據(jù)加入到GameBoard的tiles數(shù)組中,并創(chuàng)建另外一個(gè)Sprite對(duì)象。而clear函數(shù)的作用,主要是在一局游戲Game Over后開(kāi)始下一局時(shí),清空GameBoard。
有了這幾個(gè)類,游戲線程看起來(lái)就是這樣的了:
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;
}
剩下的事情,就是怎樣去操作數(shù)組,怎么樣去調(diào)用GDI在窗口上畫圖了。最終游戲效果如下圖:
這里是我的源代碼,使用VS 2008可以直接打開(kāi)。歡迎大家探討。