1. 程式人生 > >用C++語言實現貪吃蛇遊戲

用C++語言實現貪吃蛇遊戲

寫在前面
用C++語言寫遊戲再適合不過了,當然不是因為用它寫起來簡單,(相反那並不簡單),但是其效能絕對是其他語言沒法比的。所以這裡我會用C++實現一個貪吃蛇的遊戲。當然我可能有意隱瞞了你,因為我們不僅僅是用C++純語言來幹這件事,那會很彆扭,因為我們需要影象渲染、聲音、甚至是碰撞檢測(我最喜歡的一個版塊)!所以僅僅用語言是不夠的。
(注:在文章最後我會給出兩個版本的貪吃蛇原始碼及涉及到的一些資源)
寫在最前面就是為了說明我們會用其他的一些工具:DirectX(9.0)、Windows的視窗程式設計,這些真的沒那麼簡單!如果你之前沒聽說過這些,也不要太過於擔心,因為我主要是介紹貪吃蛇實現的核心邏輯,嚴格的說,你可以當成資料結構的知識來學,因為整條蛇是以連結串列為基礎的!
另外,我用純C語言

也實現過一個貪吃蛇的玩意(如果你覺得是的話),先看看遊戲的執行效果:
貪吃蛇版本1:
這裡寫圖片描述
不要小瞧它!它有音樂,也有碰撞,雖然體驗實在是不咋滴,不過他的遊戲編寫過程和遊戲元素的構成還是對之後進一步編寫更棒的遊戲提供了十足的基礎。因為他是用純語言做的,不需要其他庫等等的支援,所以很適合我們學習借鑑!

至於第二個版本的貪吃蛇就有很大的改變,儘管還有很多地方需要改進和優化,但是他已經超越了第一個版本很多!下面看看遊戲執行效果:
貪吃蛇版本2:
這裡寫圖片描述
是不是有擺脫Dos找到新大陸的感覺,他加入了新的計分模組。下面我就第二個版本的核心實現做出解釋。
版本二遊戲核心程式碼實現
1,蛇身的單個節點實現:

//蛇身單個節點
struct SNAKE {
    bool IsSurvivor;            //當前結點是否存在(被畫)
    int coor_x;                 //節點橫座標
    int coor_y;                 //節點縱座標
    SNAKE *link;                //指向下一個節點的指標
    //建構函式
    SNAKE(int x, int y, bool survivor = true,SNAKE *link = NULL) {
        //初始化座標值,賦值方式為tail派生
        coor_x = x;
        coor_y = y;
    }
};

的確,一個十分明白的結構體,我無需做出任何解釋!
2,蛇的整個類實現(基於連結串列)

//蛇精靈類定義——基於單鏈表實現蛇身
class SnakeSprite {
public:
    SnakeSprite(int x = 300, int y = 200);
    ~SnakeSprite() { delete snakeHead; }
    bool addTail();                             //蛇尾增加長度
    void drawThisSnake();                       //繪製當前蛇身
    void positionAction();                      //完成蛇身移動(更新每個節點的座標)
    void turnLeft();                            //蛇頭的基本轉向
    void turnRight();
    void turnUp();
    void turnDown();
    void recordCurrentDirection(int d = LEFT);  //記錄蛇的當前運動方向,藉助列舉類
    int getDirection();
    bool IsDeath();                             //是否碰撞草叢,是蛇死亡返回true,否則返回false
    void getCurrentPosRect(RECT &rect);
    void getCurrentCoor(int &x, int &y);
protected:
    int len;                                    //蛇身長度_以塊為單位
    SNAKE *snakeHead;                           //蛇頭指標
    SNAKE *tail;                                //蛇尾指標
    SNAKE *beforeTail;                          //尾巴節點的前一個節點,方便移動
    int directions;
};

