• <ins id="pjuwb"></ins>
    <blockquote id="pjuwb"><pre id="pjuwb"></pre></blockquote>
    <noscript id="pjuwb"></noscript>
          <sup id="pjuwb"><pre id="pjuwb"></pre></sup>
            <dd id="pjuwb"></dd>
            <abbr id="pjuwb"></abbr>

            海邊沫沫

            相濡以沫,不如相忘于江湖
            posts - 9, comments - 113, trackbacks - 0, articles - 0
              C++博客 :: 首頁 :: 新隨筆 :: 聯(lián)系 :: 聚合  :: 管理
            學(xué)習(xí)C++的最好方式,就是使用C++來做項目。然而,我手中并沒有需要使用C++的工作,咋辦?只好自己寫個小游戲練練手了。

            我選擇的游戲是俄羅斯方塊,之所以選擇它,是因為它簡單,簡單到對于很多高手來講,實現(xiàn)這樣一個游戲,他們幾乎只需要一個.cpp源文件就夠了。然而我認(rèn)為,優(yōu)雅的代碼來自于完善的設(shè)計。下面,我把自己設(shè)計該游戲的過程寫下來,歡迎大家探討。

            首先,是選擇用戶界面。我選擇的是一個基于對話框的MFC項目。對話框在這里有兩個作用,一個是作為顯示游戲畫面的畫布,另一個就是用來接受用戶的鍵盤輸入。MFC的CDialog類對鍵盤輸入做了一些預(yù)處理,因此在OnKeyDown中根本捕獲不到上下左右四個方向鍵的按鍵動作,因此,必須重寫PreTranslateMessage函數(shù),如下:
            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函數(shù)看起來是這樣的:
            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);
            }

            這時,對話框類基本上已經(jīng)完成了自己的使命,剩下的工作只是在OnPaint函數(shù)中適當(dāng)?shù)睦L制一些提示信息而已。工作的重點進入到線程函數(shù)GameThreadFunc中。在該線程中,游戲的進行和幾個類密切相關(guān),它們分別是GameBoard類和Sprite類及Sprite的派生類。

            先來看Sprite類及其派生類,它們分別代表了游戲中可以移動和旋轉(zhuǎn)的各種類型的方塊,如下圖:
            Sprites.jpg

            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左上角的坐標(biāo),使用一個二維數(shù)組來表示Sprite的形狀,這Sprite的派生類中,只有該二維數(shù)組的值不同,rotate函數(shù)的實現(xiàn)不同,所以基類的rotate函數(shù)是虛函數(shù)。

            為了創(chuàng)建Sprite對象,還用到了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;
            }


            另外一個類GameBoard,代表的是游戲的主界面,其主要結(jié)構(gòu)是個10*20的網(wǎng)格,如下圖:
            GameBoard.jpg

            這設(shè)計的時候,我將這個網(wǎng)格設(shè)計為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對象,分別提供四個函數(shù)指揮這個這個Sprite對象向上向下向右移動和旋轉(zhuǎn),在每次移動和旋轉(zhuǎn)之前,都需要進行碰撞檢測,函數(shù)collide提供碰撞檢測的功能。如果Sprite對象下降到底部之后,就應(yīng)該調(diào)用combine函數(shù)將這個Sprite對象的tiles數(shù)組的數(shù)據(jù)加入到GameBoard的tiles數(shù)組中,并創(chuàng)建另外一個Sprite對象。而clear函數(shù)的作用,主要是在一局游戲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;
            }

            剩下的事情,就是怎樣去操作數(shù)組,怎么樣去調(diào)用GDI在窗口上畫圖了。最終游戲效果如下圖:
            RussiaBlock.jpg

            這里是我的源代碼,使用VS 2008可以直接打開。歡迎大家探討。

            Feedback

            # re: 寫個小游戲練一練手  回復(fù)  更多評論   

            2007-12-16 20:07 by TD
            Very good!
            很詳細(xì),設(shè)計的也很好,博主很棒!

            雞蛋里面挑點骨頭試試~~
            1.關(guān)于bool值的判斷:m_isGameOver 本身就是bool值,使用m_isGameOver ==false當(dāng)然沒錯,只是有點怪,你覺得如何?

            2.程序中出現(xiàn)的一些常數(shù),如Board的長和寬12*22,還有些別的,改成宏定義或者enum應(yīng)該更好點,哪天覺得想改一下尺寸,會比較方便

            3.線程同步的問題

            # re: 寫個小游戲練一練手  回復(fù)  更多評論   

            2007-12-16 22:47 by 夢在天涯
            強,看到這里越來越多的高手,超高興,希望以后多交流奧!

            # re: 寫個小游戲練一練手  回復(fù)  更多評論   

            2007-12-17 01:13 by Fox
            我以前在TC里面寫過一個,可惜后來源碼丟了大半,一個執(zhí)行檔也不是最終版。
            和大多數(shù)俄羅斯方塊不同的地方在于,我對方塊和面板的存儲都是用位存儲。

            # re: 寫個小游戲練一練手  回復(fù)  更多評論   

            2007-12-17 01:14 by Fox
            看看最近有沒有時間,也用MFC實現(xiàn)過來

            # re: 寫個小游戲練一練手  回復(fù)  更多評論   

            2007-12-17 09:24 by 絕對零度
            在symbian上寫過,我也是用位來存儲的。

            # re: 寫個小游戲練一練手  回復(fù)  更多評論   

            2007-12-17 10:29 by 崔友志
            博主真厚道 愿更上一層樓!

            # re: 寫個小游戲練一練手  回復(fù)  更多評論   

            2007-12-17 12:44 by cpp
            用地圖數(shù)據(jù)應(yīng)該是比較方便的,我用的是bool類型的地圖,方塊信息用十進制數(shù)據(jù),用的時候轉(zhuǎn)化為二進制。當(dāng)時寫的時候沒有用面向?qū)ο蟮乃枷?,?shù)據(jù)結(jié)構(gòu)用的都是全局變量。擴展性還可以,代碼寫得就有點亂了。Cppblog上也是高手如云。

            # re: 寫個小游戲練一練手  回復(fù)  更多評論   

            2007-12-17 13:37 by 海邊沫沫
            @TD

            你說得不錯,游戲中確實需要考慮線程同步問題。首先是多個線程訪問同一個變量的時候,最好能加入競爭機制。在我這個游戲中,就沒有對m_GameActionQue的訪問進行控制,不過兩個線程在尾部寫,一個線程在頭部讀,倒沒有出現(xiàn)問題。

            其次是這里
            if(pDlg->m_GameActionQue.empty())
            {
            ::Sleep(100);
            }
            我在一個循環(huán)中不斷進行判斷隊列是否為空,這種做法雖然可行,但是不好,因為在隊列為空的時候不斷循環(huán)判斷很占用CPU時間。正確的做法應(yīng)該是調(diào)用WaitForSingleObject使線程阻塞起來,當(dāng)別的線程向隊列中添加消息的時候解除阻塞。

            # re: 寫個小游戲練一練手  回復(fù)  更多評論   

            2007-12-17 13:49 by 秦歌
            強人!

            # re: 寫個小游戲練一練手  回復(fù)  更多評論   

            2007-12-18 21:20 by DeathKnight
            贊一個 設(shè)計很簡潔明快 我喜歡

            # re: 寫個小游戲練一練手  回復(fù)  更多評論   

            2007-12-19 18:33 by 柯南
            不錯,很有意思

            # re: 寫個小游戲練一練手  回復(fù)  更多評論   

            2007-12-19 19:13 by 海邊沫沫
            我今天稍微把游戲修改了一下,不用
            if(pDlg->m_GameActionQue.empty())
            {
            ::Sleep(100);
            }
            了,而是在對話框類中加入了一個CEvent類的變量m_event

            然后,讀取隊列的時候使用
            if(pDlg->m_GameActionQue.empty())
            {
            pDlg->m_event.Lock();
            }
            而在其他寫入隊列的時候調(diào)用
            m_event.SetEvent();

            這樣修改之后,游戲?qū)︽I盤的響應(yīng)就基本上沒有延時了,而且占用CPU要下降不少。

            # re: 寫個小游戲練一練手  回復(fù)  更多評論   

            2007-12-20 13:53 by 秦歌
            厲害

            # re: 寫個小游戲練一練手  回復(fù)  更多評論   

            2009-04-23 19:13 by 向往
            不錯,不錯,看起來有些老練.繼續(xù)加油.
            久久精品水蜜桃av综合天堂| 99久久中文字幕| 一本色道久久88加勒比—综合| 亚洲精品高清久久| 久久影院午夜理论片无码| 精品久久久无码21p发布| 久久电影网一区| 人妻精品久久无码专区精东影业| 国产婷婷成人久久Av免费高清| 久久精品成人| 色综合久久综精品| 久久久精品2019免费观看| 97久久精品人人做人人爽| 色综合久久天天综线观看| 亚洲狠狠久久综合一区77777| 久久亚洲精品无码aⅴ大香| 韩国三级中文字幕hd久久精品 | 久久精品夜色噜噜亚洲A∨| 亚洲国产欧美国产综合久久| 亚洲伊人久久综合中文成人网| 国产综合精品久久亚洲| 久久久久亚洲av无码专区导航| 久久这里有精品| 99久久国产亚洲综合精品| 国产免费久久精品99久久| 26uuu久久五月天| 久久99精品久久久久久动态图 | 久久99国产一区二区三区| 国产精品久久午夜夜伦鲁鲁| 久久精品国产99久久久古代| 亚洲精品无码专区久久同性男 | 亚洲va久久久噜噜噜久久| 中文字幕精品无码久久久久久3D日动漫| 色综合久久天天综合| 66精品综合久久久久久久| 国产精品久久成人影院| 久久久久人妻精品一区| 国产精品久久久天天影视| 久久精品成人国产午夜| 青青青伊人色综合久久| 久久精品国产99国产精品|