使用C++設計貪食蛇小遊戲
說明:所有程式碼均可在Visual Studio 2013上編譯執行。並未測試在其它編譯器上編譯情況。
遊戲規則
貪食蛇遊戲要求玩家控制方向鍵(或WSAD鍵)來控制小蛇的前進方向,以使蛇吃掉面板上隨即位置上的食物。每次成功吃掉食物後小蛇體長將增加一點,得分增加。當小蛇撞到邊界或者蛇頭與蛇身相撞時,蛇將掛掉,遊戲隨之結束。
遊戲結構設計
遊戲應當包含初始歡迎介面,遊戲介面,遊戲結束介面。
建立一個CGame類,來管理遊戲的進度。該類放在Game.h檔案中,在該類中可以分別定義NewGame(),PlayGame(),SetGame()和ExitGame()四個函式來控制遊戲的各個單元,為了使整個程式看起來更像個遊戲,可以採取更加漂亮的介面來展示遊戲各部分。
- NewGame()函式設定遊戲歡迎介面。可以簡單地輸出了一些方塊字元組成的遊戲名SNAKE和一句提示“Press any key to start…”。點選任意鍵後,遊戲程式將轉入SetGame()中繼續執行。可以加上一些動態效果,讓提示”Press any keyto start…”不斷跳動。
- SetGame()中包括遊戲的設定內容。可以選擇Easy,Normal,Hard三個選項。這三個選項將對應小蛇不同的的移動速度,具體來說將體現在PlayGame()函式中每次迴圈執行速度。設定完成後,遊戲程式將轉入PlayGame()繼續執行。
- PlayGame()函式主體將是一個死迴圈,因為可將遊戲考慮成一個無窮的迴圈,迴圈中迭代的每一步都依次進行:判斷使用者是否輸入、然後根據使用者輸入調整遊戲內容(如果沒有輸入則按預設方式繼續執行遊戲)、判斷是否符合規則(不符合則跳出迴圈,轉入ExitGame()退出遊戲)、判斷是否需要加分扣分。執行完以上這些步驟後,將進行下一次迭代。當然進行遊戲之前,還要執行必要的初始化工作,來顯示大體框架和提示資訊。
- EitGame()中將顯示遊戲得分,並詢問玩家是否再玩一次。這裡拼出了一個骷髏頭的圖案,表示Game Over。
以上為遊戲的主體內容,這四個函式設定了遊戲的基本結構,剩餘部分將繼續考慮細節問題。然後再展示Game.h的細節內容。
建立遊戲物件
先建立一系列類表示遊戲物件,其中應包括對遊戲物件的處理方式(函式)。分析遊戲,可以知道遊戲主體是小蛇和食物。
所有的遊戲物件,包括蛇和食物,都是由控制檯上的一系列點組成的。因此需要很多處理點物件的方法。可建立Point.h來定義CPoint物件,來簡化其他物件的處理。
Point.h檔案內容如下:
#pragma once
#include<iostream>
#include<windows.h>
using std::cout;
using std::cin;
class CPoint
{
public:
CPoint(){}
CPoint(int x, int y)
{
this->x = x;
this->y = y;
}
void Plot(HANDLE hOut)
{
SetOutputPosition(x, y,hOut);
cout << "■";
}
void Clear(HANDLE hOut)
{
SetOutputPosition(x, y, hOut);
cout << " ";
}
void Clear()
{
HANDLE hOut = GetStdHandle(STD_OUTPUT_HANDLE);
cout << " ";
}
//靜態方法,用於設定輸出點在控制檯的位置
static void SetOutputPosition(int x, int y, HANDLE hOut)
{
COORD position;
position.X = x;
position.Y = y;
SetConsoleCursorPosition(hOut, position);
}
bool operator == (CPoint& point)
{
return (point.x==this->x) && (point.y==this->y);
}
//改變CPoint物件的位置
void SetPosition(int x, int y)
{
this->x = x;
this->y = y;
}
int GetX(){ return x; };
int GetY(){ return y; };
private:
int x, y;
};
蛇和食物都是由控制檯面板上的一系列點來表示的。所以這決定了小蛇和食物的類資料成員將用來儲存這些點集的具體資訊。可以建立一個檔案snake.h和food.h來儲存建立的類。
蛇是由一系列點組成,可以用簡單的陣列,連結串列等來儲存這些點。考慮一下蛇運動的方式:蛇整體前進一步,可以考慮為蛇頭根據輸入方向移動一格,然後剩餘所有點都移動到它前一個點之前所在的位置上;也可以表示為在蛇頭前面根據使用者輸入方向新增一個點,蛇尾部移除一個點。前者需要處理構成蛇的所有點,後者只需要處理蛇頭和蛇尾兩個點。很明顯,後者效率更高。這裡採用第二個移動方案,並相應地採取雙端佇列來儲存點的資料。
方便起見,可以使用C++ STL(Standard Template Library)中定義的容器類(雙端佇列)deque<T>來儲存點集,使用push_front()和pop_back()來處理蛇的移動過程。
Snake.h如下所示:
#pragma once
#include<iostream>
#include "Point.h"
#include<deque>
#include "Point.h"
#include "Food.h"
using std::cout;
using std::cin;
using std::deque;
class CSnake
{
public:
enum moveDirection{ UP, LEFT ,DOWN,RIGHT }; //蛇只有四個移動方向,所以可以定義了一個蛇運動方向的列舉型別
CSnake() //蛇初始時將只由兩個點元素組成,初始移動方向設定為向右
{
snake.push_back(CPoint(18, 16));
snake.push_back(CPoint(16, 16));
direction = moveDirection::RIGHT;
}
//蛇整體向前移動一步,即更新點集snake
void move()
{
switch (direction)//根據此時的方向來判斷該如何移動。移動時為提高效率將只需要處理首尾點元素
{
case moveDirection::DOWN:
snake.push_front(CPoint(snake.begin()->GetX(), snake.begin()->GetY() + 1)); break;
case moveDirection::LEFT:
snake.push_front(CPoint(snake.begin()->GetX() - 2, snake.begin()->GetY())); break;
case moveDirection::RIGHT:
snake.push_front(CPoint(snake.begin()->GetX() + 2, snake.begin()->GetY())); break;
case moveDirection::UP:
snake.push_front(CPoint(snake.begin()->GetX(), snake.begin()->GetY() - 1)); break;
}
snake.pop_back();
}
bool ChangeDirection(moveDirection direction)
{
if ((direction + 2) % 4 == this->direction) //玩家輸入方向與蛇當前移動方向相反將不改變此時蛇的前進方向
return false;
else
this->direction = direction;
return true;
}
//把蛇整個地畫出來。為提高效率,該函式應該只在遊戲初始化時呼叫
void PaintSnake(HANDLE hOut)
{
for (CPoint& point : snake)
{
CPoint::SetOutputPosition(point.GetX(), point.GetY(), hOut);
point.Plot(hOut);
}
}
//檢查蛇頭是否和蛇身部分重合,用於判斷是否犯規
bool HitItself()
{
for (CPoint& point : snake)
{
if (point == *snake.begin())
{
if (&point == &(*snake.begin())) //將忽略蛇頭與蛇頭重合的情況。
continue;
else
return true;
}
}
return false;
}
//檢查某點是否和蛇身重合,判斷隨機產生的食物是否放到了蛇身上。
bool Hit(CPoint& point)
{
for (CPoint& pointInSnake : snake)
{
if (point == pointInSnake)
return true;
}
return false;
}
//檢查蛇頭是否撞牆,用於判斷是否犯規
bool HitEdge()
{
int x = snake.begin()->GetX();
int y = snake.begin()->GetY();
if ((x == 0) || (y == 2) || (x == 78) || (y == 24)) //和預設牆的位置進行比較
return true;
else
return false;
}
//經常需要處理蛇頭和蛇尾,所以定義了這兩個函式。
CPoint& Head()
{
return *snake.begin();
}
CPoint& Tail()
{
return *(snake.end()-1);
}
bool Eat(CFood& food)
{
//複雜的判斷語句,用於判斷食物是否在蛇頭移動方向上的前方
int foodx = food.GetPosition().GetX();
int foody = food.GetPosition().GetY();
int headx = Head().GetX();
int heady = Head().GetY();
bool toEat = ((foodx == headx) && (foody == (heady + 1)) && (direction == moveDirection::DOWN))
|| ((foodx == headx) && (foody == (heady - 1)) && (direction == moveDirection::UP))
|| (((foodx + 2) == headx) && (foody == heady) && (direction == moveDirection::LEFT))
|| (((foodx - 2) == headx) && (foody == heady) && (direction == moveDirection::RIGHT));
if(toEat)
{
snake.push_front(food.GetPosition());
return true;
}
else
return false;
}
private:
deque<CPoint> snake;
moveDirection direction;
};
Food.h相比之下就簡單得多。用”$$”來在控制檯上顯示食物。不用“$”的原因是蛇由“■”組成,而“■”在控制檯上在一行上佔了兩個普通字元的位置(無奈)。Food.h如下所示:
#pragma once
#include<iostream>
#include "Point.h"
using std::cout;
class CFood
{
public:
CFood()
{
position.SetPosition(20, 20);
}
CFood(int x, int y)
{
position.SetPosition(x, y);
}
void PlaceFood(int x, int y)
{
position.SetPosition(x, y);
HANDLE hOut = GetStdHandle(STD_OUTPUT_HANDLE);
SetConsoleTextAttribute(hOut, FOREGROUND_BLUE | FOREGROUND_GREEN | FOREGROUND_INTENSITY);
CPoint::SetOutputPosition(x, y, hOut);
cout << "$$";
}
CPoint& GetPosition()
{
return position;
}
void Show()
{
cout << '(' << position.GetX() << ',' << position.GetY() << ')';
}
private:
CPoint position;
};
下面是Game.h的所有內容。由於要輸出很多圖案,Game.h略顯臃腫:
#pragma once
#include<iostream>
#include "Snake.h"
#include "Food.h"
#include "Point.h"
#include "conio.h"
#include<windows.h>
#include<time.h>
using std::endl;
class CSnake;
class CGame
{
public:
CGame()
{
length =80;
width = 24;
score = 0;
exit = false;
}
bool Exit(){ return exit; };
void NewGame()
{
//SMALL_RECT rc = { 0, 0, 80 - 1, 25 - 1 }; // 重置視窗位置和大小
system("cls");
HANDLE hOut = GetStdHandle(STD_OUTPUT_HANDLE);
SetConsoleTextAttribute(hOut, FOREGROUND_GREEN|FOREGROUND_RED);
//system("color 81");
COORD position = { 17, 6 };
SetConsoleCursorPosition(hOut, position);
//設定輸出“SNAKE”
SetConsoleTextAttribute(hOut, FOREGROUND_GREEN | FOREGROUND_INTENSITY);
cout << " ■■■ ■ ■ ■■■ ■ ■ ■■■■";
SetConsoleTextAttribute(hOut, FOREGROUND_GREEN);
CPoint::SetOutputPosition(17, 7, hOut);
cout << "■ ■ ■■ ■ ■ ■ ■ ■ ■";
CPoint::SetOutputPosition(17, 8, hOut);
SetConsoleTextAttribute(hOut, FOREGROUND_RED|FOREGROUND_INTENSITY);
cout << "■ ■ ■ ■ ■ ■ ■ ■ ■";
CPoint::SetOutputPosition(17, 9, hOut);
SetConsoleTextAttribute(hOut, FOREGROUND_BLUE | FOREGROUND_GREEN | FOREGROUND_INTENSITY);
cout << " ■■■ ■ ■ ■ ■■■■ ■■ ■■■";
CPoint::SetOutputPosition(17, 10, hOut);
SetConsoleTextAttribute(hOut, FOREGROUND_BLUE|FOREGROUND_RED|FOREGROUND_INTENSITY);
cout << " ■ ■ ■■ ■ ■ ■ ■ ■";
CPoint::SetOutputPosition(17, 11, hOut);
SetConsoleTextAttribute(hOut, FOREGROUND_GREEN|FOREGROUND_RED|FOREGROUND_INTENSITY);
cout << "■ ■ ■ ■ ■ ■ ■ ■ ■";
CPoint::SetOutputPosition(17, 12, hOut);
SetConsoleTextAttribute(hOut, FOREGROUND_BLUE|FOREGROUND_INTENSITY);
cout << " ■■■ ■ ■ ■ ■ ■ ■ ■■■■";
while (true)
{
CPoint::SetOutputPosition(27, 20, hOut);
cout << "Press any key to start...";
Sleep(70);
for (int i = 0; i < 25; i++)
cout << "\b \b";
Sleep(70);
if (_kbhit())
break;
}
cin.get();
}
void SetGame()
{
char chooseLevel;
system("cls");
HANDLE hOut = GetStdHandle(STD_OUTPUT_HANDLE);
SetConsoleTextAttribute(hOut, FOREGROUND_GREEN | FOREGROUND_INTENSITY);
cout << " ■■■ ■■■■ ■■■ ■■■ ■■ ■ ■ ■■■ ■■■ "<< endl;
SetConsoleTextAttribute(hOut, FOREGROUND_GREEN | FOREGROUND_INTENSITY);
cout << "■ ■ ■ ■ ■ ■ ■■ ■ ■ ■ ■ ■"<< endl;
SetConsoleTextAttribute(hOut, FOREGROUND_RED | FOREGROUND_INTENSITY);
cout << "■ ■ ■ ■ ■ ■ ■ ■ ■ ■ "<< endl;
SetConsoleTextAttribute(hOut, FOREGROUND_BLUE | FOREGROUND_GREEN | FOREGROUND_INTENSITY);
cout << " ■■■ ■■■ ■ ■ ■ ■ ■ ■ ■ ■■ ■■■ "<< endl;
SetConsoleTextAttribute(hOut, FOREGROUND_BLUE | FOREGROUND_RED | FOREGROUND_INTENSITY);
cout << " ■ ■ ■ ■ ■ ■ ■■ ■ ■ ■"<< endl;
SetConsoleTextAttribute(hOut, FOREGROUND_GREEN | FOREGROUND_RED | FOREGROUND_INTENSITY);
cout << "■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■"<< endl;
SetConsoleTextAttribute(hOut, FOREGROUND_BLUE | FOREGROUND_INTENSITY);
cout << " ■■■ ■■■■ ■ ■ ■■ ■ ■ ■■■ ■■■ "<< endl;
CPoint::SetOutputPosition(9, 15, hOut);
cout << "Chose difficulity : " << endl;
cout <<" 1)Easy" << endl
<< " 2)Normal" << endl
<< " 3)Hard" << endl;
while (true)
{
chooseLevel = _getch();
if (chooseLevel == '1' || chooseLevel == '2' || chooseLevel == '3')
{
level = chooseLevel-'0';
switch (level)
{
case 1:
speed = 500;
break;
case 2:
speed = 250;
break;
case 3:
speed = 100;
break;
}
break;
}
}
}
void PaintEdge()
{
HANDLE hOut = GetStdHandle(STD_OUTPUT_HANDLE);
SetConsoleTextAttribute(hOut, FOREGROUND_GREEN| FOREGROUND_INTENSITY);
for (int x = 0; x <= 78; x += 2)
{
for (int y = 0; y < 25; y++)
{
if ((x == 0) || (y == 0) || (x == 78) || (y == 24))
{
CPoint::SetOutputPosition(x, y, hOut);
cout << "■";
}
}
}
for (int x = 2; x <= 78; x+=2)
{
CPoint::SetOutputPosition(x, 2, hOut);
cout << "■";
}
}
void ShowState()
{
HANDLE hOut = GetStdHandle(STD_OUTPUT_HANDLE);
SetConsoleTextAttribute(hOut, BACKGROUND_GREEN| FOREGROUND_RED | BACKGROUND_INTENSITY | FOREGROUND_INTENSITY);
CPoint::SetOutputPosition(2, 1, hOut);
cout << "Difficulity : ";
switch (level)
{
case 1:
cout << "Easy "; break;
case 2:
cout << "Noraml"; break;
case 3:
cout << "Hard "; break;
}
SetConsoleTextAttribute(hOut, FOREGROUND_GREEN | FOREGROUND_INTENSITY);
cout << "■";
SetConsoleTextAttribute(hOut, BACKGROUND_GREEN | FOREGROUND_RED | BACKGROUND_INTENSITY | FOREGROUND_INTENSITY);
cout << "Press WASD to move your snake.";
SetConsoleTextAttribute(hOut, FOREGROUND_GREEN | FOREGROUND_INTENSITY);
cout << "■";
SetConsoleTextAttribute(hOut, BACKGROUND_GREEN | FOREGROUND_RED | BACKGROUND_INTENSITY | FOREGROUND_INTENSITY);
cout << "Score : " << score<<" ";
}
void UpdateScore(bool eaten)
{
if (eaten)
{
HANDLE hOut = GetStdHandle(STD_OUTPUT_HANDLE);
SetConsoleTextAttribute(hOut, BACKGROUND_GREEN | FOREGROUND_RED | BACKGROUND_INTENSITY | FOREGROUND_INTENSITY);
score += 10;
CPoint::SetOutputPosition(58, 1, hOut);
cout << "Score : "<< score;
int spaceUsed=0;
for (int s = score; s != 0; s /= 10)
spaceUsed++;
for (int i = 0; i < 10 - spaceUsed; i++)
cout << ' ';
}
}
bool DirectionChanged()
{
char ch;
if (_kbhit())
{
ch = _getch();
switch (ch)
{
case 'w':case 'W':
return snake.ChangeDirection(snake.UP);
case 'a':case 'A':
return snake.ChangeDirection(snake.LEFT);
case 's':case 'S':
return snake.ChangeDirection(snake.DOWN);
case 'd':case 'D':
return snake.ChangeDirection(snake.RIGHT);
}
}
return false;
}
void RandomFood(CFood& food)
{
srand(static_cast<unsigned>(time(NULL)));
int x, y;
while (true)
{
x = rand() % (length / 2) * 2;
y = rand() % (width / 2) * 2;
if ((x <= 2) || (y <= 6) || (x >= 70) || (y >= 20))
continue;
if (!snake.Hit(CPoint(x, y)))
break;
else
continue;
}
food.PlaceFood(x, y);
}
void PlayGame()
{
//遊戲介面初始化階段
system("cls");
PaintEdge();
ShowState();
food.PlaceFood(20,20); //設定食物初始位置
HANDLE hOut = GetStdHandle(STD_OUTPUT_HANDLE);
snake.PaintSnake(hOut);
while (true)
{
snake.Tail().Clear(hOut);
DirectionChanged();
bool eaten = snake.Eat(food);
if (!eaten)
snake.move();
else
RandomFood(food);
UpdateScore(eaten);
//將當前字型設定為黑底白色
SetConsoleTextAttribute(hOut, FOREGROUND_INTENSITY | FOREGROUND_RED | FOREGROUND_GREEN | FOREGROUND_BLUE);
snake.Head().Plot(hOut);
if (snake.HitEdge() || snake.HitItself())
{
Sleep(1000);
SetConsoleTextAttribute(hOut, FOREGROUND_RED | FOREGROUND_RED);
snake.Head().Plot(hOut);
Sleep(3000);
break;
}
Sleep(speed);
}
}
void GameOver()
{
int x = 28;
int y = 3;
system("cls");
HANDLE hOut = GetStdHandle(STD_OUTPUT_HANDLE);
SetConsoleTextAttribute(hOut, FOREGROUND_GREEN | FOREGROUND_INTENSITY);
CPoint::SetOutputPosition(x, y, hOut);
cout << " ■■■■■" ;
CPoint::SetOutputPosition(x, y+1, hOut);
cout << " ■■■■■■■■ " ;
CPoint::SetOutputPosition(x, y+2, hOut);
cout << " ■■■ ■■■ ■■■ " ;
CPoint::SetOutputPosition(x, y+3, hOut);
cout << " ■■■ ■ ■■■ ";
CPoint::SetOutputPosition(x, y+4, hOut);
cout << " ■■■ ■■ ■■■";
CPoint::SetOutputPosition(x, y+5, hOut);
cout << " ■■■■■■■■■";
CPoint::SetOutputPosition(x, y+6, hOut);
cout << " ■■■■■■■";
CPoint::SetOutputPosition(x, y+7, hOut);
cout << " ■■ ■■ ■■";
CPoint::SetOutputPosition(x, y+8, hOut);
cout << " " ;
CPoint::SetOutputPosition(x, y+9, hOut);
cout << " ■■ ■■ ■■ " ;
CPoint::SetOutputPosition(x, y+10, hOut);
cout << " ■■■■■■";
CPoint::SetOutputPosition(x+7, y + 13, hOut);
cout << "GAME OVER";
CPoint::SetOutputPosition(x + 7, y + 15, hOut);
cout << "Score : " << score;
CPoint::SetOutputPosition(x + 4, y + 17, hOut);
cout << "Play Again?(Y/N)";
char ch;
while (true)
{
ch = _getch();
if (ch == 'n' || ch == 'N')
{
exit = true;
break;
}
else if (ch == 'y' || ch == 'Y')
break;
}
CPoint::SetOutputPosition(x - 1, y + 19, hOut);
}
private:
bool exit;
int level;
int speed;
bool gameOver;
CSnake snake;
CFood food;
int length; //遊戲區域總長度
int width; //遊戲區域總寬度
int score;
};
至此遊戲的設計部分已經完成。最後建立一個Source.cpp檔案,來運行遊戲。遊戲可以理解成一個無盡的迴圈。迴圈之前是個初始化階段,迴圈中進行遊戲,執行以此迴圈後(即玩家玩過一次之後),將根據玩家選擇是否重玩遊戲來判斷這個迴圈是否需要再次執行。因此,Source.cpp的主體將是一個do-while迴圈。
Source.cpp檔案內容如下所示:
#include "Game.h"
#include<windows.h>
#include <iostream>
#include <conio.h>
#include <cstdlib>
using std::cout;
using std::cin;
using std::endl;
int main()
{
SetConsoleTitle(L"貪食蛇"); //用於設定控制檯視窗名
CGame* Game = new CGame();
Game->NewGame();
do
{
delete Game;
Game = new CGame();
Game->SetGame();
Game->PlayGame();
Game->GameOver();
} while (!Game->Exit());
Sleep(2000);
return 0;
}
實際執行結果:
設定介面:
遊戲介面,上面是狀態和提示:
當蛇頭撞牆或者撞到蛇身時會用紅色顯示,然後才會進入Game Over介面:
Game Over介面:
這個遊戲雖然很簡單,但是確實說明了一般遊戲的結構。遊戲其實就是一個無盡的迴圈。
感想:
1.寫程式碼時一定要認真。我剛開始把一個Y寫成了X,由於存在這樣的函式,所以這並沒有語法問題,但很明顯會讓遊戲出現一些奇怪的bug。這花了我數小時來發現這個簡單的拼寫問題。
2.程式效率問題。我剛開始對蛇移動方法的設定和上面所說的並不相同。我在CSnake類中定義了兩個函式分別用來徹底清除控制檯上的現實的蛇和依次顯示構成蛇的所有點。然後在CGame::PlayGame()的迴圈的每一次迭代中都要重新徹底清除上一次迭代時蛇的畫面,然後填充上蛇移動之後的畫面。但是後來進行遊戲時發現,當蛇身比較長時,每移動一步,由於輸出游標的原因,就造成蛇身不斷閃動,當選擇Hard難度模式時,閃動非常明顯,影響了整體體驗。為解決這個問題,我採用每次移動時只處理蛇頭和蛇尾的方法,來提高效率。結果就是,蛇移動時幾乎無法感覺到游標的存在,蛇移動得非常穩定。
2013年12月9日 星期一
歡迎轉載,請註明出處:點選開啟連結