当前位置:   article > 正文

C语言——贪吃蛇(详解)_c语言贪吃蛇

c语言贪吃蛇

目录

功能介绍

作者写的游戏提取

Win32 API 相关知识介绍

控制台程序

COORD

GetStdHandle

GetConsoleCursorInfo

​编辑

SetConsoleCursorInfo

SetConsoleCursorPosition

GetAsyncKeyState

贪吃蛇准备阶段(GameStart)

定位函数包装(SetPos)

贪吃蛇欢迎信息界面(WelComeToGame)

本地化函数 (setlocale)

贪吃蛇地图绘制(CreatMap)

链表定义蛇身

结构体维护贪吃蛇游戏

游戏界面提示信息打印(PrintHelpInfo)

初始化蛇(InitSnake)

malloc申请蛇节点

打印蛇身与其他信息的初始化

初始化食物(CreateFood)

生成食物坐标

创建并打印食物

GameStart 函数代码

贪吃蛇游玩阶段(GameRun)

按键判断

上下左右

Esc

空格键

加速减速

蛇走一步(SnakeMove)

下一个节点的创立 & 方向判断

判断下一个节点是不是食物(NextIsFood)

是食物就吃掉食物(EatFood)

不是食物就走一步(NotEatFood)

撞到墙结束游戏(KillByWall)

撞到自己结束游戏(KillBySelf)

整合 SnakeMove 函数

贪吃蛇收尾阶段(GameEnd)

判断状态 & 打开原神官网

释放资源

代码总和

背景音乐设置

结语&总代码


贪吃蛇,一个相当经典的小游戏,相信各位或多或少都玩过或是听过。而如果要实现这个小游戏的话,我们就需要熟悉 结构体、指针 以及 链表 的相关知识

功能介绍

  1. 背景音乐播放
  2. 贪吃蛇地图的打印
  3. 吃食物边长
  4. 贪吃蛇的移动
  5. 计算得分
  6. 撞墙与撞自身结束游戏并打开原神相关网站
  7. 贪吃蛇的加速减速
  8. 暂停游戏

作者写的游戏提取

https://pan.baidu.com/s/1br9QsxLJpWF8Rgq-xE2JRg?pwd=1111

提取码: 1111

游戏已经放在上面了,各位可以在电脑上玩玩看

Win32 API 相关知识介绍

Win32 API 中有许多函数,我们今天将会学习里面的几种函数以帮助我们实现贪吃蛇小游戏

控制台程序

首先,我们需要设置控制台的大小与名字,我们平常运行程序时出现的框框就是控制台,如下

这里我们需要用到  system  函数,引头文件    # include <stdlib.h>

system("mode con cols=100 lines=30");

通过如上代码,我们就可以将控制台设置成一个 100 * 30 的矩形

同时我们还可以将控制台的名字改成游戏的名字

system("title 贪吃蛇");

单独使用如上代码时,我们会发现控制台并没有依照我们的预期将名字改成贪吃蛇

这是因为程序运行得太快了,当程序结束的时候,名字也就自动恢复了

对此,我们可以使用  system("pause");  将程序暂停一下以观察效果,如下

 

COORD

COORDWin32 API中定义的一个结构体,使用时需要引头文件 #include <windows.h>,如下

  1. typedef struct _COORD {
  2. SHORT X;
  3. SHORT Y;
  4. } COORD, * PCOORD;

当我们需要在我们想要的位置输入内容时,比如想在控制台中间输入“ 欢迎加入贪吃蛇 ”,我们就需要用到COORD(因为控制台总是在最左上角的位置开始打印)

 COORD pos = { 1, 1 };

通过如上代码,我们就可以对坐标进行赋值

GetStdHandle

这是一个非常重要的函数,其作用是获得特定设备句柄(标识不同设备的数值)

看到这里可能会有人不知道 句柄 是什么,那我就简单解释一下(以下内容单纯是为了理解):

我们提桶时需要一个把手,这样才能更好地将这个桶提起来,而这个把手,就是桶的句柄; 炒菜时用,锅的把手就是锅的句柄

而我们程序运行时,有了程序的句柄,我们才能更好地进行各种操作,而程序的句柄就是一个数值,每个数值都代表一个特定的设备,GetStdHandle 函数的结构如下

HANDLE GetStdHandle(DWORD nStdHandle);

可以看到,该函数会返回一个 HANDLE 类型的数据,我们在使用时就只需要创建一个 HANDLE 类型的数据,并且使用该数据接收 GetStdHandle 函数的返回值就可以了

而该函数所需的参数是什么?如下:

该函数需要的参数就上面三种,我们实现贪吃蛇游戏所需的就是上面的STD_OUTPUT_HANDLE

使用实例如下:

HANDLE  handle = GetStdHandle(STD_OUTPUT_HANDLE);

依照如上代码,我们就能获得设备的句柄

GetConsoleCursorInfo

为了隐藏光标,所以我们学习这个函数,因为游戏运行时总有光标在闪就不大美观

看该函数的名称就知道,这个函数是获取控制台光标信息的,而其语法结构如下:

  1. BOOL WINAPI GetConsoleCursorInfo(
  2. _In_  HANDLE               hConsoleOutput,
  3. _Out_ PCONSOLE_CURSOR_INFO lpConsoleCursorInfo
  4. );

如上,该函数的第一个参数是设备的句柄,我们通过 GetStdHandle 函数可以获取

第二个参数是一个指针,一个指向 CONSOLE_CURSOR_INFO 结构的指针,该结构体语法结构如下:

  1. typedef struct _CONSOLE_CURSOR_INFO {
  2. DWORD dwSize;
  3. BOOL  bVisible;
  4. } CONSOLE_CURSOR_INFO, *PCONSOLE_CURSOR_INFO;

