When the night gets dark,remember that the Sun is also a star.
当夜幕降临时,请记住太阳也是一颗星星。
————《去月球海滩篇》
目录
文章目录
一、《贪吃蛇》游戏介绍
二、WIN32部分接口简单介绍
2.1 控制台窗口大小设置
2.2 命令行窗口的名称的变更
2.3 vs平台下的调试窗口的设置
2.4 隐藏光标
2.5 控制光标的位置
2.6 获取键盘的值的情况
2.7 字符问题
三、贪吃蛇的实现
3.1 游戏开始前的准备 GameStart函数
3.1.1 设置好控制台界面的行和列,隐藏控制台光标
3.1.2 打印欢迎界面
3.1.3 绘制地图
3.1.4 初始化蛇身
3.1.5 创建食物
3.2 游戏运行 GameRun函数
3.2.1 打印帮助信息
3.2.2 判断玩家按下的按键
3.2.3 蛇的移动
3.2.3.1 NextFood
3.2.3.2 EatFood
3.2.3.3 Kill_Wall
3.2.3.4 Kill_Self
3.2.4 .Sleep函数、循环移动
3.3 贪吃蛇游戏结束的善后
3.4 再来一局,不言弃!!
一、《贪吃蛇》游戏介绍
也许正在看的你玩过一款名为《贪吃蛇》的小游戏,《贪食蛇》中玩家控制一条不断移动的蛇,在屏幕上吃掉出现的食物。每吃掉一个食物,蛇的身体就会变长。游戏的目标是尽可能长时间地生存下去,同时避免蛇头撞到自己的身体或屏幕边缘。玩家需要灵活操作,利用策略在有限的空间内避免碰撞,挑战高分。
通过对经典的《贪吃蛇》的简单介绍,可以知道这篇文章中实现的贪吃蛇选哟实现以下的几点:
- 通过按方向键上下左右,来改变蛇的移动方向
- 通过按F3键实现蛇的加速行进,按F4键可以降低蛇的移速。
- 按空格键可实现暂停,暂停后按任意键继续游戏。
- 按Esc键可直接退出游戏。
除此之外,本游戏还拥有计分系统,可保存玩家的历史最高记录。和食物分值显示,蛇的移速的不同可以改变食物的分值。
二、WIN32部分接口简单介绍
在贪吃蛇的实现过程当中,会使用部分WIN32的部分接口,此处简单介绍贪吃蛇的实现过程当中会使用到的API的功能以及简单说明:
2.1 控制台窗口大小设置
mode con cols=100 lines=30 #控制窗口,cols为行长度,lines为列行数
按住Win键+R键,打开Windows命令运行框,输入:cmd---------打开Windows控制台命令窗口,输入该指令,就可以调整窗口的大小了,效果如下:
回车运行后,控制台窗口的大小被修改了,通过观察窗口的属性值与我们设定一致
2.2 命令行窗口的名称的变更
命令行窗口的名称的变更,可以如下通过命令的方式来更改:
title 贪吃蛇#更改命令行窗口的名称
效果如下:
2.3 vs平台下的调试窗口的设置
在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;
}
2.4 隐藏光标
终端可以看作尾一个坐标系,左上角为坐标原点,向右为x轴,向下位y轴,如下图所示:
在windows窗口上描述一个坐标需要使用一个windows API中定义的一个结构体 COORD,表示一个字符在控制台屏幕缓冲区上的坐标,在C语言中,我们需要包含 windows.h 头文件才能使用,使用实例如下:
#include<stdio.h>
#include<windows.h>//调用该api需要的头文件
#include<stdlib.h>int main()
{COORD pos = { 10, 10 };//使用第一个参数为行,第二参数为列return 0;
}
为了实现光标隐藏,我们需要先调用 GetStdHandle 函数来获取标准输出句柄,使用这个句柄可以操作设备。
HANDLE output = NULL;//HANDLE为结构体指针类型
//获取标准输出句柄来表示不同设备的数值
output = GetStdHandle(STD_OUTPUT_HANDLE);
标准输出通常是指控制台窗口中打印文本的位置。通过使用GetStdHandle函数,可以获取表示标准输出的句柄,以便可以对其进行操作。函数的参数STD_OUTPUT_HANDLE是一个宏,它表示标准输出句柄的常量值。通过将此参数传递给GetStdHandle函数,可以获得标准输出的句柄。获取标准输出句柄后,可以使用其他Windows API函数来更改控制台的输出属性、位置和文本颜色等,以实现更复杂的控制台输出。
要隐藏光标,需要先获得一个光标信息,上面我们已经获取了标准输出相关设备的句柄,接下来我们创建 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 参数,设置光标的可见性,默认尾true;如果光标不可见,设置为false。
我们调用结构体的第二个参数设置为false(注意:C语言要包含 stdbool.h 头文件才能使用布尔类型)
GetConsoleCursorInfo 函数介绍
然后再调用 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来输入输出信息,检测是否隐藏光标成功
下面是光标未被隐藏的结果,输入字符或者数字后,末尾会有一个光标在闪烁:
2.5 控制光标的位置
设置终端光标输出位置,我们首先要获取想要输出位置的坐标,上面我们介绍了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 SearchLocal(int x,int y) {//获取控制台的句柄,控制权限,HANDLE 结构体指针HANDLE handle = GetStdHandle(STD_OUTPUT_HANDLE);COORD pos = { x,y };// 获取设备句柄,传入坐标COORDSetConsoleCursorPosition(handle, pos);
}
2.6 获取键盘的值的情况
控制贪吃蛇的移动,加速或者减速,我们都是通过键盘按下对应的按键进行控制,那系统是如何知道我们按下的按键是哪一个呢?
使用 GetAsyncKeyState 函数来获取按键情况,此函数函数原型如下:
SHORT GetAsyncKeyState(int vKey);
将键盘上的键值传给函数,通过函数返回值来判断按键的状态。GetAsyncKeyState 返回值是short类型,在上一次调用此函数后,如果返回的16位的short数据中,最高位是1,说明按键的状态是按下,如果最高位是0,说明按键的状态是抬起;如果最低位被置为1,则说明该按键被按过,否则位0。
如果我们要判断按键是否被按过,只需要判断返回值最低值是否为1即可,我们可以按位与上0x1来获取最低位的值,那么我们就可这样来编写函数:
//检测按键是否被按过,间返回的虚拟值按位与上1,相同为1,相异为0,进行检测
//封装为一个宏
#define KEY_PRESS(vk) (GetAsyncKeyState(vk)&0x1?1:0)
前面提出的系统是如何判断我们按下的按键爱是哪一个的疑问,我们可以通过虚拟键码来判断是不同按键的不同状态,这样就可以实现一些按键响应的功能了。 Virtual-Key 代码 (Winuser.h) - Win32 apps | Microsoft Learnhttps://learn.microsoft.com/zh-cn/windows/win32/inputdev/virtual-key-codes
本文贪吃蛇中需要用到的虚拟键值如下:
2.7 字符问题
我们在打印蛇身和墙体的时候,总不能用数字或者字符来表示吧。所以需要用到特殊字符——宽字符。宽字符的长度为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",才可正常打印宽字符。此处打印宽字符用到了wprintf函数,这里简单介绍一下:
wprintf 是C语言中用于宽字符输出的函数,它类似于 printf,但是用于处理宽字符(wchar_t)和宽字符串(wchar_t*)。这个函数定义在 wchar.h 头文件中,并且可以用来将格式化的数据打印到标准输出。
格式说明符:
在 wprintf 函数中,格式说明符的使用与 printf 函数相似。例如,%lc 用于宽字符,%ls 用于宽字符串。与 printf 不同的是,wprintf 需要使用 L 前缀来指定宽字符串字面量
#include<stdio.h>
#include<locale.h>int main()
{setlocale(LC_ALL, "");wchar_t ch[] = L"你好哇";wprintf(L"%ls\n", ch);return 0;
}
运行结果如下:
三、贪吃蛇的实现
贪吃蛇流程简述:贪吃蛇游戏分为三个大步骤,游戏开始,游戏运行,游戏结束,我们分别用三个函数来完成。
//贪吃蛇游戏界面初始化
GameStart(&snake);
//贪吃蛇游戏运行
GameRun(&snake);
//贪吃蛇游戏结束的善后
GameEnd(&snake);
在设计游戏之前,我们需要先写出蛇的相关信息,例如:蛇移动的方向,蛇的状态,食物的分数,当前游戏总分等......我们将这些信息用结构体和枚举列出:
//贪吃蛇游戏状态
enum GAMSTATUS {OK=1, //正常运行EXIT, //按ESC键退出KILL_BY_WALL, //撞墙了!KILL_BY_SELF //咬到自己了!
};//贪吃蛇行走方向
enum SNAKEWAY {UP = 1, //前进DOWN, //向下LEFT, //左拐RIGHT //右拐
};//贪吃蛇,蛇身节点的定义
typedef struct SnakeNode {//节点的位置int x;int y;//下一个蛇身节点struct SnakeNode* next;
}SnakeNode;//整局游戏贪吃蛇的维护
typedef struct Snake {//贪吃蛇,蛇头指针,方便找到贪吃蛇的位置/维护整条蛇的指针SnakeNode* SnakePhead;//指向食物的指针SnakeNode* FoodNode;//玩家当前得分int Scour;//每个食物的得分int FoofScour;//蛇休眠的时间,休眠的时间越短,蛇的速度越快,休眠的时间月长,蛇的速度越慢int SleepTime;//贪吃蛇游戏状态enum GAMSTATUS Status;//贪吃蛇行进的方向enum SNAKEWAY way;
}Snake;
3.1 游戏开始前的准备 GameStart函数
下面是GameStart函数:
void GameStart(Snake* ps) {//设定控制台屏幕大小system("mode con cols=100 lines=30");//设置控制台标题system("title 贪吃蛇");//设定光标,进行隐藏//获取控制台句柄HANDLE handle = GetStdHandle(STD_OUTPUT_HANDLE);//创建结构体来接收控制台光标的数据//有两个参数:dwSize 光标占空比; bVisible 光标可见度 默认trueCONSOLE_CURSOR_INFO cursor_info = { 0 };GetConsoleCursorInfo(handle, &cursor_info);//隐藏光标cursor_info.bVisible = false;//将修改的光标属性值传给控制台进行控制,参数类型跟GetConsoleCursorInfo一样SetConsoleCursorInfo(handle, &cursor_info);//游戏开始欢迎界面welcome();//绘制地图GameMap();//贪吃蛇初始化InitSnake(ps);//创建食物CreatFood(ps);
}
接下来我们来逐一来了解一下GameStaert的答题流程,以及它实现了哪些功能。
3.1.1 设置好控制台界面的行和列,隐藏控制台光标
在这个大块中,我们首先需要设置好控制台界面的行和列,隐藏控制台光标:
3.1.2 打印欢迎界面
其次打印欢迎界面,用SearchLocal函数来找到合适的位置,使界面变得美观,期间使用了system("cls")来进行清屏操作:
//初始化贪吃蛇界面
void welcome() {//将光标定位到界面的中间位置SearchLocal(35, 10);printf("欢迎来到贪吃蛇小游戏\n");SearchLocal(38, 20);system("pause");system("cls");//功能介绍SearchLocal(15, 10);printf("用↑ . ↓ . ← . → 来控制蛇的移动,F3为加速按键,F4为减速\n");SearchLocal(35, 12);printf("加速能够获得更高的分数!!!!");SearchLocal(38, 20);system("pause");system("cls");
}
界面展示如下:
3.1.3 绘制地图
接着,绘制地图,墙壁的范围在控制台窗口的范围如图所示:
如果想在控制台的窗口中指定位置输出信息,我们需要得知该位置的坐标,所以⾸先介绍⼀下控制台窗口的坐标知识。
在控制台窗口中,行和列的大小也是不一样的:
可以看到,在控制台窗口中,行明显要比列更大,那在我们打印墙壁时,就需要用到宽字符:'□',宽字符与普通字符不同,一个宽字符的大小是两个字节,并且一个宽字符在窗口中占一行两列(两个坐标的大小),而一个普通字符的大小是一个字节,一个普通字符在窗口中占一行一列(一个坐标的大小),打印出来会更加美观。
这里我们把墙壁的行列定义为宏,后面可以根据自己的需要修改墙壁的大小,本文用到的是27行,58列的墙壁
#define ROW 26 //游戏区行数
#define COL 56 //游戏区列数
这里列的偏移值每次加2,是因为宽字符的x轴的密度与y轴的密度不一致,而设置每次偏移2,这样打印下一个宽字符的时候不会重叠
//绘制地图
void GameMap() {//上SearchLocal(0, 0);for (int i = 0; i < COL; i += 2) {wprintf(L"%c", WALL);}//左for (int i = 1; i < ROW; i++) {SearchLocal(0, i);wprintf(L"%lc", WALL);}//右for (int i = 1; i < ROW; i++) {SearchLocal(COL-1, i);wprintf(L"%lc", WALL);}//下SearchLocal(0, ROW-1);for (int i = 0; i < COL; i += 2) {wprintf(L"%lc", WALL);}
}
3.1.4 初始化蛇身
在这里,得用到数据结构中的链表,每个蛇身视为一个节点(游戏开始时有5个节点),将这5个节点的坐标相邻(利用头插法初始化),依次打印(最后要初始化一下蛇的属性):
初始蛇头的位置:
#define Origin_x 24 // 初始蛇头的x坐标
#define Origin_y 5 // 初始蛇头的y坐标
//打印蛇身
void SnakePrint(Snake* ps) {SnakeNode* pcur = ps->SnakePhead;while (pcur) {SearchLocal(pcur->x, pcur->y);wprintf(L"%lc", BODY);pcur = pcur->next;}
}//初始化蛇
void InitSnake(Snake*ps) {//创建5个蛇身节点SnakeNode* pcur = NULL;for (int i = 0; i < 5; i++) {pcur = (SnakeNode*)malloc(sizeof(SnakeNode));if (pcur == NULL) {perror("malloc");return;//exit(1)}//头插pcur->x = Origin_x+2*i;pcur->y = Origin_y;pcur->next = NULL;if (ps->SnakePhead == NULL) { ps->SnakePhead = pcur;}else {pcur->next = ps->SnakePhead;ps->SnakePhead = pcur;}}//打印蛇身SnakePrint(ps);//初始化ps->Scour = 0;ps->FoodNode = NULL;ps->FoofScour = 10;ps->SleepTime = 200;ps->Status = OK;ps->way = RIGHT;
}
3.1.5 创建食物
将食物也当成一个普通的SnakeNode节点,因为食物也具有和蛇身节点相同的属性,只不过它的next指针永远为空,而它的x和y坐标是随机生成的,所以这里我们需要用到rand函数来生成随机数,不过生成的随机数也需要具备如下特点:
- x坐标的值为2~54,y坐标点值为1~25(不能在墙壁上或墙壁外)
- 必须为2的倍数(保证蛇可以吃到食物)
- 不能和蛇身的坐标重合
确定好以上条件后,我们就可以将食物打印在地图上了。
//创建食物
void CreatFood(Snake* ps) {//随机生成食物的坐标(x,y)//1.不能超过墙,且不能重叠 x:2~54 -- 52+2 y:1~24 -- 23+1//2.食物不能与蛇身重叠,遍历蛇身//创建食物节点SnakeNode* FoodNode = (SnakeNode*)malloc(sizeof(SnakeNode));if (FoodNode == NULL) {perror("creat FoodNode fail");return;}int x = 0;int y = 0;again:do {x = rand() % (COL-4) + 2;y = rand() % (ROW-2) + 1;} while (x % 2 != 0); //1.不能超过墙,且不能重叠//2.食物不能与蛇身重叠,遍历蛇身SnakeNode* pcur = ps->SnakePhead;while (pcur) {if (x == pcur->x && y == pcur->y) {goto again;}pcur = pcur->next;}//真正创建了属于食物的坐标FoodNode->x = x;FoodNode->y = y;FoodNode->next = NULL;ps->FoodNode = FoodNode;//打印食物SearchLocal(x,y);wprintf(L"%lc", FOOD);
}
由于食物是随机生成的(在蛇可爬行区域内),而食物有几率生成到蛇的身上,所以每生成一个食物节点后,得遍历整条蛇;如果重叠,就重新生成一个食物,直到不重叠为止。此处用到了again:,这里简略的介绍一下:
在 C 语言中, again: 不是一个关键字或预定义标识符,它只是一个标识符的名称,可以用作标签(label)。 在 C 语言中,标签通常用于在嵌套循环或 switch 语句中进行无条件跳转,从而提高代码的可读性和灵活性。 例如,使用 again: 标签可以在循环中使用 goto 语句来实现无限循环。 但是,使用 goto 语句会使代码变得难以理解和维护,因此应该谨慎使用。
3.2 游戏运行 GameRun函数
下面是 GameRun函数:
//贪吃蛇游戏运行
void GameRun(Snake* ps) {//打印操作介绍PrintOption();do{//打印当前得分情况SearchLocal(65, 11);printf("当前得分:%d",ps->Scour);SearchLocal(78, 11);printf("当前食物分值:%02d", ps->FoofScour); //每次移动分值可能变化放到循环中//检测按键是否按过if (KEY_PRESS(VK_UP)&& ps->way !=DOWN) {ps->way = UP;}else if(KEY_PRESS(VK_DOWN) && ps->way != UP) {ps->way = DOWN;}else if (KEY_PRESS(VK_LEFT) && ps->way != RIGHT) {ps->way = LEFT;}else if (KEY_PRESS(VK_RIGHT) && ps->way != LEFT) {ps->way = RIGHT;}else if (KEY_PRESS(VK_ESCAPE)) {ps->way = EXIT;break;}//空格进入暂停,死循环睡眠,在次按下空格进行游戏else if (KEY_PRESS(VK_SPACE)) {pause();}//F3加速,睡眠时间减少,食物分值增加,限制条件睡眠时间不为负数else if (KEY_PRESS(VK_F3)) {//时间不能为负,每次-30,至多减到20;if (ps->SleepTime > 20) {ps->SleepTime -= 30;ps->FoofScour += 2;}}//F4减速,睡眠时间增加,食物分值减少,限制条件食物分值不为0else if (KEY_PRESS(VK_F4)) {if (ps->FoofScour > 2) {ps->SleepTime += 30;ps->FoofScour -= 2;}}//贪吃蛇开始移动SnakeMove(ps);//睡眠一下Sleep(ps->SleepTime);} while (ps->Status == OK);
}
3.2.1 打印帮助信息
游戏开始后的帮助信息进行打印:
将光标移到指定位置(如果更改墙体的大小,帮助信息的位置光标需要做对应的修改,还有如果超出了窗口大小,可以调整窗口的大小),打印即可:
//打印帮助信息
void PrintOption() {SearchLocal(63, 17);printf("用↑ . ↓ . ← . → 来控制蛇的移动");SearchLocal(63, 18);printf("space(空格)为暂停/回到游戏按键");SearchLocal(68, 19);printf("F3为加速按键,F4为减速"); SearchLocal(72, 20);printf("ESC为退出按键");SearchLocal(63, 21);printf("注意:不能撞墙,不能咬到自己!!!");
}
3.2.2 判断玩家按下的按键
接下来,玩家会通过按方向键(↑ . ↓ . ← . →)以及F3、F4来控制蛇的移动,我们就需要用到我们上面所定义好的宏,传入各个键的虚拟键值,利用if else语句来判断玩家所按下的键,但是需要注意的是:如果蛇正在向下移动,则蛇的移动方向不能突然变为向上,否则会撞到自己(即向下移动时,按上方向键无效),左右上方向时同理:
//休眠函数
void pause() {while (1) {Sleep(200);if (KEY_PRESS(VK_ESCAPE)) {break;}}
}
3.2.3 蛇的移动
可以把下一步要移动到的坐标当做一个SnakeNode节点,malloc出一个新节点(这里命名为pnext),再根据蛇的方向设置这个节点的x,y值,接着,我们需要判断移动到的下一个坐标是不是食物,如果是食物则头插进蛇体中(不要忘记释放掉pnext节点(因为食物节点和pnext的位置是重复的));如果不是食物,也需要将pnext节点头插到蛇体中,要将最后一个节点用" "进行覆盖,不让这个节点出现在屏幕上。然后接着判断这次移动是否撞到了墙或者自己。
//贪吃蛇开始移动
void SnakeMove(Snake*ps){//创建指针记录蛇移动的下一个位置SnakeNode* pnext = (SnakeNode*)malloc(sizeof(SnakeNode));if (pnext == NULL) {perror("SnakeMove::malloc");return;}pnext->next = NULL;//记录蛇移动的下一个位置switch (ps->way){case UP:pnext->x = ps->SnakePhead->x;pnext->y = ps->SnakePhead->y - 1;break;case DOWN:pnext->x = ps->SnakePhead->x;pnext->y = ps->SnakePhead->y + 1;break;case LEFT:pnext->x = ps->SnakePhead->x-2;pnext->y = ps->SnakePhead->y;break;case RIGHT:pnext->x = ps->SnakePhead->x + 2;pnext->y = ps->SnakePhead->y;break;}//判断下一个位置是否为食物//是食物,吃掉 --- 头插if (NextFood(ps,pnext)) {EatFood(ps, pnext);}//不是食物,头插删除尾节点else {NotEatFood(ps, pnext);}//打印蛇身SnakePrint(ps);//检测是否穿墙Kill_Wall(ps);//检测是否咬到自己Kill_Self(ps);
}
3.2.3.1 NextFood
判断下一个节点的坐标是否与食物的节点的坐标一致,是则返回1;反之返回0。
//下一个位置是否为食物;
int NextFood(Snake* ps, SnakeNode* pnext) {if (pnext->x == ps->FoodNode->x && pnext->y == ps->FoodNode->y) {return 1;}else{return 0;}
}
3.2.3.2 EatFood
//吃食物
void EatFood(Snake* ps, SnakeNode* pnext) {pnext->next = ps->SnakePhead;ps->SnakePhead = pnext;//得分增加ps->Scour += ps->FoofScour;//释放旧的食物free(ps->FoodNode);ps->FoodNode = NULL;//重新创建食物CreatFood(ps);
}
3.2.3.3 Kill_Wall
//检测是否穿墙
void Kill_Wall(Snake* ps) {if (ps->SnakePhead->x == 0 ||ps->SnakePhead->x == (COL-1) ||ps->SnakePhead->y == 0 ||ps->SnakePhead->y == (ROW-1)) {ps->Status = KILL_BY_WALL;}
}
3.2.3.4 Kill_Self
//检测是否咬到自己
void Kill_Self(Snake* ps) {SnakeNode* pcur = ps->SnakePhead->next;while (pcur) {if (ps->SnakePhead->x == pcur->x && ps->SnakePhead->y == pcur->y) {ps->Status = KILL_BY_SELF;return;}pcur = pcur->next;}
}
3.2.4 .Sleep函数、循环移动
需要一段时间来间断一下蛇体的移动,并判断蛇的状态进行循环:
此处用到了Sleep函数,依旧简略的介绍一番:
Sleep()
函数是 Windows 平台下的一个 API,定义在windows.h
头文件中。其功能是挂起(暂停)当前线程的执行一段时间。输入参数是毫秒数。
3.3 贪吃蛇游戏结束的善后
打印出提示信息告诉玩家游戏失败的原因,最后,因为蛇身的节点是我们动态malloc开辟的,为了避免内存泄露,我们需要一一free掉。
//贪吃蛇游戏结束的善后
void GameEnd(Snake* ps) {SearchLocal(15, 10);//打印结束状态switch (ps->Status) {case EXIT:printf("正常退出,游戏结束了");break;case KILL_BY_WALL:printf("很遗憾,游戏结束,你撞到墙了!!!");break;case KILL_BY_SELF:printf("不小心咬到自己了,游戏结束了!!!");break;}//释放空间SnakeNode* pcur = ps->SnakePhead;SnakeNode* del = NULL;while (pcur) {del = pcur->next;free(pcur);pcur = del;}free(ps->FoodNode);ps = NULL;}
3.4 再来一局,不言弃!!
游戏失败后,为了使游戏更完整,设置循环提示玩家是否再来一局,这里用do while循环来实现:
void snakegame() {int ch = 0;do {//创建贪吃蛇Snake snake = { 0 };//贪吃蛇游戏界面初始化GameStart(&snake);//贪吃蛇游戏运行GameRun(&snake);//贪吃蛇游戏结束的善后GameEnd(&snake);SearchLocal(15, 8);printf("要再来一局吗?? Y/N:");ch = getchar();getchar();// 清理\nSearchLocal(0, 27);} while (ch == 'Y' || ch == 'y');
}