似乎也沒什麼特別之處,但是有幾個地方需要注意,我會在下面著重強調。
3,部分函式實現的解釋
我想強調的就在這裡,貪吃蛇整個遊戲的確簡單,但是真正編寫的時候則需要考慮全面,因為遊戲的邏輯還是特別強的。
1》整個蛇動起來的立足點:
我們必須記住,遊戲中的蛇並不是你想象的那樣在隨著你的控制而‘遊動’,他是電腦在以飛快的速率重新整理螢幕,而你只是改變了蛇的節點座標,而人的眼睛是存在視覺暫留的,這樣就會給你一種遊戲精靈在走動的效果!
2》怎樣用鍵盤控制蛇?

//輸入控制
    if (Key_Down(DIK_UP) && !Key_Down(DIK_RIGHT) 
        && !Key_Down(DIK_LEFT) && !Key_Down(DIK_DOWN)) {
        theSnake.turnUp();
        if (DOWN != theSnake.getDirection()) {
            theSnake.recordCurrentDirection(UP);
        }
    }
    if (Key_Down(DIK_RIGHT) && !Key_Down(DIK_UP)
        && !Key_Down(DIK_LEFT) && !Key_Down(DIK_DOWN)) {
        theSnake.turnRight();
        if (LEFT != theSnake.getDirection()) {
            theSnake.recordCurrentDirection(RIGHT);
        }
    }
    if (Key_Down(DIK_LEFT) && !Key_Down(DIK_UP)
        && !Key_Down(DIK_RIGHT) && !Key_Down(DIK_DOWN)) {
        theSnake.turnLeft();
        if (RIGHT != theSnake.getDirection()) {
            theSnake.recordCurrentDirection(LEFT);
        }
    }
    if (Key_Down(DIK_DOWN) && !Key_Down(DIK_UP) 
        && !Key_Down(DIK_RIGHT) && !Key_Down(DIK_LEFT)) {
        theSnake.turnDown();
        if (UP != theSnake.getDirection()) {
            theSnake.recordCurrentDirection(DOWN);
        }
    }

//蛇類的成員函式
void SnakeSprite::turnDown() {
    //向下轉頭
    if (directions != UP) {
        snakeHead->coor_y += 1;
    }
}

void SnakeSprite::turnLeft() {
    //想左轉頭
    if (directions != RIGHT) {
        snakeHead->coor_x -= 1;
    }
}

void SnakeSprite::turnRight() {
    //向右轉頭
    if (directions != LEFT) {
        snakeHead->coor_x += 1;
    }
}

void SnakeSprite::turnUp() {
    if (directions != DOWN) {
        snakeHead->coor_y -= 1;
    }
}

應該是你想象的那樣,我每檢測到玩家按下相應的方向鍵,我會呼叫snake class的轉彎的成員函式(這就是用class的好處,多麼統一的程式碼!),然後緊接著判斷玩家是否企圖直接來個180°的大逆轉(這在貪吃蛇遊戲中是違背規則的),如果真的是這樣我就在函式中不做任何處理,玩家休想達到這種陰謀!但是如果是合法的轉彎(也就是90°),我會改變頭結點(就是蛇的頭部)的座標變化趨勢,就是程式碼中那樣做。這樣蛇就任我們控制擺佈了。

3》怎樣實現蛇的移動?

//蛇類的成員函式
void SnakeSprite::positionAction() {
    //實現蛇的自動運動,即依次更新每個節點內座標的值
    if (UP == directions) {
        snakeHead->coor_y--;
    }
    if (DOWN == directions) {
        snakeHead->coor_y++;
    }
    if (LEFT == directions) {
        snakeHead->coor_x--;
    }
    if (RIGHT == directions) {
        snakeHead->coor_x++;
    }
    SNAKE *current = snakeHead;
    int LEN = len;
    for (int i = 1; i < len; i ++) {
        current = snakeHead;
        for (int j = 1; j < LEN - 1 && len >= 3; j ++) {
            //令current迴圈到指定位置
            current = current->link;
        }
        current->link->coor_x = current->coor_x;
        current->link->coor_y = current->coor_y;
        LEN--;
    }
}