第一个成员是表示光标占比的,1% ~ 100%。我们看到的大多数光标的占比是25%,而如果我们将其设置为100%,效果将会是这样的(此处会预先使用到下面会讲的 SetConsoleCursorInfo):

第二个成员是表示光标可见性的,也就是光标能否被看见就由第二个成员决定,如果我们不想让光标显示出来的话,我们只需要将结构体成员 bVisible 置为  false 就可以了,但由于 false 编辑器不认识,所以我们需要引头文件 #include <stdbool.h>,如下:

  1. #include <stdbool.h>
  2. cursorinfo.bVisible = false;

SetConsoleCursorInfo

这个函数就好比,有个人请求你帮他修理一个东西,你拿到了这个东西,修理完之后,你得还给人家让人家检查检查是不是真的修好了

而我们前面的知识都是在讲怎么拿到东西以及怎么修理这个东西的,现在要讲的就是怎么将这个东西还回去并拿到相应的报酬

SetConsoleCursorInfo 函数的语法和 GetConsoleCursorInfo 函数是差不多的,如下:

  1. BOOL WINAPI SetConsoleCursorInfo(
  2. _In_ HANDLE hConsoleOutput,
  3. _In_ const CONSOLE_CURSOR_INFO *lpConsoleCursorInfo
  4. );

我们可以看到,这里面的两个参数分别是句柄和一个指针(结构体),和 GetConsoleCursorInfo 函数是一样的

综上,隐藏光标的代码如下:

  1. CONSOLE_CURSOR_INFO cursorinfo;
  2. //定义出CONSOLE_CURSOR_INFO类型的结构体,名字为cursorinfo
  3. GetConsoleCursorInfo(handle, &cursorinfo);
  4. //获取光标信息
  5. cursorinfo.bVisible = false;
  6. //将结构体内的光标信息更改为为不可见
  7. SetConsoleCursorInfo(handle, &cursorinfo);
  8. //设置指定设备光标的可见性

SetConsoleCursorPosition

这个函数跟我们上面看到的 SetConsoleCursorInfo 函数是非常相似的

我们先来看一看该函数的语法:

  1. BOOL WINAPI SetConsoleCursorPosition(
  2. HANDLE hConsoleOutput,
  3. COORD pos
  4. );

我们可以看到,这个函数的的第一个参数是句柄

第二个参数是坐标信息,也就是 COORD 类型结构体内的成员

所以设置光标位置的代码如下:

  1. HANDLE handle = GetStdHandle(STD_OUTPUT_HANDLE);
  2. //获得句柄
  3. COORD pos = { 46, 15 };
  4. //定义坐标信息
  5. SetConsoleCursorPosition(handle, pos);
  6. //设置光标位置

我们可以加一个 scanf 让程序停下来看看效果:

GetAsyncKeyState

如果我们要让蛇 上下左右 移动的话,那么就必须要使用到我们的键盘,但是我们编写程序又该如何知道哪个按键是否被按过呢?

这时我们就可以使用 GetAsyncKeyState 函数,其语法结构如下:

  1. SHORT GetAsyncKeyState(
  2. int vKey
  3. );

 这个函数需要你传一个虚拟键值进去,然后该函数会检测,传进去的虚拟键值所代表的按键是否被按过

返回一个 short 类型的数据

如果返回的这个数据的二进制位的最高位为1,则代表该键正在被按着

如果返回的这个数据的二进制位的最高位为0,则代表该键现在没有被按着

如果返回的这个数据的二进制位的最低位为1,则代表该键被按过

如果返回的这个数据的二进制位的最低位为0,则代表该键没被按过

我们今天就实现一个简单点的,只要按键被按过,我们就加速/减速

而要判断的话,我们可以通过按位与0X1来判断,如下是按位与的知识点

  1. 5 & 3
  2. 5 1 0 1
  3. 3 0 1 1
  4. 结果 0 0 1
  5. 所以 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

贪吃蛇准备阶段(GameStart)

定位函数包装(SetPos)

前面我们说了COORD相关的知识,但是每一次光标定位我们都需要获得句柄、坐标更改、设置坐标三步,这样代码会显得冗杂

我们不妨建立一个函数SetPos,将如上步骤都包含在内,我们只需要将坐标传进去就可以了,不需要返回值,如下:

  1. void SetPos(int x, int y)
  2. {
  3. //获得设备句柄
  4. HANDLE handle = GetStdHandle(STD_OUTPUT_HANDLE);
  5. //根据句柄设置光标位置
  6. COORD pos = { x,y };
  7. SetConsoleCursorPosition(handle, pos);
  8. }

贪吃蛇欢迎信息界面(WelComeToGame)

在游戏开始之前,我们可以先打印一些欢迎信息并告知一些规则

而如果要实现这个效果的话,我们需要 system("pause") 与刚创建好的 SetPos 函数,每切换一个界面,我们就用 system(“ cls ”)清屏就可以了

代码如下:

  1. //打印欢迎界面
  2. void WelcomeToGame()
  3. {
  4. SetPos(35, 12);
  5. printf("欢迎来到贪吃蛇小游戏\n");
  6. SetPos(38, 20);
  7. system("pause");
  8. system("cls");
  9. SetPos(28, 10);
  10. printf("用↑↓←→来控制蛇的移动,A键是加速,D键是减速");
  11. SetPos(28, 11);
  12. printf("加速能得到更高的分数");
  13. SetPos(28, 12);
  14. printf("按空格键可以暂停,按Esc键可以退出游戏");
  15. SetPos(38, 20);
  16. system("pause");
  17. system("cls");
  18. }

我们每打印一句话之后,就可以考虑再次换位,然后打印下一句话,代码效果如下:

本地化函数 (setlocale)

C语言最初是英文的,但是全世界的人们都要用的话,仅仅是英文就不够用了,不说法国、意大利之类的国家有很多其他符号,就我们中国,光汉字都有10万多个,一个字节大小最多也就256,根本无法涵盖

