【C語言】實現貪吃蛇--項目實踐(超詳細)

前言:

貪吃蛇游戲大家都玩過吧?這次我們要用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.初始化一個句柄變量hOutputNULL

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;
}

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/bicheng/16643.shtml
繁體地址,請注明出處:http://hk.pswp.cn/bicheng/16643.shtml
英文地址,請注明出處:http://en.pswp.cn/bicheng/16643.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

新計劃,不斷變更!做自己,接受不美好!豬肝移植——早讀(逆天打工人爬取熱門微信文章解讀)

時間不等人 引言Python 代碼第一篇 做自己&#xff0c;沒有很好也沒關系第二篇結尾 引言 新計劃&#xff1a; 早上一次性發幾個視頻不現實 所以更改一下 待后面有比較穩定的框架再優化 每天早上更新 早到8點 晚到10點 你剛剛好上班或者上課 然后偷瞄的看兩眼 學習一下 補充知…

SSM流浪寵物領養系統 畢業設計-附源碼 270917

摘 要 流浪寵物一直是影響城市環境與居民生活的一個不可忽略的因素。基于此&#xff0c;本文設計并實現一個流浪寵物領養系統。用戶可以通過本系統查看搜索流浪寵物的相關信息、進行領養申請&#xff0c;為其提供愛心幫助。本系統有效地解決了流浪寵物領養工作開展困難等問題&a…

STM32F1之OV7725攝像頭·SCCB總線詳解(附帶源碼編寫)

STM32F1之OV7725攝像頭-CSDN博客 STM32F1之OV7725攝像頭像素數據輸出時序、FIFO 讀寫時序以及攝像頭的驅動原理詳解-CSDN博客 目錄 1. 硬件設計 1.1 SCCB 控制相關 1.2 VGA 時序相關 1.3 FIFO 相關 1.4 XCLK 信號 2. 代碼設計 2.1 SCCB總線軟件實現 2.1.1 宏定…

推薦系統三方參與者

1.信息生產者 信息生產者是指制作和發布網絡信息內容的組織或個人。信息生產者的需求鏈路大致為&#xff1a;發布信息->期待曝光->期待閱讀->期待獎勵&#xff0c;需求得到滿足&#xff0c;持續生產&#xff0c;再次進入鏈路循環。生產者持續創作的激情和動力很大程度…

go語言內置io包中TreeReader函數的理解和使用示例

在go語言的內置io包中的這個 TreeReader函數&#xff0c;函數原型 func TeeReader(r Reader, w Writer) Reader 從函數原型中看是給他一個Reader, 和一個Writer 然后他給你返回一個Reader, 本文中我們把這個返回的Reader叫做 treeReader&#xff0c; 他是一個很特別的reader…

利用STK分析雷達干擾

利用STK中的radar模塊能夠進行干擾分析。該模塊能夠用來確定干擾源&#xff08;單站雷達、發射源&#xff09;&#xff0c;評估干擾對本機雷達性能的影響。 在分析干擾影響之前&#xff0c;首先設置一簡單場景。 1、創建一個名為RadarJam的場景&#xff1b; 2、打開場景的Prop…

上位機圖像處理和嵌入式模塊部署(f103 mcu運行freertos)

【 聲明:版權所有,歡迎轉載,請勿用于商業用途。 聯系信箱:feixiaoxing @163.com】 mcu一般有兩個用途,一個是控制,一個是非控制。控制類的應用一般要求實時性比較高,什么時候做什么,都是有嚴格的時間要求的。而非控制類的應用,則要求實現盡可能多的功能,比如…

spring boot整合j2cache 配置項目全局鍵標識 幫助定位是哪個項目產生的緩存

我們利用 j2cache 存進 redis的緩存 key 可以加個全局標識 這樣 到時看緩存 方便別人看是哪個項目存進去的 例如 這里 我們存入的 book 我們 keys * 查看 就知道是個book 但不知道具體來自那套系統 我們在 j2cache.properties 中加上 redis.namespace 項目全局鍵標識 我們…

技術分享:深入C++時間操作函數的應用與實踐

在軟件開發中&#xff0c;時間是無處不在的一個概念&#xff0c;無論是日志記錄、定時任務還是用戶界面&#xff0c;時間的處理都至關重要。C標準庫提供了一套完整的工具來幫助我們處理時間相關的操作。本文將詳細介紹幾個時間操作函數的使用場景、代碼實現以及它們在實際開發中…

