10分鐘教你用C++寫一個貪喫蛇附帶AI功能(史上最詳細最入門的貪喫蛇教程)附源代碼下載

C++編寫貪喫蛇小遊戲快速入門

剛學完C++。一時興起,就花幾天時間手動做了個貪喫蛇,後來覺得不過癮,於是又加入了AI功能。希望大家Enjoy It.

效果圖示

AI模式演示


整體規劃+原理

大體上可以分爲圖上所示的幾個類。不過……怎麼看都有點強行面向對象的味道在裏面。。[哭笑][哭笑][哭笑]。不管了……代碼寫得可能有點凌亂,下面我會爲大家一一講解。

整個程序設計的原理就是:主函數死循環,不斷刷新打印貪喫蛇和食物。這樣每循環一次,就類似電影裏面的一幀,最終顯示的效果就是蛇會動起來。

01 初始化工作-遊戲設置

遊戲設置和相關初始化放在了一個類裏面,並進行了靜態聲明。主要設置了遊戲窗口的長和款。並在GameInit()函數裏面設置了窗口大小,隱藏光標,初始化隨機數種子等。代碼如下:

//遊戲設置相關模塊,把函數都放到一個類裏面了。函數定義爲static靜態成員,不生成實體也可以直接調用
class GameSetting
{
public:
    //遊戲窗口的長寬
    static const int window_height = 40;
    static const int window_width = 80;
public:
    static void GameInit()
    {
        //設置遊戲窗口大小
        char buffer[32];
        sprintf_s(buffer, "mode con cols=%d lines=%d",window_width, window_height);
        system(buffer);

        //隱藏光標
        HANDLE handle = GetStdHandle(STD_OUTPUT_HANDLE);
        CONSOLE_CURSOR_INFO CursorInfo;
        GetConsoleCursorInfo(handle, &CursorInfo);//獲取控制檯光標信息
        CursorInfo.bVisible = false; //隱藏控制檯光標
        SetConsoleCursorInfo(handle, &CursorInfo);//設置控制檯光標狀態
        //初始化隨機數種子
        srand((unsigned int)time(0));
    }
};

用到了幾個相關的Windows API,本文不做過多介紹,大家百度即可。

02 打印信息類

該類主要是用來打印一些遊戲相關信息的。該類大體如下:

下面挑幾個重點的來講:

2.1 畫地圖邊界

這個函數主要是根據上面所給的遊戲窗口長寬來打印地圖邊界的。其中還劃分了幾個區域,主要用來放不同的信息的。

//畫地圖邊界
static void DrawMap()
{
    system("cls");
    int i, j;
    for (i = 0; i < GameSetting::window_width; i++)
        cout << "#";
    cout << endl;
    for (i = 0; i < GameSetting::window_height-2; i++)
    {
        for (j = 0; j < GameSetting::window_width; j++)
        {
            if (i == 13 && j >= GameSetting::window_width - 29)
            {
                cout << "#";
                continue;
            }

            if (j == 0 || j == GameSetting::window_width - 29 || j == GameSetting::window_width-1)
            {
                cout << "#";
            }
            else
                cout << " ";

        }
        cout << endl;
    }
    for (i = 0; i < GameSetting::window_width; i++)
        cout << "#";

}

劃分區域如下圖,#就是邊框了:

2.2 畫出分數和模式

該函數主要是在右上角畫出成績和遊戲模式的,在繪製之前會進行刷新處理。先清除,再重新打印。用到了一個gotoxy()函數。這個函數主要是移動光標到(x, y)座標處的。關於(x, y)的位置,根據實際情況調整即可。

//畫分數
static void DrawScore(int score)
{
    gotoxy(GameSetting::window_width - 22+14, 6);
    cout << "  ";
    gotoxy(GameSetting::window_width - 22+14, 4);
    cout << "  ";

    gotoxy(GameSetting::window_width - 22, 6);
    cout << "當前玩家分數: " << score << endl;
    gotoxy(GameSetting::window_width - 22, 4);
    cout << "當前遊戲速度: " << 10 - speed / 25 << endl;

}