所以,我们在创建项目之前,我们需要先让编辑器适配本地的信息,就比如我们接下来打印墙体、食物、蛇身所需要的宽字符就需要本地化

而本地化我们仅需要引头文件 #include <locale.h>,然后我们来看看这个函数的语法

char* setlocale (int category, const char* locale);

如上我们可以看到,该函数的第一个参数需要的是上面5个中的一个

这里面有改变时间的,有改变金钱单位的。而我们现在需要的,是全部都改变,所以我们就选择第一个 LC_ALL(全部都改变)

第二个参数如下

我们会看到参数有两种,“C” C语言默认环境,而 “ ” 则是适配本地环境

综上,我们本地化的代码如下:

  1. #include<locale.h>
  2. //引头文件
  3. setlocale(LC_ALL, "");
  4. //本地化

贪吃蛇地图绘制(CreatMap)

在绘制地图之前,我们需要先知道的是,编辑器的横坐标的长度是纵坐标的两倍

而如果我们要打印墙体且不想让墙体看起来很扁的话,我们就需要用到宽字符,如下:

  1. printf("ab\n");
  2. printf("中\n");
  3. wprintf(L"%lc\n", L'□');

我们会发现,宽字符□的大小是单一一个字母的两倍,而我们如果要打印墙体或者蛇身的话,我们需要用到宽字符,不然会显得蛇很扁,看起来很别扭

接下来我们就来打印墙体

我们的思路是:先用SetPos函数找到对应的位置,然后用for循环来循环打印宽字符作为墙体

但是考虑到每一次打印墙体都需要写 L'对应符号',所以我们可以定义一个宏,这样即使我们以后想让墙换一个符号的话也方便,如下:

#define WALL L'□'

我们就打印一个 58*27 的地图吧,墙体打印代码如下:

  1. //绘制地图
  2. void CreatMap()
  3. {
  4. int i = 0;
  5. //上
  6. SetPos(0, 0);
  7. for (i = 0; i <= 56; i+=2)
  8. {
  9. wprintf(L"%lc", WALL);
  10. }
  11. //下
  12. SetPos(0, 26);
  13. for (i = 0; i <= 56; i += 2)
  14. {
  15. wprintf(L"%lc", WALL);
  16. }
  17. //左
  18. for (i = 1; i <= 25; i++)
  19. {
  20. SetPos(0, i);
  21. wprintf(L"%lc", WALL);
  22. }
  23. //右
  24. for (i = 1; i <= 25; i++)
  25. {
  26. SetPos(56, i);
  27. wprintf(L"%lc", WALL);
  28. }
  29. }

链表定义蛇身

对于蛇身,我们需要在头文件中定义一个链表,如下:

  1. //蛇身结点的定义
  2. typedef struct SnakeNode
  3. {
  4. int x;
  5. int y;
  6. struct SnakeNode* next;
  7. }SnakeNode, * pSnakeNode;

结构体维护贪吃蛇游戏

一个贪吃蛇游戏,需要考虑蛇本身、食物、方向、当前状态等等

我们将这些要素全部放在一个结构体里面,将其 typedef 为 Snake,通过这个结构体我们就能找到全部变量

而其中的状态方向又分为上下左右、正常、撞到自己,撞到墙、主动退出游戏等等

对此,我们可以考虑使用枚举,一方面是因为#define定义的宏要定义多个太麻烦,另一方面是因为使用枚举方便调试,而且这种情况下使用枚举确实会好一些

代码如下:

  1. enum GAME_STATUS
  2. {
  3. OK=1,
  4. ESC,
  5. KILL_BY_WALL,
  6. KILL_BY_SELF
  7. };
  8. enum DIRECTION
  9. {
  10. UP=1,
  11. DOWN,
  12. LEFT,
  13. RIGHT
  14. };
  1. //贪吃蛇
  2. typedef struct Snake
  3. {
  4. pSnakeNode pSnake;//维护整条蛇的指针
  5. pSnakeNode pFood;//指向食物的指针
  6. int score;//当前积累的分数
  7. int FoodWeight;//一个食物的分数
  8. int SleepTime;//蛇休眠的时间,时间越短,蛇的速度越快
  9. enum GAME_STATUS status;//游戏当前的状态
  10. enum DIRECTION dir;//蛇当前走的方向
  11. }Snake,*pSnake;

游戏界面提示信息打印(PrintHelpInfo)

我们进入游戏之后会发现游戏旁边的界面有点空,我们可以打印一些提示信息上去

这个环节无非就是 SetPos 函数定位,接着 printf 打印信息,这里我就直接给代码了:

  1. void PrintHelpInfo()
  2. {
  3. SetPos(60, 12);
  4. printf("1.不能穿墙,不能咬到自己");
  5. SetPos(60, 14);
  6. printf("2.用 ↑.↓.←.→ 来控制蛇的移动");
  7. SetPos(60, 16);
  8. printf("3.A键是加速,D键是减速");
  9. SetPos(60, 18);
  10. printf("4.按空格键可以暂停,按Esc键可以退出游戏");
  11. SetPos(60, 20);
  12. printf("嘉鑫版");
  13. }

初始化蛇(InitSnake)

我们可以先创建一条初始长度为5的蛇,那么我们需要先用 for 循环 malloc 5个节点,然后依次头插,形成一条链表

接下来,我们先假设开局就是如上所示。

malloc申请蛇节点

