使用C++設計貪食蛇小遊戲__C++

來源:互聯網
上載者:User
說明:所有代碼均可在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;}//蛇整體向前移動一步,即更新點集snakevoid 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;elsethis->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;elsereturn 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;elsereturn 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;}elsereturn 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;elsecontinue;}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();elseRandomFood(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日 星期一


歡迎轉載,請註明出處:點擊開啟連結

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.