03 食物類

食物類定義了食物的座標,隨機生成規則,和畫出食物等一系列操作。其中食物座標我們用了一個結構體:

typedef struct
{
    int x;
    int y;
}COORDINATE;

該結構體兩個成員,分別保存座標的(x, y)。蛇身的座標也會用到這個結構體。
有關食物類的大體如下:

下面我們還是挑幾個重點來講。

3.1 隨機生成食物

隨機生成食物,原則上不允許食物出現在蛇身的位置上,如果有。我們重新生成。注意地圖的範圍,就是區域左邊一塊。實際情況根據自身的地圖範圍來調整食物座標的範圍,注意不要越界。用rand()函數獲得隨機座標。代碼如下:

void RandomXY(vector<COORDINATE> & coord)
{
    m_coordinate.x = rand() % (GameSetting::window_width - 30) + 1;
    m_coordinate.y = rand() % (GameSetting::window_height - 2) + 1;
    unsigned int i;
    //原則上不允許食物出現在蛇的位置上,如果有,重新生成
    for (i = 0; i < coord.size(); i++)
    {
        //食物出現在蛇身的位置上。重新生成
        if (coord[i].x == m_coordinate.x && coord[i].y == m_coordinate.y)
        {
            m_coordinate.x = rand() % (GameSetting::window_width - 30) + 1;
            m_coordinate.y = rand() % (GameSetting::window_height - 2) + 1;
            i = 0;
        }
    }
}

然後,在構造函數裏面傳入蛇身的座標。即可生成食物。

3.2 畫出食物

畫出食物比較簡單了,gotoxy到隨機生成的座標之後,cout就行。我們在這還設置了一個食物顏色爲紅色。代碼如下:

void DrawFood()
{
    setColor(12, 0);
    gotoxy(m_coordinate.x, m_coordinate.y);
    cout << "@";
    setColor(7, 0);
}

04 貪喫蛇類

定義貪喫蛇的移動,打印,喫食物等等。這節課我們暫時不討論AI功能,先把手動操作的貪喫蛇做了跑起來,下節課再做AI功能的介紹。該類大體如下:

4.1 成員變量

成員變量m_direction記錄每次移動的方向。m_is_alive記錄貪喫蛇是否還活着。m_coordinate則是貪喫蛇身體座標的記錄。貪喫蛇是一節一節的,整條蛇必然是由許多節組成的。因此用了一個vector來存儲蛇身,每節類型是COORDINATE結構體的。

4.2 默認構造函數

默認構造函數Snake()裏面主要是做了初始貪喫蛇的生成,以及移動方向的定義等。初始的蛇爲3節。在中間位置,向上移動。代碼如下:

        //移動方向向上
    m_direction = 1;
    m_is_alive = true;
    COORDINATE snake_head;
    //蛇頭生成位置
    snake_head.x = GameSetting::window_width / 2 - 15;
    snake_head.y = GameSetting::window_height / 2;

    this->m_coordinate.push_back(snake_head);
    snake_head.y++;
    this->m_coordinate.push_back(snake_head);
    snake_head.y++;
    this->m_coordinate.push_back(snake_head); //初始蛇身長度三節

4.3 監聽鍵盤

監聽鍵盤用了C裏面的一個庫函數。_kbhit()非阻塞函數,可以不斷監聽鍵盤的情況從而不產生阻塞。有鍵盤按下的時候,就獲取按下的鍵盤是哪個。然後做出相應的變化,其實是方向的調整。需要注意的是,當我們的蛇往上走的時候,按下方向的鍵,我們是不做處理的。其它方向一樣。還有一個調整遊戲速度的,speed是休眠時間,speed越小,速度越快。反之速度越慢。

    //監聽鍵盤