這是一個相當重要的功能,因為只有可以動起來才有遊戲的感覺。就是上面這個簡單的函式實現了蛇的移動,他在主函式中是迴圈呼叫的,所以他的核心功能就是改變蛇頭節點的座標,讓蛇頭節點可以沿著當前的運動方向一直移動下去。你也許會問,那蛇的身子是怎麼跟著蛇頭運動的呢?那就是函式中最後的一個雙重迴圈,內層迴圈會通過一個指標沿著蛇身連結串列找到蛇的尾巴的前一個節點,然後把此節點內的座標值給尾巴節點,這樣就實現了尾巴‘跟著動’的效果。第二次進入後內層迴圈會找到蛇尾巴前一個節點的前一個節點,然後把他的座標給了尾巴的前一個節點,這樣倒數第二個節點也跟上了!之後便一直重複上述迴圈,其實就是在用每個節點的座標來重新整理其後一個節點的座標,這樣不就讓每一段蛇身都與蛇頭形影不離了嗎!如果還是感覺理解上有困難,可以看看下面的模擬圖:
這裡寫圖片描述
這裡應該十分注意賦值的順序!,我為何要‘多此一舉’地用迴圈先找到倒數第二個節點,而不是直接從頭部開始,因為那樣會讓蛇的座標提前丟失,導致我們沒法把真正有效的座標值更新到對應的節點中,從而只看到蛇頭在移動!不信?你可以在本子上比劃比劃。
4》蛇的死亡碰撞事件檢測!

bool SnakeSprite::IsDeath() {
    //判斷是否超出規定範圍 67<x<468/87<y<470
    if (snakeHead->coor_x > 67 && snakeHead->coor_x < 460
        && snakeHead->coor_y > 87 && snakeHead->coor_y < 470) {
        return false;
    }
    else {
        return true;
    }
}

嗯,這取決於你在螢幕上蛇的移動場地面積的尺寸,當檢測的某個方向的座標超過了對應方向上的場地長度,那就GAMEOVER吧!

食物類的實現程式碼:

//FOOD CLASS
class Food {
protected:
    int coor_x;             //食物出現的橫座標
    int coor_y;             //縱座標
public:
    Food(int x = 100, int y = 100);
    bool drawThisFood(bool &again);             //繪製當前食物
    bool checkFoodPosition();                   //檢查當前食物出現的位置是否合法(即不能與蛇體重合)
    void getRandCoor(int & x, int & y);         //食物的隨機座標生成
};

//APPLE CLASS
class Apple : public Food { //蘋果是Food的一種
private:
    int color;              //擴充套件功能,標定當前Apple的顏色
public:
    bool beenCollision(RECT snakeRect); //檢測apple是否被碰撞到,是返回true否則返回false
    void getCurrentPosRect(RECT &rect);         //得到當前的位置矩形
    //bool beenCollision2(int x, int y);
};

Food::Food(int x, int y) : coor_x(x), coor_y(y)
{}

void Apple::getCurrentPosRect(RECT &rect) {
    RECT currentRect = {coor_x, coor_y, coor_x + 19, coor_y + 22};
    rect = currentRect;
}

bool Apple::beenCollision(RECT snakeRect) {
    RECT rect_apple, rect;
    getCurrentPosRect(rect_apple);
    if (IntersectRect(&rect, &rect_apple, &snakeRect)) {
        return true;
    }
    else {
        return false;
    }
}

bool Food::drawThisFood(bool &again) {
    //繪製當前食物到螢幕
    int posX , posY ;
    if (again) {
        getRandCoor(posX, posY);
        again = false;
    }

    RECT rectApple = { 0, 0, 19, 22 };
    D3DXVECTOR3 position(coor_x, coor_y, 0);
    D3DCOLOR red = D3DCOLOR_XRGB(255, 255, 255);
    spriteoj->Draw(apple, &rectApple, NULL, &position, red);

    return true;
}

