贪吃蛇
贪吃蛇实现出来的效果如下:
贪吃蛇小游戏录屏
完整代码:
#define _CRT_SECURE_NO_WARNINGS 1 #include<time.h> #include<stdio.h> #include<stdlib.h> #include<windows.h> #include<stdbool.h> #include<locale.h> #define WALL L'□' #define BODY L'●' #define FOOD L'★' #define POS_X 24 #define POS_Y 5 #define KEY_PRESS(VK) ((GetAsyncKeyState(VK) & 0x1) ? 1 : 0) enum DIRECTION//蛇头方向 { UP = 1, DOWN, LEFT, RIGHT }; enum GAEM_STATUS//蛇运行状态 { RUNNING,//运行状态 EXIT_NORMAL,//正常退出状态 KILL_BY_WALL,//撞墙 KILL_BY_SELF//撞到自己 }; typedef struct SnakeNode { //位置坐标 int x; int y; struct SnakeNode* next; }SnakeNode, * pSnakeNode; typedef struct Snake { pSnakeNode _psnake;//贪吃蛇头结点 pSnakeNode _pFood;//指向食物的节点 int _score;//目前得分情况 int _FoodWeight;//一个食物的分数 int _SleepTime;//定义休眠时间 enum DIRECTION _Dir;//蛇的方向 enum GAEM_STATUS _Status;//游戏状态 }Snake, * pSnake; int SetPos(short x, short y)//重定位光标位置 { COORD pos = { x, y }; HANDLE output = NULL; //获取标准输出的句柄,表示不同设备的数值 output = GetStdHandle(STD_OUTPUT_HANDLE); //设置标准输出上的光标位置为pos位置 SetConsoleCursorPosition(output, pos); return 1; } void StartUI() { //定位光标 SetPos(36, 13); printf("Welcome to Snacks Game!\n"); SetPos(36, 25);//重定位终端提示符,把终端提示信息放在上面那句话下面 system("pause"); system("cls"); SetPos(36, 13); printf("using ↑,↓,←,→,\ \n\t\t\tControl the direction in which the snake moves,\ \n\t\t\t\tF3 is hasten, F4 is decelerate"); SetPos(36, 25); system("pause"); system("cls"); return; } void InitSnake(pSnake ps) { pSnakeNode cur = NULL; for (int i = 0; i < 5; i++)//蛇身五个节点 { cur = (pSnakeNode)malloc(sizeof(pSnakeNode)); if (cur == NULL)//检测空指针 { perror("InitSnake::alloc()"); return; } cur->x = POS_X + 2 * i;//初始化蛇,让蛇身在开始的时候是横着放的,一个蛇身字符是两个字节,所以要乘2 cur->y = POS_Y;//横着的蛇身,y不用变 cur->next = NULL;//将指针域置为空 //节点头插 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->_Status = RUNNING;//游戏状态置为运行中 ps->_score = 0;//游戏分数置为0 ps->_pFood = NULL;//食物节点先设置为空 ps->_SleepTime = 200;//刷新时间 ps->_FoodWeight = 10;//食物重量设置10 ps->_Dir = RIGHT;//蛇头方向 return; } void CreateMap() { //上 SetPos(0, 0); for (int i = 0; i <= 56; i += 2) { wprintf(L"%lc", WALL); } //下 SetPos(0, 26); for (int i = 0; i <= 56; i += 2) { wprintf(L"%lc", WALL); } //左 for (int i = 0; i <= 25; i++) { SetPos(0, i); wprintf(L"%lc", WALL); } //右 for (int i = 0; i <= 25; i++) { SetPos(56, i); wprintf(L"%lc", WALL); } return; } void CreateFood(pSnake ps) { int x = 0, y = 0; again: do { x = rand() % 53 + 2; y = rand() % 25 + 1; } while (x % 2 != 0); pSnakeNode cur = ps->_psnake; while (cur) { if (cur->x == x || cur->y == y) { goto again; } cur = cur->next; } pSnakeNode pFood = (pSnakeNode)malloc(sizeof(pSnakeNode)); if (pFood == NULL) { perror("CreateFood::alloc()"); return; } pFood->x = x; pFood->y = y; ps->_pFood = pFood; SetPos(x, y); wprintf(L"%lc", FOOD); return; } void GameStart(pSnake ps) { //设置终端窗口大小 system("mode con cols=100 lines=30"); system("title 贪吃蛇"); //把光标隐藏 HANDLE output = GetStdHandle(STD_OUTPUT_HANDLE); //隐藏光标具体操作(调用微软提供的C接口) CONSOLE_CURSOR_INFO CursorInfo; GetConsoleCursorInfo(output, &CursorInfo);//获取控制台光标信息 CursorInfo.bVisible = false;//将光标显示设置为false SetConsoleCursorInfo(output, &CursorInfo);//设置控制台光标状态 //欢迎界面 StartUI(); //打印地图 CreateMap(); //初始化贪吃蛇 InitSnake(ps); //创建食物 CreateFood(ps); return; } void PrintHelpInfo() { SetPos(62, 9); printf("1、you can't hit the wall"); SetPos(62, 10); printf("or bite yourself"); SetPos(62, 12); printf("2、using ↑,↓,←,→"); SetPos(62, 13); printf(",Control snake moving"); SetPos(62, 15); printf("3、F3 is hasten, F4 is decelerate"); SetPos(62, 17); printf("4、ESC-Exit Geme, space-time out"); return; } void TimeOut() { while (1) { Sleep(100); if (KEY_PRESS(VK_SPACE)) { break; } } } int NextIsFood(pSnake ps, pSnakeNode pnext) { if (ps->_pFood->x == pnext->x && ps->_pFood->y == pnext->y) { return 1; } else { return 0; } } void EatFood(pSnake ps, pSnakeNode pnext) { //头插 pnext->next = ps->_psnake; ps->_psnake = pnext; //打印蛇 pSnakeNode cur = ps->_psnake; while (cur) { SetPos(cur->x, cur->y); wprintf(L"%lc", BODY); cur = cur->next; } free(ps->_pFood); ps->_score += ps->_FoodWeight; CreateFood(ps); return; } void Space(pSnake ps, pSnakeNode pnext)//表示前面位置为空地 { //头插 pnext->next = ps->_psnake; ps->_psnake = pnext; //打印蛇 pSnakeNode cur = ps->_psnake; while (cur->next->next) { SetPos(cur->x, cur->y); wprintf(L"%lc", BODY); cur = cur->next; } SetPos(cur->next->x, cur->next->y); printf(" "); /*pSnakeNode tmp = cur->next; free(tmp);*/ cur->next = NULL; return; } //是否撞墙 void KillByWall(pSnake ps) { if (ps->_psnake->x == 00 || ps->_psnake->x == 56 || ps->_psnake->y == 0 || ps->_psnake->y == 26) ps->_Status = KILL_BY_WALL; return; } //是否咬到自己 void KillBySelf(pSnake ps) { pSnakeNode cur = ps->_psnake->next; while (cur) { if (ps->_psnake->x == cur->x && ps->_psnake->y == cur->y) { ps->_Status = KILL_BY_SELF; } cur = cur->next; } } void SnakeMove(pSnake ps) { pSnakeNode pNext = (pSnakeNode)malloc(sizeof(pSnakeNode)); if (pNext == NULL) { perror("SnakeMove::alloc()"); return; } pNext->next = NULL; switch (ps->_Dir) { case UP: pNext->x = ps->_psnake->x; pNext->y = ps->_psnake->y - 1; break; case DOWN: pNext->x = ps->_psnake->x; pNext->y = ps->_psnake->y + 1; break; case LEFT: pNext->x = ps->_psnake->x - 2; pNext->y = ps->_psnake->y; break; case RIGHT: pNext->x = ps->_psnake->x + 2; pNext->y = ps->_psnake->y; break; } //判断下个位置是不是食物 if (NextIsFood(ps, pNext)) { //吃食物 EatFood(ps, pNext); } else { //不是食物 Space(ps, pNext); } //是否撞墙 KillByWall(ps); //是否咬到自己 KillBySelf(ps); return; } void GameRun(pSnake ps) { PrintHelpInfo(); do { SetPos(64, 3); printf("Score: %05d", ps->_score); SetPos(64, 4); printf("Every food's score:%2d", ps->_FoodWeight); 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_ESCAPE)) { ps->_Status = EXIT_NORMAL; break; } else if (KEY_PRESS(VK_SPACE)) { TimeOut(); } else if (KEY_PRESS(VK_F3)) { if (ps->_SleepTime >= 80) { ps->_SleepTime -= 30; ps->_FoodWeight += 2; } } else if (KEY_PRESS(VK_F4)) { if (ps->_SleepTime < 320) { ps->_SleepTime += 30; ps->_FoodWeight -= 2; } } Sleep(ps->_SleepTime); SnakeMove(ps); } while (ps->_Status == RUNNING);//游戏运行时各个信息设置打印等 return; } void GameEnd(pSnake ps) { SetPos(20, 12); switch (ps->_Status) { case EXIT_NORMAL: printf("Exit the game\n"); break; case KILL_BY_WALL: printf("Kill yourself, gameover\n"); break; case KILL_BY_SELF: printf("Hit the wall, you are die\n"); break; } SetPos(0, 27); pSnakeNode cur = ps->_psnake; while (cur) { pSnakeNode del = cur; cur = cur->next; //free(del); } ps->_psnake = NULL; system("pause"); return; } void Test() { char ch = 0; do { Snake snake = { 0 };//创建贪吃蛇对象 //开始游戏 GameStart(&snake); //运行游戏 GameRun(&snake); //游戏结束 GameEnd(&snake); SetPos(20, 10); printf("Another round?(Y/N)\n"); SetPos(40, 10); ch = getchar(); getchar(); } while (ch == 'Y' || ch == 'y'); SetPos(0, 26); return; } int main() { //设置本地中文字符 setlocale(LC_ALL, ""); srand((unsigned int)time(NULL)); Test(); return 0; }
使用到的WIN32一些接口简单介绍
实现过程使用了WIN32的一些API,这里简单介绍一下这些API的功能。
控制台窗口大小
设置控制台窗口大小,在windows界面的cmd中我们可以输入这样的指令来控制窗口的大小:
mode con cols=100 lines=30 #控制窗口,cols为行长度,lines为列行数
打开win的终端输入该指令,就可以调整窗口的大小了,效果如下:
命令行窗口的名称也可以通过命令的方式来更改:
title 贪吃蛇#更改命令行窗口的名称
同样,打开windows的cmd输入指令,效果如下:
在C语言中,我们需要使用system接口来改变终端 窗口的大小 以及 窗口名称,使用system接口需要包含 stdlib.h 头文件,例如下面代码:
#include<stdio.h> #include<stdlib.h//使用system接口的头文件 int main() { system("title 贪吃蛇");//将命令行窗口的名字更改为需要的名字 system("mode con cols=100 lines=30");//设置命令行窗口的大小 //其他操作 return 0; }
隐藏光标
通常,我们的终端也可看作坐标系,左上角为坐标原点,向右为x轴,向下位y轴,如下图所示:
我们在windows窗口上描述一个坐标需要使用一个windows API中定义的一个结构体 COORD,表示一个字符在控制台屏幕缓冲区上的坐标,在C语言中,我们需要包含 windows.h 头文件才能使用,使用实例如下:
#include<stdio.h> #include<windows.h>//调用该api需要的头文件 #include<stdlib.h> int main() { COORD pos = { 20, 20 };//使用第一个参数为行,第二参数为列 return 0; }
实现光标隐藏,我们需要先调用 GetStdHandle 函数来获取标准输出句柄(什么是句柄可以看这个blogger的文章:戳我跳转),使用这个句柄可以操作设备。
HANDLE output = NULL;//HANDLE为结构体指针类型 //获取标准输出句柄来表示不同设备的数值 output = GetStdHandle(STD_OUTPUT_HANDLE);
要隐藏光标,我们就先要获得一个光标信息,上面我们已经获取了标准输出相关设备的句柄,接下来我们创建 CONSOLE_CORSOR_INFO 结构体对象(接收有关主机光标信息的结构体),再调用 GetConsoleCursorInfo 函数来获得光标信息:
#include<stdio.h> #include<windows.h>//调用win32 api所需要的头文件 int main() { HANDLE output = NULL; //获取标准输出句柄来表示不同设备的数值 output = GetStdHandle(STD_OUTPUT_HANDLE); CONSOLE_CURSOR_INFO cursor_info; GetConsoleCursorInfo(output, &cursor_info);//获取光标的信息 return 0; }
CONSOLE_CURSOR_INFO这个结构体包含了控制台光标信息:
typedef struct _CONSOLE_CURSOLE_INFO { DWORD dwSize; BOOL bVisible; }CONSOLE_CURSOR_INFO, *PCONSOLE_CURSOR_INFO;
- dwSize 参数,由光标填充的字符单元格的百分比。值范围为1到100。光标外观会变化,范围从完全填充单元格到单元底部的水平线条。
- bVisible 参数,设置光标的可见性,如果光标不可见,设置为false。
我们调用结构体的第二个参数设置为false(C语言要包含 stdbool.h 头文件才能使用布尔类型),然后再调用 SetConsoleCursorInfo 函数来设置更改的光标信息。
#include<stdio.h> #include<stdbool.h> #include<windows.h> int main() { HANDLE output = NULL; //获取标准输出句柄来表示不同设备的数值 output = GetStdHandle(STD_OUTPUT_HANDLE); CONSOLE_CURSOR_INFO cursor_info; GetConsoleCursorInfo(output, &cursor_info);//获取光标的信息 cursor_info.bVisible = false; SetConsoleCursorInfo(output, &cursor_info);//设置更改信息 int ch = getchar(); putchar(ch); return 0; }
使用getchar putchar来输入输出信息,检测是否隐藏光标成功:
控制光标的位置
设置终端光标输出位置,我们首先要获取想要输出位置的坐标,上面我们介绍了COORD结构体,用来设置位置坐标。获取完坐标之后,我们可以调用 SetConsoleCorsorPosition 函数将光标位置设置到获取的坐标位置。
BOOL SetConsoleCorsorPosition{ HANDLE output;//句柄 COORD pos;//位置 };
有了这个接口我们就可以将光标输出的信息放在想要的位置上了:
#include<stdio.h> #include<stdbool.h> #include<windows.h> int main() { HANDLE output = NULL; //获取标准输出句柄来表示不同设备的数值 output = GetStdHandle(STD_OUTPUT_HANDLE); COORD pos = { 20, 20 }; SetConsoleCursorPosition(output, pos); int ch = getchar(); putchar(ch); return 0; }
效果如下:
向上面这样写未免有些麻烦,我们可能会多次改变光标的输出位置,因此我们不妨把其封装成一个函数,使其一条语句也能完成光标定位:
void SetPos(int x, int y) { HANDLE output = GetStdHandle(STD_OUTPUT_HANDLE); COORD pos = { 20, 20 }; SetConsoleCursorPosition(output, pos); }
这样我们在定位光标的时候就简单多了:
#include<stdio.h> #include<stdbool.h> #include<windows.h> void SetPos(int x, int y) { HANDLE output = GetStdHandle(STD_OUTPUT_HANDLE); COORD pos = { 20, 20 }; SetConsoleCursorPosition(output, pos); } int main() { SetPos(20, 20); int ch = getchar(); putchar(ch); return 0; }
执行结果如下:
这样就能实现控制光标输出位置了。
获取键盘的值的情况
完贪吃蛇我们一定需要用键盘来控制一些功能,我们可以使用 GetAsyncKeyState 函数来获取按键情况,此函数函数原型如下:
SHORT GetAsyncKeyState(int vKey);
将键盘上的键值传给函数,通过函数返回值来判断按键的状态。GetAsyncKeyState 返回值是short类型,在上一次调用此函数后,如果返回的16位的short数据中,最高位是1,说明按键的状态是按下,如果最高位是0,说明按键的状态是抬起;如果最低位被置为1,则说明该按键被按过,否则位0。
如果我们要判断按键是否被按过,只需要判断返回值最低值是否为1即可,我们可以按位与上0x1来获取最低位的值,那么我们就可这样来编写函数:
#define KEY_PRESS(VK) ((GetAsyncKeyState(VK) & 0x1) ? 1 : 0)//返回1表示按过,返回0表示没有按过
我们可以通过虚拟键码(虚拟键码:戳我查看)来判断是不同按键的不同状态,这样就可以实现一些按键响应的功能了。
字符问题
我们在打印蛇身和墙体的时候,是需要特殊字符——宽字符 宽字符的长度为2字节,因为不同地区的语言不同,计算机中描述的方式也不太一样,普通的单字节字符并不适合我们的地区,因此C语言加入了宽字符(字符类型:wchar_t 需要包含 locale.h 头文件)允许程序员针对特定地区调整程序行为函数。
类项: 通过修改地区,程序可以改变它的行为来适应世界的不同区域。但是地区改变可能会影响库的许多部分,其中一部分可能是我们不希望修改的,所以C语言针对不同类型的类项进行修改,下面的一个宏指定一个类项:
- LC_COLLATE:影响字符串比较函数
- LC_CTYPE:影响字符处理函数行为
- LC_MONETARY:影响货币格式
- LC_NUMERIC:影响printf()函数
- LC_TIME:影响时间格式
- LC_ALL:针对所有类项修改,将以上所有类别设定为给定的语言环境
我们使用 setlocale 函数修改类项:
char* setlocale(int category, const char* locale);
函数的第一个参数可以是前面说明的类项中的一个,那么每次只会影响一个类项,如果第一个参数是LC_ALL,就会影响所有类项。C标准给第二个参数定义了2种可能取值:“C”(正常模式)和“”(本地模式) 在任意程序执行开始,默认隐式调用:
setlocale(LC_ALL, "C");
我们需要切换到本地环境输出字符,所以:
setlocale(LC_ALL, " ");//切换为本地环境
我们想要打印宽字符也是与普通打印不同的,宽字符字面量前必须加上L,否则C语言就会将其当为窄字符,且占位符应当为"%lc",和"%ls",才可正常打印宽字符。
#include<stdio.h> #include<locale.h> int main() { setlocale(LC_ALL, ""); wchar_t ch = L'蛇'; wprintf(L"%lc\n", ch); return 0; }
效果如下:
游戏逻辑
我们采用链式结构类表示贪吃蛇,所以我们需要一个结构体来描述蛇的节点以及蛇的一些属性等:
#define WALL L'□' #define BODY L'●' #define FOOD L'★' #define POS_X 24 #define POS_Y 5 #define KEY_PRESS(VK) ((GetAsyncKeyState(VK) & 0x1) ? 1 : 0) enum DIRECTION//蛇头方向 { UP = 1, DOWN, LEFT, RIGHT }; enum GAEM_STATUS//蛇运行状态 { RUNNING,//运行状态 EXIT_NORMAL,//正常退出状态 KILL_BY_WALL,//撞墙 KILL_BY_SELF//撞到自己 }; typedef struct SnakeNode //蛇的节点 { //位置坐标 int x; int y; struct SnakeNode* next;//指针域 }SnakeNode, *pSnakeNode; typedef struct Snake { pSnakeNode _psnake;//贪吃蛇头结点 pSnakeNode _pFood;//指向食物的节点 int _score;//目前得分情况 int _FoodWeight;//一个食物的分数 int _SleepTime;//定义休眠时间 enum DIRECTION _Dir;//蛇的方向 enum GAEM_STATUS _Status;//游戏状态 }Snake, *pSnake;
描述完蛇的所有情况之后,我们可以把游戏拆分成三部分:游戏开始前,游戏运行时,游戏结束时。
void test() { Snake snake = { 0 };//创建贪吃蛇对象 //开始游戏 GameStart(&snake); //运行游戏 GameRun(&snake); //游戏结束 GameEnd(&snake); return; }
开始游戏
游戏开始的时候我们隐藏光标、设置窗口大小及名称,随后我们打印欢迎界面,打印地图,初始化贪吃蛇以及创建食物等操作。
打印地图
需要注意的是,打印地图的时候,其实我们终端x轴的密度约是y轴的两倍,也就是说x轴两个单位约等于y轴一个单位,这里我给了一个合适的值:
void CreateMap() { //上 SetPos(0, 0); for (int i = 0; i <= 56; i += 2) { wprintf(L"%lc", WALL); } //下 SetPos(0, 26); for (int i = 0; i <= 56; i += 2) { wprintf(L"%lc", WALL); } //左 for (int i = 0; i <= 25; i++) { SetPos(0, i); wprintf(L"%lc", WALL); } //右 for (int i = 0; i <= 25; i++) { SetPos(56, i); wprintf(L"%lc", WALL); } return; }
地图效果:
初始化贪吃蛇
我这里初始化采用链表的头插法来进行插入,设置最初蛇的长度为5。节点创建完了之后打印出蛇身,最后再设置蛇的一系列初始状态:
void InitSnake(pSnake ps) { pSnakeNode cur = NULL; for (int i = 0; i < 5; i++)//蛇身五个节点 { cur = (pSnakeNode)malloc(sizeof(pSnakeNode)); if (cur == NULL)//检测空指针 { perror("InitSnake::alloc()"); return; } cur->x = POS_X + 2 * i;//初始化蛇,让蛇身在开始的时候是横着放的,一个蛇身字符是两个字节,所以要乘2 cur->y = POS_Y;//横着的蛇身,y不用变 cur->next = NULL;//将指针域置为空 //节点头插 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->_Status = RUNNING;//游戏状态置为运行中 ps->_score = 0;//游戏分数置为0 ps->_pFood = NULL;//食物节点先设置为空 ps->_SleepTime = 200;//刷新时间 ps->_FoodWeight = 10;//食物重量设置10 ps->_Dir = RIGHT;//蛇头方向 return; }
创建食物
因为蛇是采用链式结构,所以我们的食物也采用节点的方式来存储,首先贪吃蛇的食物是随机刷新的,并且在蛇吃完后才会刷新另一个。而我们地图大小是56 * 27的,食物也是一个宽字符,所以需要保证不能越界,且创建食物时,不能将食物创建在蛇身上:
int x = 0, y = 0; again: do { x = rand() % 53 + 2;//横坐标2-54范围刚好不会越界 y = rand() % 25 + 1;//纵坐标1-25范围也不会越界 } while (x % 2 != 0);//食物位置正确打印,保证与蛇在一条线上 pSnakeNode cur = ps->_psnake; while (cur)//保证创建食物不在蛇身上 { if (cur->x == x || cur->y == y) { goto again; } cur = cur->next; }
所以这里采用goto还是很合适的,最后再创建出正确的食物节点,将节点赋值给ps的食物节点,在对应位置打印出食物:
运行游戏
游戏运行时,首先打印帮助信息,再打印食物的分数信息,然后根据按键按下的状态执行下一步的操作
这里要注意的是,如果是要控制蛇的方向,如果当前蛇头的位置朝右,那我们就不能向左走,同理,蛇头位置朝上,我们不能朝下走…
除此之外,还需要判断当前按键是不是退出、暂停、加速、减速等状态,如果对应了状态就做对用的事情,并且这些信息是需要不断刷新的,因此,将其放在循环中在合适不过,当游戏状态为RUNNING时,就一直循环:
do { SetPos(64, 3); printf("Score: %05d", ps->_score); SetPos(64, 4); printf("Every food's score:%3d", ps->_FoodWeight); 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_ESCAPE)) { ps->_Status = EXIT_NORMAL; break; } else if (KEY_PRESS(VK_SPACE)) { TimeOut(); } else if (KEY_PRESS(VK_F3))//加速 { if (ps->_SleepTime >= 80) { ps->_SleepTime -= 30;//刷新时间减少 ps->_FoodWeight += 2;//食物重量增加 } } else if (KEY_PRESS(VK_F4))//减速 { if (ps->_SleepTime < 320) { ps->_SleepTime += 30;//刷新时间增加 ps->_FoodWeight -= 2;//食物重量减少 } } Sleep(ps->_SleepTime);//休眠时间 SnakeMove(ps);//控制蛇的移动 } while (ps -> _Status == RUNNING);//游戏运行时各个信息设置打印等
控制蛇的移动
控制蛇的移动,可以根据按键的状态来对蛇的坐标进行定位,上面我们已经将蛇的_Dir状态置为了对应的宏,再根据这个宏来进行方向选择,将坐标变换。
如果是向右,就将向右的方向x位置+2(密度原因所以+2),y轴方向不变。同理,向下时y+1,x不变…
新位置用一个临时节点来接收,因为需要判断下面的位置是不是食物,以及蛇到底是不是撞墙或者咬到自己了,判断食物与死亡比较简单,大家可以看源码来分析,这里就不具体展示了。
void SnakeMove(pSnake ps) { pSnakeNode pNext = (pSnakeNode)malloc(sizeof(pSnakeNode));//新节点接收 if (pNext == NULL) { perror("SnakeMove::alloc()"); return; } pNext->next = NULL; switch (ps->_Dir)//判断蛇头朝向,以及下一步该如何走 { case UP: pNext->x = ps->_psnake->x; pNext->y = ps->_psnake->y - 1; break; case DOWN: pNext->x = ps->_psnake->x; pNext->y = ps->_psnake->y + 1; break; case LEFT: pNext->x = ps->_psnake->x - 2; pNext->y = ps->_psnake->y; break; case RIGHT: pNext->x = ps->_psnake->x + 2; pNext->y = ps->_psnake->y; break; } //判断下个位置是不是食物 if (NextIsFood(ps, pNext)) { //吃食物 EatFood(ps, pNext); } else { //不是食物,为空地 Space(ps, pNext); } //是否撞墙 KillByWall(ps); //是否咬到自己 KillBySelf(ps); return; }
运行结束
结束的逻辑也比较简单,判断退出状态,如果为正常退出,打印一句提示信息,如果为撞墙或者咬到自己,也各自打印一句提示信息。最后游戏结束,将蛇的链式节点全部释放即可。
void GameEnd(pSnake ps) { SetPos(20, 12);//在合适的位置打印提示信息 switch (ps->_Status) { case EXIT_NORMAL: printf("Exit the game\n"); break; case KILL_BY_WALL: printf("Hit the wall, you are die\n"); break; case KILL_BY_SELF: printf("Kill yourself, gameover\n"); break; } SetPos(0, 27);//将进程退出信息放到最下面 pSnakeNode cur = ps->_psnake; while (cur)//将蛇的节点全部销毁 { pSnakeNode del = cur; cur = cur->next; free(del); } ps->_psnake = NULL; system("pause"); return; }
游戏结束后,我们可以安排是否再来一局,只需要在外层套上循环即可,输入一个提示信息,如果信息正确,则再来一局,否则退出。
void Test() { char ch = 0; do { Snake snake = { 0 };//创建贪吃蛇对象 //开始游戏 GameStart(&snake); //运行游戏 GameRun(&snake); //游戏结束 GameEnd(&snake); SetPos(20, 10); printf("Another round?(Y/N)\n"); SetPos(40, 10); ch = getchar(); getchar(); } while (ch == 'Y' || ch == 'y'); SetPos(0, 26); return; }
用单链表的形式写贪吃蛇还是挺简单的,这也可以检测你C语言到底学的扎不扎实,如果我写的有些问题,欢迎各位佬在评论区里指出更正~~