void listen_key_borad()
{
    char ch;

    if (_kbhit())                   //kbhit 非阻塞函數 
    {
        ch = _getch();  //使用 getch 函數獲取鍵盤輸入 
        switch (ch)
        {
        case 'w':
        case 'W':
            if (this->m_direction == DOWN)
                break;
            this->m_direction = UP;
            break;
        case 's':
        case 'S':
            if (this->m_direction == UP)
                break;
            this->m_direction = DOWN;
            break;
        case 'a':
        case 'A':
            if (this->m_direction == RIGHT)
                break;
            this->m_direction = LEFT;
            break;
        case 'd':
        case 'D':
            if (this->m_direction == LEFT)
                break;
            this->m_direction = RIGHT;
            break;
        case '+':
            if (speed >= 25)
            {
                speed -= 25;
            }
            break;
        case '-':
            if (speed < 250)
            {
                speed += 25;
            }
            break;
        }
    }
}

4.4 移動貪喫蛇

移動貪喫蛇,我們用了一個方向變量,在監聽鍵盤的時候獲取移動的方向,然後在根據方向移動貪喫蛇的蛇頭。這裏的移動我們是這樣處理的,首先,貪喫蛇每移動一次,需要改變的只有蛇頭和蛇尾兩節。我們只需要把新的蛇頭插進去,最後再畫出來就可以了。至於蛇尾,如果我們不刪除蛇尾的話,蛇會不斷變長的。因此我們的做法是:喫到食物的時候插入蛇頭而不刪除蛇尾,沒有喫到食物的時候插入蛇頭同時刪除蛇尾。這樣就完美搞定了。

    //移動貪喫蛇
void move_snake()
{
    //監聽鍵盤
    listen_key_borad();
    //蛇頭
    COORDINATE head = m_coordinate[0];
    //direction方向:1 上  2 下  3 左  4 右
    switch (this->m_direction)
    {
    case UP:
        head.y--;
        break;
    case DOWN:
        head.y++;
        break;
    case LEFT:
        head.x--;
        break;
    case RIGHT:
        head.x++;
        break;
    }
    //插入移動後新的蛇頭。是否刪除蛇尾,在後續喫到食物判斷那裏做
    m_coordinate.insert(m_coordinate.begin(), head);
}

4.5 是否喫到食物

判斷是否喫到食物,就是看看蛇頭的座標等不等於食物的座標。如果等於,就重新生成食物,不刪除蛇尾,蛇變長一節。不等於,就刪除蛇尾,蛇長不變。

bool is_eat_food(Food & f)
{
    //獲取食物座標
    COORDINATE food_coordinate = f.GetFoodCoordinate();
    //喫到食物,食物重新生成,不刪除蛇尾
    if (m_coordinate[HEAD].x == food_coordinate.x && m_coordinate[HEAD].y == food_coordinate.y)
    {
        f.RandomXY(m_coordinate);
        return true;
    }
    else
    {
        //沒有喫到食物,刪除蛇尾
        m_coordinate.erase(m_coordinate.end() - 1);
        return false;
    }
}

4.6判斷蛇是否還存活

判斷蛇是否GG,主要是看是否超出邊界,是否碰到自己身體其他部分。

//判斷貪喫蛇死了沒
bool snake_is_alive()
{
    if (m_coordinate[HEAD].x <= 0 ||
        m_coordinate[HEAD].x >= GameSetting::window_width - 29 ||
        m_coordinate[HEAD].y <= 0 ||
        m_coordinate[HEAD].y >= GameSetting::window_height - 1)
    {
        //超出邊界
        m_is_alive = false;
        return m_is_alive;
    }
    //和自己碰到一起
    for (unsigned int i = 1; i < m_coordinate.size(); i++)
    {
        if (m_coordinate[i].x == m_coordinate[HEAD].x && m_coordinate[i].y == m_coordinate[HEAD].y)
        {
            m_is_alive = false;
            return m_is_alive;
        }
    }
    m_is_alive = true;

    return m_is_alive;
}

4.7 畫出貪喫蛇

畫出貪喫蛇比較簡單,gotoxy到身體的每一節,然後cout就行。在此之前設置了顏色爲淺綠色。

