C++編寫貪吃蛇小游戲快速入門
剛學完C++。一時興起,就花幾天時間手動做了個貪吃蛇,后來覺得不過癮,於是又加入了AI功能。希望大家Enjoy It.
效果圖示
AI模式演示
image
image
整體規划+原理
image
大體上可以分為圖上所示的幾個類。不過……怎么看都有點強行面向對象的味道在里面。。[哭笑][哭笑][哭笑]。不管了……代碼寫得可能有點凌亂,下面我會為大家一一講解。
整個程序設計的原理就是:主函數死循環,不斷刷新打印貪吃蛇和食物。這樣每循環一次,就類似電影里面的一幀,最終顯示的效果就是蛇會動起來。
01 初始化工作-游戲設置
游戲設置和相關初始化放在了一個類里面,並進行了靜態聲明。主要設置了游戲窗口的長和款。並在GameInit()函數里面設置了窗口大小,隱藏光標,初始化隨機數種子等。代碼如下:
1//游戲設置相關模塊,把函數都放到一個類里面了。函數定義為static靜態成員,不生成實體也可以直接調用
2class GameSetting
3{
4public:
5 //游戲窗口的長寬
6 static const int window_height = 40;
7 static const int window_width = 80;
8public:
9 static void GameInit()
10 {
11 //設置游戲窗口大小
12 char buffer[32];
13 sprintf_s(buffer, "mode con cols=%d lines=%d",window_width, window_height);
14 system(buffer);
15
16 //隱藏光標
17 HANDLE handle = GetStdHandle(STD_OUTPUT_HANDLE);
18 CONSOLE_CURSOR_INFO CursorInfo;
19 GetConsoleCursorInfo(handle, &CursorInfo);//獲取控制台光標信息
20 CursorInfo.bVisible = false; //隱藏控制台光標
21 SetConsoleCursorInfo(handle, &CursorInfo);//設置控制台光標狀態
22 //初始化隨機數種子
23 srand((unsigned int)time(0));
24 }
25};
用到了幾個相關的Windows API,本文不做過多介紹,大家百度即可。
02 打印信息類
該類主要是用來打印一些游戲相關信息的。該類大體如下:
image
下面挑幾個重點的來講:
2.1 畫地圖邊界
這個函數主要是根據上面所給的游戲窗口長寬來打印地圖邊界的。其中還划分了幾個區域,主要用來放不同的信息的。
1//畫地圖邊界
2static void DrawMap()
3{
4 system("cls");
5 int i, j;
6 for (i = 0; i < GameSetting::window_width; i++)
7 cout << "#";
8 cout << endl;
9 for (i = 0; i < GameSetting::window_height-2; i++)
10 {
11 for (j = 0; j < GameSetting::window_width; j++)
12 {
13 if (i == 13 && j >= GameSetting::window_width - 29)
14 {
15 cout << "#";
16 continue;
17 }
18
19 if (j == 0 || j == GameSetting::window_width - 29 || j == GameSetting::window_width-1)
20 {
21 cout << "#";
22 }
23 else
24 cout << " ";
25
26 }
27 cout << endl;
28 }
29 for (i = 0; i < GameSetting::window_width; i++)
30 cout << "#";
31
32}
划分區域如下圖,#就是邊框了:
image
2.2 畫出分數和模式
該函數主要是在右上角畫出成績和游戲模式的,在繪制之前會進行刷新處理。先清除,再重新打印。用到了一個gotoxy()函數。這個函數主要是移動光標到(x, y)坐標處的。關於(x, y)的位置,根據實際情況調整即可。
1//畫分數
2static void DrawScore(int score)
3{
4 gotoxy(GameSetting::window_width - 22+14, 6);
5 cout << " ";
6 gotoxy(GameSetting::window_width - 22+14, 4);
7 cout << " ";
8
9 gotoxy(GameSetting::window_width - 22, 6);
10 cout << "當前玩家分數: " << score << endl;
11 gotoxy(GameSetting::window_width - 22, 4);
12 cout << "當前游戲速度: " << 10 - speed / 25 << endl;
13
14}
03 食物類
食物類定義了食物的坐標,隨機生成規則,和畫出食物等一系列操作。其中食物坐標我們用了一個結構體:
1typedef struct
2{
3 int x;
4 int y;
5}COORDINATE;
該結構體兩個成員,分別保存坐標的(x, y)。蛇身的坐標也會用到這個結構體。
有關食物類的大體如下:
image
下面我們還是挑幾個重點來講。
3.1 隨機生成食物
隨機生成食物,原則上不允許食物出現在蛇身的位置上,如果有。我們重新生成。注意地圖的范圍,就是區域左邊一塊。實際情況根據自身的地圖范圍來調整食物坐標的范圍,注意不要越界。用rand()函數獲得隨機坐標。代碼如下:
1void RandomXY(vector<COORDINATE> & coord)
2{
3 m_coordinate.x = rand() % (GameSetting::window_width - 30) + 1;
4 m_coordinate.y = rand() % (GameSetting::window_height - 2) + 1;
5 unsigned int i;
6 //原則上不允許食物出現在蛇的位置上,如果有,重新生成
7 for (i = 0; i < coord.size(); i++)
8 {
9 //食物出現在蛇身的位置上。重新生成
10 if (coord[i].x == m_coordinate.x && coord[i].y == m_coordinate.y)
11 {
12 m_coordinate.x = rand() % (GameSetting::window_width - 30) + 1;
13 m_coordinate.y = rand() % (GameSetting::window_height - 2) + 1;
14 i = 0;
15 }
16 }
17}
然后,在構造函數里面傳入蛇身的坐標。即可生成食物。
3.2 畫出食物
畫出食物比較簡單了,gotoxy到隨機生成的坐標之后,cout就行。我們在這還設置了一個食物顏色為紅色。代碼如下:
1void DrawFood()
2{
3 setColor(12, 0);
4 gotoxy(m_coordinate.x, m_coordinate.y);
5 cout << "@";
6 setColor(7, 0);
7}
04 貪吃蛇類
定義貪吃蛇的移動,打印,吃食物等等。這節課我們暫時不討論AI功能,先把手動操作的貪吃蛇做了跑起來,下節課再做AI功能的介紹。該類大體如下:
image
4.1 成員變量
成員變量m_direction記錄每次移動的方向。m_is_alive記錄貪吃蛇是否還活着。m_coordinate則是貪吃蛇身體坐標的記錄。貪吃蛇是一節一節的,整條蛇必然是由許多節組成的。因此用了一個vector來存儲蛇身,每節類型是COORDINATE結構體的。
4.2 默認構造函數
默認構造函數Snake()里面主要是做了初始貪吃蛇的生成,以及移動方向的定義等。初始的蛇為3節。在中間位置,向上移動。代碼如下:
1 //移動方向向上
2 m_direction = 1;
3 m_is_alive = true;
4 COORDINATE snake_head;
5 //蛇頭生成位置
6 snake_head.x = GameSetting::window_width / 2 - 15;
7 snake_head.y = GameSetting::window_height / 2;
8
9 this->m_coordinate.push_back(snake_head);
10 snake_head.y++;
11 this->m_coordinate.push_back(snake_head);
12 snake_head.y++;
13 this->m_coordinate.push_back(snake_head); //初始蛇身長度三節
4.3 監聽鍵盤
監聽鍵盤用了C里面的一個庫函數。_kbhit()非阻塞函數,可以不斷監聽鍵盤的情況從而不產生阻塞。有鍵盤按下的時候,就獲取按下的鍵盤是哪個。然后做出相應的變化,其實是方向的調整。需要注意的是,當我們的蛇往上走的時候,按下方向的鍵,我們是不做處理的。其它方向一樣。還有一個調整游戲速度的,speed是休眠時間,speed越小,速度越快。反之速度越慢。
1 //監聽鍵盤
2void listen_key_borad()
3{
4 char ch;
5
6 if (_kbhit()) //kbhit 非阻塞函數
7 {
8 ch = _getch(); //使用 getch 函數獲取鍵盤輸入
9 switch (ch)
10 {
11 case 'w':
12 case 'W':
13 if (this->m_direction == DOWN)
14 break;
15 this->m_direction = UP;
16 break;
17 case 's':
18 case 'S':
19 if (this->m_direction == UP)
20 break;
21 this->m_direction = DOWN;
22 break;
23 case 'a':
24 case 'A':
25 if (this->m_direction == RIGHT)
26 break;
27 this->m_direction = LEFT;
28 break;
29 case 'd':
30 case 'D':
31 if (this->m_direction == LEFT)
32 break;
33 this->m_direction = RIGHT;
34 break;
35 case '+':
36 if (speed >= 25)
37 {
38 speed -= 25;
39 }
40 break;
41 case '-':
42 if (speed < 250)
43 {
44 speed += 25;
45 }
46 break;
47 }
48 }
49}
4.4 移動貪吃蛇
移動貪吃蛇,我們用了一個方向變量,在監聽鍵盤的時候獲取移動的方向,然后在根據方向移動貪吃蛇的蛇頭。這里的移動我們是這樣處理的,首先,貪吃蛇每移動一次,需要改變的只有蛇頭和蛇尾兩節。我們只需要把新的蛇頭插進去,最后再畫出來就可以了。至於蛇尾,如果我們不刪除蛇尾的話,蛇會不斷變長的。因此我們的做法是:吃到食物的時候插入蛇頭而不刪除蛇尾,沒有吃到食物的時候插入蛇頭同時刪除蛇尾。這樣就完美搞定了。
1 //移動貪吃蛇
2void move_snake()
3{
4 //監聽鍵盤
5 listen_key_borad();
6 //蛇頭
7 COORDINATE head = m_coordinate[0];
8 //direction方向:1 上 2 下 3 左 4 右
9 switch (this->m_direction)
10 {
11 case UP:
12 head.y--;
13 break;
14 case DOWN:
15 head.y++;
16 break;
17 case LEFT:
18 head.x--;
19 break;
20 case RIGHT:
21 head.x++;
22 break;
23 }
24 //插入移動后新的蛇頭。是否刪除蛇尾,在后續吃到食物判斷那里做
25 m_coordinate.insert(m_coordinate.begin(), head);
26}
4.5 是否吃到食物
判斷是否吃到食物,就是看看蛇頭的坐標等不等於食物的坐標。如果等於,就重新生成食物,不刪除蛇尾,蛇變長一節。不等於,就刪除蛇尾,蛇長不變。
1bool is_eat_food(Food & f)
2{
3 //獲取食物坐標
4 COORDINATE food_coordinate = f.GetFoodCoordinate();
5 //吃到食物,食物重新生成,不刪除蛇尾
6 if (m_coordinate[HEAD].x == food_coordinate.x && m_coordinate[HEAD].y == food_coordinate.y)
7 {
8 f.RandomXY(m_coordinate);
9 return true;
10 }
11 else
12 {
13 //沒有吃到食物,刪除蛇尾
14 m_coordinate.erase(m_coordinate.end() - 1);
15 return false;
16 }
17}
4.6判斷蛇是否還存活
判斷蛇是否GG,主要是看是否超出邊界,是否碰到自己身體其他部分。
1//判斷貪吃蛇死了沒
2bool snake_is_alive()
3{
4 if (m_coordinate[HEAD].x <= 0 ||
5 m_coordinate[HEAD].x >= GameSetting::window_width - 29 ||
6 m_coordinate[HEAD].y <= 0 ||
7 m_coordinate[HEAD].y >= GameSetting::window_height - 1)
8 {
9 //超出邊界
10 m_is_alive = false;
11 return m_is_alive;
12 }
13 //和自己碰到一起
14 for (unsigned int i = 1; i < m_coordinate.size(); i++)
15 {
16 if (m_coordinate[i].x == m_coordinate[HEAD].x && m_coordinate[i].y == m_coordinate[HEAD].y)
17 {
18 m_is_alive = false;
19 return m_is_alive;
20 }
21 }
22 m_is_alive = true;
23
24 return m_is_alive;
25}
4.7 畫出貪吃蛇
畫出貪吃蛇比較簡單,gotoxy到身體的每一節,然后cout就行。在此之前設置了顏色為淺綠色。
1//畫出貪吃蛇
2void draw_snake()
3{
4 //設置顏色為淺綠色
5 setColor(10, 0);
6 for (unsigned int i = 0; i < this->m_coordinate.size(); i++)
7 {
8 gotoxy(m_coordinate[i].x, m_coordinate[i].y);
9 cout << "*";
10 }
11 //恢復原來的顏色
12 setColor(7, 0);
13}
4.8 清除屏幕上的貪吃蛇
我們是死循環不斷刷新打印貪吃蛇的,因此每移動一次,必然會在屏幕上留下上一次貪吃蛇的痕跡。因此我們每次在畫蛇之前,不是添足,而是清理一下上次遺留的蛇身。我們知道,蛇每次移動,變的只有蛇頭和蛇尾,因此該函數我們只需要清理蛇尾就行。gotoxy到蛇尾的坐標,cout<<" ";就行。
1gotoxy(m_coordinate[this->m_coordinate.size()-1].x, m_coordinate[this->m_coordinate.size() - 1].y);
2cout << " ";
05 主函數,組裝我們的游戲
我們的游戲在主函數里面進行組裝。然后開始運行。
首先我們做游戲相關的初始化和模式選擇。
1GameSetting setting;
2PrintInfo print_info;
3Snake snake;
4//初始化游戲
5setting.GameInit();
6//游戲模式選擇
7print_info.DrawChoiceInfo();
8
9char ch = _getch();
10switch (ch)
11{
12case '1':
13 snake.set_model(true);
14 speed = 50;
15 break;
16case '2':
17 snake.set_model(false);
18 break;
19default:
20 gotoxy(GameSetting::window_width / 2 - 10, GameSetting::window_height / 2 + 3);
21 cout << "輸入錯誤,Bye!" << endl;
22 cin.get();
23 cin.get();
24 return 0;
25}
26gotoxy(GameSetting::window_width / 2 - 10, GameSetting::window_height / 2 + 3);
27system("pause");
然后就是畫地圖邊框,打印游戲相關信息和說明。生成食物了。
1//畫地圖
2print_info.DrawMap();
3print_info.DrawGameInfo(snake.GetModel());
4//生成食物
5Food food(snake.m_coordinate);
最后就是游戲死循環,在死循環里面,我們需要不斷移動蛇,畫蛇,判斷蛇的狀態,判斷食物的狀態,是否吃到食物等等。具體代碼:
1//游戲死循環
2while (true)
3{
4 //打印成績
5 print_info.DrawScore(snake.GetSnakeSize());
6 //畫出食物
7 food.DrawFood();
8 //清理蛇尾,每次畫蛇前必做
9 snake.ClearSnake();
10 //判斷是否吃到食物
11 snake.is_eat_food(food);
12 //根據用戶模式選擇不同的調度方式
13 if (snake.GetModel() == true)
14 {
15 snake.move_snake();
16 }
17 else
18 {
19 snake.AI_find_path(food);
20 snake.AI_move_snake();
21 }
22 //畫蛇
23 snake.draw_snake();
24 //判斷蛇是否還活着
25 if (!snake.snake_is_alive())
26 {
27 print_info.GameOver(snake.GetSnakeSize());
28 break;
29 }
30 //控制游戲速度
31 Sleep(speed);
32}
最終,我們的代碼就可以Run起來了。具體效果放在開頭了。界面算不上好看,但是整個程序向大家展示了最基本最核心的功能和代碼,大家可以在這個基礎上開發自己喜歡的各種美麗的界面哦。
06 AI部分和完善
代碼是畫了幾天間間斷斷寫出來的,水平不算很高,代碼也寫得亂七八糟的。不過代碼會在后期不斷優化,盡量做到精簡優美。至於AI功能,等下一篇博文寫吧。
代碼獲取
欲獲取代碼,請關注我們的微信公眾號【程序猿聲】,在后台回復:aisnake。即可下載。
微信公眾號
推薦文章:10分鍾教你用Python做個打飛機小游戲超詳細教程