void Food::getRandCoor(int & x, int & y) {
    //指定範圍內的隨機函式生成器
    srand((unsigned)time(NULL));        //隨機種子,以系統時間作為基數
    x = foodAllowPosX + (rand() % 350);
    y = foodAllowPosY + (rand() % 330);
    coor_x = x;
    coor_y = y;
}

bool Food::checkFoodPosition() {
    //檢查食物位置的合法性
    return true;
}

這裡我用FOOD作為基類,然後用APPLE來繼承它,這主要是想在以後擴充套件這個遊戲的時候加入一些新的玩法,讓每種不同的食物都有自己各自屬性和反應事件。
5》食物的隨機位置產生

void Food::getRandCoor(int & x, int & y) {
    //指定範圍內的隨機函式生成器
    srand((unsigned)time(NULL));        //隨機種子,以系統時間作為基數
    x = foodAllowPosX + (rand() % 350);
    y = foodAllowPosY + (rand() % 330);
    coor_x = x;
    coor_y = y;
}

這個成員函式用了隨機數生成器來產生指定區間內的座標,並把這個座標當做食物出現的座標。因為螢幕是在無休止重新整理的,所以食物的擦除就不勞我們費心了。
6》其他
我在這裡只是調了一些關鍵的地方作了闡述。其他還有瑣碎的地方都需要一塊塊完善,但是都相對簡單。至於將蛇繪製到螢幕上,這是件麻煩事!我不能展開講,我的水平也不敢講,但是這真的會令你沮喪,如果你只是看某些實現邏輯而其他的可以自己搞定,那麼上面的解釋還是挺有幫助的;如果你是個新手,那就會覺得知道邏輯和流程卻無法把他們繪製到螢幕上,似乎是本我狠狠的放了鴿子。
也許不必那麼沮喪!因為我講了你也未必能懂(哈哈),你有其他途徑可以實現自己的貪吃蛇遊戲:
(1)實現純語言版本的,沒錯,就是在那個黑黑的Dos框裡的,因為他的實現相對簡單,關鍵是他避免了圖形渲染和Windows的視窗建立!這真的是一個不錯的入門Demo。你還可以在網上找一些關於他的程式碼來提高自己的開發效率,如果你仍然感覺邏輯上有困難,那麼我也會盡快整理出他的寫作思路~
(2)學習一些圖形渲染的庫和工具(如DirectX),抑或是一些簡單的遊戲引擎,如果那樣的話,你真的會瞧不起我做的這個貪吃蛇。再不就看看我提供的兩套原始碼吧!

4,不足之處
上面的程式碼儘管解決了一些核心的遊戲邏輯,但是依然存在不足。計分系統由於食物出現的位置不當而暴增、食物萬一出現在蛇身上怎麼辦?而我在食物位置的合法性檢查上只是返回了個字面量true!希望我們一塊交流和改進。

5,資源連結
說明:你休想直接複製貼上上面的程式碼塊來放在編譯器裡執行它,並且天真的等待遊戲畫面的出現,因為我早說過這些需要相應工具的支援!你甚至不能執行起版本2的EXE程式,因為可能需要DirectX的遊戲環境,而恰好你的機器上沒有!但是版本1的貪吃蛇是可以執行起來的,原始碼也是可以編譯的(如果你的編譯器正常的話!),因為他真的是用純語言做的,當然效能也會有不足。
貪吃蛇版本一資源連結:http://pan.baidu.com/s/1c2EUVc8 密碼:na6m
貪吃蛇版本二資源連結:http://pan.baidu.com/s/1hsb7k92 密碼:6sbg

最後謝謝大家可以看我分享的一些經驗,這些都是在專案過程中遇到的麻煩,希望大家可以收穫到一些知識,少走點彎路!