//畫出貪喫蛇
void draw_snake()
{
    //設置顏色爲淺綠色
    setColor(10, 0);
    for (unsigned int i = 0; i < this->m_coordinate.size(); i++)
    {
        gotoxy(m_coordinate[i].x, m_coordinate[i].y);
        cout << "*";
    }
    //恢復原來的顏色
    setColor(7, 0);
}

4.8 清除屏幕上的貪喫蛇

我們是死循環不斷刷新打印貪喫蛇的,因此每移動一次,必然會在屏幕上留下上一次貪喫蛇的痕跡。因此我們每次在畫蛇之前,不是添足,而是清理一下上次遺留的蛇身。我們知道,蛇每次移動,變的只有蛇頭和蛇尾,因此該函數我們只需要清理蛇尾就行。gotoxy到蛇尾的座標,cout<<” “;就行。

gotoxy(m_coordinate[this->m_coordinate.size()-1].x, m_coordinate[this->m_coordinate.size() - 1].y);
cout << " ";

05 主函數,組裝我們的遊戲

我們的遊戲在主函數裏面進行組裝。然後開始運行。
首先我們做遊戲相關的初始化和模式選擇。

GameSetting setting;
PrintInfo print_info;
Snake  snake;
//初始化遊戲
setting.GameInit();
//遊戲模式選擇
print_info.DrawChoiceInfo();

char ch = _getch();
switch (ch)
{
case '1':
    snake.set_model(true);
    speed = 50;
    break;
case '2':
    snake.set_model(false);
    break;
default:
    gotoxy(GameSetting::window_width / 2 - 10, GameSetting::window_height / 2 + 3);
    cout << "輸入錯誤,Bye!" << endl;
    cin.get();
    cin.get();
    return 0;
}
gotoxy(GameSetting::window_width / 2 - 10, GameSetting::window_height / 2 + 3);
system("pause");

然後就是畫地圖邊框,打印遊戲相關信息和說明。生成食物了。

//畫地圖
print_info.DrawMap();
print_info.DrawGameInfo(snake.GetModel());
//生成食物
Food food(snake.m_coordinate);

最後就是遊戲死循環,在死循環裏面,我們需要不斷移動蛇,畫蛇,判斷蛇的狀態,判斷食物的狀態,是否喫到食物等等。具體代碼:

//遊戲死循環
while (true)
{
    //打印成績
    print_info.DrawScore(snake.GetSnakeSize());
    //畫出食物
    food.DrawFood();
    //清理蛇尾,每次畫蛇前必做
    snake.ClearSnake();
    //判斷是否喫到食物
    snake.is_eat_food(food);
    //根據用戶模式選擇不同的調度方式
    if (snake.GetModel() == true)
    {
        snake.move_snake();
    }
    else
    {
        snake.AI_find_path(food);
        snake.AI_move_snake();
    }
    //畫蛇
    snake.draw_snake();
    //判斷蛇是否還活着
    if (!snake.snake_is_alive())
    {
        print_info.GameOver(snake.GetSnakeSize());
        break;
    }
    //控制遊戲速度
    Sleep(speed);
}

最終,我們的代碼就可以Run起來了。具體效果放在開頭了。界面算不上好看,但是整個程序向大家展示了最基本最核心的功能和代碼,大家可以在這個基礎上開發自己喜歡的各種美麗的界面哦。

06 AI部分和完善

代碼是畫了幾天間間斷斷寫出來的,水平不算很高,代碼也寫得亂七八糟的。不過代碼會在後期不斷優化,儘量做到精簡優美。至於AI功能,等下一篇博文寫吧。

代碼獲取

欲獲取代碼,請關注我們的微信公衆號【程序猿聲】,在後臺回覆:aisnake。即可下載。

微信公衆號

推薦文章:10分鐘教你用Python做個打飛機小遊戲超詳細教程

推薦文章:10分鐘教你用python下載和拼接微信好友頭像圖片

推薦文章:10分鐘教你用python一行代碼搞點大新聞

推薦文章:10分鐘教你用python打造貪喫蛇超詳細教程

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章