前言:
貪吃蛇游戲大家都玩過吧?這次我們要用C語言來親手制作一個!這個項目不僅能讓我們復習C語言的知識,還能了解游戲是怎么一步步做出來的。我們會一起完成蛇的移動、食物的生成,還有碰撞檢測等有趣的部分。準備好了嗎?讓我們一起開始這個簡單又好玩的貪吃蛇小項目吧!
一、游戲背景
貪吃蛇是久負盛名的游戲,它也和俄羅斯?塊,掃雷等游戲位列經典游戲的?列。 在編程語?的教學中,我們以貪吃蛇為例,從設計到代碼實現來提升編程能?和邏輯能?。
二、游戲效果演示
https://live.csdn.net/v/389301
三、項目目標
使?C語?在Windows環境的控制臺中模擬實現經典?游戲貪吃蛇。
實現基本的功能:
貪吃蛇地圖繪制
蛇吃?物的功能 (上、下、左、右?向鍵控制蛇的動作)
蛇撞墻死亡
蛇撞??死亡
計算得分
蛇?加速、減速
暫停游戲
四、技術要點
C語?函數、枚舉、結構體、動態內存管理、預處理指令、鏈表、Win32 API等。
如果大家不了解Win32 API是什么的請看Win32 API介紹,如果會的可以跳過這部分的知識。
五. Win32 API介紹
本次實現貪吃蛇會使?到的?些Win32 API知識,接下來我們就學習?下。
1.Win32 API
Windows 這個多作業系統除了協調應?程序的執?、分配內存、管理資源之外, 它同時也是?個很? 的服務中?,調?這個服務中?的各種服務(每?種服務就是?個函數),可以幫應?程序達到開啟 視窗、描繪圖形、使?周邊設備等?的,由于這些函數服務的對象是應?程序(Application), 所以便 稱之為 Application Programming Interface,簡稱 API 函數。WIN32 API也就是Microsoft Windows 32位平臺的應?程序編程接?。
2.控制臺程序
控制臺程序 平常我們運?起來的?框程序其實就是控制臺程序 我們可以使?cmd命令來設置控制臺窗?的?寬:設置控制臺窗?的??,30?,100列
mode con cols=100 lines=30
也可以通過命令設置控制臺窗?的名字:
title
這些能在控制臺窗?執?的命令,也可以調?C語?函數system來執?。
例如:
#include <stdio.h>
int main()
{//設置控制臺窗?的?寬:設置控制臺窗?的??,30?,100列system("mode con cols=100 lines=30");//設置cmd窗?名稱system("title 貪吃蛇");return 0;
}
3.控制臺屏幕上的坐標COORD
COORD 是Windows API中定義的?個結構體,表??個字符在控制臺屏幕幕緩沖區上的坐標,坐標系 (0,0) 的原點位于緩沖區的頂部左側單元格。
COORD類型的聲明:
typedef struct _COORD {SHORT X;SHORT Y;
} COORD, *PCOORD;
給坐標賦值:
COORD pos = { 10, 15 };
4.GetStdHandle
GetStdHandle是?個Windows API函數。它?于從?個特定的標準設備(標準輸?、標準輸出或標 準錯誤)中取得?個句柄(?來標識不同設備的數值),使?這個句柄可以操作設備。
HANDLE GetStdHandle(DWORD nStdHandle);
實例:
HANDLE hOutput = NULL;
//獲取標準輸出的句柄(?來標識不同設備的數值)
hOutput = GetStdHandle(STD_OUTPUT_HANDLE);
它的類型為HANDLE
5.GetConsoleCursorInfo
檢索有關指定控制臺屏幕緩沖區的光標??和可?性的信息
PCONSOLE_CURSOR_INFO 是指向 CONSOLE_CURSOR_INFO 結構的指針,該結構接收有關主機游標 (光標)的信息
實例:
HANDLE hOutput = NULL;
//獲取標準輸出的句柄(?來標識不同設備的數值)
hOutput = GetStdHandle(STD_OUTPUT_HANDLE);
CONSOLE_CURSOR_INFO CursorInfo;
GetConsoleCursorInfo(hOutput, &CursorInfo);//獲取控制臺光標信息
1.初始化一個句柄變量hOutput
為NULL
2.獲取標準輸出的句柄,并將這個句柄的值賦值給變量?hOutput
3.定義一個CONSOLE_CURSOR_INFO
結構體變量CursorInfo
。這個結構體用于存儲有關控制臺光標的信息,如光標的大小和可見性。
4.使用GetConsoleCursorInfo
函數來獲取當前控制臺光標的信息,并將這些信息存儲在CursorInfo
結構體中。函數的第一個參數是控制臺輸出的句柄(在這里是hOutput
),第二個參數是一個指向CONSOLE_CURSOR_INFO
結構體的指針,用于存儲獲取到的光標信息。
5.1CONSOLE_CURSOR_INFO
這個結構體,包含有關控制臺光標的信息
typedef struct _CONSOLE_CURSOR_INFO {DWORD dwSize;BOOL bVisible;
} CONSOLE_CURSORINFO, *PCONSOLE_CURSOR_INFO;
dwSize:由光標填充的字符單元格的百分?。 此值介于1到100之間。 光標外觀會變化,范圍從完 全填充單元格到單元底部的?平線條。
bVisible:游標的可?性。 如果光標可?,則此成員為 TRUE。
CursorInfo.bVisible = false; //隱藏控制臺光標
CursorInfo.dwSize = 100;//修改光標的占比
5.2 SetConsoleCursorInfo
設置指定控制臺屏幕緩沖區的光標的??和可?性
BOOL WINAPI SetConsoleCursorInfo(HANDLE hConsoleOutput,const CONSOLE_CURSOR_INFO *lpConsoleCursorInfo
);
實例:
int main()
{HANDLE hOutput = GetStdHandle(STD_OUTPUT_HANDLE);//影藏光標操作CONSOLE_CURSOR_INFO CursorInfo;GetConsoleCursorInfo(hOutput, &CursorInfo);//獲取控制臺光標信息CursorInfo.bVisible = false; //隱藏控制臺光標//修改光標的占比CursorInfo.dwSize = 100;//光標的大小SetConsoleCursorInfo(hOutput, &CursorInfo);//設置控制臺光標狀態system("pause");return 0;
}
?這是默認光標:
?隱藏控制臺光標
?
?修改光標的占比
6. SetConsoleCursorPosition
設置指定控制臺屏幕緩沖區中的光標位置,我們將想要設置的坐標信息放在COORD類型的pos中,調 ?SetConsoleCursorPosition函數將光標位置設置到指定的位置。
BOOL WINAPI SetConsoleCursorPosition(HANDLE hConsoleOutput,COORD pos
);
實例:
COORD pos = { 10, 5};HANDLE hOutput = NULL;//獲取標準輸出的句柄(?來標識不同設備的數值)hOutput = GetStdHandle(STD_OUTPUT_HANDLE);//設置標準輸出上光標的位置為posSetConsoleCursorPosition(hOutput, pos);
SetPos:封裝?個設置光標位置的函數
//設置光標的坐標
void SetPos(short x, short y)
{COORD pos = { x, y };HANDLE hOutput = NULL;//獲取標準輸出的句柄(?來標識不同設備的數值)hOutput = GetStdHandle(STD_OUTPUT_HANDLE);//設置標準輸出上光標的位置為posSetConsoleCursorPosition(hOutput, pos);
}
之后我們想要定位光標的位置調用這個函數即可
7.GetAsyncKeyState
獲取按鍵情況,GetAsyncKeyState的函數原型如下:
SHORT GetAsyncKeyState(int vKey
);
將鍵盤上每個鍵的虛擬鍵值傳遞給函數,函數通過返回值來分辨按鍵的狀態。
GetAsyncKeyState 的返回值是short類型,在上?次調? GetAsyncKeyState 函數后,如果 返回的16位的short數據中,最?位是1,說明按鍵的狀態是按下,如果最?是0,說明按鍵的狀態是抬 起;如果最低位被置為1則說明,該按鍵被按過,否則為0。 如果我們要判斷?個鍵是否被按過,可以檢測GetAsyncKeyState返回值的最低值是否為1.
#define KEY_PRESS(VK) ( (GetAsyncKeyState(VK) & 0x1) ? 1 : 0 )
實例:檢測數字鍵
#define KEY_PRESS(vk) ((GetAsyncKeyState(vk)&1)?1:0)int main()
{while (1){if (KEY_PRESS(0x30)){printf("0\n");}else if (KEY_PRESS(0x31)){printf("1\n");}else if (KEY_PRESS(0x32)){printf("2\n");}else if (KEY_PRESS(0x33)){printf("3\n");}else if (KEY_PRESS(0x34)){printf("4\n");}else if (KEY_PRESS(0x35)){printf("5\n");}else if (KEY_PRESS(0x36)){printf("6\n");}else if (KEY_PRESS(0x37)){printf("7\n");}else if (KEY_PRESS(0x38)){printf("8\n");}else if (KEY_PRESS(0x39)){printf("9\n");}}return 0;
}
參考虛擬鍵碼:虛擬鍵碼 (Winuser.h) - Win32 apps | Microsoft Learn
Win32 API介紹到此結束,接下來我們開始講解貪吃蛇,實現貪吃蛇小游戲
六、貪吃蛇游戲設計與分析
1.地圖
我們最終的貪吃蛇?綱要是這個樣?,那我們的地圖如何布置呢?
這?不得不講?下控制臺窗?的?些知識,如果想在控制臺的窗?中指定位置輸出信息,我們得知道 該位置的坐標,所以?先介紹?下控制臺窗?的坐標知識。
控制臺窗?的坐標如下所?,橫向的是X軸,從左向右依次增?,縱向是Y軸,從上到下依次增?。?
在游戲地圖上,我們打印墻體使?寬字符:□,打印蛇使?寬字符●,打印?物使?寬字符★ 普通的字符是占?個字節的,這類寬字符是占?2個字節。
使用寬字符需要設置本地模式
setlocale(LC_ALL, " ");//切換到本地環境
切換到我們的本地模式后就?持寬字符(漢字)的輸出等。
普通字符和寬字符打印出寬度的展?如下:
1.1 地圖坐標
我們假設實現?個棋盤27?,58列的棋盤(?和列可以根據??的情況修改),再圍繞地圖畫出墻, 如下:
2.蛇?和?物
初始化狀態,假設蛇的?度是5,蛇?的每個節點是●,在固定的?個坐標處,?如(24, 5)處開始出現 蛇,連續5個節點。
注意:蛇的每個節點的x坐標必須是2個倍數,否則可能會出現蛇的?個節點有?半?出現在墻體中, 另外?般在墻外的現象,坐標不好對?。
關于?物,就是在墻體內隨機?成?個坐標(x坐標必須是2的倍數),坐標不能和蛇的?體重合,然 后打印★。
3.數據結構設計
在游戲運?的過程中,蛇每次吃?個?物,蛇的?體就會變??節,如果我們使?鏈表存儲蛇的信 息,那么蛇的每?節其實就是鏈表的每個節點。每個節點只要記錄好蛇?節點在地圖上的坐標就?, 所以蛇節點結構如下:
/蛇身的節點類型
typedef struct SnakeNode
{//坐標int x;int y;//指向下一個節點的指針struct SnakeNode* next;
}SnakeNode,*pSnakeNode;
要管理整條貪吃蛇,我們再封裝?個Snake的結構來維護整條貪吃蛇:
//貪吃蛇
typedef struct Snake
{pSnakeNode _pSnake;//指向蛇頭的指針pSnakeNode _pFood;//指向食物節點的指針enum DIRECTION _dir;//蛇的方向enum GAME_STATUS _status;//游戲的狀態int _food_weight; //一個食物的分數int _score; //總成績int _sleep_time; //休息時間,時間越短,速度越快,時間越長、速度越慢
}Snake,*pSnake;
蛇的?向,可以??列舉,使?枚舉
enum GAME_STATUS
{OK, //正常KILL_BY_WALL, //撞墻KILL_BY_SELF, //撞到自己END_NORMAL //正常退出
};
游戲狀態,可以??列舉,使?枚舉
//游戲狀態
enum GAME_STATUS
{OK,//正常運?KILL_BY_WALL,//撞墻KILL_BY_SELF,//咬到??END_NOMAL//正常結束
};
4.游戲流程設計
游戲主邏輯 程序開始就設置程序?持本地模式,然后進?游戲的主邏輯。
主邏輯分為3個過程:
游戲開始(GameStart)完成游戲的初始化
游戲運?(GameRun)完成游戲運?邏輯的實現
游戲結束(GameEnd)完成游戲結束的說明,實現資源釋放
5.核?邏輯實現分析
游戲開始(GameStart)
這個模塊完成游戲的初始化任務:?
初始化游戲
1. 打印環境界面
2. 功能介紹
3. 繪制地圖
4. 創建蛇
5. 創建食物
6. 設置游戲的相關信息
第一步:我們先用Windows命令設置控制臺的大小,然后在給窗口改個名字,再把控制臺的光標給隱藏了,光標的隱藏上面我們已經講過了。
//游戲的初始化
void GameStart(pSnake ps)
{//設置控制臺窗?的??,30?,100列//mode 為DOS命令system("mode con cols= 100 lines=30");//給窗口改個名字system("title 貪吃蛇");HANDLE houtput = GetStdHandle(STD_OUTPUT_HANDLE);//隱藏光標操作CONSOLE_CURSOR_INFO CursorInfo;GetConsoleCursorInfo(houtput, &CursorInfo);//獲取控制臺光標信息CursorInfo.bVisible = false;//隱藏控制臺光標SetConsoleCursorInfo(houtput, &CursorInfo);//設置控制臺光標狀態//1.打印環境界面和功能介紹WelcomeToGame();//2. 繪制地圖CreateMap();//3. 創建蛇InitSnake(ps);//4. 創建食物CreateFood(ps);}
第二步:打印環境界面和功能介紹
我們把它單獨封住成一個函數?
//歡迎界面的打印
void WelcomeToGame()
{SetPos(40, 14);wprintf(L"歡迎來到貪吃蛇小游戲\n");SetPos(42, 20);system("pause");system("cls");SetPos(25, 14);wprintf(L"用 ↑. ↓ . ← . → 來控制蛇的移動,按F3加速,F4減速\n");SetPos(25,15);wprintf(L"加速能夠得到更高的分數\n");SetPos(42, 20);system("pause");system("cls");
}
這里面我們需要定位光標的位置,所以我們單獨封裝一個函數來定位光標位置
void SetPos(short x, short y)
{//獲得標準輸出設備的句柄HANDLE houtput = NULL;houtput = GetStdHandle(STD_OUTPUT_HANDLE);//定義光標的位置COORD pos = { x,y };SetConsoleCursorPosition(houtput, pos);
}
第三步:繪制地圖
創建地圖就是將墻打印出來,因為是寬字符打印,所有使?wprintf函數,打印格式串前使?L
打印地圖的關鍵是要算好坐標,才能在想要的位置打印墻體。
墻體打印的寬字符:
#define WALL L'□' //在頭文件中定義一個宏,之后打印□可以直接用WALL
易錯點:就是坐標的計算
上:(0,0)到(56,0)
下:(0,26)到(56,26)
左:(0,1)到(0,25)
右:(56,1)到(56,25)
繪制地圖我們也給它單獨封成一個函數
//創建地圖
void CreateMap()
{//上(0,0)-(56, 0)int i = 0;for (i = 0; i < 29; i++){wprintf(L"%lc", WALL);}//下(0,26)-(56, 26)SetPos(0, 26);for (i = 0; i < 29; i++){wprintf(L"%lc", WALL);}//左//x是0,y從1開始增?for (i = 1; i <= 25; i++){SetPos(0, i);wprintf(L"%lc", WALL);}//右//x是56,y從1開始增?for (i = 1; i <= 25; i++){SetPos(56, i);wprintf(L"%lc", WALL);}
}
第四步:創建蛇
蛇最開始?度為5節,每節對應鏈表的?個節點,蛇?的每?個節點都有??的坐標。 創建5個節點,然后將每個節點存放在鏈表中進?管理。
創建完蛇?后,將蛇的每?節打印在屏幕上。
蛇的初始位置從 (24,5) 開始。
再設置當前游戲的狀態,蛇移動的速度,默認的?向,初始成績,每個?物的分數。
? 游戲狀態是:OK
? 蛇的移動速度:200毫秒
? 蛇的默認?向:RIGHT(右)
? 初始成績:0
? 每個?物的分數:10
蛇?打印的寬字符:
#define BODY L'●' //頭文件中
void InitSnake(pSnake ps)
{int i = 0;pSnakeNode cur = NULL;for (i = 0; i < 5; i++){cur = (pSnakeNode)malloc(sizeof(SnakeNode));if (cur == NULL){perror("InitSnake()::malloc()");return;}cur->next = NULL;cur->x = POS_X + 2 * i;cur->y = POS_Y;//頭插法插入鏈表 if (ps->_pSnake == NULL)//空鏈表{ps->_pSnake = cur;}else//非空{cur->next = ps->_pSnake;ps->_pSnake = cur;}}cur = ps->_pSnake;while (cur){SetPos(cur->x, cur->y);wprintf(L"%lc", BODY);cur = cur->next;}//設置貪吃蛇的屬性ps->_dir = RIGHT;//默認從右ps->_score = 0;//分數ps->_food_weight = 10;//一個食物的分數ps->_sleep_time = 200;//單位是毫秒ps->_status = OK;}
這里我們使用頭插的方式
第五步:創建?物
先隨機?成?物的坐標,x坐標必須是2的倍數?,?物的坐標不能和蛇?每個節點的坐標重復
創建?物節點,打印?物
?物打印的寬字符:
#define FOOD L'★'
void CreateFood(pSnake ps)
{int x = 0;int y = 0;//生成x是2的倍數//x:2~52//y:1~25
again:do{x = rand() % 53 + 2;y = rand() % 25 + 1;} while (x % 2 != 0);//x和y的坐標不能和蛇的身體坐標沖突pSnakeNode cur = ps->_pSnake;while (cur){if (x == cur->x && y == cur->y){goto again;}cur = cur->next;}//創建食物的節點pSnakeNode pFood = (pSnakeNode)malloc(sizeof(SnakeNode));if (pFood == NULL){perror("CreateFood()::malloc()");return;}pFood->x = x;pFood->y = y;pFood->next = NULL;SetPos(x, y);//定位位置wprintf(L"%lc", FOOD);ps->_pFood = pFood;}
游戲運?(GameRun)
游戲運?期間,右側打印幫助信息,提?玩家,坐標開始位置(64, 15)
根據游戲狀態檢查游戲是否繼續,如果是狀態是OK,游戲繼續,否則游戲結束。
如果游戲繼續,就是檢測按鍵情況,確定蛇下?步的?向,或者是否加速減速,是否暫停或者退出游 戲。
需要的虛擬按鍵的羅列:
? 上:VK_UP
? 下:VK_DOWN
? 左:VK_LEFT
? 右:VK_RIGHT
? 空格:VK_SPACE
? ESC:VK_ESCAPE
? F3:VK_F3
? F4:VK_F4
確定了蛇的?向和速度,蛇就可以移動了。
//檢測按鍵狀態,我們封裝了?個宏
#define KEY_PRESS(VK) ((GetAsyncKeyState(VK)&0x1) ? 1 : 0)
void GameRun(pSnake ps)
{//打印幫助信息PrintHelpInfo();do{//打印總分數和食物的分值SetPos(64, 10);printf("總分數:%d\n", ps->_score);SetPos(64, 11);printf("當前食物的分數:%2d\n", ps->_food_weight);//按鍵if (KEY_PRESS(VK_UP) && ps->_dir != DOWN){ps->_dir = UP;}else if (KEY_PRESS(VK_DOWN) && ps->_dir != UP){ps->_dir = DOWN;}else if (KEY_PRESS(VK_LEFT) && ps->_dir != RIGHT){ps->_dir = LEFT;}else if (KEY_PRESS(VK_RIGHT) && ps->_dir != LEFT){ps->_dir = RIGHT;}else if (KEY_PRESS(VK_SPACE)){//空格暫停Pause();}else if (KEY_PRESS(VK_ESCAPE)){//正常退出游戲ps->_status = END_NORMAL;}else if (KEY_PRESS(VK_F3)){//加速if (ps->_sleep_time > 80){ps->_sleep_time -= 30;ps->_food_weight += 2;}}else if (KEY_PRESS(VK_F4)){//減速if (ps->_food_weight > 2){ps->_sleep_time += 30;ps->_food_weight -= 2;}}//蛇每次?定之間要休眠的時間,時間短,蛇移動速度就快SnakeMove(ps);//蛇走一步的過程Sleep(ps->_sleep_time);}while (ps->_status == OK);}
如果我們按了空格鍵那么就需要暫停游戲,在按一下空格就需要開始游戲,我們把這個函數單獨封裝起來
void Pause()
{while (1){Sleep(200);if (KEY_PRESS(VK_SPACE)){break;}}
}
打印幫助信息我們把它封裝成一個函數
void PrintHelpInfo()
{SetPos(64, 14);wprintf(L"%ls", L"不能穿墻,不能咬到自己");SetPos(64, 15);wprintf(L"%ls", L"用 ↑. ↓ . ← . → 來控制蛇的移動");SetPos(64, 16);wprintf(L"%ls", L"按F3加速,F4減速");SetPos(64, 17);wprintf(L"%ls", L"按ESC退出游戲,按空格暫停游戲");SetPos(64, 19);wprintf(L"%ls", L"小陳制作");
}
蛇?移動
先創建下?個節點,根據移動?向和蛇頭的坐標,蛇移動到下?個位置的坐標。
確定了下?個位置后,看下?個位置是否是?物(NextIsFood),是?物就做吃?物處理 (EatFood),如果不是?物則做前進?步的處理(NoFood)。
蛇?移動后,判斷此次移動是否會造成撞墻(KillByWall)或者撞上??蛇?(KillBySelf),從?影 響游戲的狀態。
void SnakeMove(pSnake ps)
{//創建一個節點,表示蛇即將到的下一個節點pSnakeNode pNextNode = (pSnakeNode)malloc(sizeof(SnakeNode));if (pNextNode == NULL){perror("SnakeMove()::malloc()");return;}switch (ps->_dir){//上case UP:pNextNode->x = ps->_pSnake->x;pNextNode->y = ps->_pSnake->y - 1;break;//下case DOWN:pNextNode->x = ps->_pSnake->x;pNextNode->y = ps->_pSnake->y + 1;break;//左case LEFT:pNextNode->x = ps->_pSnake->x - 2;pNextNode->y = ps->_pSnake->y;break;//右case RIGHT:pNextNode->x = ps->_pSnake->x + 2;pNextNode->y = ps->_pSnake->y;break;}//檢查下一個坐標處是否是食物if (NextIsFood(pNextNode, ps)){EatFood(pNextNode, ps);}//下一個不是食物else{NoFood(pNextNode, ps);}//檢測蛇是否撞墻KILLBywall(ps);//檢測蛇是否撞到自己KillBySelf(ps);
}
判斷下一個坐標是否為食物,我們把它封裝成函數,如果是則為真,否則為假,為假那么下一個坐標不是食物
int NextIsFood(pSnakeNode pn, pSnake ps)
{return (ps->_pFood->x == pn->x && ps->_pFood->y == pn->y);
}
是食物就要把它吃掉
void EatFood(pSnakeNode pn, pSnake ps)
{//頭插法ps->_pFood->next = ps->_pSnake;ps->_pSnake = ps->_pFood;//釋放下一個位置的節點free(pn);pn = NULL;pSnakeNode cur = ps->_pSnake;//打印蛇while(cur){SetPos(cur->x, cur->y);wprintf(L"%lc", BODY);cur = cur->next;}ps->_score += ps->_food_weight;//重新創建食物CreateFood(ps);
}
如果不是?物則做前進?步的處理
將下?個節點頭插?蛇的?體,并將之前蛇?最后?個節點打印為空格,釋放掉蛇?的最后?個節 點。
易錯點:這?最容易錯誤的是,釋放最后?個結點后,還得將指向在最后?個結點的指針改為NULL, 保證蛇尾打印可以正常結束,不會越界訪問。
void NoFood(pSnakeNode pn, pSnake ps)
{//頭插法pn->next = ps->_pSnake;ps->_pSnake = pn;pSnakeNode cur = ps->_pSnake;while (cur->next->next != NULL){SetPos(cur->x, cur->y);wprintf(L"%lc", BODY);cur = cur->next;}//把最后一個節點打印成空格SetPos(cur->next->x, cur->next->y);printf(" ");//釋放最后一個節點free(cur->next);//把倒數第二個節點的地址置為NULLcur->next = NULL;
}
判斷蛇頭的坐標是否和墻的坐標沖突
void KILLBywall(pSnake ps)
{if (ps->_pSnake->x == 0 || ps->_pSnake->x == 56 || ps->_pSnake->y == 0 || ps->_pSnake->y == 26){ps->_status = KILL_BY_WALL;}
}
判斷蛇頭的坐標是否和蛇?體的坐標沖突
void KillBySelf(pSnake ps)
{pSnakeNode cur = ps->_pSnake->next;while (cur){if (cur->x == ps->_pSnake->x && cur->y == ps->_pSnake->y){ps->_status = KILL_BY_SELF;break;}cur = cur->next;}
}
游戲結束(GameEnd)
游戲狀態不再是OK(游戲繼續)的時候,要告知游戲結束的原因,并且釋放蛇?節點。
void GameEnd(pSnake ps)
{SetPos(20, 12);switch (ps->_status){case END_NORMAL:wprintf(L"您主動結束游戲\n");break;case KILL_BY_WALL:wprintf(L"您撞到墻上了,游戲結束\n");break;case KILL_BY_SELF:wprintf(L"您撞到了自己,游戲結束\n");break;}//釋放蛇身的鏈表pSnakeNode cur = ps->_pSnake;while (cur){pSnakeNode del = cur;cur = cur->next;free(del);}
}
七、參考代碼
完整代碼實現,分3個?件實現
.h
#pragma once
#include <windows.h>
#include <stdbool.h>
#include <stdlib.h>
#include <stdio.h>
#include <time.h>
#include <string.h>
//類型的聲明#define WALL L'□'
#define BODY L'●'
#define FOOD L'★'
#define POS_X 24
#define POS_Y 5
//蛇的方向
enum DIRECTION
{UP = 1,//上DOWN,//下LEFT,//左RIGHT//右
};//蛇的狀態
//正常、撞墻、撞到自己、正常退出enum GAME_STATUS
{OK, //正常KILL_BY_WALL, //撞墻KILL_BY_SELF, //撞到自己END_NORMAL //正常退出
};//蛇身的節點類型
typedef struct SnakeNode
{//坐標int x;int y;//指向下一個節點的指針struct SnakeNode* next;
}SnakeNode,*pSnakeNode;//typedef struct SnakeNode* pSnakeNode;//貪吃蛇
typedef struct Snake
{pSnakeNode _pSnake;//指向蛇頭的指針pSnakeNode _pFood;//指向食物節點的指針enum DIRECTION _dir;//蛇的方向enum GAME_STATUS _status;//游戲的狀態int _food_weight; //一個食物的分數int _score; //總成績int _sleep_time; //休息時間,時間越短,速度越快,時間越長、速度越慢
}Snake,*pSnake;//函數的聲明
void SetPos(short x, short y);
//游戲的初始化
void GameStart(pSnake ps);//歡迎界面的打印
void WelcomeToGame();//創建地圖
void CreateMap();//初始化蛇身void InitSnake(pSnake ps);//創建食物void CreateFood(pSnake ps);//游戲運行的邏輯
void GameRun(pSnake ps);//蛇的移動-走一步
void SnakeMove(pSnake ps);//判斷下一個坐標是否是食物
int NextIsFood(pSnakeNode pn, pSnake ps);//下一個位置是食物,就吃掉食物
void EatFood(pSnakeNode pn, pSnake ps);//下一個位置不是食物
void NoFood(pSnakeNode pn, pSnake ps);//檢測是否撞墻
void KILLBywall(pSnake ps);//檢測是否撞到自己
void KillBySelf(pSnake ps);//游戲善后工作
void GameEnd(pSnake ps);
.c
#define _CRT_SECURE_NO_WARNINGS 1
#include "Snake.h"
void SetPos(short x, short y)
{//獲得標準輸出設備的句柄HANDLE houtput = NULL;houtput = GetStdHandle(STD_OUTPUT_HANDLE);//定義光標的位置COORD pos = { x,y };SetConsoleCursorPosition(houtput, pos);
}//歡迎界面的打印
void WelcomeToGame()
{SetPos(40, 14);wprintf(L"歡迎來到貪吃蛇小游戲\n");SetPos(42, 20);system("pause");system("cls");SetPos(25, 14);wprintf(L"用 ↑. ↓ . ← . → 來控制蛇的移動,按F3加速,F4減速\n");SetPos(25,15);wprintf(L"加速能夠得到更高的分數\n");SetPos(42, 20);system("pause");system("cls");
}
//創建地圖
void CreateMap()
{//上(0,0)-(56, 0)int i = 0;for (i = 0; i < 29; i++){wprintf(L"%lc", WALL);}//下(0,26)-(56, 26)SetPos(0, 26);for (i = 0; i < 29; i++){wprintf(L"%lc", WALL);}//左//x是0,y從1開始增?for (i = 1; i <= 25; i++){SetPos(0, i);wprintf(L"%lc", WALL);}//右//x是56,y從1開始增?for (i = 1; i <= 25; i++){SetPos(56, i);wprintf(L"%lc", WALL);}
}void InitSnake(pSnake ps)
{int i = 0;pSnakeNode cur = NULL;for (i = 0; i < 5; i++){cur = (pSnakeNode)malloc(sizeof(SnakeNode));if (cur == NULL){perror("InitSnake()::malloc()");return;}cur->next = NULL;cur->x = POS_X + 2 * i;cur->y = POS_Y;//頭插法插入鏈表 if (ps->_pSnake == NULL)//空鏈表{ps->_pSnake = cur;}else//非空{cur->next = ps->_pSnake;ps->_pSnake = cur;}}cur = ps->_pSnake;while (cur){SetPos(cur->x, cur->y);wprintf(L"%lc", BODY);cur = cur->next;}//設置貪吃蛇的屬性ps->_dir = RIGHT;//默認從右ps->_score = 0;//分數ps->_food_weight = 10;//一個食物的分數ps->_sleep_time = 200;//單位是毫秒ps->_status = OK;}void CreateFood(pSnake ps)
{int x = 0;int y = 0;//生成x是2的倍數//x:2~52//y:1~25
again:do{x = rand() % 53 + 2;y = rand() % 25 + 1;} while (x % 2 != 0);//x和y的坐標不能和蛇的身體坐標沖突pSnakeNode cur = ps->_pSnake;while (cur){if (x == cur->x && y == cur->y){goto again;}cur = cur->next;}//創建食物的節點pSnakeNode pFood = (pSnakeNode)malloc(sizeof(SnakeNode));if (pFood == NULL){perror("CreateFood()::malloc()");return;}pFood->x = x;pFood->y = y;pFood->next = NULL;SetPos(x, y);//定位位置wprintf(L"%lc", FOOD);ps->_pFood = pFood;}//游戲的初始化
void GameStart(pSnake ps)
{//system("mode con cols= 100 lines=30");system("title 貪吃蛇");HANDLE houtput = GetStdHandle(STD_OUTPUT_HANDLE);//隱藏光標操作CONSOLE_CURSOR_INFO CursorInfo;GetConsoleCursorInfo(houtput, &CursorInfo);//獲取控制臺光標信息CursorInfo.bVisible = false;//隱藏控制臺光標SetConsoleCursorInfo(houtput, &CursorInfo);//設置控制臺光標狀態//1.打印環境界面和功能介紹WelcomeToGame();//2. 繪制地圖CreateMap();//3. 創建蛇InitSnake(ps);//4. 創建食物CreateFood(ps);}
void PrintHelpInfo()
{SetPos(64, 14);wprintf(L"%ls", L"不能穿墻,不能咬到自己");SetPos(64, 15);wprintf(L"%ls", L"用 ↑. ↓ . ← . → 來控制蛇的移動");SetPos(64, 16);wprintf(L"%ls", L"按F3加速,F4減速");SetPos(64, 17);wprintf(L"%ls", L"按ESC退出游戲,按空格暫停游戲");SetPos(64, 19);wprintf(L"%ls", L"小陳制作");
}#define KEY_PRESS(vk) ((GetAsyncKeyState(vk)&1)?1:0)void Pause()
{while (1){Sleep(200);if (KEY_PRESS(VK_SPACE)){break;}}
}int NextIsFood(pSnakeNode pn, pSnake ps)
{return (ps->_pFood->x == pn->x && ps->_pFood->y == pn->y);
}
void EatFood(pSnakeNode pn, pSnake ps)
{//頭插法ps->_pFood->next = ps->_pSnake;ps->_pSnake = ps->_pFood;//釋放下一個位置的節點free(pn);pn = NULL;pSnakeNode cur = ps->_pSnake;//打印蛇while(cur){SetPos(cur->x, cur->y);wprintf(L"%lc", BODY);cur = cur->next;}ps->_score += ps->_food_weight;//重新創建食物CreateFood(ps);
}
void NoFood(pSnakeNode pn, pSnake ps)
{//頭插法pn->next = ps->_pSnake;ps->_pSnake = pn;pSnakeNode cur = ps->_pSnake;while (cur->next->next != NULL){SetPos(cur->x, cur->y);wprintf(L"%lc", BODY);cur = cur->next;}//把最后一個節點打印成空格SetPos(cur->next->x, cur->next->y);printf(" ");//釋放最后一個節點free(cur->next);//把倒數第二個節點的地址置為NULLcur->next = NULL;
}void KILLBywall(pSnake ps)
{if (ps->_pSnake->x == 0 || ps->_pSnake->x == 56 || ps->_pSnake->y == 0 || ps->_pSnake->y == 26){ps->_status = KILL_BY_WALL;}
}void KillBySelf(pSnake ps)
{pSnakeNode cur = ps->_pSnake->next;while (cur){if (cur->x == ps->_pSnake->x && cur->y == ps->_pSnake->y){ps->_status = KILL_BY_SELF;break;}cur = cur->next;}
}
void SnakeMove(pSnake ps)
{//創建一個節點,表示蛇即將到的下一個節點pSnakeNode pNextNode = (pSnakeNode)malloc(sizeof(SnakeNode));if (pNextNode == NULL){perror("SnakeMove()::malloc()");return;}switch (ps->_dir){//上case UP:pNextNode->x = ps->_pSnake->x;pNextNode->y = ps->_pSnake->y - 1;break;//下case DOWN:pNextNode->x = ps->_pSnake->x;pNextNode->y = ps->_pSnake->y + 1;break;//左case LEFT:pNextNode->x = ps->_pSnake->x - 2;pNextNode->y = ps->_pSnake->y;break;//右case RIGHT:pNextNode->x = ps->_pSnake->x + 2;pNextNode->y = ps->_pSnake->y;break;}//檢查下一個坐標處是否是食物if (NextIsFood(pNextNode, ps)){EatFood(pNextNode, ps);}//下一個不是食物else{NoFood(pNextNode, ps);}//檢測蛇是否撞墻KILLBywall(ps);//檢測蛇是否撞到自己KillBySelf(ps);
}void GameRun(pSnake ps)
{//打印幫助信息PrintHelpInfo();do{//打印總分數和食物的分值SetPos(64, 10);printf("總分數:%d\n", ps->_score);SetPos(64, 11);printf("當前食物的分數:%2d\n", ps->_food_weight);//按鍵if (KEY_PRESS(VK_UP) && ps->_dir != DOWN){ps->_dir = UP;}else if (KEY_PRESS(VK_DOWN) && ps->_dir != UP){ps->_dir = DOWN;}else if (KEY_PRESS(VK_LEFT) && ps->_dir != RIGHT){ps->_dir = LEFT;}else if (KEY_PRESS(VK_RIGHT) && ps->_dir != LEFT){ps->_dir = RIGHT;}else if (KEY_PRESS(VK_SPACE)){//空格暫停Pause();}else if (KEY_PRESS(VK_ESCAPE)){//正常退出游戲ps->_status = END_NORMAL;}else if (KEY_PRESS(VK_F3)){//加速if (ps->_sleep_time > 80){ps->_sleep_time -= 30;ps->_food_weight += 2;}}else if (KEY_PRESS(VK_F4)){//減速if (ps->_food_weight > 2){ps->_sleep_time += 30;ps->_food_weight -= 2;}}SnakeMove(ps);//蛇走一步的過程Sleep(ps->_sleep_time);}while (ps->_status == OK);}void GameEnd(pSnake ps)
{SetPos(20, 12);switch (ps->_status){case END_NORMAL:wprintf(L"您主動結束游戲\n");break;case KILL_BY_WALL:wprintf(L"您撞到墻上了,游戲結束\n");break;case KILL_BY_SELF:wprintf(L"您撞到了自己,游戲結束\n");break;}//釋放蛇身的鏈表pSnakeNode cur = ps->_pSnake;while (cur){pSnakeNode del = cur;cur = cur->next;free(del);}
}
.c測試文件
#define _CRT_SECURE_NO_WARNINGS 1
#include <locale.h>
#include "snake.h"//完成的是游戲的測試邏輯
void test()
{int ch = 0;do{system("cls");//清理屏幕//創建貪吃蛇Snake snake = { 0 };//初始化游戲//1. 打印環境界面//2. 功能介紹//3. 繪制地圖//4. 創建蛇//5. 創建食物//6. 設置游戲的相關信息GameStart(&snake);//運行游戲GameRun(&snake);//結束游戲 - 善后工作GameEnd(&snake);SetPos(20, 15);printf("再來一局嗎?(Y/N):");ch = getchar();getchar();//清理//while (getchar() != '\n');} while (ch == 'Y' || ch == 'y');SetPos(0, 27);}
int main()
{//設置適配本地環境setlocale(LC_ALL, "");srand((unsigned int)time(NULL));//生成隨機數test();return 0;
}