先将我们的蛇的结构体传过去,然后将头节点置为空,如下:

  1. //Snake.h
  2. typedef struct SnakeNode
  3. {
  4. int x;
  5. int y;
  6. struct SnakeNode* next;
  7. }SnakeNode, * pSnakeNode;
  8. typedef struct Snake
  9. {
  10. pSnakeNode pSnake;//维护整条蛇的指针
  11. pSnakeNode pFood;//指向食物的指针
  12. int score;//当前积累的分数
  13. int FoodWeight;//一个食物的分数
  14. int SleepTime;//蛇休眠的时间,时间越短,蛇的速度越快
  15. enum GAME_STATUS status;//游戏当前的状态
  16. enum DIRECTION dir;//蛇当前走的方向
  17. }Snake,*pSnake;
  18. //Snake.c
  19. void InitSnake(pSnake ps)
  20. {
  21. //创建5个蛇身的结点
  22. ps->pSnake = NULL;
  23. }

接着 for 循环 malloc 5个类型为SnakeNode的节点,而该结构体内的 X 可以先初始化成一个(具体的值 + 2 * i),Y 可以直接初始化成一个具体的值,因为如上图所示,我们初始长度的蛇的 Y 坐标都相同

当然,你也可以将这个值用 #define 定义为一个宏,方便以后修改

代码如下:

  1. #define POS_X 24
  2. #define POS_Y 5
  3. void InitSnake(pSnake ps)
  4. {
  5. //创建5个蛇身的结点
  6. ps->pSnake = NULL;
  7. int i = 0;
  8. pSnakeNode cur = NULL;
  9. for (i = 0; i < 5; i++)
  10. {
  11. cur = (pSnakeNode)malloc(sizeof(SnakeNode));
  12. //申请节点
  13. if (cur == NULL)
  14. {
  15. //判断开辟空间是否成功
  16. perror("malloc fail!");
  17. return;
  18. }
  19. //初始化刚申请的节点的成员
  20. cur->x = POS_X + 2 * i;
  21. cur->y = POS_Y;
  22. cur->next = NULL;
  23. //头插法
  24. if (ps->pSnake == NULL)
  25. {
  26. ps->pSnake = cur;
  27. //判断没有节点的情况
  28. }
  29. else
  30. {
  31. cur->next = ps->pSnake;
  32. ps->pSnake = cur;
  33. }
  34. }
  35. }

这里我再来讲一讲头插代码

如上,cur 指向的是新开辟的节点

我们先让 cur 指向链表的头节点,然后再将 ps->pSnake 定义为新的头

打印蛇身与其他信息的初始化

打印蛇身相对简单,我们的思路就是:先通过 SetPos 函数找到对应节点,然后宽字符打印

考虑到后续代码中还有蛇身要打印,所以这里就将蛇身的符号用 #define 定义起来

  1. #define BODY L'●'
  2. //打印蛇身
  3. cur = ps->pSnake;
  4. //将cur定义为新的头
  5. while (cur)
  6. {
  7. SetPos(cur->x, cur->y);
  8. wprintf(L"%lc", BODY);
  9. cur = cur->next;
  10. //寻找下一个要打印的节点,直到为空
  11. }

而我们其他信息的初始化就相对轻松一些,代码如下:

  1. //贪吃蛇其他信息初始化
  2. ps->dir = RIGHT;
  3. ps->FoodWeight = 10;
  4. ps->pFood = NULL;
  5. ps->score = 0;
  6. ps->SleepTime = 200;
  7. ps->status = OK;

综上,我们先申请了节点,接着打印蛇身,最后将其他信息给初始化了

初始化蛇的总代码如下:

  1. void InitSnake(pSnake ps)
  2. {
  3. //创建5个蛇身的结点
  4. ps->pSnake = NULL;
  5. int i = 0;
  6. pSnakeNode cur = NULL;
  7. for (i = 0; i < 5; i++)
  8. {
  9. cur = (pSnakeNode)malloc(sizeof(SnakeNode));
  10. if (cur == NULL)
  11. {
  12. perror("malloc fail!");
  13. return;
  14. }
  15. cur->x = POS_X + 2 * i;
  16. cur->y = POS_Y;
  17. cur->next = NULL;
  18. //头插法
  19. if (ps->pSnake == NULL)
  20. {
  21. ps->pSnake = cur;
  22. }
  23. else
  24. {
  25. cur->next = ps->pSnake;
  26. ps->pSnake = cur;
  27. }
  28. }
  29. //打印蛇身
  30. cur = ps->pSnake;
  31. while (cur)
  32. {
  33. SetPos(cur->x, cur->y);
  34. wprintf(L"%lc", BODY);
  35. cur = cur->next;
  36. }
  37. //贪吃蛇其他信息初始化
  38. ps->dir = RIGHT;
  39. ps->FoodWeight = 10;
  40. ps->pFood = NULL;
  41. ps->score = 0;
  42. ps->SleepTime = 200;
  43. ps->status = OK;
  44. }

初始化食物(CreateFood)

初始化了蛇,接下来我们就该初始化食物了 

生成食物坐标

贪吃蛇中的食物随机出现在地图中的任意位置(墙和蛇身除外),我们可以用 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同理,如下:

  1. x = rand() % 53 + 2;
  2. y = rand() % 24 + 1;

但是这时发现了一个问题,因为一个字符在 x 轴上的大小是 y 轴的一半,而且无论是墙体,蛇身还是食物,打印的都是宽字符

这也就意味着,我们随机出来的 x 必须是偶数,不然就会出现下面的场景

我们没有办法让 rand 函数每次的随机数都是偶数,但是我们可以设置一个循环。如果这次随机出来的 x 不是一个偶数,那我们就让其再随机生成一次,而判断偶数就只需要%2看等不等于0就行了

代码如下:

  1. do
  2. {
  3. x = rand() % 53 + 2;
  4. y = rand() % 24 + 1;
  5. } while (x % 2 != 0);

接着我们需要再判断一下,随机出来的坐标在不在蛇身上

这时我们只需要用一个while循环,创建一个cur指针指向头节点,每次查看完之后向后走一个节点,当cur指向空时就停下来

