赞
踩
目录
贪吃蛇,一个相当经典的小游戏,相信各位或多或少都玩过或是听过。而如果要实现这个小游戏的话,我们就需要熟悉 结构体、指针 以及 链表 的相关知识
https://pan.baidu.com/s/1br9QsxLJpWF8Rgq-xE2JRg?pwd=1111
提取码: 1111
游戏已经放在上面了,各位可以在电脑上玩玩看
Win32 API 中有许多函数,我们今天将会学习里面的几种函数以帮助我们实现贪吃蛇小游戏
首先,我们需要设置控制台的大小与名字,我们平常运行程序时出现的框框就是控制台,如下
这里我们需要用到 system 函数,引头文件 # include <stdlib.h>
system("mode con cols=100 lines=30");
通过如上代码,我们就可以将控制台设置成一个 100 * 30 的矩形
同时我们还可以将控制台的名字改成游戏的名字
system("title 贪吃蛇");
单独使用如上代码时,我们会发现控制台并没有依照我们的预期将名字改成贪吃蛇
这是因为程序运行得太快了,当程序结束的时候,名字也就自动恢复了
对此,我们可以使用 system("pause"); 将程序暂停一下以观察效果,如下
COORD是Win32 API中定义的一个结构体,使用时需要引头文件 #include <windows.h>,如下
- typedef struct _COORD {
- SHORT X;
- SHORT Y;
- } COORD, * PCOORD;
当我们需要在我们想要的位置输入内容时,比如想在控制台中间输入“ 欢迎加入贪吃蛇 ”,我们就需要用到COORD(因为控制台总是在最左上角的位置开始打印)
COORD pos = { 1, 1 };
通过如上代码,我们就可以对坐标进行赋值
这是一个非常重要的函数,其作用是获得特定设备的句柄(标识不同设备的数值)
看到这里可能会有人不知道 句柄 是什么,那我就简单解释一下(以下内容单纯是为了理解):
我们提桶时需要一个把手,这样才能更好地将这个桶提起来,而这个把手,就是桶的句柄; 炒菜时用锅,锅的把手就是锅的句柄
而我们程序运行时,有了程序的句柄,我们才能更好地进行各种操作,而程序的句柄就是一个数值,每个数值都代表一个特定的设备,GetStdHandle 函数的结构如下
HANDLE GetStdHandle(DWORD nStdHandle);
可以看到,该函数会返回一个 HANDLE 类型的数据,我们在使用时就只需要创建一个 HANDLE 类型的数据,并且使用该数据接收 GetStdHandle 函数的返回值就可以了
而该函数所需的参数是什么?如下:
该函数需要的参数就上面三种,我们实现贪吃蛇游戏所需的就是上面的STD_OUTPUT_HANDLE
使用实例如下:
HANDLE handle = GetStdHandle(STD_OUTPUT_HANDLE);
依照如上代码,我们就能获得设备的句柄
为了隐藏光标,所以我们学习这个函数,因为游戏运行时总有光标在闪就不大美观
看该函数的名称就知道,这个函数是获取控制台光标信息的,而其语法结构如下:
- BOOL WINAPI GetConsoleCursorInfo(
- _In_ HANDLE hConsoleOutput,
- _Out_ PCONSOLE_CURSOR_INFO lpConsoleCursorInfo
- );
如上,该函数的第一个参数是设备的句柄,我们通过 GetStdHandle 函数可以获取
第二个参数是一个指针,一个指向 CONSOLE_CURSOR_INFO 结构的指针,该结构体语法结构如下:
- typedef struct _CONSOLE_CURSOR_INFO {
- DWORD dwSize;
- BOOL bVisible;
- } CONSOLE_CURSOR_INFO, *PCONSOLE_CURSOR_INFO;
第一个成员是表示光标占比的,1% ~ 100%。我们看到的大多数光标的占比是25%,而如果我们将其设置为100%,效果将会是这样的(此处会预先使用到下面会讲的 SetConsoleCursorInfo):
第二个成员是表示光标可见性的,也就是光标能否被看见就由第二个成员决定,如果我们不想让光标显示出来的话,我们只需要将结构体成员 bVisible 置为 false 就可以了,但由于 false 编辑器不认识,所以我们需要引头文件 #include <stdbool.h>,如下:
- #include <stdbool.h>
-
- cursorinfo.bVisible = false;
这个函数就好比,有个人请求你帮他修理一个东西,你拿到了这个东西,修理完之后,你得还给人家让人家检查检查是不是真的修好了
而我们前面的知识都是在讲怎么拿到东西以及怎么修理这个东西的,现在要讲的就是怎么将这个东西还回去并拿到相应的报酬
SetConsoleCursorInfo 函数的语法和 GetConsoleCursorInfo 函数是差不多的,如下:
- BOOL WINAPI SetConsoleCursorInfo(
- _In_ HANDLE hConsoleOutput,
- _In_ const CONSOLE_CURSOR_INFO *lpConsoleCursorInfo
- );
我们可以看到,这里面的两个参数分别是句柄和一个指针(结构体),和 GetConsoleCursorInfo 函数是一样的
综上,隐藏光标的代码如下:
- CONSOLE_CURSOR_INFO cursorinfo;
- //定义出CONSOLE_CURSOR_INFO类型的结构体,名字为cursorinfo
- GetConsoleCursorInfo(handle, &cursorinfo);
- //获取光标信息
- cursorinfo.bVisible = false;
- //将结构体内的光标信息更改为为不可见
- SetConsoleCursorInfo(handle, &cursorinfo);
- //设置指定设备光标的可见性
这个函数跟我们上面看到的 SetConsoleCursorInfo 函数是非常相似的
我们先来看一看该函数的语法:
- BOOL WINAPI SetConsoleCursorPosition(
- HANDLE hConsoleOutput,
- COORD pos
- );
我们可以看到,这个函数的的第一个参数是句柄
第二个参数是坐标信息,也就是 COORD 类型结构体内的成员
所以设置光标位置的代码如下:
- HANDLE handle = GetStdHandle(STD_OUTPUT_HANDLE);
- //获得句柄
- COORD pos = { 46, 15 };
- //定义坐标信息
- SetConsoleCursorPosition(handle, pos);
- //设置光标位置
我们可以加一个 scanf 让程序停下来看看效果:
如果我们要让蛇 上下左右 移动的话,那么就必须要使用到我们的键盘,但是我们编写程序又该如何知道哪个按键是否被按过呢?
这时我们就可以使用 GetAsyncKeyState 函数,其语法结构如下:
- SHORT GetAsyncKeyState(
- int vKey
- );
这个函数需要你传一个虚拟键值进去,然后该函数会检测,传进去的虚拟键值所代表的按键是否被按过
返回一个 short 类型的数据
如果返回的这个数据的二进制位的最高位为1,则代表该键正在被按着
如果返回的这个数据的二进制位的最高位为0,则代表该键现在没有被按着
如果返回的这个数据的二进制位的最低位为1,则代表该键被按过
如果返回的这个数据的二进制位的最低位为0,则代表该键没被按过
我们今天就实现一个简单点的,只要按键被按过,我们就加速/减速
而要判断的话,我们可以通过按位与0X1来判断,如下是按位与的知识点
- 5 & 3
- 5 1 0 1
- 3 0 1 1
- 结果 0 0 1
- 所以 5 & 3 的结果为1
如果返回值被 按位与(&)一个0X1的话,那么如果该按键被按过,那么返回值的最低位就一定为1,又因为按位与(&)是两个都为 1 结果才为 1
所以当我们将返回值按位与 1,那么结果如果为1,就代表这个键被按过;如果结果为0,则代表该键没有被按过
但是如果我们每次都要将其结果 &1 的话,那么就会显得代码很冗杂,我们可以用 #define 定义一个宏,而结果的话我们可以用三目操作符,这样我们就可以返回int型的1与0,如下:
#define KEY_PRESS(VK) ( GetAsyncKeyState(VK) & 0x1 ? 1 : 0 )
如上,我们仅需要输入一个 KEY_PRESS(虚拟键值)就可以获取当前按键的状态了
虚拟键值表如下:
https://learn.microsoft.com/zh-cn/windows/win32/inputdev/virtual-key-codes
前面我们说了COORD相关的知识,但是每一次光标定位我们都需要获得句柄、坐标更改、设置坐标三步,这样代码会显得冗杂
我们不妨建立一个函数SetPos,将如上步骤都包含在内,我们只需要将坐标传进去就可以了,不需要返回值,如下:
- void SetPos(int x, int y)
- {
- //获得设备句柄
- HANDLE handle = GetStdHandle(STD_OUTPUT_HANDLE);
- //根据句柄设置光标位置
- COORD pos = { x,y };
- SetConsoleCursorPosition(handle, pos);
- }
在游戏开始之前,我们可以先打印一些欢迎信息,并告知一些规则
而如果要实现这个效果的话,我们需要 system("pause") 与刚创建好的 SetPos 函数,每切换一个界面,我们就用 system(“ cls ”)清屏就可以了
代码如下:
- //打印欢迎界面
- void WelcomeToGame()
- {
- SetPos(35, 12);
- printf("欢迎来到贪吃蛇小游戏\n");
- SetPos(38, 20);
- system("pause");
- system("cls");
-
- SetPos(28, 10);
- printf("用↑↓←→来控制蛇的移动,A键是加速,D键是减速");
- SetPos(28, 11);
- printf("加速能得到更高的分数");
- SetPos(28, 12);
- printf("按空格键可以暂停,按Esc键可以退出游戏");
- SetPos(38, 20);
- system("pause");
- system("cls");
- }
我们每打印一句话之后,就可以考虑再次换位,然后打印下一句话,代码效果如下:
C语言最初是英文的,但是全世界的人们都要用的话,仅仅是英文就不够用了,不说法国、意大利之类的国家有很多其他符号,就我们中国,光汉字都有10万多个,一个字节大小最多也就256,根本无法涵盖
所以,我们在创建项目之前,我们需要先让编辑器适配本地的信息,就比如我们接下来打印墙体、食物、蛇身所需要的宽字符就需要本地化
而本地化我们仅需要引头文件 #include <locale.h>,然后我们来看看这个函数的语法
char* setlocale (int category, const char* locale);
如上我们可以看到,该函数的第一个参数需要的是上面5个中的一个
这里面有改变时间的,有改变金钱单位的。而我们现在需要的,是全部都改变,所以我们就选择第一个 LC_ALL(全部都改变)
第二个参数如下
我们会看到参数有两种,“C” 是C语言默认环境,而 “ ” 则是适配本地环境
综上,我们本地化的代码如下:
- #include<locale.h>
- //引头文件
-
- setlocale(LC_ALL, "");
- //本地化
在绘制地图之前,我们需要先知道的是,编辑器的横坐标的长度是纵坐标的两倍
而如果我们要打印墙体且不想让墙体看起来很扁的话,我们就需要用到宽字符,如下:
- printf("ab\n");
- printf("中\n");
- wprintf(L"%lc\n", L'□');
我们会发现,宽字符□的大小是单一一个字母的两倍,而我们如果要打印墙体或者蛇身的话,我们需要用到宽字符,不然会显得蛇很扁,看起来很别扭
接下来我们就来打印墙体
我们的思路是:先用SetPos函数找到对应的位置,然后用for循环来循环打印宽字符作为墙体
但是考虑到每一次打印墙体都需要写 L'对应符号',所以我们可以定义一个宏,这样即使我们以后想让墙换一个符号的话也方便,如下:
#define WALL L'□'
我们就打印一个 58*27 的地图吧,墙体打印代码如下:
- //绘制地图
- void CreatMap()
- {
- int i = 0;
- //上
- SetPos(0, 0);
- for (i = 0; i <= 56; i+=2)
- {
- wprintf(L"%lc", WALL);
- }
- //下
- SetPos(0, 26);
- for (i = 0; i <= 56; i += 2)
- {
- wprintf(L"%lc", WALL);
- }
- //左
- for (i = 1; i <= 25; i++)
- {
- SetPos(0, i);
- wprintf(L"%lc", WALL);
- }
- //右
- for (i = 1; i <= 25; i++)
- {
- SetPos(56, i);
- wprintf(L"%lc", WALL);
- }
-
- }
对于蛇身,我们需要在头文件中定义一个链表,如下:
- //蛇身结点的定义
- typedef struct SnakeNode
- {
- int x;
- int y;
- struct SnakeNode* next;
- }SnakeNode, * pSnakeNode;
一个贪吃蛇游戏,需要考虑蛇本身、食物、方向、当前状态等等
我们将这些要素全部放在一个结构体里面,将其 typedef 为 Snake,通过这个结构体我们就能找到全部变量
而其中的状态和方向又分为上下左右、正常、撞到自己,撞到墙、主动退出游戏等等
对此,我们可以考虑使用枚举,一方面是因为#define定义的宏要定义多个太麻烦,另一方面是因为使用枚举方便调试,而且这种情况下使用枚举确实会好一些
代码如下:
- enum GAME_STATUS
- {
- OK=1,
- ESC,
- KILL_BY_WALL,
- KILL_BY_SELF
- };
-
- enum DIRECTION
- {
- UP=1,
- DOWN,
- LEFT,
- RIGHT
- };
- //贪吃蛇
- typedef struct Snake
- {
- pSnakeNode pSnake;//维护整条蛇的指针
- pSnakeNode pFood;//指向食物的指针
- int score;//当前积累的分数
- int FoodWeight;//一个食物的分数
- int SleepTime;//蛇休眠的时间,时间越短,蛇的速度越快
- enum GAME_STATUS status;//游戏当前的状态
- enum DIRECTION dir;//蛇当前走的方向
-
- }Snake,*pSnake;
我们进入游戏之后会发现游戏旁边的界面有点空,我们可以打印一些提示信息上去
这个环节无非就是 SetPos 函数定位,接着 printf 打印信息,这里我就直接给代码了:
- void PrintHelpInfo()
- {
- SetPos(60, 12);
- printf("1.不能穿墙,不能咬到自己");
- SetPos(60, 14);
- printf("2.用 ↑.↓.←.→ 来控制蛇的移动");
- SetPos(60, 16);
- printf("3.A键是加速,D键是减速");
-
- SetPos(60, 18);
- printf("4.按空格键可以暂停,按Esc键可以退出游戏");
- SetPos(60, 20);
- printf("嘉鑫版");
- }
我们可以先创建一条初始长度为5的蛇,那么我们需要先用 for 循环 malloc 5个节点,然后依次头插,形成一条链表。
接下来,我们先假设开局就是如上所示。
先将我们的蛇的结构体传过去,然后将头节点置为空,如下:
- //Snake.h
- 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 GAME_STATUS status;//游戏当前的状态
- enum DIRECTION dir;//蛇当前走的方向
- }Snake,*pSnake;
-
-
- //Snake.c
- void InitSnake(pSnake ps)
- {
- //创建5个蛇身的结点
- ps->pSnake = NULL;
- }
接着 for 循环 malloc 5个类型为SnakeNode的节点,而该结构体内的 X 可以先初始化成一个(具体的值 + 2 * i),Y 可以直接初始化成一个具体的值,因为如上图所示,我们初始长度的蛇的 Y 坐标都相同
当然,你也可以将这个值用 #define 定义为一个宏,方便以后修改
代码如下:
- #define POS_X 24
- #define POS_Y 5
-
- void InitSnake(pSnake ps)
- {
- //创建5个蛇身的结点
- ps->pSnake = NULL;
- int i = 0;
- pSnakeNode cur = NULL;
- for (i = 0; i < 5; i++)
- {
- cur = (pSnakeNode)malloc(sizeof(SnakeNode));
- //申请节点
- if (cur == NULL)
- {
- //判断开辟空间是否成功
- perror("malloc fail!");
- return;
- }
- //初始化刚申请的节点的成员
- cur->x = POS_X + 2 * i;
- cur->y = POS_Y;
- cur->next = NULL;
-
- //头插法
- if (ps->pSnake == NULL)
- {
- ps->pSnake = cur;
- //判断没有节点的情况
- }
- else
- {
- cur->next = ps->pSnake;
- ps->pSnake = cur;
- }
- }
- }
这里我再来讲一讲头插代码
如上,cur 指向的是新开辟的节点
我们先让 cur 指向链表的头节点,然后再将 ps->pSnake 定义为新的头
打印蛇身相对简单,我们的思路就是:先通过 SetPos 函数找到对应节点,然后宽字符打印
考虑到后续代码中还有蛇身要打印,所以这里就将蛇身的符号用 #define 定义起来
- #define BODY L'●'
-
- //打印蛇身
- cur = ps->pSnake;
- //将cur定义为新的头
- while (cur)
- {
- SetPos(cur->x, cur->y);
- wprintf(L"%lc", BODY);
-
- cur = cur->next;
- //寻找下一个要打印的节点,直到为空
- }
而我们其他信息的初始化就相对轻松一些,代码如下:
- //贪吃蛇其他信息初始化
- ps->dir = RIGHT;
- ps->FoodWeight = 10;
- ps->pFood = NULL;
- ps->score = 0;
- ps->SleepTime = 200;
- ps->status = OK;
综上,我们先申请了节点,接着打印蛇身,最后将其他信息给初始化了
初始化蛇的总代码如下:
- void InitSnake(pSnake ps)
- {
- //创建5个蛇身的结点
- ps->pSnake = NULL;
- int i = 0;
- pSnakeNode cur = NULL;
- for (i = 0; i < 5; i++)
- {
- cur = (pSnakeNode)malloc(sizeof(SnakeNode));
- if (cur == NULL)
- {
- perror("malloc fail!");
- return;
- }
- cur->x = POS_X + 2 * i;
- cur->y = POS_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->dir = RIGHT;
- ps->FoodWeight = 10;
- ps->pFood = NULL;
- ps->score = 0;
- ps->SleepTime = 200;
- ps->status = OK;
- }
初始化了蛇,接下来我们就该初始化食物了
贪吃蛇中的食物随机出现在地图中的任意位置(墙和蛇身除外),我们可以用 rand 函数设置随机,接着用time(时间戳)改变种子( srand(unsigned seed) ),也就是 srand( (unsigned) time (NULL) )
而要使食物不出现在墙上的话,先来讨论 x 坐标(注意,我们的地图大小为 58*27),但是x从0开始,所以食物的 x 坐标的范围就是2~54
我们可以先将rand的结果%53,得到的就是 0~52 以内的随机数,再将这个结果+2,得到的就是2~54以内的随机值,y同理,如下:
- x = rand() % 53 + 2;
- y = rand() % 24 + 1;
但是这时发现了一个问题,因为一个字符在 x 轴上的大小是 y 轴的一半,而且无论是墙体,蛇身还是食物,打印的都是宽字符
这也就意味着,我们随机出来的 x 必须是偶数,不然就会出现下面的场景
我们没有办法让 rand 函数每次的随机数都是偶数,但是我们可以设置一个循环。如果这次随机出来的 x 不是一个偶数,那我们就让其再随机生成一次,而判断偶数就只需要%2看等不等于0就行了
代码如下:
- do
- {
- x = rand() % 53 + 2;
- y = rand() % 24 + 1;
- } while (x % 2 != 0);
接着我们需要再判断一下,随机出来的坐标在不在蛇身上
这时我们只需要用一个while循环,创建一个cur指针指向头节点,每次查看完之后向后走一个节点,当cur指向空时就停下来
每到一个节点就拿随机生成的 x、y 坐标和节点内的 x、y 坐标进行比较,如果有相同的,就再随机生成一次食物坐标
- int x = 0, y = 0;
- again:
- do
- {
- x = rand() % 53 + 2;
- y = rand() % 24 + 1;
- } while (x % 2 != 0);
-
- //判断坐标在不在蛇身上,与每个节点作比较
- pSnakeNode cur = ps->pSnake;
- while (cur)
- {
- if (x == cur->x && y == cur->y)
- {
- goto again;
- }
- cur = cur->next;
- }
在这里我们可以使用goto语句,面对循环嵌套之类的情况使用goto语句会方便很多
我们可以这么理解,食物就是蛇身的一部分,只不过不跟蛇身连在一起,当玩家吃到食物之后,就直接将食物头插在蛇身上,唯一的区别就是打印的时候,用到宽字符不是一个符号而已
所以同样的,我们也是用 malloc 开辟一块空间,初始化,最后将其打印出来
食物的话我们可以跟蛇身、墙体一样用 #define 定义一个宏
#define FOOD L'★'
- //创建食物
- //开辟食物的空间
- pSnakeNode pFood = (pSnakeNode)malloc(sizeof(SnakeNode));
- //判断是否开辟成功
- if (pFood == NULL)
- {
- perror("malloc fail!");
- return;
- }
- //初始化
- pFood->x = x;
- pFood->y = y;
-
- ps->pFood = pFood;
- //打印食物
- SetPos(x, y);
- wprintf(L"%lc", FOOD);
综上,初始化食物这段代码如下:
- void CreateFood(pSnake ps)
- {
- int x = 0, y = 0;
- again:
- do
- {
- x = rand() % 53 + 2;
- y = rand() % 24 + 1;
- } while (x % 2 != 0);
-
- //判断坐标在不在蛇身上,与每个节点作比较
- 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("malloc fail!");
- return;
- }
-
- pFood->x = x;
- pFood->y = y;
-
- ps->pFood = pFood;
- SetPos(x, y);
- wprintf(L"%lc", FOOD);
-
- }
综上,游戏开始前我们调整了控制台,隐藏了光标,打印了地图,初始化了蛇和食物以及其他游戏信息,代码如下:
- void GameStart(pSnake ps)
- {
- //控制控制台的信息,窗口大小,窗口名
- system("mode con cols=100 lines=30");
- system("title 贪吃蛇");
-
- //隐藏光标
- HANDLE handle = GetStdHandle(STD_OUTPUT_HANDLE);//获得句柄
- CONSOLE_CURSOR_INFO CursorInfo;
- GetConsoleCursorInfo(handle, &CursorInfo);//获取控制台光标信息
- CursorInfo.bVisible = false;//隐藏光标
- SetConsoleCursorInfo(handle, &CursorInfo);
-
- //打印欢迎信息
- WelcomeToGame();
-
- //绘制地图
- CreatMap();
-
- //打印提示信息
- PrintHelpInfo();
-
- //初始化蛇
- InitSnake(ps);
-
- //创建食物
- CreateFood(ps);
- }
首先,我们需要知道每个键的虚拟键值,如下:
https://learn.microsoft.com/zh-cn/windows/win32/inputdev/virtual-key-codes
游戏运行时,我们需要判断哪个键被按过——加速、减速、上下左右、暂停、退出等
我们可以这么做,上下左右按键判断时,我们只需改变维护整个贪吃蛇结构体里的方向状态就行了
而其他的按键判断我们就设置一个 do...while 循环,条件就是判断状态是否为OK,如果不为OK,就退出循环,游戏结束
状态和方向的设定,以及维护整个贪吃蛇的结构体如下:
- enum GAME_STATUS
- {
- OK=1,
- ESC,
- KILL_BY_WALL,
- KILL_BY_SELF
- };
-
- enum DIRECTION
- {
- UP=1,
- DOWN,
- LEFT,
- RIGHT
- };
-
- //贪吃蛇
- typedef struct Snake
- {
- pSnakeNode pSnake;//维护整条蛇的指针
- pSnakeNode pFood;//指向食物的指针
- int score;//当前积累的分数
- int FoodWeight;//一个食物的分数
- int SleepTime;//蛇休眠的时间,时间越短,蛇的速度越快
- enum GAME_STATUS status;//游戏当前的状态
- enum DIRECTION dir;//蛇当前走的方向
-
- }Snake,*pSnake;
因为我们一次就只能按一个按键,所以我们可以用 if...else if...else 语句来进行判断
如果为上下左右,那我们还需判断一下:
当方向向上/下的时候,不能按下向下/上的按键
当方向向左/右的时候,不能按下向右/左的按键
代码如下:
- void GameRun(pSnake 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;
- }
-
- } while (ps->status == OK);
- }
可能有人会疑惑,单单改变一个方向的状态,就真的能让蛇的方向改变吗?当然不能
我们改变状态是为了在后续贪吃蛇行动时,我们可以通过这个状态进行 switch...case 操作决定下一个节点是在贪吃蛇头部的哪一个方向
而我们如果要判断退出(Esc)的话,我们只需要改变 ps->status 就行了,这样子的话游戏进行下来,当再次进入循环条件判断时,状态不为 OK,就会退出循环,游戏自然也会结束
- else if (KEY_PRESS(VK_ESCAPE))
- {
- ps->status = ESC;
- break;
- }
而如果按下的是空格键的话,我们可以建立一个函数 pause,函数的内容就是死循环地 Sleep(时间),只有当玩家再次按下空格键或者 Esc 时,才会退出循环
- void pause(pSnake ps)
- {
- while (1)
- {
- Sleep(1000);
- if (KEY_PRESS(VK_SPACE))
- {
- break;
- }
- else if (KEY_PRESS(VK_ESCAPE))
- {
- ps->status = ESC;
- break;
- }
- }
- }
-
-
- else if (KEY_PRESS(VK_SPACE))
- {
- pause(ps);
- }
除了以上的键位之外,我们还有加速和减速功能需要实现
这里不太推荐 F1~F0 的键位作为加速或减速的键位,这是因为现在的电脑 F1~F0 键已经不像早期的电脑那么纯粹了,上面除了原有的功能之外,还有了调整亮度、声音、截屏等功能,需要手动按 Fn 键进行切换,如果别人没有注意到的话,很可能会出现按了加速减速却没反应的情况,这是我们需要避免的
我们可以用 A 代表加速,D 代表减速,两个键的虚拟键值分别是 0X41 和 0X44
贪吃蛇游戏的原理就是走一步休眠一下,休眠的时间越短,视觉上看来蛇的速度就越快。所以我们判断到玩家按下了 A 键时,我们就将休眠时间调整得短一点,按下 D 键同理
同时,因为速度快了,我们可以令每一个食物的分值上升,同时设定一个速度的上限
代码如下:
- else if (KEY_PRESS(0X41))
- {
- if (ps->SleepTime >= 80)
- {
- ps->SleepTime -= 30;
- ps->FoodWeight += 2;
- }
- }
- else if (KEY_PRESS(0X44))
- {
- if (ps->FoodWeight > 2)
- {
- ps->SleepTime += 30;
- ps->FoodWeight -= 2;
- }
- }
贪吃蛇走一步的原理是:
将贪吃蛇要走的下一个节点找出来,头插该节点
如若下一个节点是食物,那么就头插食物就够了
如若下一个节点不是食物,那么我们就在头插下一个节点的同时,删除尾节点并打印成空格
所以我们在方向判断之前,我们还需要 malloc 下一个节点,内部的 x、y 通过方向来初始化
- pSnakeNode pNext = (pSnakeNode)malloc(sizeof(SnakeNode));
- if (pNext == NULL)
- {
- perror("malloc fail!!");
- return;
- }
- pNext->next = NULL;
在进入该函数之前,我们已经判断过方向了,而我们是用枚举类型定义的方向,所以在这里我们可以用 switch...case 语句判断贪吃蛇要走的下一个节点在哪里
比如方向向左,那下一个节点的坐标,就在相对贪吃蛇头节点 x-2,y 不变的位置
比如向上,那么下一个节点就在坐标,就在相对贪吃蛇头节点 x 不变,y-1 的位置
向右向下同理
代码如下:
- 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;
- }
这个函数的实现比较简单,我们直接拿 食物的坐标 和 贪吃蛇头节点的下一个节点的坐标 比较一下,如果相等,那么下一个节点就是食物,不相等,那就不是
代码如下:
- int NextIsFood(pSnake ps, pSnakeNode pNext)
- {
- if (ps->pFood->x == pNext->x && ps->pFood->y == pNext->y)
- {
- return 1;
- }
- else
- {
- return 0;
- }
- }
如果下一个节点是食物的话,我们先将下一个节点头插到贪吃蛇上,然后打印蛇身
同时,我们之前定义食物的时候还 malloc 了一块空间,我们既然拿下一个节点的空间头插到贪吃蛇上面,那我们就应该将原本食物的节点给销毁(free)
吃掉了食物之后,我们原先在地图上的食物就被覆盖了,那么这时我们就应该再创建一个食物
同时,我们吃掉了一个食物之后,我们的分数也应该变高,就让原先的分数加上一个食物的分数
代码如下:
- 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;
- //向后走一个位置
- }
-
- ps->score += ps->FoodWeight;
-
- //释放旧的食物
- free(ps->pFood);
- //新建食物
- CreateFood(ps);
-
- }
当下一个节点不是食物的时候,我们就先将下一个节点头插到贪吃蛇上面
同时我们需要知道,本来贪吃蛇是已经被打印出来了的,所以我们只需要将新的头节点打印出来,同时将尾节点打印成空格,我们就能在视觉上实现贪吃蛇向后走一步的效果
至于找到尾节点,我们可以用一个 while 循环,定义一个 cur 指针,让 cur 指针遍历一遍链表,当cur->next 指向空的时候,循环结束,此时我们的 cur 指针指向的就是尾节点
然后我们 SetPos 到这个位置之后打印两个空格,注意,是两个空格,因为 x 的大小是 y 的两倍
代码如下:
- void NotEatFood(pSnake ps, pSnakeNode pNext)
- {
- //头插
- pNext->next = ps->pSnake;
- ps->pSnake = pNext;
-
- //释放尾结点
- //顺便打印尾节点
- pSnakeNode cur = ps->pSnake;
-
- SetPos(cur->x, cur->y);
- wprintf(L"%lc", BODY);
-
- //找尾节点
- while (cur->next->next)
- {
- cur = cur->next;
- }
-
- //将尾节点置空 打印 ' '
- SetPos(cur->next->x, cur->next->y);
- printf(" ");//两个空格!!!
-
- free(cur->next);
- cur->next = NULL;
- }
判断蛇是否撞到墙,我们只需要将贪吃蛇的头节点是否在墙壁所圈定的范围之内,或者是在墙上,如果不在这个范围内,那就证明蛇已经撞到墙了
接着,我们需要将游戏的状态更改为 KILL_BY_WALL
当蛇向后走了一步时,判断到状态不为 OK,就会跳出循环,游戏结束
我们打印的地图的大小是 58*27,但是坐标是从(0,0)开始的,所以只要 x 不在 2~55 这个范围内,y 不在 1~26 这个范围内,那么就说明蛇撞到墙了
代码如下:
- 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;
- }
- }
我们要判断蛇是否会撞到自己,我们只需要将头节点和蛇身的每一个坐标一一比对,当发现有相同的时候,就说明蛇已经咬到自己了
但是蛇的前三个节点没有相撞的可能性,如下图
所以我们可以从第四个节点开始判断,至于如何找到第四个节点,我们只需要将创建一个新指针,让这个新指针 = 头指针->next->next->next,这时,这个新指针指向的就是第四个节点
当我们发现头节点的 x、y 和第四个结点之后的节点的 x、y 的值相同的时候,我们就可以修改状态为 KILL_BY_SELF
代码如下:
- void KillBySelf(pSnake ps)
- {
- pSnakeNode cur = ps->pSnake->next->next->next;
-
- while (cur)
- {
- if (cur->x == ps->pSnake->x && cur->y == ps->pSnake->y)
- {
- ps->status = KILL_BY_SELF;
- return;
- }
- cur = cur->next;
- }
- }
我们在令蛇向后走完了一步之后,需要让贪吃蛇睡眠一段时间,而这段时间我们设置在了维护整个贪吃蛇的结构体里,并将其初始化为 200毫秒 ,具体的初始化内容各位可以看回 初始化蛇(InitSnake) 部分
综上,GameRun函数 代码如下:
- void GameRun(pSnake ps)
- {
- do
- {
- SetPos(62, 9);
- printf("总分:%5d\n", ps->score);
- SetPos(62, 10);
- printf("食物的分值:%02d\n", ps->FoodWeight);
-
- //检测按键
- //上、下、左、右、ESC、空格、F3、F4
- 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 = ESC;
- break;
- }
- else if (KEY_PRESS(VK_SPACE))
- {
- pause(ps);
- }
- else if (KEY_PRESS(0X41))
- {
- if (ps->SleepTime >= 80)
- {
- ps->SleepTime -= 30;
- ps->FoodWeight += 2;
- }
- }
- else if (KEY_PRESS(0X44))
- {
- if (ps->FoodWeight > 2)
- {
- ps->SleepTime += 30;
- ps->FoodWeight -= 2;
- }
- }
-
- //走一步
- SnakeMove(ps);
-
- //睡眠一下
- Sleep(ps->SleepTime);
-
- } while (ps->status == OK);
- }
由于我们的状态是用枚举类型定义的,所以我们可以用 switch...case 语句来进行分类讨论
当状态为 ESC 时,我们就打印提示信息并 break
当状态为 KILL_BY_WALL 或 KILL_BY_SELF 时,我们就浅浅嘲讽一下,比如打开原神官网
打开网站可以使用 system ( " start + 网站 " );
代码如下:
- SetPos(15, 12);
- switch (ps->status)
- {
- case ESC:
- printf("别啊,怎么就不玩了,不会是太菜了吧(滑稽)");
- break;
- case KILL_BY_SELF:
- printf("即将为您打开您的最爱!!!");
- SetPos(15, 13);
- printf("即将为您打开您的最爱!!!");
- SetPos(15, 14);
- printf("即将为您打开您的最爱!!!");
-
-
- for (int i = 3; i >=0; i--)
- {
- SetPos(28, 15);
- printf("%d", i);
- Sleep(1000);
- }
-
- system("start https://ys.mihoyo.com/");
- break;
- case KILL_BY_WALL:
- printf("即将为您打开您的最爱!!!");
- SetPos(15, 13);
- printf("即将为您打开您的最爱!!!");
- SetPos(15, 14);
- printf("即将为您打开您的最爱!!!");
-
- for (int i = 3; i > 0; i--)
- {
- SetPos(28, 15);
- printf("%d", i);
- Sleep(1000);
- }
- system("start https://ys.mihoyo.com/");
- break;
- }
游戏结束了之后,我们需要释放蛇的空间的同时,还要释放食物的空间(因为两者都是malloc开辟)
至于删除蛇身,我们可以定义两个指针,一个指向要删除的节点,一个指向下一个节点
这是因为如果我们将该节点的空间释放掉之后,我们就找不到下一个节点了,所以我们才需要两个节点,而循环的条件就是当指针 del 指向 NULL 的时候,循环停止
在释放完之后,不忘释放食物的空间
代码如下:
- //释放资源
- pSnakeNode cur = ps->pSnake;
- pSnakeNode del = NULL;
-
- while (cur)
- {
- del = cur;
- cur = cur->next;
- free(del);
- }
- SetPos(0, 28);
- free(ps->pFood);
- ps = NULL;
- void GameEnd(pSnake ps)
- {
- SetPos(15, 12);
- switch (ps->status)
- {
- case ESC:
- printf("别啊,怎么就不玩了,不会是太菜了吧(滑稽)");
- break;
- case KILL_BY_SELF:
- printf("即将为您打开您的最爱!!!");
- SetPos(15, 13);
- printf("即将为您打开您的最爱!!!");
- SetPos(15, 14);
- printf("即将为您打开您的最爱!!!");
-
-
- for (int i = 3; i >=0; i--)
- {
- SetPos(28, 15);
- printf("%d", i);
- Sleep(1000);
- }
-
- system("start https://ys.mihoyo.com/");
- break;
- case KILL_BY_WALL:
- printf("即将为您打开您的最爱!!!");
- SetPos(15, 13);
- printf("即将为您打开您的最爱!!!");
- SetPos(15, 14);
- printf("即将为您打开您的最爱!!!");
-
- for (int i = 3; i > 0; i--)
- {
- SetPos(28, 15);
- printf("%d", i);
- Sleep(1000);
- }
- system("start https://ys.mihoyo.com/");
- break;
- }
-
- //释放资源
- pSnakeNode cur = ps->pSnake;
- pSnakeNode del = NULL;
-
- while (cur)
- {
- del = cur;
- cur = cur->next;
- free(del);
- }
- SetPos(0, 28);
- free(ps->pFood);
- ps = NULL;
- }
首先我们需要引头文件
- #include <mmsystem.h>//导入声音头文件
- #pragma comment(lib,"Winmm.lib")
接着我们需要一个wav类型的音频,将其放在 debug 文件下
最后在main函数内部,我们插入下方代码:
- PlaySound(TEXT("zaoan.wav"), NULL, SND_FILENAME | SND_ASYNC | SND_LOOP);
- //zaoan要替换成音频名字
- PlaySound(TEXT("音频名字.wav"), NULL, SND_FILENAME | SND_ASYNC | SND_LOOP);
到这里,我们的贪吃蛇就完结,撒花啦!
各位如果要看总代码的话,可以点开下方我的 gitee
https://gitee.com/qingchen_zhaomu/daily-code-collection/tree/master/test_2024_1_26/test_2024_1_26
如果各位喜欢的话,希望可以多多支持!!!
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。