網(wǎng)易做網(wǎng)站百度助手安卓版下載
文章目錄
- 游戲背景介紹
- 實(shí)現(xiàn)目標(biāo)
- 適合人群
- 所需技術(shù)
- 淺玩Window API
- 什么是API
- 控制臺(tái)程序
- 窗口大小,名稱設(shè)置
- Handle(句柄)
- 獲取句柄
- 坐標(biāo)結(jié)構(gòu)體
- 設(shè)置光標(biāo)位置
- 光標(biāo)屬性
- 獲取光標(biāo)屬性
- 設(shè)置光標(biāo)屬性
- 按鍵信息獲取
- 貪吃蛇游戲設(shè)計(jì)
- 游戲前的初始化
- 設(shè)置窗口的大小和名稱
- 本地化設(shè)置
- 寬字符
- Waht is 寬字符
- 寬字符的打印
- 光標(biāo)的設(shè)置
- 歡迎界面打印
- 地圖繪制
- 幫助信息
- 蛇
- 蛇身的管理
- 貪吃蛇游戲的管理(插播,重要)
- 蛇的初始化
- 食物的管理
- 蛇的移動(dòng)
- 游戲結(jié)束
- 拓展建議
- 寄語(yǔ)
- 源碼
游戲背景介紹
貪吃蛇是一個(gè)非常經(jīng)典的小游戲,筆者曾今在古早的案件手機(jī),mp4上面玩過(guò)這款游戲,今天就讓我們使用C語(yǔ)言一起復(fù)刻這個(gè)簡(jiǎn)單的小游戲吧~,好玩簡(jiǎn)單-
實(shí)現(xiàn)目標(biāo)
在這個(gè)游戲中,我們需要控制一條可以上下左右移動(dòng)的小蛇,在指定的墻體內(nèi)進(jìn)行移動(dòng),吃到食物后,蛇的身體會(huì)變長(zhǎng),當(dāng)蛇撞墻或者撞到自己的身體的時(shí)候,游戲結(jié)束,我們需要實(shí)現(xiàn)的功能有:
- 蛇的移動(dòng)
- 食物的生成
- 蛇的身體的增長(zhǎng)
- 游戲結(jié)束的判斷
- 游戲結(jié)束后的處理
適合人群
這是我學(xué)習(xí)完C語(yǔ)言語(yǔ)法和順序表,鏈表之后的一個(gè)小項(xiàng)目,也同樣適合像我一樣剛學(xué)完C語(yǔ)言的同學(xué),通過(guò)這個(gè)項(xiàng)目,鞏固一下C語(yǔ)言的基礎(chǔ)知識(shí),也可以學(xué)習(xí)一下C語(yǔ)言的一些高級(jí)知識(shí),比如Windows API的使用,寬字符的打印等等
所需技術(shù)
- C語(yǔ)言基礎(chǔ)
- 鏈表
- 簡(jiǎn)單的Windows API
- 動(dòng)態(tài)內(nèi)存分配
- 等
淺玩Window API
什么是API
API全稱Application Programming Interface,翻譯過(guò)來(lái)就是應(yīng)用程序接口,是一組預(yù)先定義的函數(shù),類,協(xié)議的集合,這些函數(shù),類,協(xié)議可以被其他程序調(diào)用,用來(lái)實(shí)現(xiàn)一些功能,比如Windows API就是用來(lái)實(shí)現(xiàn)Windows系統(tǒng)的一些功能的
上面說(shuō)的有些復(fù)雜,我們可以把API想象成一個(gè)工廠,我們只需要知道向工廠輸入什么,工廠會(huì)輸出什么,而不需要知道工廠內(nèi)部是怎么實(shí)現(xiàn)的
比如你向一個(gè)面包工廠輸入了一些小麥,工廠會(huì)給你輸出一些面包,而工廠內(nèi)部可能是用了一些機(jī)器,工人等等來(lái)實(shí)現(xiàn)的,但是你不需要知道這些,你只需要知道你給他小麥,他給你面包就行了
控制臺(tái)程序
控制臺(tái)程序其實(shí)就是我們平時(shí)編譯完文件之后運(yùn)行打開(kāi)的那個(gè)黑框框,我們可以在這個(gè)黑框框里面輸入一些命令,然后程序會(huì)給我們輸出一些結(jié)果,這個(gè)黑框框就是一個(gè)控制臺(tái),我們可以通過(guò)控制臺(tái)程序來(lái)和用戶進(jìn)行交互
窗口大小,名稱設(shè)置
既然是貪吃蛇小游戲,那么我們需要一個(gè)固定的窗口和一個(gè)有趣的名字,我們可以怎么做呢?
可以使用cmd命令在控制臺(tái)程序中設(shè)置窗口的大小,名稱
mode con cols=100 lines=30
title 貪吃蛇
- mode con cols=100 lines=30 設(shè)置窗口的大小為100列,30行
- title 貪吃蛇 設(shè)置窗口的名稱為貪吃蛇
但是,我們只希望貪吃蛇程序運(yùn)行后自動(dòng)設(shè)置窗口的大小和名稱,而不是讓用戶手動(dòng)輸入這些命令,因此我們可以使用system函數(shù)來(lái)調(diào)用這些命令,讓程序自動(dòng)設(shè)置窗口的大小和名稱
#include <stdlib.h>
int main()
{system("mode con cols=100 lines=30");system("title 貪吃蛇");return 0;
}
Handle(句柄)
如果我們想要對(duì)控制臺(tái)的一些屬性進(jìn)行設(shè)置,比如設(shè)置光標(biāo)的位置,設(shè)置控制臺(tái)的顏色等等,我們就需要使用句柄來(lái)進(jìn)行操作,我們可以把句柄想象成是控制臺(tái)大哥的身份標(biāo)識(shí),只有知道大哥的身份標(biāo)識(shí)才能找到大哥并更改他的一些屬性
獲取句柄
我們可以使用GetStdHandle函數(shù)來(lái)獲取控制臺(tái)的句柄,這個(gè)函數(shù)的原型是
HANDLE GetStdHandle(DWORD nStdHandle);
我們可以使用一個(gè)HANDLE類型變量來(lái)接受返回值
坐標(biāo)結(jié)構(gòu)體
控制臺(tái)上面的每一個(gè)字符都有一個(gè)坐標(biāo),
坐標(biāo)是從左上角開(kāi)始計(jì)算的,左上角的坐標(biāo)是(0,0),向右是x軸正方向,向下是y軸正方向
我們可以使用一個(gè)結(jié)構(gòu)體來(lái)表示這個(gè)坐標(biāo)
COORD是Windows API中的一個(gè)結(jié)構(gòu)體,定義如下
typedef struct _COORD {SHORT X;SHORT Y;
} COORD, *PCOORD;
設(shè)置光標(biāo)位置
我們平時(shí)在使用printf打印字符的時(shí)候,每打印一個(gè)字符,光標(biāo)都會(huì)自動(dòng)向后移動(dòng)一個(gè)位置,我們可以使用SetConsoleCursorPosition函數(shù)來(lái)手動(dòng)設(shè)置光標(biāo)的位置
BOOL SetConsoleCursorPosition(HANDLE hConsoleOutput,COORD dwCursorPosition
);
eg:
#include <windows.h>int main()
{HANDLE hOut = GetStdHandle(STD_OUTPUT_HANDLE); // 獲取控制臺(tái)句柄COORD pos = {10, 10}; // 設(shè)置坐標(biāo)SetConsoleCursorPosition(hOut, pos); // 設(shè)置光標(biāo)位置printf("hello world");return 0;
}
光標(biāo)屬性
我們平時(shí)運(yùn)行控制臺(tái)程序的時(shí)候,會(huì)發(fā)現(xiàn)光標(biāo)是一個(gè)閃爍的小方塊,那我們有沒(méi)有什么辦法可以讓小方塊變大變小或是直接隱藏呢?
答案肯定是有噠
我們先來(lái)介紹一下光標(biāo)的兩個(gè)屬性
- bVisible : 是否可見(jiàn)
- dwSize : 光標(biāo)的大小 當(dāng)這個(gè)值是100的時(shí)候,光標(biāo)是一個(gè)小方塊█,這個(gè)值也就是顯示這個(gè)方塊的百分比,比如50就是顯示一半的方塊
獲取光標(biāo)屬性
我們可以使用GetConsoleCursorInfo函數(shù)來(lái)獲取光標(biāo)的屬性
BOOL GetConsoleCursorInfo(HANDLE hConsoleOutput,PCONSOLE_CURSOR_INFO lpConsoleCursorInfo
);
eg:
#include <windows.h>int main()
{HANDLE hOut = GetStdHandle(STD_OUTPUT_HANDLE); // 獲取控制臺(tái)句柄CONSOLE_CURSOR_INFO cursorInfo;GetConsoleCursorInfo(hOut, &cursorInfo;);printf("bVisible:%d, dwSize:%d\n", cursorInfo.bVisible, cursorInfo.dwSize);return 0;
}
大家可以自行運(yùn)行試一試
設(shè)置光標(biāo)屬性
在貪吃蛇游戲中,我們肯定希望沒(méi)有光標(biāo)的出現(xiàn),因此我們可以使用SetConsoleCursorInfo函數(shù)來(lái)設(shè)置光標(biāo)的屬性
BOOL SetConsoleCursorInfo(HANDLE hConsoleOutput,const CONSOLE_CURSOR_INFO *lpConsoleCursorInfo
);
eg:
#include <windows.h>int main()
{HANDLE hOut = GetStdHandle(STD_OUTPUT_HANDLE); // 獲取控制臺(tái)句柄CONSOLE_CURSOR_INFO cursorInfo;cursorInfo.bVisible = 0; // 設(shè)置光標(biāo)不可見(jiàn) 也可以寫flasecursorInfo.dwSize = 100; // 設(shè)置光標(biāo)大小SetConsoleCursorInfo(hOut, &cursorInfo);return 0;
}
按鍵信息獲取
我們需要使用↑↓←→來(lái)控制蛇的移動(dòng),因此我們可以使用GetAsyncKeyState函數(shù)來(lái)獲取按鍵信息
SHORT GetAsyncKeyState(int vKey
);
在返回的值中,如果最高位是1,表示這個(gè)鍵正在被按下,如果最低位是1,表示這個(gè)鍵被按過(guò)
我們可以使用一個(gè)預(yù)定義的宏來(lái)判斷這個(gè)鍵是否被按下
#define KEY_PRESS(VK) (GetAsyncKeyState(VK) & 0x01 ? 1 : 0)
這個(gè)大家肯定看的懂,如果不懂的話可以去學(xué)習(xí)一下預(yù)編譯和位運(yùn)算
貪吃蛇游戲設(shè)計(jì)
我們可以把貪吃蛇游戲分為三個(gè)部分:
- 游戲前的初始化,包括窗口的設(shè)置,光標(biāo)的設(shè)置,歡迎界面打印,地圖的初始化,蛇的初始化,食物的初始化等等
- 游戲中的循環(huán),包括蛇的移動(dòng),食物的生成,蛇的身體的增長(zhǎng),游戲結(jié)束的判斷等等
- 游戲結(jié)束后的處理,包括釋放資源,打印游戲結(jié)束的信息等等
補(bǔ)充:當(dāng)程序需要實(shí)現(xiàn)按任意鍵繼續(xù)的時(shí)候,我們使用system(“pause”)函數(shù)即可
游戲前的初始化
設(shè)置窗口的大小和名稱
void CmdInit(void)
{// 設(shè)置控制臺(tái)窗口大小 100列 30行system("mode con cols=100 lines=30");// 設(shè)置控制臺(tái)名稱system("title snake");
}
本地化設(shè)置
正常在C語(yǔ)言中,我們使用的是ASCII字符集,他只使用了一個(gè)字節(jié)來(lái)表示一個(gè)字符,而在不同的國(guó)家和地區(qū),字符集是不一樣的,因此我們可以使用setlocale函數(shù)來(lái)設(shè)置字符集
char *setlocale(int category, const char *locale);
在C標(biāo)準(zhǔn)庫(kù)中,我們可以更改的地區(qū)設(shè)置有以下這些:
- LC_ALL:所有的地區(qū)設(shè)置
- LC_COLLATE:字符串比較
- LC_CTYPE:字符分類和轉(zhuǎn)換
- LC_MONETARY:貨幣格式
- LC_NUMERIC:非貨幣數(shù)字格式
- LC_TIME:時(shí)間格式
我么可以使用setlocale函數(shù)來(lái)設(shè)置地區(qū),比如把地區(qū)設(shè)置為當(dāng)前地區(qū)
#include <locale.h>
int main()
{setlocale(LC_ALL, "");return 0;
}
寬字符
Waht is 寬字符
寬字符是指一個(gè)字符占用兩個(gè)字節(jié)的字符,中文以及一些特殊字符都是寬字符,
而且寬字符在控制臺(tái)中是占用兩個(gè)x坐標(biāo)的
graph LR
A[寬字符] --> B[占用兩個(gè)坐標(biāo)位置] --> D[普通字符:█]
B --> E[寬字符:██]
A --> C[占用兩個(gè)字節(jié)]
寬字符的打印
在C語(yǔ)言中,我們可以使用wprintf函數(shù)來(lái)打印寬字符,在打印寬字符之前,我們需要進(jìn)行本地化設(shè)置
int wprintf(const wchar_t *format, ...);
eg:
#include <stdio.h>
int main()
{setlocale(LC_ALL, "");wchar_t str[] = L"你好,世界\n";wprintf(L"%s", str);return 0;
}
光標(biāo)的設(shè)置
- 定義一個(gè)變量來(lái)接受控制臺(tái)的句柄
- 定義一個(gè)變量來(lái)接受光標(biāo)的屬性
- 改變光標(biāo)的屬性
- 通過(guò)SetConsoleCursorInfo函數(shù),輸入句柄和光標(biāo)屬性,設(shè)置光標(biāo)的屬性
歡迎界面打印
這里我們需要設(shè)置光標(biāo)位置并打印所需信息,因此我們可以封裝一個(gè)函數(shù)來(lái)快捷地設(shè)置光標(biāo)位置
void SetPos(int x, int y)
{HANDLE hOut = GetStdHandle(STD_OUTPUT_HANDLE);COORD pos = {x, y};SetConsoleCursorPosition(hOut, pos);
}
然后我們就可以打印歡迎信息和游戲規(guī)則了
// 打印歡迎信息
void WelcomeToGame(void)
{SetPos(45, 10);wprintf(L"歡迎來(lái)到貪吃蛇游戲");// 暫停SetPos(45, 20);system("pause");
}// 游戲介紹
void GameIntroduction(void)
{// 清屏system("cls");SetPos(45, 10);wprintf(L"游戲介紹:");SetPos(45, 12);wprintf(L"1. 使用↑,↓,←,→控制蛇的移動(dòng)");SetPos(45, 14);wprintf(L"2. 吃到食物蛇的長(zhǎng)度加1");SetPos(45, 16);wprintf(L"2. F3加速, F4減速");SetPos(45, 18);wprintf(L"3. 空格暫停");SetPos(45, 20);wprintf(L"4. Esc退出游戲");// 暫停SetPos(45, 22);system("pause");
}
地圖繪制
我們這里使用□來(lái)表示墻體,然后運(yùn)用寬字符的打印知識(shí)來(lái)繪制一個(gè)27 * 27的地圖
// 地圖繪制
// 上(0,0) - (56,0)
// 下(0,26) - (56,26)
// 左(0,0) - (0,26)
// 右(56,0) - (56,26)
// 注意打印的是寬字符 占兩個(gè)x坐標(biāo) 因此左右打印的時(shí)候要每打印一個(gè)x坐標(biāo)加2
void MapDraw(void)
{ // 清屏system("cls");// 上墻for (int i = 0; i < 57; i+=2){SetPos(i, 0);wprintf(L"%c", WALL);}// 下墻for (int i = 0; i < 57; i+=2){SetPos(i, 26);wprintf(L"%c", WALL);}// 最上面和最下面已經(jīng)打印過(guò)了// 左墻for (int i = 1; i < 26; i++){SetPos(0, i);wprintf(L"%c", WALL);}// 右墻for (int i = 1; i < 26; i++){SetPos(56, i);wprintf(L"%c", WALL);}
}
幫助信息
我們?cè)诘貓D的右側(cè)打印一些幫助信息,比如當(dāng)前的分?jǐn)?shù),按鍵操作等等
// 打印靜態(tài)幫助信息
// 單個(gè)食物分?jǐn)?shù),總分?jǐn)?shù)的名稱
// 操作說(shuō)明
void PrintStaticHelp(void)
{SetPos(70, 5);wprintf(L"單個(gè)食物分?jǐn)?shù): 10");SetPos(70, 7);wprintf(L"總分?jǐn)?shù): 0");SetPos(70, 9);wprintf(L"操作說(shuō)明:");SetPos(70, 11);wprintf(L"↑ : 上移");SetPos(70, 13);wprintf(L"↓ : 下移");SetPos(70, 15);wprintf(L"← : 左移");SetPos(70, 17);wprintf(L"→ : 右移");SetPos(70, 19);wprintf(L"F3: 加速");SetPos(70, 21);wprintf(L"F4: 減速");SetPos(70, 23);wprintf(L"空格: 暫停");SetPos(70, 25);wprintf(L"Esc: 退出");
}
蛇
蛇身的管理
我們可以將蛇看作是一個(gè)一個(gè)節(jié)點(diǎn)相互連接組成,因此我們可以使用鏈表來(lái)管理蛇的身體,我們先創(chuàng)建一個(gè)結(jié)構(gòu)體來(lái)管理蛇身的節(jié)點(diǎn)
// 蛇節(jié)點(diǎn)
typedef struct SnakeNode
{int x;int y;struct SnakeNode *next;
} SnakeNode, * pSnakeNode;
貪吃蛇游戲的管理(插播,重要)
使用一個(gè)結(jié)構(gòu)體來(lái)管理整個(gè)貪吃蛇游戲,包括:
- 蛇身
- 食物
- 單個(gè)食物分?jǐn)?shù)
- 總分?jǐn)?shù)
- 睡眠時(shí)間
- 方向
- 游戲狀態(tài)
// 方向
typedef enum Direction
{UP = 1, // 上DOWN, // 下LEFT, // 左RIGHT // 右} Direction;// 游戲狀態(tài)
typedef enum GameStatus
{OK = 0, //運(yùn)行中KILL_BY_WALL, //撞墻KILL_BY_SELF, //撞自己PAUSE, //暫停ESC //退出
} GameStatus;// 貪吃蛇游戲
typedef struct Snake
{pSnakeNode _pSnake; // 蛇頭pSnakeNode _pFood; // 食物 可以共用蛇節(jié)點(diǎn)比較方便,下面會(huì)提int _foodScore; // 單個(gè)食物分?jǐn)?shù)int _totalScore; // 總分?jǐn)?shù)int _sleepTime; // 睡眠時(shí)間 -- 控制速度Direction _dir; // 方向GameStatus _status; // 游戲狀態(tài)
} Snake, * pSnake;
在游戲開(kāi)始前對(duì)這個(gè)結(jié)構(gòu)體進(jìn)行初始化
pSnake ps = (pSnake)malloc(sizeof(Snake));
蛇的初始化
我們可以使用一個(gè)鏈表來(lái)存儲(chǔ)蛇的身體,這里我們創(chuàng)建一個(gè)有5個(gè)節(jié)點(diǎn)的蛇,并且初始化蛇的位置
#define BODY L'●'
#define POS_X 6
#define POS_Y 6// 蛇初始化
// 在地圖的(6,6)位置初始化蛇
// 蛇的長(zhǎng)度為5
// 使用頭插法
void InitSnake(pSnake ps)
{pSnakeNode cur = NULL;int i = 0;// 創(chuàng)建蛇身節(jié)點(diǎn) 并初始化坐標(biāo) for (i = 0; i < 5; i++){pSnakeNode node = (pSnakeNode)malloc(sizeof(SnakeNode));if (node == NULL){perror("pSnakeNode malloc failed");exit(1);}node->x = POS_X + i * 2;node->y = POS_Y;node->next = NULL;// 頭插法if (ps->_pSnake == NULL){ps->_pSnake = node;}else{node->next = ps->_pSnake;ps->_pSnake = node;}}// 打印蛇cur = ps->_pSnake;while (cur != NULL){SetPos(cur->x, cur->y);wprintf(L"%c", BODY);cur = cur->next;}
}
食物的管理
我們可以把食物也看作是一個(gè)蛇節(jié)點(diǎn),只不過(guò)還沒(méi)有加入到蛇身體中,因此我們可以使用蛇身結(jié)構(gòu)體來(lái)存儲(chǔ)食物
我們?cè)谟螒蜷_(kāi)始和蛇吃到食物的時(shí)候,生成一個(gè)食物節(jié)點(diǎn),并且打印食物到屏幕上
// 創(chuàng)建食物節(jié)點(diǎn)
void CreatFood(pSnake ps)
{// 申請(qǐng)節(jié)點(diǎn)空間pSnakeNode foodnode = (pSnakeNode)malloc(sizeof(SnakeNode));if (foodnode == NULL){perror("foodnode malloc failed");exit(1);}foodnode->next = NULL;// 隨機(jī)生成食物坐標(biāo) 在墻范圍內(nèi)但是不能在蛇身上while (1){int x = rand() % 54 + 2;int y = rand() % 24 + 2;pSnakeNode cur = ps->_pSnake;while (cur != NULL){if (cur->x == x && cur->y == y){break;}cur = cur->next;}if (cur == NULL){foodnode->x = x;foodnode->y = y;break;}}// 打印食物SetPos(foodnode->x, foodnode->y);wprintf(L"%c", FOOD);
}
這里的隨機(jī)數(shù)并不是真正的隨機(jī)數(shù),而是偽隨機(jī)數(shù),因此我們可以在mian里面使用srand函數(shù)來(lái)設(shè)置隨機(jī)數(shù)的種子
int main()
{srand(time(NULL));return 0;
}
蛇的移動(dòng)
- 按鍵檢測(cè), 改變方向
- 判斷是否吃到食物
- 吃到食物,頭插食物節(jié)點(diǎn),創(chuàng)建新的食物節(jié)點(diǎn)
- 沒(méi)吃到食物,頭插一個(gè)新的節(jié)點(diǎn),新的節(jié)點(diǎn)是蛇頭移動(dòng)到的下一個(gè)坐標(biāo),尾刪一個(gè)節(jié)點(diǎn)
- 蛇移動(dòng)后進(jìn)行打印,判斷是否撞到自己或者墻壁,是則游戲結(jié)束
這里封裝了一個(gè)宏來(lái)讀取按鍵狀態(tài),如果最低位是1,則返回1,反之返回0
也就是一個(gè)按鍵按下過(guò)返回1,否則返回0
#define KEY_PRESS(VK) (GetAsyncKeyState(VK) & 0x01 ? 1 : 0)
// 蛇的移動(dòng) -- 主游戲程序 在此循環(huán)
// 吃到食物 創(chuàng)建新的食物節(jié)點(diǎn)
// 沒(méi)有吃到食物頭插新節(jié)點(diǎn)刪除尾節(jié)點(diǎn)并釋放空間
// 減速睡眠時(shí)間-30 睡眠時(shí)間最少減4次 單個(gè)食物分?jǐn)?shù)加2
// 加速睡眠時(shí)間+30 睡眠時(shí)間最多加4次 單個(gè)食物分?jǐn)?shù)減2
void SnakeMove(pSnake ps)
{int x = 0;int y = 0;again:while (ps->_status == OK){// 按鍵檢測(cè)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)){ps->_status = PAUSE;}else if (KEY_PRESS(VK_F3) && ps->_foodScore < 20){ps->_sleepTime -= 30;ps->_foodScore += 2;// 更改單個(gè)食物分?jǐn)?shù)SetPos(84, 5);printf("%2d", ps->_foodScore);}else if (KEY_PRESS(VK_F4) && ps->_foodScore > 2){ps->_sleepTime += 30;ps->_foodScore -= 2;// 更改單個(gè)食物分?jǐn)?shù)SetPos(84, 5);printf("%2d", ps->_foodScore);}else if (KEY_PRESS(VK_ESCAPE)){ps->_status = ESC;}// 設(shè)置下一個(gè)節(jié)點(diǎn)的x,y坐標(biāo)switch (ps->_dir){case UP:x = ps->_pSnake->x;y = ps->_pSnake->y - 1;break;case DOWN:x = ps->_pSnake->x;y = ps->_pSnake->y + 1;break;case LEFT:x = ps->_pSnake->x - 2;y = ps->_pSnake->y;break;case RIGHT:x = ps->_pSnake->x + 2;y = ps->_pSnake->y;break;}// 判斷是否吃到食物if (ps->_pFood->x == x && ps->_pFood->y == y){EatFood(ps);}else {NotEatFood(ps, x, y);}// 撞墻檢測(cè)if (IsKillByWall(ps)){ps->_status = KILL_BY_WALL;}// 撞自己檢測(cè)if (IsKillBySelf(ps)){ps->_status = KILL_BY_SELF;}Sleep(ps->_sleepTime);}while (ps->_status == PAUSE){if (KEY_PRESS(VK_SPACE)){ps->_status = OK;}goto again; //使用goto 返回到前面}// 撞墻打印信息if (IsKillByWall(ps)){SetPos(45, 10);wprintf(L"墻墻被你撞西了,110把你抓走了");}// 撞自己打印信息if (IsKillBySelf(ps)){SetPos(45, 10);wprintf(L"自己撞自己了,120把你帶走了");}return;
}
EatFood函數(shù)
void EatFood(pSnake ps)
{// 加分ps->_totalScore += ps->_foodScore;// 打印總分?jǐn)?shù)SetPos(82, 7);printf("%4d", ps->_totalScore);// 是 頭插食物節(jié)點(diǎn) 創(chuàng)建新節(jié)點(diǎn)ps->_pFood->next = ps->_pSnake;ps->_pSnake = ps->_pFood;// 打印新蛇頭SetPos(ps->_pSnake->x, ps->_pSnake->y);wprintf(L"%c", BODY);// 創(chuàng)建新食物CreatFood(ps);
}
NotEatFood函數(shù)
// 沒(méi)有遲到食物的處理
void NotEatFood(pSnake ps, int x, int y)
{// 創(chuàng)建新節(jié)點(diǎn)并頭插pSnakeNode node = (pSnakeNode)malloc(sizeof(SnakeNode));if (node == NULL){perror("node malloc failed");exit(1);}node->x = x;node->y = y;node->next = ps->_pSnake;ps->_pSnake = node;// 打印新蛇 順便刪除尾節(jié)點(diǎn) 釋放空間 打印空格pSnakeNode cur = ps->_pSnake;while (cur->next->next != NULL){SetPos(cur->x, cur->y);wprintf(L"%c", BODY);cur = cur->next;}SetPos(cur->x, cur->y);wprintf(L"%c", BODY);SetPos(cur->next->x, cur->next->y);wprintf(L" ");free(cur->next);cur->next = NULL;
}
IsKillByWall函數(shù)
// 撞墻檢測(cè)
// 撞墻返回1 否則返回0
int IsKillByWall(pSnake ps)
{if (ps->_pSnake->x == 0 || ps->_pSnake->x == 56 || ps->_pSnake->y == 0 || ps->_pSnake->y == 26){return 1;}return 0;
}
IsKillBySelf函數(shù)
// 撞自己檢測(cè)
// 撞自己返回1 否則返回0
int IsKillBySelf(pSnake ps)
{pSnakeNode cur = ps->_pSnake->next;// 從第二個(gè)節(jié)點(diǎn)開(kāi)始遍歷 并判斷是否和蛇頭坐標(biāo)相同while (cur != NULL){if (cur->x == ps->_pSnake->x && cur->y == ps->_pSnake->y){return 1;}cur = cur->next;}return 0;
}
除了提到的以外,我們還要對(duì)加速減速的按鍵進(jìn)行處理,還有總分的統(tǒng)計(jì),失敗的打印信息等等,這里就由大家自由發(fā)揮啦,不行的話也可以看我的源碼
游戲結(jié)束
恭喜你,你已經(jīng)得到了一個(gè)屬于你的小小貪吃蛇游戲了,童年的游戲已經(jīng)被你復(fù)刻,但是你現(xiàn)在還不可以拍拍屁股走人哦,我們還需要做好善后工作啦
- 釋放資源
// 善后 釋放蛇節(jié)點(diǎn) 以及食物節(jié)點(diǎn)
void GameEnd(pSnake ps)
{// 釋放蛇節(jié)點(diǎn)pSnakeNode cur = NULL;while (ps->_pSnake != NULL){cur = ps->_pSnake;ps->_pSnake = ps->_pSnake->next;free(cur);}// 釋放食物節(jié)點(diǎn)free(ps->_pFood);
}
拓展建議
本文會(huì)有一些不足之處,但不影響整體的學(xué)習(xí),如果大家有問(wèn)題可以用自己的方法解決或者發(fā)在評(píng)論區(qū)
拓展建議:
- 可以優(yōu)化畫面,多加一些符號(hào),可以fancy一點(diǎn),比如這樣
// 使用--,|,/ 繪制英文SNAKE
/* _____ _ _____| \ |\_ | / \ | _/ ||_____ | \ | / \ |__/ |_____| | \_ | /_____\ | \_ |\____| | \| / \ | \ |_____*/
void PrintSnake(void)
{SetPos(32, 5);printf(" _____ _ _____");SetPos(32, 6);printf(" | \\ |\\_ | / \\ | _/ |");SetPos(32, 7);printf(" |_____ | \\ | / \\ |__/ |_____");SetPos(32, 8);printf(" | | \\_ | /_____\\ | \\_ |");SetPos(32, 9);printf(" \\____| | \\| / \\ | \\ |_____");
}
- 可以配合EasyX圖形庫(kù),做一個(gè)圖形化的貪吃蛇游戲
- 可以加入再來(lái)一局的功能
- 可以加入排行榜,計(jì)分等,配合寫入讀取文件
- 可以加入音效,配合Windows API的Beep函數(shù),不過(guò)會(huì)有阻塞
- 加入雙人模式,可以使用WSAD控制第二條蛇
- 等等等等還有好多啦,大家可以自行探索
寄語(yǔ)
感謝每一位看到這里的自己🌹🌹🌹
分享一句話:
我將玫瑰藏于身后,風(fēng)起花落,從此鮮花贈(zèng)自己,縱馬踏花向自由
源碼
test.c
#include "snake.h"int main()
{// 使用當(dāng)前的時(shí)間作為種子值srand(time(NULL));pSnake ps = (pSnake)malloc(sizeof(Snake));SnakeInit(ps);GameStart(ps); // 游戲初始化GameRun(ps); // 游戲運(yùn)行GameEnd(ps); // 游戲結(jié)束return 0;
}
snake.h
#pragma once#include <Windows.h>
#include <locale.h>
#include <wchar.h>
#include <stdio.h>
#include <stdlib.h>
#include <time.h>// 方向
typedef enum Direction
{UP = 1, // 上DOWN, // 下LEFT, // 左RIGHT // 右} Direction;// 游戲狀態(tài)
typedef enum GameStatus
{OK = 0, //運(yùn)行中KILL_BY_WALL, //撞墻KILL_BY_SELF, //撞自己PAUSE, //暫停ESC //退出
} GameStatus;// 蛇節(jié)點(diǎn)
// struct SnakeNode 取別名 SnakeNode
// struct SnakeNode * 取別名 pSnakeNode
typedef struct SnakeNode
{int x;int y;struct SnakeNode *next;
} SnakeNode, * pSnakeNode;// 貪吃蛇游戲
typedef struct Snake
{pSnakeNode _pSnake; // 蛇頭pSnakeNode _pFood; // 食物int _foodScore; // 單個(gè)食物分?jǐn)?shù)int _totalScore; // 總分?jǐn)?shù)int _sleepTime; // 睡眠時(shí)間 -- 控制速度Direction _dir; // 方向GameStatus _status; // 游戲狀態(tài)
} Snake, * pSnake;void GameStart(pSnake ps);
void SnakeInit(pSnake ps);
void GameRun(pSnake ps);
void GameEnd(pSnake ps);
snake.c
#include "snake.h"
#include <stdio.h>
#include <stdlib.h>#define KEY_PRESS(VK) (GetAsyncKeyState(VK) & 0x01 ? 1 : 0)
#define WALL L'□'
#define BODY L'●'
#define FOOD L'★'
#define POS_X 6
#define POS_Y 6// ↑↓←→●□★ // 貪吃蛇游戲結(jié)構(gòu)體初始化
void SnakeInit(pSnake ps)
{ps->_pSnake = NULL;ps->_pFood = NULL;ps->_foodScore = 10;ps->_totalScore = 0;ps->_status = OK;ps->_dir = RIGHT;ps->_sleepTime = 200;
}// 設(shè)置控制臺(tái)窗口大小和名稱
void CmdInit(void)
{// 設(shè)置控制臺(tái)窗口大小 100列 30行system("mode con cols=100 lines=30");// 設(shè)置控制臺(tái)名稱system("title snake");
}// 光標(biāo)隱藏
void CursorHide(void)
{// 獲取句柄HANDLE handle = GetStdHandle(STD_OUTPUT_HANDLE);// 光標(biāo)信息CONSOLE_CURSOR_INFO cursor_info = {0};// 獲取光標(biāo)信息GetConsoleCursorInfo(handle, &cursor_info);// 設(shè)置光標(biāo)屬性cursor_info.dwSize = 10;cursor_info.bVisible = 0;SetConsoleCursorInfo(handle, &cursor_info);
}// 設(shè)置光標(biāo)位置
void SetPos(int x, int y)
{// 獲取句柄HANDLE handle = GetStdHandle(STD_OUTPUT_HANDLE);// 設(shè)置光標(biāo)位置COORD pos = {x, y};SetConsoleCursorPosition(handle, pos);
}void PrintSnake(void)
{SetPos(45, 10);wprintf(L"歡迎來(lái)到貪吃蛇游戲");
}// 打印歡迎信息
void WelcomeToGame(void)
{PrintSnake();// 暫停SetPos(45, 20);system("pause");
}// 游戲介紹
void GameIntroduction(void)
{// 清屏system("cls");SetPos(45, 10);wprintf(L"游戲介紹:");SetPos(45, 12);wprintf(L"1. 使用↑,↓,←,→控制蛇的移動(dòng)");SetPos(45, 14);wprintf(L"2. 吃到食物蛇的長(zhǎng)度加1");SetPos(45, 16);wprintf(L"2. F3加速, F4減速");SetPos(45, 18);wprintf(L"3. 空格暫停");SetPos(45, 20);wprintf(L"4. Esc退出游戲");// 暫停SetPos(45, 22);system("pause");
}// 地圖繪制
// 上(0,0) - (56,0)
// 下(0,26) - (56,26)
// 左(0,0) - (0,26)
// 右(56,0) - (56,26)
// 注意打印的是寬字符 占兩個(gè)x坐標(biāo) 因此左右打印的時(shí)候要每打印一個(gè)x坐標(biāo)加2
void MapDraw(void)
{ // 清屏system("cls");// 上墻for (int i = 0; i < 57; i+=2){SetPos(i, 0);wprintf(L"%c", WALL);}// 下墻for (int i = 0; i < 57; i+=2){SetPos(i, 26);wprintf(L"%c", WALL);}// 最上面和最下面已經(jīng)打印過(guò)了// 左墻for (int i = 1; i < 26; i++){SetPos(0, i);wprintf(L"%c", WALL);}// 右墻for (int i = 1; i < 26; i++){SetPos(56, i);wprintf(L"%c", WALL);}
}// 打印靜態(tài)幫助信息
// 單個(gè)食物分?jǐn)?shù),總分?jǐn)?shù)的名稱
// 操作說(shuō)明
void PrintStaticHelp(void)
{SetPos(70, 5);wprintf(L"單個(gè)食物分?jǐn)?shù): 10");SetPos(70, 7);wprintf(L"總分?jǐn)?shù): 0");SetPos(70, 9);wprintf(L"操作說(shuō)明:");SetPos(70, 11);wprintf(L"↑ : 上移");SetPos(70, 13);wprintf(L"↓ : 下移");SetPos(70, 15);wprintf(L"← : 左移");SetPos(70, 17);wprintf(L"→ : 右移");SetPos(70, 19);wprintf(L"F3: 加速");SetPos(70, 21);wprintf(L"F4: 減速");SetPos(70, 23);wprintf(L"空格: 暫停");SetPos(70, 25);wprintf(L"Esc: 退出");
}// 蛇初始化
// 在地圖的(6,6)位置初始化蛇
// 蛇的長(zhǎng)度為5
// 使用頭插法
void InitSnake(pSnake ps)
{pSnakeNode cur = NULL;int i = 0;// 創(chuàng)建蛇身節(jié)點(diǎn) 并初始化坐標(biāo) for (i = 0; i < 5; i++){pSnakeNode node = (pSnakeNode)malloc(sizeof(SnakeNode));if (node == NULL){perror("pSnakeNode malloc failed");exit(1);}node->x = POS_X + i * 2;node->y = POS_Y;node->next = NULL;// 頭插法if (ps->_pSnake == NULL){ps->_pSnake = node;}else{node->next = ps->_pSnake;ps->_pSnake = node;}}// 打印蛇cur = ps->_pSnake;while (cur != NULL){SetPos(cur->x, cur->y);wprintf(L"%c", BODY);cur = cur->next;}
}// 創(chuàng)建食物節(jié)點(diǎn)
void CreatFood(pSnake ps)
{// 申請(qǐng)節(jié)點(diǎn)空間pSnakeNode foodnode = (pSnakeNode)malloc(sizeof(SnakeNode));if (foodnode == NULL){perror("foodnode malloc failed");exit(1);}foodnode->next = NULL;// 隨機(jī)生成食物坐標(biāo) 在墻范圍內(nèi)但是不能在蛇身上 而且x坐標(biāo)是偶數(shù)while (1){int x = rand() % 54 + 2;int y = rand() % 24 + 2;pSnakeNode cur = ps->_pSnake;while (cur != NULL){if (cur->x == x && cur->y == y || x % 2 != 0){break;}cur = cur->next;}if (cur == NULL){foodnode->x = x;foodnode->y = y;break;}}// 打印食物SetPos(foodnode->x, foodnode->y);wprintf(L"%c", FOOD);ps->_pFood = foodnode;
}// 撞墻檢測(cè)
// 撞墻返回1 否則返回0
int IsKillByWall(pSnake ps)
{if (ps->_pSnake->x == 0 || ps->_pSnake->x == 56 || ps->_pSnake->y == 0 || ps->_pSnake->y == 26){return 1;}return 0;
}// 撞自己檢測(cè)
// 撞自己返回1 否則返回0
int IsKillBySelf(pSnake ps)
{pSnakeNode cur = ps->_pSnake->next;// 從第二個(gè)節(jié)點(diǎn)開(kāi)始遍歷 并判斷是否和蛇頭坐標(biāo)相同while (cur != NULL){if (cur->x == ps->_pSnake->x && cur->y == ps->_pSnake->y){return 1;}cur = cur->next;}return 0;
}// 吃掉食物后的處理
void EatFood(pSnake ps)
{// 加分ps->_totalScore += ps->_foodScore;// 打印總分?jǐn)?shù)SetPos(82, 7);printf("%4d", ps->_totalScore);// 是 頭插食物節(jié)點(diǎn) 創(chuàng)建新節(jié)點(diǎn)ps->_pFood->next = ps->_pSnake;ps->_pSnake = ps->_pFood;// 打印新蛇頭SetPos(ps->_pSnake->x, ps->_pSnake->y);wprintf(L"%c", BODY);// 創(chuàng)建新食物CreatFood(ps);
}// 沒(méi)有遲到食物的處理
void NotEatFood(pSnake ps, int x, int y)
{// 創(chuàng)建新節(jié)點(diǎn)并頭插pSnakeNode node = (pSnakeNode)malloc(sizeof(SnakeNode));if (node == NULL){perror("node malloc failed");exit(1);}node->x = x;node->y = y;node->next = ps->_pSnake;ps->_pSnake = node;// 打印新蛇 順便刪除尾節(jié)點(diǎn) 釋放空間 打印空格pSnakeNode cur = ps->_pSnake;while (cur->next->next != NULL){SetPos(cur->x, cur->y);wprintf(L"%c", BODY);cur = cur->next;}SetPos(cur->x, cur->y);wprintf(L"%c", BODY);SetPos(cur->next->x, cur->next->y);wprintf(L" ");free(cur->next);cur->next = NULL;
}// 蛇的移動(dòng) -- 主游戲程序 在此循環(huán)
// 吃到食物 創(chuàng)建新的食物節(jié)點(diǎn)
// 沒(méi)有吃到食物頭插新節(jié)點(diǎn)刪除尾節(jié)點(diǎn)并釋放空間
// 減速睡眠時(shí)間-30 睡眠時(shí)間最少減4次 單個(gè)食物分?jǐn)?shù)加2
// 加速睡眠時(shí)間+30 睡眠時(shí)間最多加4次 單個(gè)食物分?jǐn)?shù)減2
void SnakeMove(pSnake ps)
{int x = 0;int y = 0;again:while (ps->_status == OK){// 按鍵檢測(cè)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)){ps->_status = PAUSE;}else if (KEY_PRESS(VK_F3) && ps->_foodScore < 20){ps->_sleepTime -= 30;ps->_foodScore += 2;// 更改單個(gè)食物分?jǐn)?shù)SetPos(84, 5);printf("%2d", ps->_foodScore);}else if (KEY_PRESS(VK_F4) && ps->_foodScore > 2){ps->_sleepTime += 30;ps->_foodScore -= 2;// 更改單個(gè)食物分?jǐn)?shù)SetPos(84, 5);printf("%2d", ps->_foodScore);}else if (KEY_PRESS(VK_ESCAPE)){ps->_status = ESC;}// 設(shè)置下一個(gè)節(jié)點(diǎn)的x,y坐標(biāo)switch (ps->_dir){case UP:x = ps->_pSnake->x;y = ps->_pSnake->y - 1;break;case DOWN:x = ps->_pSnake->x;y = ps->_pSnake->y + 1;break;case LEFT:x = ps->_pSnake->x - 2;y = ps->_pSnake->y;break;case RIGHT:x = ps->_pSnake->x + 2;y = ps->_pSnake->y;break;}// 判斷是否吃到食物if (ps->_pFood->x == x && ps->_pFood->y == y){EatFood(ps);}else {NotEatFood(ps, x, y);}// 撞墻檢測(cè)if (IsKillByWall(ps)){ps->_status = KILL_BY_WALL;}// 撞自己檢測(cè)if (IsKillBySelf(ps)){ps->_status = KILL_BY_SELF;}Sleep(ps->_sleepTime);}while (ps->_status == PAUSE){if (KEY_PRESS(VK_SPACE)){ps->_status = OK;}goto again;}// 撞墻打印信息if (IsKillByWall(ps)){SetPos(45, 10);wprintf(L"墻墻被你撞西了,110把你抓走了");}// 撞自己打印信息if (IsKillBySelf(ps)){SetPos(45, 10);wprintf(L"自己撞自己了,120把你帶走了");}return;
}// 游戲前的初始化
void GameStart(pSnake ps)
{// 初始化控制臺(tái)CmdInit();CursorHide();// 本地化配置setlocale(LC_ALL, "");WelcomeToGame();GameIntroduction();MapDraw();InitSnake(ps);CreatFood(ps);
}// 游戲運(yùn)行
void GameRun(pSnake ps)
{PrintStaticHelp();SnakeMove(ps);SetPos(45, 28);system("pause");
}// 善后 釋放蛇節(jié)點(diǎn) 以及食物節(jié)點(diǎn)
void GameEnd(pSnake ps)
{// 釋放蛇節(jié)點(diǎn)pSnakeNode cur = NULL;while (ps->_pSnake != NULL){cur = ps->_pSnake;ps->_pSnake = ps->_pSnake->next;free(cur);}// 釋放食物節(jié)點(diǎn)free(ps->_pFood);
}