每到一个节点就拿随机生成的 x、y 坐标和节点内的 x、y 坐标进行比较,如果有相同的,就再随机生成一次食物坐标

  1. int x = 0, y = 0;
  2. again:
  3. do
  4. {
  5. x = rand() % 53 + 2;
  6. y = rand() % 24 + 1;
  7. } while (x % 2 != 0);
  8. //判断坐标在不在蛇身上,与每个节点作比较
  9. pSnakeNode cur = ps->pSnake;
  10. while (cur)
  11. {
  12. if (x == cur->x && y == cur->y)
  13. {
  14. goto again;
  15. }
  16. cur = cur->next;
  17. }

在这里我们可以使用goto语句,面对循环嵌套之类的情况使用goto语句会方便很多

创建并打印食物

我们可以这么理解,食物就是蛇身的一部分,只不过不跟蛇身连在一起,当玩家吃到食物之后,就直接将食物头插在蛇身上,唯一的区别就是打印的时候,用到宽字符不是一个符号而已

所以同样的,我们也是用 malloc 开辟一块空间,初始化,最后将其打印出来

食物的话我们可以跟蛇身、墙体一样用 #define 定义一个宏

#define FOOD L'★'
  1. //创建食物
  2. //开辟食物的空间
  3. pSnakeNode pFood = (pSnakeNode)malloc(sizeof(SnakeNode));
  4. //判断是否开辟成功
  5. if (pFood == NULL)
  6. {
  7. perror("malloc fail!");
  8. return;
  9. }
  10. //初始化
  11. pFood->x = x;
  12. pFood->y = y;
  13. ps->pFood = pFood;
  14. //打印食物
  15. SetPos(x, y);
  16. wprintf(L"%lc", FOOD);

综上,初始化食物这段代码如下:

  1. void CreateFood(pSnake ps)
  2. {
  3. int x = 0, y = 0;
  4. again:
  5. do
  6. {
  7. x = rand() % 53 + 2;
  8. y = rand() % 24 + 1;
  9. } while (x % 2 != 0);
  10. //判断坐标在不在蛇身上,与每个节点作比较
  11. pSnakeNode cur = ps->pSnake;
  12. while (cur)
  13. {
  14. if (x == cur->x && y == cur->y)
  15. {
  16. goto again;
  17. }
  18. cur = cur->next;
  19. }
  20. //创建食物
  21. pSnakeNode pFood = (pSnakeNode)malloc(sizeof(SnakeNode));
  22. if (pFood == NULL)
  23. {
  24. perror("malloc fail!");
  25. return;
  26. }
  27. pFood->x = x;
  28. pFood->y = y;
  29. ps->pFood = pFood;
  30. SetPos(x, y);
  31. wprintf(L"%lc", FOOD);
  32. }

GameStart 函数代码

综上,游戏开始前我们调整了控制台,隐藏了光标,打印了地图,初始化了蛇和食物以及其他游戏信息,代码如下:

  1. void GameStart(pSnake ps)
  2. {
  3. //控制控制台的信息,窗口大小,窗口名
  4. system("mode con cols=100 lines=30");
  5. system("title 贪吃蛇");
  6. //隐藏光标
  7. HANDLE handle = GetStdHandle(STD_OUTPUT_HANDLE);//获得句柄
  8. CONSOLE_CURSOR_INFO CursorInfo;
  9. GetConsoleCursorInfo(handle, &CursorInfo);//获取控制台光标信息
  10. CursorInfo.bVisible = false;//隐藏光标
  11. SetConsoleCursorInfo(handle, &CursorInfo);
  12. //打印欢迎信息
  13. WelcomeToGame();
  14. //绘制地图
  15. CreatMap();
  16. //打印提示信息
  17. PrintHelpInfo();
  18. //初始化蛇
  19. InitSnake(ps);
  20. //创建食物
  21. CreateFood(ps);
  22. }

贪吃蛇游玩阶段(GameRun)

按键判断

首先,我们需要知道每个键的虚拟键值,如下:

https://learn.microsoft.com/zh-cn/windows/win32/inputdev/virtual-key-codes

游戏运行时,我们需要判断哪个键被按过——加速、减速、上下左右、暂停、退出等

我们可以这么做,上下左右按键判断时,我们只需改变维护整个贪吃蛇结构体里的方向状态就行了

而其他的按键判断我们就设置一个 do...while 循环条件就是判断状态是否为OK,如果不为OK,就退出循环,游戏结束

状态和方向的设定,以及维护整个贪吃蛇的结构体如下:

  1. enum GAME_STATUS
  2. {
  3. OK=1,
  4. ESC,
  5. KILL_BY_WALL,
  6. KILL_BY_SELF
  7. };
  8. enum DIRECTION
  9. {
  10. UP=1,
  11. DOWN,
  12. LEFT,
  13. RIGHT
  14. };
  15. //贪吃蛇
  16. typedef struct Snake
  17. {
  18. pSnakeNode pSnake;//维护整条蛇的指针
  19. pSnakeNode pFood;//指向食物的指针
  20. int score;//当前积累的分数
  21. int FoodWeight;//一个食物的分数
  22. int SleepTime;//蛇休眠的时间,时间越短,蛇的速度越快
  23. enum GAME_STATUS status;//游戏当前的状态
  24. enum DIRECTION dir;//蛇当前走的方向
  25. }Snake,*pSnake;

因为我们一次就只能按一个按键,所以我们可以用 if...else if...else 语句来进行判断

上下左右

如果为上下左右,那我们还需判断一下:

当方向向上/下的时候,不能按下向下/上的按键

当方向向左/右的时候,不能按下向右/左的按键