42-5 應急響應之日志分析

一、Windows 系統日志排查 1)日志概述 在 Windows 2000 專業版、Windows XP 和 Windows Server 2003 中: 系統日志的位置為 C:\WINDOWS\System32\config\SysEvent.evt安全性日志的位置為 C:\WINDOWS\System32\config\SecEvent.evt應用程序日志的位置為 C:\WINNT\System32\c…

牛客周賽 Round 44VP

1.簽到&#xff1a;https://ac.nowcoder.com/acm/contest/82526/A AC代碼&#xff1a; #include<bits/stdc.h> using namespace std; int n; int main() {cin>>n;cout<<n/3; } 2.思維&#xff1a;https://ac.nowcoder.com/acm/contest/82526/B 就是判斷最…

【漏洞復現】海康威視綜合安防管理平臺 iSecure Center applyCT fastjson 遠程代碼執行

0x01 漏洞名稱 海康威視綜合安防管理平臺 iSecure Center applyCT fastjson 遠程代碼執行 0x02 漏洞影響 0x03 搜索引擎 app"HIKVISION-綜合安防管理平臺"0x04 漏洞詳情 POST /bic/ssoService/v1/applyCT HTTP/1.1 User-Agent: Mozilla/5.0 (Windows NT 10.0; Wi…

NASA數據集——阿爾法噴氣式大氣實驗二氧化碳和甲烷數據

Alpha Jet Atmospheric eXperiment Carbon Dioxide and Methane Data 阿爾法噴氣式大氣實驗二氧化碳和甲烷數據 簡介 Alpha Jet Atmospheric eXperiment (AJAX) 是美國國家航空航天局艾姆斯研究中心與 H211, L.L.C. 公司的合作項目&#xff0c;旨在促進對加利福尼亞、內華達…

Kafka 實戰 - Kafka Consumer 重置 Offset

在開發測試過程中&#xff0c;可能需要消費一段時間的消息&#xff0c;來驗證數據的可靠性&#xff0c;這里需要消費者&#xff08;Consumer&#xff09;重置其消費的偏移量&#xff08;Offset&#xff09;。 以下是幾種常用的方法來重置Kafka Consumer的Offset&#xff1a; …

vue+iview tabs context-menu 彈出框怎么修改樣式

今天遇到一個需求說頁面頂部的菜單右鍵彈出框離得有點遠 代碼是這樣 <Tabs type"card" closable class"main-tags-col-tabs" v-model"activeTab" on-click"handleClickTag" :before-remove"handleBeforeRemove" capt…

什么是容器:從基礎到進階的全面介紹

?? 歡迎大家來訪Srlua的博文&#xff08;づ&#xffe3;3&#xffe3;&#xff09;づ╭?&#xff5e;?? &#x1f31f;&#x1f31f; 歡迎各位親愛的讀者&#xff0c;感謝你們抽出寶貴的時間來閱讀我的文章。 我是Srlua小謝&#xff0c;在這里我會分享我的知識和經驗。&am…

libjpeg_example.txt

/* 示例.txt該文件說明了如何使用IJG代碼作為子程序庫讀取或寫入JPEG圖像文件。你應該看看這段代碼與文檔文件 libjpeg.txt 結合使用。這段代碼按原樣不會做任何有用的事情&#xff0c;但它可能會有所幫助用于構建調用 JPEG 庫的例程的骨架。我們以 JPEG 代碼中使用的相同編碼…

Java中的內部類及其用途

一、技術難點 在Java中&#xff0c;內部類是一個定義在另一個類內部的類。這種嵌套的結構帶來了一些技術上的難點和挑戰&#xff1a; 訪問控制&#xff1a;內部類可以直接訪問外部類的所有成員&#xff08;包括私有成員&#xff09;&#xff0c;但外部類不能直接訪問內部類的…

Vue3實戰筆記(44)—vue3組件的ref屬性

文章目錄 前言一、組件的ref用法總結總結 前言 之前學習過ref聲明響應式對象&#xff0c;前幾天讀代碼遇到了發懵的地方&#xff0c;詳細學習了一下才發現&#xff0c;用法還有很多&#xff0c;遂總結一下ref的用法備忘。 一、組件的ref用法總結 Vue3 中的 ref 是一種創建響應…