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做個打飛機小遊戲超詳細教程