代码如下:

  1. void GameRun(pSnake ps)
  2. {
  3. do
  4. {
  5. //检测按键
  6. //上、下、左、右
  7. if (KEY_PRESS(VK_UP) && ps->dir != DOWN)
  8. {
  9. ps->dir = UP;
  10. }
  11. else if (KEY_PRESS(VK_DOWN) && ps->dir != UP)
  12. {
  13. ps->dir = DOWN;
  14. }
  15. else if (KEY_PRESS(VK_LEFT) && ps->dir != RIGHT)
  16. {
  17. ps->dir = LEFT;
  18. }
  19. else if (KEY_PRESS(VK_RIGHT) && ps->dir != LEFT)
  20. {
  21. ps->dir = RIGHT;
  22. }
  23. } while (ps->status == OK);
  24. }

可能有人会疑惑,单单改变一个方向的状态,就真的能让蛇的方向改变吗?当然不能

我们改变状态是为了在后续贪吃蛇行动时,我们可以通过这个状态进行 switch...case 操作决定下一个节点是在贪吃蛇头部的哪一个方向

Esc

而我们如果要判断退出(Esc)的话,我们只需要改变 ps->status 就行了,这样子的话游戏进行下来,当再次进入循环条件判断时,状态不为 OK,就会退出循环,游戏自然也会结束

  1. else if (KEY_PRESS(VK_ESCAPE))
  2. {
  3. ps->status = ESC;
  4. break;
  5. }
空格键

而如果按下的是空格键的话,我们可以建立一个函数 pause,函数的内容就是死循环地 Sleep(时间),只有当玩家再次按下空格键或者 Esc 时,才会退出循环

  1. void pause(pSnake ps)
  2. {
  3. while (1)
  4. {
  5. Sleep(1000);
  6. if (KEY_PRESS(VK_SPACE))
  7. {
  8. break;
  9. }
  10. else if (KEY_PRESS(VK_ESCAPE))
  11. {
  12. ps->status = ESC;
  13. break;
  14. }
  15. }
  16. }
  17. else if (KEY_PRESS(VK_SPACE))
  18. {
  19. pause(ps);
  20. }

除了以上的键位之外,我们还有加速和减速功能需要实现

加速减速

这里不太推荐 F1~F0 的键位作为加速或减速的键位,这是因为现在的电脑 F1~F0 键已经不像早期的电脑那么纯粹了,上面除了原有的功能之外,还有了调整亮度、声音、截屏等功能,需要手动按 Fn 键进行切换,如果别人没有注意到的话,很可能会出现按了加速减速却没反应的情况,这是我们需要避免的

我们可以用 A 代表加速,D 代表减速,两个键的虚拟键值分别是 0X41 和 0X44

贪吃蛇游戏的原理就是走一步休眠一下,休眠的时间越短,视觉上看来蛇的速度就越快。所以我们判断到玩家按下了 A 键时,我们就将休眠时间调整得短一点,按下 D 键同理

同时,因为速度快了,我们可以令每一个食物的分值上升,同时设定一个速度的上限

代码如下:

  1. else if (KEY_PRESS(0X41))
  2. {
  3. if (ps->SleepTime >= 80)
  4. {
  5. ps->SleepTime -= 30;
  6. ps->FoodWeight += 2;
  7. }
  8. }
  9. else if (KEY_PRESS(0X44))
  10. {
  11. if (ps->FoodWeight > 2)
  12. {
  13. ps->SleepTime += 30;
  14. ps->FoodWeight -= 2;
  15. }
  16. }

蛇走一步(SnakeMove)

下一个节点的创立 & 方向判断

贪吃蛇走一步的原理是:

将贪吃蛇要走的下一个节点找出来,头插该节点

如若下一个节点是食物,那么就头插食物就够了

如若下一个节点不是食物,那么我们就在头插下一个节点的同时,删除尾节点并打印成空格

所以我们在方向判断之前,我们还需要 malloc 下一个节点,内部的 x、y 通过方向来初始化

  1. pSnakeNode pNext = (pSnakeNode)malloc(sizeof(SnakeNode));
  2. if (pNext == NULL)
  3. {
  4. perror("malloc fail!!");
  5. return;
  6. }
  7. pNext->next = NULL;

在进入该函数之前,我们已经判断过方向了,而我们是用枚举类型定义的方向,所以在这里我们可以用 switch...case 语句判断贪吃蛇要走的下一个节点在哪里

比如方向向左,那下一个节点的坐标,就在相对贪吃蛇头节点 x-2,y 不变的位置

比如向上,那么下一个节点就在坐标,就在相对贪吃蛇头节点 x 不变,y-1 的位置

向右向下同理

代码如下:

  1. switch (ps->dir)
  2. {
  3. case UP:
  4. pNext->x = ps->pSnake->x;
  5. pNext->y = ps->pSnake->y - 1;
  6. break;
  7. case DOWN:
  8. pNext->x = ps->pSnake->x;
  9. pNext->y = ps->pSnake->y + 1;
  10. break;
  11. case LEFT:
  12. pNext->x = ps->pSnake->x - 2;
  13. pNext->y = ps->pSnake->y;
  14. break;
  15. case RIGHT:
  16. pNext->x = ps->pSnake->x + 2;
  17. pNext->y = ps->pSnake->y;
  18. break;
  19. }

判断下一个节点是不是食物(NextIsFood)

这个函数的实现比较简单,我们直接拿 食物的坐标 和 贪吃蛇头节点的下一个节点的坐标 比较一下,如果相等,那么下一个节点就是食物,不相等,那就不是

代码如下:

  1. int NextIsFood(pSnake ps, pSnakeNode pNext)
  2. {
  3. if (ps->pFood->x == pNext->x && ps->pFood->y == pNext->y)
  4. {
  5. return 1;
  6. }
  7. else
  8. {
  9. return 0;
  10. }
  11. }

是食物就吃掉食物(EatFood)

如果下一个节点是食物的话,我们先将下一个节点头插贪吃蛇上,然后打印蛇身

