淘宝客网站一定要备案,茂名中小企业网站制作,wordpress相册点击弹出框,网站建设和seo讲师要求一.游戏背景
贪吃蛇是一款在世界上盛名已久的小游戏#xff0c;贪食蛇游戏操作简单#xff0c;可玩性比较高。这个游戏难度最大的不是蛇长得很长的时候#xff0c;而是开始。那个时候蛇身很短#xff0c;看上去难度不大#xff0c;却最容易死掉#xff0c;因为把玩一条小…一.游戏背景
贪吃蛇是一款在世界上盛名已久的小游戏贪食蛇游戏操作简单可玩性比较高。这个游戏难度最大的不是蛇长得很长的时候而是开始。那个时候蛇身很短看上去难度不大却最容易死掉因为把玩一条小短蛇让人容易走神失去耐心。由于难度小你会不知不觉加快调整方向的速度在游走自如的时候蛇身逐渐加长了而玩家却没有意识到危险在最得意洋洋的一刻突然死亡。
接下来我们利用编程来实现这个小游戏既可以巩固我们的C语言的学习也可以提高我们的学习兴趣。
二.Win32API介绍
在编写该小游戏之前我们需要了解一下win32api中某些函数的使用方法在后续的编写中会用到。在使用之前我们要将我们的控制台程序平常运行程序的黑框改成windows控制台主机。
如果运行起来不是这种的而是下面这种黑款的话就需要改成上面的windows控制台主机否则无法实现贪吃蛇小程序。
1.mode命令
我们使用mode命令就可以调整我们的控制台窗口的大小。
system(mode con cols100 lines30);
cols控制的是列数lines控制的行数使用system函数必须包含头文件#includestdlib.h。 由上图可知在控制台窗口行和列不是11的关系所以在后续坐标的计算上就会有些不同。
2.title命令
如果我们运行程序之后在运行窗口就会显示名称。
我们可以利用title命令来修改控制台的名称。
system(title 贪吃蛇); 但是我们执行该语句名字却并没有发生变化这是为什么呢这是因为该程序已经结束了title命令的作用只在程序还在运行的时候生效。所以我们让程序暂停观察名称。
pause命令可以使程序暂停。
3.控制台上的坐标COORD
在控制台上坐标的表示和我们的直角坐标系有所不同 而在win32api上定义了一个COORD的结构体用来描述控制台窗口的坐标。COORD 结构 - Windows Console | Microsoft Learn
该结构有两个成员分别是x和y用来描述x坐标和y坐标。
使用win32api函数要包含头文件#includewindows.h。
COORD pos { 14,32 };
我们可以这样来定义一个坐标信息。
4.GetStdHandle函数
该函数的作用是检索指定标准设备的句柄标准输入、标准输出或标准错误。GetStdHandle 函数 - Windows Console | Microsoft Learn
HANDLE WINAPI GetStdHandle(_In_ DWORD nStdHandle
);
该函数的参数已经给出了三种选择。
那么什么是句柄呢在我的理解来看该函数就是得到你传的参数的操作权。例如你要获取屏幕标准输出设备的控制权你只需要将第二个参数传给GetStdHandle函数然后用一个handle类型的句柄接收即可接下来就可以用该句柄来操纵屏幕。
HANDLE houtput GetStdHandle(STD_OUTPUT_HANDLE);
接下来我们就可以利用houtput来操作屏幕。
5.GetConsoleCursorInfo 函数
该函数的功能是检索有关指定控制台屏幕缓冲区的游标大小和可见性的信息。GetConsoleCursorInfo 函数 - Windows Console | Microsoft Learn
BOOL WINAPI GetConsoleCursorInfo(_In_ HANDLE hConsoleOutput,_Out_ PCONSOLE_CURSOR_INFO lpConsoleCursorInfo
);
我们可以看到该函数接收两个参数第一个参数就是我们上面介绍的句柄第二个参数是指向 CONSOLE_CURSOR_INFO 结构的指针该结构接收有关控制台游标的信息
我们接下来看一下CONSOLE_CURSOR_INFO 结构 - Windows Console | Microsoft Learn该结构体
typedef struct _CONSOLE_CURSOR_INFO {DWORD dwSize;BOOL bVisible;
} CONSOLE_CURSOR_INFO, *PCONSOLE_CURSOR_INFO;
该结构体有两个成员分别是dwSize和bVisible。
dwSize是游标大小占的百分比 该值介于 1 到 100 之间。 游标外观各不相同范围从完全填充单元到显示为单元底部的横线。
bVisible是游标的可见性。如果游标可见则该成员为true否则为false。
6.SetConsoleCursorInfo 函数
该函数的作用是为指定的控制台屏幕缓冲区设置光标的大小和可见性。SetConsoleCursorInfo 函数 - Windows Console | Microsoft Learn
BOOL WINAPI SetConsoleCursorInfo(_In_ HANDLE hConsoleOutput,_In_ const CONSOLE_CURSOR_INFO *lpConsoleCursorInfo
);
我们可以看到该函数的参数与上面的参数相同。第一个参数是指定控制台的句柄第二个参数是游标的结构体。
该函数要和上面的函数同时使用先获取指定控制台的操作权然后获取光标信息进行修改后然后在设置光标信息。
我们现在可以利用上面介绍的几个函数来设置光标的大小
int main()
{//获取屏幕的句柄操作权HANDLE houtput GetStdHandle(STD_OUTPUT_HANDLE);//定义一个光标信息的结构体CONSOLE_CURSOR_INFO cursor_info { 0 };//获取屏幕光标的操作权GetConsoleCursorInfo(houtput, cursor_info);//修改光标大小cursor_info.dwSize 100;//设置光标信息SetConsoleCursorInfo(houtput, cursor_info);getchar();return 0;
} 我们运行该代码看看如何
我们看到当我们修改光标大小为100后的确在屏幕上可以观察出来。右图为默认的光标大小。
我们还可以修改光标的可见性其余代码不变
//修改光标的可见性
cursor_info.bVisible false; 运行起来后光标确实被隐藏了。要使用ture和false这类布尔类型需要包含头文件#include stdbool.h 。
7.SetConsoleCursorPosition 函数
该函数的功能是设置指定控制台屏幕缓冲区中的光标位置。SetConsoleCursorPosition 函数 - Windows Console | Microsoft Learn
BOOL WINAPI SetConsoleCursorPosition(_In_ HANDLE hConsoleOutput,_In_ COORD dwCursorPosition
);
第一个参数就是指定控制台的句柄也就是操纵权第二个是一个COORD结构体类的数据表示新的光标位置信息。
我们可以利用这个函数定位光标的位置让打印信息出现在我们期望的位置上。
int main()
{//设置控制台大小system(mode con cols100 lines30);//获取屏幕句柄HANDLE houtput GetStdHandle(STD_OUTPUT_HANDLE);//定位新坐标COORD pos { 50,15 };//设置光标位置SetConsoleCursorPosition(houtput, pos);printf(hello world\n);system(pause);
} 我们看到我们利用该函数定位光标位置打印的信息机会从我们定义的位置开始打印。
7.1SetPos函数
在接下来的贪吃蛇小游戏中我们会多次用到定位光标这一操作而对于定位光标来说每一次的操作都是相同的区别就在于传的xy不同所以我们就可以将定位光标这一操作封装成一个函数这样就可以方便我们后续定位光标。
void SetPos(int x, int y)
{//设置控制台大小system(mode con cols100 lines30);//获取屏幕句柄HANDLE houtput GetStdHandle(STD_OUTPUT_HANDLE);//定位新坐标COORD pos { x,y };//设置光标位置SetConsoleCursorPosition(houtput, pos);
}
8.GetAsyncKeyState函数
该函数的作用是检测指定键是否被按下。getAsyncKeyState 函数 (winuser.h) - Win32 apps | Microsoft Learn
SHORT GetAsyncKeyState([in] int vKey
);
该函数的参数是键盘上每个键的虚拟键代码。虚拟键码 (Winuser.h) - Win32 apps | Microsoft Learn
那我们如何检测某个键是否被按下呢
getAsyncKeyState的返回值是short类型如果我们在调用该函数的时候该返回值的最高位是1则表示该键的状态是按下如果是0则表示该键的状态是抬起。如果最低位是1则表示该键被按过0则表示该键没有被按过。
而在我们的贪吃蛇小游戏中我们并不需要检测键的状态我们只需要知道该键有没有被按过即可。方法就是我们让该函数的返回值按位与上0x1就行了。如果结果是1则表示该键被按过如果是0则表示该键没有被按过。
我们可以看到这样就可以巧妙地将检查最后一位是1或0转化成检查按位与后的结果是1还是0了。
我们借助下面的测试代码来验证我们将数字键盘0~9的虚拟代码传给该函数并将返回值按位与1检测返回值如果为1则在屏幕上打印该数否则不打印。
#include stdio.h
#include windows.hint main()
{//设置控制台窗口大小system(mode con cols100 lines30);while (1){if ((GetAsyncKeyState(VK_NUMPAD0) 0x1)){printf(0\n);} else if (GetAsyncKeyState(VK_NUMPAD1) 0x1){printf(1\n);}else if (GetAsyncKeyState(VK_NUMPAD2) 0x1){printf(2\n);}else if (GetAsyncKeyState(VK_NUMPAD3) 0x1){printf(3\n);}else if (GetAsyncKeyState(VK_NUMPAD4) 0x1){printf(4\n);}else if (GetAsyncKeyState(VK_NUMPAD5) 0x1){printf(5\n);}else if (GetAsyncKeyState(VK_NUMPAD6) 0x1){printf(6\n);}else if (GetAsyncKeyState(VK_NUMPAD7) 0x1){printf(7\n);}else if (GetAsyncKeyState(VK_NUMPAD8) 0x1){printf(8\n);}else if (GetAsyncKeyState(VK_NUMPAD9) 0x1){printf(9\n);}}return 0;
} 我们可以看到当我们按下哪个键就会打印出对应的数字。但是我们看到这个检测键是否被按过的代码都非常相似那么我们可以将该代码写成宏来使代码更清晰些
#define KEY_PRESS(vk) (GetAsyncKeyState(vk)0x1)?1:0;
我们以后要检测某个键是否被按过的时候直接调用这个宏即可。
三.贪吃蛇游戏的设计与分析
我们依旧采用函数声明与函数实现分离的方法来实现贪吃蛇。我们需要用到三个文件Snake.h函数声明以及头文件等内容
Snake.c函数的实现
test.c游戏的运行逻辑。
1.地图
我们在上面已经讲到了控制台窗口的坐标轴分布那么我们怎么在控制台窗口上打印我们的地图呢这里我们采用宽字符‘□’表示墙体宽字符‘●’表示蛇的身体宽字符‘★’表示食物。普通字符一个占一个字节而宽字符一个占两个字节。我们的汉字就是宽字符
我们看到汉字在打印的时候就占了两个字节而字母只占一个字节。
C语言是英国人发明的所以起初C语言只适用于英语国家地区然而随着C语言的发展国际C语言组织为了C语言能够适配其他非英语地区引入了locale.h头文件方便程序员对某些函数进行区域化的调整。
1.1locale.h头文件
locale.h头文件是C标准库中的一个头文件用于支持程序的国际化和本地化。它提供了一组函数和宏来设置或查询程序的本地化信息例如日期、时间、货币、数字格式等。
1.2库宏
locale.h头文件中定义了一些宏供头文件中的函数使用。
1.3setlocale函数
setlocale()函数是locale.h中的一个库函数用于设置或查询程序的本地化信息。它允许程序员指定用于字符分类、字符转换、货币格式、日期和时间格式、数字格式等的区域设置。
#include locale.hchar *setlocale(int category, const char *locale);C 库函数 – setlocale() | 菜鸟教程 (runoob.com)
该函数的第一个参数就是上面的库宏第二个参数有两种选择
NULL查询当前的本地化信息
设置为用户环境变量中的默认设置。
#include stdio.h
#include locale.h
int main()
{//第二个参数为NULL此时函数的功能是查询当前的本地化信息char* ret setlocale(LC_ALL, NULL);printf(%s\n, ret);//当第二个参数是时此时函数的功能是适配本地化ret setlocale(LC_ALL, );printf(%s\n, ret);return 0;
}
C代表的就是标准模式Chinese就代表了已经将默认设置修改为本地模式。
1.4宽字符的打印
宽字符的打印需要用到wprintf且格式控制前面需要加上L占位符用%lc或者%ls。
#include stdio.h
#include wchar.hint main() {wchar_t* wideStr L宽字符打印;wprintf(L%ls\n, wideStr);return 0;
}
2.地图的打印
我们为了主函数的清晰所以将游戏的运行逻辑封装成函数我们在函数中完成游戏的实现。
int main()
{//程序一开始就先设置本地化setlocale(LC_ALL, );//游戏运行逻辑Game();return 0;
}
void Game()
{//游戏的初始化GameInit();//游戏运行GameRun();//游戏结束GameOver();
}
我们地图的打印、控制台窗口的设置以及欢迎界面和帮助信息的打印都在游戏初始化中完成。下面是游戏初始化函数的逻辑。
//游戏的初始化
void GameInit()
{//初始化窗口InitWindow();//欢迎界面的打印welcome();//地图的打印CreatMap();
}
2.1窗口设置
//初始化窗口
void InitWindow()
{//设置窗口大小system(mode con cols100 lines30);//设置窗口名称system(title 贪吃蛇);//隐藏光标//获取屏幕的句柄操作权HANDLE houtput GetStdHandle(STD_OUTPUT_HANDLE);//定义一个光标信息的结构体CONSOLE_CURSOR_INFO cursor_info { 0 };//获取屏幕光标的操作权GetConsoleCursorInfo(houtput, cursor_info);//修改光标的可见性cursor_info.bVisible false;//设置光标信息SetConsoleCursorInfo(houtput, cursor_info);
}
2.2欢迎界面的打印 我们地图的打印以及欢迎界面的打印都要在游戏的初始化中完成。
//欢迎界面的打印
void welcome()
{//定位光标SetPos(40, 15);printf(欢迎来到贪吃蛇\n);SetPos(35, 25);system(pause);system(cls);//打印帮助信息SetPos(25, 15);wprintf(L用 ↑. ↓ . ← . → 来控制蛇的移动按shift加速ctrl减速\n);SetPos(35, 25);system(pause);system(cls);
}
上面就是欢迎界面以及帮助信息的打印SetPos函数是我们之前写过的定位光标函数。
2.2地图的打印
我们给出地图的样例我们要做成的就是像这样子的地图58列x27行的大小。 而我们在前面已经知道一个宽字符占两个字节所以我们第一行只需要打印29个方格然后再让光标定位到第27行再打印29个方格。然后在打印左边的墙和右边的墙。我们只需要固定x坐标让y坐标由1增加到26即可然后再固定x为56y坐标由1增加到26即可。
//地图的打印
void CreatMap()
{int i 0;//上墙for (i 0; i 29; i){wprintf(L%lc , L□);}//下墙SetPos(0, 26);for (i 0; i 29; i){wprintf(L%lc , L□);}//左墙for (i 1; i 27; i){SetPos(0, i);wprintf(L%lc, L□);}//右墙for (i 1; i 27; i){SetPos(56,i);wprintf(L%lc, L□);}
}
大家注意不知道为什么我的电脑显示宽字符的时候依旧是占一个字节所以我在打印上下墙的时候每次都多打印了一个空格。
3.数据结构的分析与设计
贪吃蛇的身体其实就是一个一个的节点连接起来的而在我们链表中节点依次相连就构成了我们链表。所以我们可以利用链表这一数据结构来描述贪吃蛇。
//蛇身的节点
typedef struct SnakeNode
{int x;int y;struct SnakeNode* Next;
}SnakeNode,* pSnakeNode;
而对于一个贪吃蛇来说并不是一个简简单单的节点就搞定了还有很多其他的属性蛇的方向、蛇的状态、食物、蛇头、单个食物的分值、总分数、每走一步的休眠时间。所以我们可以将上述内容再定义成一个结构体类型Snake用该结构体来维护贪吃蛇的所有属性。
//蛇的方向
enum SnakeDirection
{UP,//向上DOWN,//向下LEFT,//向左RIGHT//向右
};//蛇的状态
enum SnakeStatue
{OK,//正常KILL_BY_SELF,//撞到自己KILL_BY_WALL,//撞到墙END_NOEMAL//正常退出
};typedef struct Snake
{pSnakeNode _pSnake;//维护蛇头的指针pSnakeNode _pFood;//维护食物的指针int _Food_Weight;//食物权重int ——score;//总分数enum SnskeDirection _dir;//蛇的方向enum SnakeStatue _Statue;//蛇的状态int _Sleep_Time;//休眠时间
}Snake,*pSnake;蛇的方向以及蛇的状态都是可以一一列举出来的所以我们可以直接用枚举类型给出蛇的状态和方向。在以后游戏的运行过程中我们就直接用Snake来维护贪吃蛇。
3.1蛇身的初始化
蛇身其实就是一个一个的节点所以我们默认蛇身有两个节点然后给出默认的出现坐标。这样就将节点创建好了。然后我们需要将每一个节点连接起来这里我们用到了头插法不了解的可以看这篇文章C——单链表-CSDN博客。
将链表连接起来之后蛇的身体也就连接起来了。然后我们通过打印宽字符的方式打印蛇的身体。注意每一次打印的时候要先定位光标。
打印完成后我们就要初始化贪吃蛇的其他属性了。这些都是在游戏开始之前就要设置好的。大家可以根据自己的需求自己更改初始化的各种数据这里只是给出一个例子。
//蛇身的初始化
void InitSnake(pSnake ps)
{//假设初始蛇有两个节点int i 0;pSnakeNode newnode NULL;for (i 0; i 2; i){//创建蛇的节点newnode (pSnakeNode)malloc(sizeof(SnakeNode));if (newnode NULL){perror(InitSnake()::malloc());return;}newnode-Next NULL;//设置坐标//默认蛇节点从(12,4)开始newnode-x 12 2 * i;newnode-y 4;//利用头插法将新节点连接到链表中if (ps-_pSnake NULL){//空链表ps-_pSnake newnode;}else{//非空链表newnode-Next ps-_pSnake;ps-_pSnake newnode;}}//打印蛇身体pSnakeNode cur ps-_pSnake;while (cur){//定位坐标SetPos(cur-x, cur-y);wprintf(L%lc, L※);cur cur-Next;}//设置蛇的属性ps-_dir RIGHT;//默认方向向右ps-_Statue OK;//默认状态OKps-_Food_Weight 10;//默认一个食物10分ps-_score 0;//默认起始总分数为0分ps-_Sleep_Time 200;//默认休眠时间单位毫秒
} 我们这样就初始化好了蛇身以及墙体。但是万一我们以后 想要改变墙体或者蛇的身体的时候就非常麻烦要在多个地方进行修改。我们可以将墙体和蛇身定义成宏以后想要修改的时候直接在头文件中修改即可。
#define WALL L□
#define BODY L※
后面遇到需要用墙体和蛇身的时候直接用WALL和BODY替代。
当然了默认的初始位置也是可以改变的。所以我们将初始位置也定义成宏放在头文件中。
#define POX_X 12
#define POX_Y 43.2食物的初始化
食物是在地图上随机生成的所以我们要用到rand来产生随机数而调用rand要使用srand。
srand((unsigned int)time(NULL));
我们知道不管是墙还是蛇身还是食物都是宽字符占两个字节所以他们的x坐标应该都是2的倍数。所以这就是为什么我之前再初始化蛇身的坐标是会乘上2。
而对于食物来说不仅是要x坐标是2的倍数而且还得再墙体呢。我们墙x坐标的范围是0~58又因为一个墙占两个字节所以食物的x范围应该在2~54之间。 墙的y坐标的范围是0~26所以食物的y范围应该在1~25之间。
而对于rand来说只能产生0~100的随机数。那怎么办呢
2~54可以看作0~5221~25可以看作0~241。
所以当我们要产生x的随机数时让其模上53加上2就可以产生2~54之间的随机数产生y的随机数时让其模上25加上1就可以产生1~25之间的随机数。
产生随机数后我们还得判断x坐标是否为2的倍数然后在判断是否与蛇身的节点重合。满足这些要求之后才可以作为食物的坐标。对于食物来说也一样为了修改方便我们将其定义成宏放在头文件中。
#define FOOD L★
//食物的初始化
void InitFood(pSnake ps)
{int x 0;int y 0;
again:do{//食物的坐标要随机生成x rand() % 53 2;//产生2~54的随机数y rand() % 25 1;//产生1~26的随机数} while (x % 2 ! 0);//食物不能和蛇的身体重叠pSnakeNode cur ps-_pSnake;while (cur){if (cur-x x cur-y y){goto again;}cur cur-Next;}//创建食物节点pSnakeNode food (pSnakeNode)malloc(sizeof(SnakeNode));if (food NULL){perror(InitFood()::malloc());return;}food-Next NULL;food-x x;food-y y;//打印食物SetPos(food-x, food-y);wprintf(L%lc, FOOD);//将食物让pSnake贪吃蛇维护ps-_pFood food;
} 从上图可以很明显的看出食物的节点是随机出现的也并没有与蛇的节点重合。
四.游戏的运行
经过上面的步骤我们已经完成了游戏的初始化部分接下来就是游戏的运行逻辑了。
1.打印帮助信息
当我们进入游戏界面后我们最好在地图旁边的空白部分加上一些帮助信息帮助游戏人更好的进行游戏。我们可以告诉他们游戏的使用方法游戏的一些功能以及游戏失败的分类。
void PrintHelpInfo(pSnake ps)
{//定位光标SetPos(65, 18);wprintf(L用 ↑. ↓ . ← . → 来控制蛇的移动);SetPos(65, 19);printf(按Shift加速Ctrl减速);SetPos(65, 20);printf(加速可以得到更高的分数);SetPos(65, 21);printf(小心不要撞到自己和墙);SetPos(65, 22);printf(ESC退出游戏SPACE暂定游戏);
} 大家可以先不管上面的score以及foodweight。
2.蛇移动的准备工作
蛇的移动其实就是每次走一步的过程之所以游戏里面看起来好像一直在走就是因为休眠的时间很短。而蛇走的前提就是蛇的状态得是正常的。所以我们将蛇的移动逻辑写成do while循环来保证蛇可以一直移动判断条件就是蛇的状态是正常的就可以移动如果不是则推出循环游戏结束。
2.1蛇移动的方向
我们之前规定了蛇默认是向右移动的但是在其运动过程中方向是可以更改的。所以在移动之前我们先设置蛇移动的方向。如果没有设置那就默认向右。
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;
}
这里就需要用到我们之前判断的是否按过某个键。如果我们按了一次上键那么我们就将蛇的方向设为向上但是前提是蛇不能向下移动。如果贪吃蛇正在向下移动你却按了上键此时是没法反应的。所以其他方向也同理。
2.2加速减速
在移动之前除了要设置方向外还要设置速度。速度反应的其实就是休眠的时间长短。我们前面规定了shift为加速ctrl为减速。当我们按了shift或者ctrl之后就要对贪吃蛇的休眠时间属性进行修改。当然了加速之后食物的权重就可以增加反之减小。
//加速
ps-_Sleep_Time - 20;
ps-_Food_Weight 2;
//减速
ps-_Sleep_Time 20;
ps-_Food_Weight - 2;
但是我们可以让他一直加速或者减速下去么当然不行了。所以对于速度的上限和下限都要有限定。我们既可以用时间限定也可以用分值限定。
else if (KEY_PRESS(VK_SHIFT))
{//加速//限定速度最大值if(ps-_Sleep_Time 100){ps-_Sleep_Time - 20;ps-_Food_Weight 2;}
}
else if (KEY_PRESS(VK_CONTROL))
{//减速//限定最小分数分数不能为0if(ps-_Food_Weight 2){ps-_Sleep_Time 20;ps-_Food_Weight - 2;}
}
2.3暂停与退出
我们在运行过程中可能按空格使游戏暂停也可能按ESC退出游戏。所以在移动之前也要判断我们是否按下了这些键。
退出游戏其实就是将贪吃蛇的状态设置成了END_NORAML走完一一步后do while循环检查贪吃蛇状态发现不是OK此时就会退出该循环使游戏结束。
else if (KEY_PRESS(VK_ESCAPE))
{//退出ps-_Statue END_NOEMAL;
}
使游戏暂停其实就是增长了休眠时间我们可以写一个休眠函数让他死循环的进行休眠当然在休眠的过程中也要判断是否按下了空格键打破了暂停或者按下了ESC退出了游戏。
//暂停
void Suspend_time_out(pSnake ps)
{do{Sleep(300);if (KEY_PRESS(VK_SPACE)){break;}else if (KEY_PRESS(VK_ESCAPE)){ps-_Statue END_NOEMAL;}} while (1);
}else if (KEY_PRESS(VK_SPACE))
{//暂停Suspend_time_out(ps);
}
到此就完成了我们贪吃蛇走一步的准备工作下面将整个准备工作合并在一块就行了。
//游戏运行
void GameRun(pSnake ps)
{//首先在游戏右边打印帮助信息PrintHelpInfo(ps);//检测按过哪个键do{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_SHIFT)){//加速//限定速度最大值if(ps-_Sleep_Time 100){ps-_Sleep_Time - 20;ps-_Food_Weight 2;}}else if (KEY_PRESS(VK_CONTROL)){//减速//限定最小分数分数不能为0if(ps-_Food_Weight 2){ps-_Sleep_Time 20;ps-_Food_Weight - 2;}}else if (KEY_PRESS(VK_SPACE)){//暂停Suspend_time_out(ps);}else if (KEY_PRESS(VK_ESCAPE)){//退出ps-_Statue END_NOEMAL;}} while (ps-_Statue OK);
}
3.蛇走一步
蛇肯定不会只走一步所以蛇走一步这个过程也应该在do while循环中。蛇走一步的逻辑其实可以这样来完成我们已经知道了蛇移动的方向所以我们可以先计算出蛇下一个位置的坐标然后创建一个新节点将这个坐标信息保存起来。接下来分析这个节点是否使食物如果是食物那就吃掉食物如果不是食物那就往前走一步。完成这一步之后还得判断是否撞到了墙或者自己。
所以蛇走一步的逻辑大概是这样的
void SnakeMove(pSnake ps)
{//创建新节点//计算下一个位置的坐标并保存到新结点中//判断下一个位置是不是食物//吃掉食物//不是食物//判断是否撞到墙或者自己
}
3.1计算下一个位置的坐标
我们计算完坐标之后还要将其保存到一个新结点中。所以我们先创建一个新的节点
//先创建一个新节点
pSnakeNode node (pSnakeNode)malloc(sizeof(SnakeNode));
if (node NULL)
{perror(SnskeMove()::malloc());return;
}
计算下一个位置的坐标
计算的前提是知道蛇移动的方向。如果蛇向上移动那么x坐标不变y-1如果蛇向下移动那么x坐标不变y1如果蛇向左移动那么y坐标不变x-2如果蛇向右移动那么y坐标不变x2。
//计算下一个位置的坐标
switch (ps-_dir)
{
case UP:
{node-x ps-_pSnake-x;node-y ps-_pSnake-y - 1;break;
}
case DOWN:
{node-x ps-_pSnake-x;node-y ps-_pSnake-y 1;break;
}
case LEFT:
{node-x ps-_pSnake-x - 2;node-y ps-_pSnake-y;break;
}
case RIGHT:
{node-x ps-_pSnake-x 2;node-y ps-_pSnake-y;break;
}
}
3.2判断下一个位置是不是食物
判断是不是食物我们只需要将该节点的xy坐标与贪吃蛇的属性中的食物的xy坐标进行比较就行。
//下一个位置是不是食物
int NextPositionIsFood(pSnake ps,pSnakeNode pnode)
{return ((ps-_pFood-x pnode-x) (ps-_pFood-y pnode-y));
}
3.3下一个位置是食物
如果下一个位置是食物那么我们就吃掉食物也就是将食物节点头插到链表中即可。而这里的节点和食物指向的是同一个位置但是我们却动态开辟了两次所以我们只需要将其中一个连接到链表中然后将另一个销毁掉防止内存泄漏。然后再打印蛇身并创建一个新的食物。吃掉食物之后我们的总分也应该增加所以总分还要加上此时的分值。
//吃食物
void EatFood(pSnake ps,pSnakeNode pnode)
{//此时pnode和食物节点是同一个节点我们可任选一个头插到链表中然后经另一个给释放掉pnode-Next ps-_pSnake;ps-_pSnake pnode;//释放食物节点free(ps-_pFood);ps-_pFood NULL;//创建一个新食物InitFood(ps);//吃了一个食物总分数要增加ps-_score ps-_Food_Weight;//打印蛇身pSnakeNode cur ps-_pSnake;while (cur){//定位光标SetPos(cur-x, cur-y);wprintf(L%lc, BODY);cur cur-Next;}
}
3.4下一个位置不是食物
如果下一个位置不是食物的话我们就要往前走一步。其实走一步的过程可以转化为将新节点作为贪吃蛇的头部然后释放掉最后一个节点即可。所以我们依旧先将节点头插到链表中然后打印蛇身同时还要找到尾节点此时就该释放尾节点了。释放了就完了么
当然不是尾节点这个位置已经打印了一次蛇身为了清除掉这个蛇身我们还要再这个位置上打印两个空白字符将不用的身体给覆盖掉。
那么怎么找尾节点呢
我们可以利用cur-next-next来作为判断条件来寻找尾节点。就如上图cur首先指向头节点此时cur-next-ntxtNULL所以打印这个节点然后cur走到下一个位置在进行判断此时cur-next-next NULL所以此时不进入while循环这就找到了倒数第二个节点也就相当于找到了尾节点。
//不是食物
void NoFood(pSnake ps, pSnakeNode pnode)
{//将pnode节点作为新的蛇头然后释放掉贪吃蛇的最后一个节点并在其位置上打印两个空白字符覆盖掉原先的蛇身pnode-Next ps-_pSnake;ps-_pSnake pnode;pSnakeNode cur ps-_pSnake;while (cur-Next-Next){SetPos(cur-x, cur-y);wprintf(L%lc, BODY);cur cur-Next;}//此时cur为倒数第二个节点SetPos(cur-Next-x, cur-Next-y);//打印两个空格覆盖原来的蛇身printf( );free(cur-Next);cur-Next NULL;
}
有的人可能会有疑问了蛇身明明有两个而while循环只执行了一次所以不是少打印了一个蛇身么这代码不是有问题么
其实是没有问题的为什么呢因为这个位置上本来已经打印了一次蛇身了。就算不打印也不会影响。所以只需要打印除了倒数第一和倒数第二的蛇身即可。然后在尾节点出打印两个空白格即可。
到这里我们的贪吃蛇已经可以走起来了 QQ202475-183434 我们从视频中得出虽然我们完成了贪吃蛇移动的过程但是此时的贪吃蛇还没有完成撞到墙或者自己导致游戏结束的情况。我们接下来完成这两步。
4.撞墙
其实贪吃蛇是否撞墙的判断是非常简单的我们只需要判断蛇头与x轴和y轴的关系即可。蛇头的x坐标不可以等于0、56否则就会撞到左右两边的墙蛇头的y坐标不可以等于0、26否则就会撞到上下两边的墙。
如果撞到了那就将蛇的状态设置为KILL_BY_WALL然后返回如果没有撞到直接返回就行。
//撞墙
void KillByWall(pSnake ps)
{pSnakeNode cur ps-_pSnake;if (cur-x 0 || cur-x 56){ps-_Statue KILL_BY_WALL;return;}else if (cur-y 0 || cur-y 26){ps-_Statue KILL_BY_WALL;return;}else{return;}
}
5.撞自己
撞到自己的判定条件和撞到墙相差不大只不过是要将头节点与蛇的剩余节点进行比较只要蛇头与某个蛇身的xy坐标都相等就说明撞上了否则就没有。
//撞到自己
void KillBySelf(pSnake ps)
{pSnakeNode next ps-_pSnake-Next;while (next){if (ps-_pSnake-x next-x ps-_pSnake-y next-y){ps-_Statue KILL_BY_SELF;return;}next next-Next;}
}
6.总分数以及食物权重
我们在视频中看到在地图的右边显示了我们的得分以及食物的权重我们每走一步就要打印这些信息以免他们发生变化。所以他们也是包含在游戏运行的do while()循环中。
//打印总分数以及单个食物的分数
SetPos(69, 7);
printf(Score%2d, ps-_score);
SetPos(69, 8);
printf(Food Weight%2d, ps-_Food_Weight);
到这里我们贪吃蛇的移动就完成了。接下来就是游戏结束后的善后工作。
五.游戏结束
我们游戏结束之后可以提示一下玩家他们是怎么输的将信息打印到屏幕上。然后就是蛇身的销毁。蛇身都是一个一个的节点是通过malloc动态申请的内存空间。当我们使用完后最好将链表给销毁掉防止内存泄漏。
//销毁链表
void ReleaseSnake(pSnake ps)
{//销毁链表pSnakeNode cur ps-_pSnake;pSnakeNode next NULL;while (cur){next cur-Next;free(cur);cur next;}ps NULL;
}//游戏结束
//善后工作
void GameOver(pSnake ps)
{//判断游戏是怎样结束的if (ps-_Statue END_NOEMAL){SetPos(16, 7);printf(You voluntarily quit the game);}else if (ps-_Statue KILL_BY_WALL){SetPos(16, 7);printf(Youve hit a wall);}else if (ps-_Statue KILL_BY_SELF){SetPos(16, 7);printf(You bumped into yourself);}//释放节点ReleaseSnake(ps);ps NULL;
}
六.游戏完善
到这里游戏已经写完了我们在这里再进行完善。如果我们玩儿完了一局还想再开一局此时就要重新运行程序很麻烦我们可不可以在游戏结束后给出选项yes or no选择yes就直接再来一局选择no就退出游戏。
我们的逻辑就只有三个 //游戏的初始化GameInit(s);//游戏运行GameRun(s);//游戏结束GameOver(s);
所有的功能都在这三个函数中执行。所以为了能够多次进行游戏我们可以将这三个函数写进循环中。
void Game()
{Snake s { 0 };int ch 0;do{//游戏的初始化GameInit(s);//游戏运行GameRun(s);//游戏结束GameOver(s);s._pSnake NULL;Sleep(1000);system(cls);SetPos(35, 12);printf(Do you want another round?);SetPos(45, 13);printf(Y/N);ch _getch();} while (ch Y || ch y);
}
利用这个循环我们就可以实现多次游戏。当然如果我们不想玩了此时就回到了主函数我们在主函数也要打印退出游戏的信息来提示用户。
int main()
{//程序一开始就先设置本地化setlocale(LC_ALL, );srand((unsigned int)time(NULL));//游戏运行逻辑Game();system(cls);SetPos(37, 12);printf(Exit the game);SetPos(0, 25);return 0;
}
到这里我们的贪吃蛇就已经写完了。大家可以根据自己的想法再添加一些其他的功能。下面给出源代码。
七.Snake.h
#pragma once
#include stdio.h
#include stdlib.h
#include windows.h
#include stdbool.h
#include locale.h
#include time.h
#include conio.h#define WALL L□
#define BODY L※
#define POX_X 12
#define POX_Y 4
#define FOOD L★
#define KEY_PRESS(VK) ((GetAsyncKeyState(VK)0x1) ? 1 : 0)//蛇身的节点或者食物节点
typedef struct SnakeNode
{int x;int y;struct SnakeNode* Next;
}SnakeNode,* pSnakeNode;//蛇的方向
enum SnakeDirection
{UP,//向上DOWN,//向下LEFT,//向左RIGHT//向右
};//蛇的状态
enum SnakeStatue
{OK,//正常KILL_BY_SELF,//撞到自己KILL_BY_WALL,//撞到墙END_NOEMAL//正常退出
};typedef struct Snake
{pSnakeNode _pSnake;//维护蛇头的指针pSnakeNode _pFood;//维护食物的指针int _Food_Weight;//食物权重int _score;//总分数enum SnskeDirection _dir;//蛇的方向enum SnakeStatue _Statue;//蛇的状态int _Sleep_Time;//休眠时间
}Snake,*pSnake;//游戏运行逻辑
void Game();//定位光标
void SetPos(int x, int y);//游戏的初始化
void GameInit(pSnake ps);//初始化窗口
void InitWindow();//欢迎界面的打印
void welcome();//地图的打印
void CreatMap();//蛇身的初始化
void InitSnake(pSnake ps);//食物的初始化
void InitFood(pSnake ps);//游戏运行
void GameRun(pSnake ps);//蛇走一步
void SnakeMove(pSnake ps);//吃食物
void EatFood(pSnake ps, pSnakeNode pnode);//不是食物
void NoFood(pSnake ps, pSnakeNode pnode);//撞墙
void KillByWall(pSnake ps);//撞到自己
void KillBySelf(pSnake ps);//游戏结束
void GameOver(pSnake ps);
八.Snake.c
#define _CRT_SECURE_NO_WARNINGS 1
#include Snake.h//定位光标
void SetPos(int x, int y)
{//获取屏幕句柄HANDLE houtput GetStdHandle(STD_OUTPUT_HANDLE);//定位新坐标COORD pos { x,y };//设置光标位置SetConsoleCursorPosition(houtput, pos);
}//初始化窗口
void InitWindow()
{//设置窗口大小system(mode con cols100 lines30);//设置窗口名称system(title 贪吃蛇);//隐藏光标//获取屏幕的句柄操作权HANDLE houtput GetStdHandle(STD_OUTPUT_HANDLE);//定义一个光标信息的结构体CONSOLE_CURSOR_INFO cursor_info { 0 };//获取屏幕光标的操作权GetConsoleCursorInfo(houtput, cursor_info);//修改光标的可见性cursor_info.bVisible false;//设置光标信息SetConsoleCursorInfo(houtput, cursor_info);
}//欢迎界面的打印
void welcome()
{//定位光标SetPos(40, 15);printf(欢迎来到贪吃蛇\n);SetPos(35, 25);system(pause);system(cls);//打印帮助信息SetPos(25, 15);wprintf(L用 ↑. ↓ . ← . → 来控制蛇的移动按Shift加速Ctrl减速\n);SetPos(35, 25);system(pause);system(cls);
}//地图的打印
void CreatMap()
{int i 0;//上墙for (i 0; i 29; i){wprintf(L%lc , WALL);}//下墙SetPos(0, 26);for (i 0; i 29; i){wprintf(L%lc , WALL);}//左墙for (i 1; i 27; i){SetPos(0, i);wprintf(L%lc, WALL);}//右墙for (i 1; i 27; i){SetPos(56,i);wprintf(L%lc, WALL);}
}//蛇身的初始化
void InitSnake(pSnake ps)
{//假设初始蛇有两个节点int i 0;pSnakeNode newnode NULL;for (i 0; i 2; i){//创建蛇的节点newnode (pSnakeNode)malloc(sizeof(SnakeNode));if (newnode NULL){perror(InitSnake()::malloc());return;}newnode-Next NULL;//设置坐标//默认蛇节点从(12,4)开始newnode-x POX_X 2 * i;newnode-y POX_Y;//利用头插法将新节点连接到链表中if (ps-_pSnake NULL){//空链表ps-_pSnake newnode;}else{//非空链表newnode-Next ps-_pSnake;ps-_pSnake newnode;}}//打印蛇身体pSnakeNode cur ps-_pSnake;while (cur){//定位坐标SetPos(cur-x, cur-y);wprintf(L%lc, BODY);cur cur-Next;}//设置蛇的属性ps-_dir RIGHT;//默认方向向右ps-_Statue OK;//默认状态OKps-_Food_Weight 10;//默认一个食物10分ps-_score 0;//默认起始总分数为0分ps-_Sleep_Time 200;//默认休眠时间单位毫秒
}//食物的初始化
void InitFood(pSnake ps)
{int x 0;int y 0;
again:do{//食物的坐标要随机生成x rand() % 53 2;//产生2~54的随机数y rand() % 25 1;//产生1~26的随机数} while (x % 2 ! 0);//食物不能和蛇的身体重叠pSnakeNode cur ps-_pSnake;while (cur){if (cur-x x cur-y y){goto again;}cur cur-Next;}//创建食物节点pSnakeNode food (pSnakeNode)malloc(sizeof(SnakeNode));if (food NULL){perror(InitFood()::malloc());return;}food-Next NULL;food-x x;food-y y;//打印食物SetPos(food-x, food-y);wprintf(L%lc, FOOD);//将食物让pSnake贪吃蛇维护ps-_pFood food;
}//游戏的初始化
void GameInit(pSnake ps)
{//初始化窗口InitWindow();//欢迎界面的打印welcome();//地图的打印CreatMap();//蛇身的初始化InitSnake(ps);//食物的初始化InitFood(ps);
}void PrintHelpInfo(pSnake ps)
{//定位光标SetPos(65, 18);wprintf(L用 ↑. ↓ . ← . → 来控制蛇的移动);SetPos(65, 19);printf(按Shift加速Ctrl减速);SetPos(65, 20);printf(加速可以得到更高的分数);SetPos(65, 21);printf(小心不要撞到自己和墙);SetPos(65, 22);printf(ESC退出游戏SPACE暂定游戏);
}//暂停
void Suspend_time_out(pSnake ps)
{do{Sleep(300);if (KEY_PRESS(VK_SPACE)){break;}else if (KEY_PRESS(VK_ESCAPE)){ps-_Statue END_NOEMAL;}} while (1);
}//下一个位置是不是食物
int NextPositionIsFood(pSnake ps,pSnakeNode pnode)
{return ((ps-_pFood-x pnode-x) (ps-_pFood-y pnode-y));
}//吃食物
void EatFood(pSnake ps,pSnakeNode pnode)
{//此时pnode和食物节点是同一个节点我们可任选一个头插到链表中然后经另一个给释放掉pnode-Next ps-_pSnake;ps-_pSnake pnode;//释放食物节点free(ps-_pFood);ps-_pFood NULL;//创建一个新食物InitFood(ps);//吃了一个食物总分数要增加ps-_score ps-_Food_Weight;//打印蛇身pSnakeNode cur ps-_pSnake;while (cur){//定位光标SetPos(cur-x, cur-y);wprintf(L%lc, BODY);cur cur-Next;}
}//不是食物
void NoFood(pSnake ps, pSnakeNode pnode)
{//将pnode节点作为新的蛇头然后释放掉贪吃蛇的最后一个节点并在其位置上打印两个空白字符覆盖掉原先的蛇身pnode-Next ps-_pSnake;ps-_pSnake pnode;pSnakeNode cur ps-_pSnake;while (cur-Next-Next){SetPos(cur-x, cur-y);wprintf(L%lc, BODY);cur cur-Next;}//此时cur为倒数第二个节点SetPos(cur-Next-x, cur-Next-y);//打印两个空格覆盖原来的蛇身printf( );free(cur-Next);cur-Next NULL;
}//撞墙
void KillByWall(pSnake ps)
{pSnakeNode cur ps-_pSnake;if (cur-x 0 || cur-x 56){ps-_Statue KILL_BY_WALL;return;}else if (cur-y 0 || cur-y 26){ps-_Statue KILL_BY_WALL;return;}else{return;}
}//撞到自己
void KillBySelf(pSnake ps)
{pSnakeNode next ps-_pSnake-Next;while (next){if (ps-_pSnake-x next-x ps-_pSnake-y next-y){ps-_Statue KILL_BY_SELF;return;}next next-Next;}
}//蛇走一步
void SnakeMove(pSnake ps)
{//先创建一个新节点pSnakeNode node (pSnakeNode)malloc(sizeof(SnakeNode));if (node NULL){perror(SnskeMove()::malloc());return;}//计算下一个位置的坐标switch (ps-_dir){case UP:{node-x ps-_pSnake-x;node-y ps-_pSnake-y - 1;break;}case DOWN:{node-x ps-_pSnake-x;node-y ps-_pSnake-y 1;break;}case LEFT:{node-x ps-_pSnake-x - 2;node-y ps-_pSnake-y;break;}case RIGHT:{node-x ps-_pSnake-x 2;node-y ps-_pSnake-y;break;}}//判断下一个位置是不是食物//是食物if (NextPositionIsFood(ps,node)){//吃食物EatFood(ps,node);}//不是食物else{NoFood(ps,node);}//蛇在走一步的过程中可能会撞到墙或者自己//撞墙KillByWall(ps);//撞到自己KillBySelf(ps);
}//游戏运行
void GameRun(pSnake ps)
{//首先在游戏右边打印帮助信息PrintHelpInfo(ps);//检测按过哪个键do{//打印总分数以及单个食物的分数SetPos(69, 7);printf(Score%2d, ps-_score);SetPos(69, 8);printf(Food Weight%2d, 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_SHIFT)){//加速//限定速度最大值if(ps-_Sleep_Time 100){ps-_Sleep_Time - 20;ps-_Food_Weight 2;}}else if (KEY_PRESS(VK_CONTROL)){//减速//限定最小分数分数不能为0if(ps-_Food_Weight 2){ps-_Sleep_Time 20;ps-_Food_Weight - 2;}}else if (KEY_PRESS(VK_SPACE)){//暂停Suspend_time_out(ps);}else if (KEY_PRESS(VK_ESCAPE)){//退出ps-_Statue END_NOEMAL;break;}//蛇走一步SnakeMove(ps);Sleep(ps-_Sleep_Time);} while (ps-_Statue OK);
}//销毁链表
void ReleaseSnake(pSnake ps)
{//销毁链表pSnakeNode cur ps-_pSnake;pSnakeNode next NULL;while (cur){next cur-Next;free(cur);cur next;}ps NULL;
}//游戏结束
//善后工作
void GameOver(pSnake ps)
{//判断游戏是怎样结束的if (ps-_Statue END_NOEMAL){SetPos(16, 7);printf(You voluntarily quit the game);}else if (ps-_Statue KILL_BY_WALL){SetPos(16, 7);printf(Youve hit a wall);}else if (ps-_Statue KILL_BY_SELF){SetPos(16, 7);printf(You bumped into yourself);}//释放节点ReleaseSnake(ps);ps NULL;
}
九.test.c
#define _CRT_SECURE_NO_WARNINGS 1
#include Snake.hvoid Game()
{Snake s { 0 };int ch 0;do{//游戏的初始化GameInit(s);//游戏运行GameRun(s);//游戏结束GameOver(s);s._pSnake NULL;Sleep(1000);system(cls);SetPos(35, 12);printf(Do you want another round?);SetPos(45, 13);printf(Y/N);ch _getch();} while (ch Y || ch y);
}int main()
{//程序一开始就先设置本地化setlocale(LC_ALL, );srand((unsigned int)time(NULL));//游戏运行逻辑Game();system(cls);SetPos(37, 12);printf(Exit the game);SetPos(0, 25);return 0;
}
七八九均是文件名