同时,我们之前定义食物的时候还 malloc 了一块空间,我们既然拿下一个节点的空间头插到贪吃蛇上面,那我们就应该将原本食物的节点给销毁(free)

吃掉了食物之后,我们原先在地图上的食物就被覆盖了,那么这时我们就应该再创建一个食物

同时,我们吃掉了一个食物之后,我们的分数也应该变高,就让原先的分数加上一个食物的分数

代码如下:

  1. void EatFood(pSnake ps, pSnakeNode pNext)
  2. {
  3. //头插
  4. pNext->next = ps->pSnake;
  5. ps->pSnake = pNext;
  6. pSnakeNode cur = ps->pSnake;
  7. //打印蛇
  8. while (cur)
  9. {
  10. SetPos(cur->x, cur->y);
  11. wprintf(L"%lc", BODY);
  12. cur = cur->next;
  13. //向后走一个位置
  14. }
  15. ps->score += ps->FoodWeight;
  16. //释放旧的食物
  17. free(ps->pFood);
  18. //新建食物
  19. CreateFood(ps);
  20. }

不是食物就走一步(NotEatFood)

当下一个节点不是食物的时候,我们就先将下一个节点头插到贪吃蛇上面

同时我们需要知道,本来贪吃蛇是已经被打印出来了的,所以我们只需要将新的头节点打印出来同时将尾节点打印成空格,我们就能在视觉上实现贪吃蛇向后走一步的效果

至于找到尾节点,我们可以用一个 while 循环,定义一个 cur 指针,让 cur 指针遍历一遍链表,当cur->next 指向空的时候,循环结束,此时我们的 cur 指针指向的就是尾节点

然后我们 SetPos 到这个位置之后打印两个空格,注意,是两个空格,因为 x 的大小是 y 的两倍

代码如下:

  1. void NotEatFood(pSnake ps, pSnakeNode pNext)
  2. {
  3. //头插
  4. pNext->next = ps->pSnake;
  5. ps->pSnake = pNext;
  6. //释放尾结点
  7. //顺便打印尾节点
  8. pSnakeNode cur = ps->pSnake;
  9. SetPos(cur->x, cur->y);
  10. wprintf(L"%lc", BODY);
  11. //找尾节点
  12. while (cur->next->next)
  13. {
  14. cur = cur->next;
  15. }
  16. //将尾节点置空 打印 ' '
  17. SetPos(cur->next->x, cur->next->y);
  18. printf(" ");//两个空格!!!
  19. free(cur->next);
  20. cur->next = NULL;
  21. }

撞到墙结束游戏(KillByWall)

判断蛇是否撞到墙,我们只需要将贪吃蛇的头节点是否在墙壁所圈定的范围之内,或者是在墙上,如果不在这个范围内,那就证明蛇已经撞到墙了

接着,我们需要将游戏的状态更改为  KILL_BY_WALL

当蛇向后走了一步时,判断到状态不为 OK,就会跳出循环,游戏结束

我们打印的地图的大小是 58*27,但是坐标是从(0,0)开始的,所以只要 x 不在 2~55 这个范围内,y 不在 1~26 这个范围内,那么就说明蛇撞到墙了

代码如下:

  1. void KillByWall(pSnake ps)
  2. {
  3. if (ps->pSnake->x == 0 ||
  4. ps->pSnake->x == 56 ||
  5. ps->pSnake->y == 0 ||
  6. ps->pSnake->y == 26)
  7. {
  8. ps->status = KILL_BY_WALL;
  9. }
  10. }

撞到自己结束游戏(KillBySelf)

我们要判断蛇是否会撞到自己,我们只需要将头节点蛇身的每一个坐标一一比对,当发现有相同的时候,就说明蛇已经咬到自己

但是蛇的前三个节点没有相撞的可能性,如下图

所以我们可以从第四个节点开始判断,至于如何找到第四个节点,我们只需要将创建一个新指针,让这个新指针 = 头指针->next->next->next,这时,这个新指针指向的就是第四个节点

当我们发现头节点的 x、y第四个结点之后的节点的 x、y 相同的时候,我们就可以修改状态为  KILL_BY_SELF

代码如下:

  1. void KillBySelf(pSnake ps)
  2. {
  3. pSnakeNode cur = ps->pSnake->next->next->next;
  4. while (cur)
  5. {
  6. if (cur->x == ps->pSnake->x && cur->y == ps->pSnake->y)
  7. {
  8. ps->status = KILL_BY_SELF;
  9. return;
  10. }
  11. cur = cur->next;
  12. }
  13. }

整合 SnakeMove 函数

我们在令蛇向后走完了一步之后,需要让贪吃蛇睡眠一段时间,而这段时间我们设置在了维护整个贪吃蛇的结构体里,并将其初始化为  200毫秒  ,具体的初始化内容各位可以看回  初始化蛇(InitSnake)  部分

综上,GameRun函数 代码如下:

  1. void GameRun(pSnake ps)
  2. {
  3. do
  4. {
  5. SetPos(62, 9);
  6. printf("总分:%5d\n", ps->score);
  7. SetPos(62, 10);
  8. printf("食物的分值:%02d\n", ps->FoodWeight);
  9. //检测按键
  10. //上、下、左、右、ESC、空格、F3、F4
  11. if (KEY_PRESS(VK_UP) && ps->dir != DOWN)
  12. {
  13. ps->dir = UP;
  14. }
  15. else if (KEY_PRESS(VK_DOWN) && ps->dir != UP)
  16. {
  17. ps->dir = DOWN;
  18. }
  19. else if (KEY_PRESS(VK_LEFT) && ps->dir != RIGHT)
  20. {
  21. ps->dir = LEFT;
  22. }
  23. else if (KEY_PRESS(VK_RIGHT) && ps->dir != LEFT)
  24. {
  25. ps->dir = RIGHT;
  26. }
  27. else if (KEY_PRESS(VK_ESCAPE))
  28. {
  29. ps->status = ESC;
  30. break;
  31. }
  32. else if (KEY_PRESS(VK_SPACE))
  33. {
  34. pause(ps);
  35. }
  36. else if (KEY_PRESS(0X41))
  37. {
  38. if (ps->SleepTime >= 80)
  39. {
  40. ps->SleepTime -= 30;
  41. ps->FoodWeight += 2;
  42. }
  43. }
  44. else if (KEY_PRESS(0X44))
  45. {
  46. if (ps->FoodWeight > 2)
  47. {
  48. ps->SleepTime += 30;
  49. ps->FoodWeight -= 2;
  50. }
  51. }
  52. //走一步
  53. SnakeMove(ps);
  54. //睡眠一下
  55. Sleep(ps->SleepTime);
  56. } while (ps->status == OK);
  57. }

贪吃蛇收尾阶段(GameEnd)

判断状态 & 打开原神官网

由于我们的状态是用枚举类型定义的,所以我们可以用 switch...case 语句来进行分类讨论

当状态为 ESC 时,我们就打印提示信息并 break

当状态为 KILL_BY_WALL 或 KILL_BY_SELF 时,我们就浅浅嘲讽一下,比如打开原神官网

打开网站可以使用  system ( " start + 网站 " );

代码如下:

  1. SetPos(15, 12);
  2. switch (ps->status)
  3. {
  4. case ESC:
  5. printf("别啊,怎么就不玩了,不会是太菜了吧(滑稽)");
  6. break;
  7. case KILL_BY_SELF:
  8. printf("即将为您打开您的最爱!!!");
  9. SetPos(15, 13);
  10. printf("即将为您打开您的最爱!!!");
  11. SetPos(15, 14);
  12. printf("即将为您打开您的最爱!!!");
  13. for (int i = 3; i >=0; i--)
  14. {
  15. SetPos(28, 15);
  16. printf("%d", i);
  17. Sleep(1000);
  18. }
  19. system("start https://ys.mihoyo.com/");
  20. break;
  21. case KILL_BY_WALL:
  22. printf("即将为您打开您的最爱!!!");
  23. SetPos(15, 13);
  24. printf("即将为您打开您的最爱!!!");
  25. SetPos(15, 14);
  26. printf("即将为您打开您的最爱!!!");
  27. for (int i = 3; i > 0; i--)
  28. {
  29. SetPos(28, 15);
  30. printf("%d", i);
  31. Sleep(1000);
  32. }
  33. system("start https://ys.mihoyo.com/");
  34. break;
  35. }

释放资源

游戏结束了之后,我们需要释放蛇的空间的同时,还要释放食物的空间(因为两者都是malloc开辟)

至于删除蛇身,我们可以定义两个指针,一个指向要删除的节点,一个指向下一个节点

这是因为如果我们将该节点的空间释放掉之后,我们就找不到下一个节点了,所以我们才需要两个节点,而循环的条件就是当指针 del 指向 NULL 的时候,循环停止

在释放完之后,不忘释放食物的空间

代码如下:

  1. //释放资源
  2. pSnakeNode cur = ps->pSnake;
  3. pSnakeNode del = NULL;
  4. while (cur)
  5. {
  6. del = cur;
  7. cur = cur->next;
  8. free(del);
  9. }
  10. SetPos(0, 28);
  11. free(ps->pFood);
  12. ps = NULL;

代码总和

  1. void GameEnd(pSnake ps)
  2. {
  3. SetPos(15, 12);
  4. switch (ps->status)
  5. {
  6. case ESC:
  7. printf("别啊,怎么就不玩了,不会是太菜了吧(滑稽)");
  8. break;
  9. case KILL_BY_SELF:
  10. printf("即将为您打开您的最爱!!!");
  11. SetPos(15, 13);
  12. printf("即将为您打开您的最爱!!!");
  13. SetPos(15, 14);
  14. printf("即将为您打开您的最爱!!!");
  15. for (int i = 3; i >=0; i--)
  16. {
  17. SetPos(28, 15);
  18. printf("%d", i);
  19. Sleep(1000);
  20. }
  21. system("start https://ys.mihoyo.com/");
  22. break;
  23. case KILL_BY_WALL:
  24. printf("即将为您打开您的最爱!!!");
  25. SetPos(15, 13);
  26. printf("即将为您打开您的最爱!!!");
  27. SetPos(15, 14);
  28. printf("即将为您打开您的最爱!!!");
  29. for (int i = 3; i > 0; i--)
  30. {
  31. SetPos(28, 15);
  32. printf("%d", i);
  33. Sleep(1000);
  34. }
  35. system("start https://ys.mihoyo.com/");
  36. break;
  37. }
  38. //释放资源
  39. pSnakeNode cur = ps->pSnake;
  40. pSnakeNode del = NULL;
  41. while (cur)
  42. {
  43. del = cur;
  44. cur = cur->next;
  45. free(del);
  46. }
  47. SetPos(0, 28);
  48. free(ps->pFood);
  49. ps = NULL;
  50. }

背景音乐设置

首先我们需要引头文件

  1. #include <mmsystem.h>//导入声音头文件
  2. #pragma comment(lib,"Winmm.lib")

接着我们需要一个wav类型的音频,将其放在  debug 文件下

最后在main函数内部,我们插入下方代码:

  1. PlaySound(TEXT("zaoan.wav"), NULL, SND_FILENAME | SND_ASYNC | SND_LOOP);
  2. //zaoan要替换成音频名字
  3. 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

如果各位喜欢的话,希望可以多多支持!!!

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/笔触狂放9/article/detail/372541
推荐阅读
相关标签
  

闽ICP备14008679号