目录
5.3.1 判断移动过程中是否遇到食物(NextIsFood)
Windows 这个多作业系统除了协调应用程序的执行、分配内存、管理资源之外,它同时也是一个很大的服务中心,调用这个服务中心的各种服务(每一种服务就是一个函数),可以帮应用程序达到开启视窗、描绘图形、使用周边设备等目的,由于这些函数服务的对象是应用程序(Application),所以便称之为 Application Programming Interface,简称 API 函数。WIN32 API 也就是 Microsoft Windows 32 位平台的应用程序编程接口。
其实说人话就是:如果你要基于Windows操作系统来编写一些程序,则Windows会提供各种接口,便于你完成一些功能
mode con cols=100 lines=30
也可以通过命令设置控制台窗口的名字:
title 贪吃蛇
- #include
-
- int main()
- {
- //设置控制台窗口的长度:设置控制台窗口的大小,30行,100列
- system("mode con cols=100 lines=30");
- //设置cmd窗口名称
- system("title 贪吃蛇");
- return 0;
- }
运行效果图
执行完后我们会发现窗口大小调制好了,但窗口名却没有,这是因为程序已经结束了。
解决方法:

注意:使用COORD需要包含头文件
COORD是WindowsAPI中定义的一个结构体,表示一个字符在控制台屏幕幕缓冲区上的坐标,坐标系(0,0)的原点位于缓冲区的顶部左侧单元格。
- typedef struct _COORD {
- SHORT X;
- SHORT Y;
- } COORD, *PCOORD;
给坐标赋值:
COORD pos = { 10, 15 };
使用需要包含头文件
GetStdHandle是一个Windows API函数。它用于从一个特定的标准设备(标准输入、标准输出或标准错误)中取得一个句柄(用来标识不同设备的数值),使用这个句柄可以操作设备。HANDLE GetStdHandle(DWORD nStdHandle);
说人话:要操作特定的控制台程序就要获得它的操作权限,能要识别你在操作谁
检索有关指定控制台屏幕缓冲区的光标大小和可见性的信息
BOOL WINAPI GetConsoleCursorInfo(
HANDLE hConsoleOutput,
PCONSOLE_CURSOR_INFO lpConsoleCursorInfo
);
PCONSOLE_CURSOR_INFO是指向CONSOLE_CURSOR_INFO 结构的指针,该结构接收有关主机游标(光标)的信息总结:使用GetConsoleCursorlnfo传入的第一个参数是句柄,为何需要句柄?原因是如要隐藏光标首先需要获得当前控制台对应的光标信息。第二个参数是结构体指针
- HANDLE hOutput = NULL;
- //获取标准输出设备的句柄(⽤来标识不同设备的数值)
- hOutput = GetStdHandle(STD_OUTPUT_HANDLE);
- //定义结构体变量
- CONSOLE_CURSOR_INFO CursorInfo;
- GetConsoleCursorInfo(hOutput, &CursorInfo);//获取控制台光标信息
- //调用完这个函数后就能把hOutput对应的光标信息填充到这个结构体变量中去
- typedef struct _CONSOLE_CURSOR_INFO {
- DWORD dwSize;
- BOOL bVisible;
- } CONSOLE_CURSOR_INFO, *PCONSOLE_CURSOR_INFO;
CursorInfo.bVisible = false; //隐藏控制台光标
- BOOL WINAPI SetConsoleCursorInfo(
- HANDLE hConsoleOutput,
- const CONSOLE_CURSOR_INFO *lpConsoleCursorInfo
- );
实例:
- #include
- #include
- #include
- #include
-
- int main()
- {
- HANDLE hOutput = GetStdHandle(STD_OUTPUT_HANDLE);
- //隐藏光标操作
- CONSOLE_CURSOR_INFO CursorInfo;
- GetConsoleCursorInfo(hOutput, &CursorInfo);//获得控制台光标信息
- CursorInfo.bVisible = false;//隐藏控制台光标,false需包含头文件stdbool.h
- SetConsoleCursorInfo(hOutput, &CursorInfo);//设置控制台光标状态
- return 0;
- }
调试:

- BOOL WINAPI SetConsoleCursorPosition(
- HANDLE hConsoleOutput,
- COORD pos
- );
第一个参数传入的是句柄,第二个参数传入的是坐标信息,也就是COORD类型的结构体变量。
实例:
- #include
- #include
-
- int main()
- {
- COORD pos = { 10,5 };
- HANDLE hOutput = NULL;
- //获得标准输出的句柄(用来标识不同设备的数值)
- hOutput = GetStdHandle(STD_OUTPUT_HANDLE);
- //设置标准输出上光标的位置为pos
- SetConsoleCursorPosition(hOutput, pos);
- printf("haha\n");
- return 0;
- }
运行结果:

如果我们不去设置指定光标位置,那么haha就会在这里被输出

由于日后我们可能会多次使用设置指定光标位置,所以我们不妨封装一个设置光标位置的函数Setpos
- //设置光标位置
- void SetPos(short x, short y)
- {
- COORD pos = { x,y };
- HANDLE hOutput = NULL;
- //获得标准输出的句柄(用来标识不同设备的数值)
- hOutput = GetStdHandle(STD_OUTPUT_HANDLE);
- //设置标准输出上光标的位置为pos
- SetConsoleCursorPosition(hOutput, pos);
- }
- SHORT GetAsyncKeyState(
- int vKey
- );
这个函数需要你传一个虚拟键值进去,然后该函数会检测,传进去的虚拟键值所代表的按键是否被按过,函数通过返回值来分辨按键的状态。(返回值类似是short)
如果返回的这个数据的二进制位的最高位为1,则代表按键状态是按下
如果返回的这个数据的二进制位的最高位为0,则代表按键状态是抬起
如果返回的这个数据的二进制位的最低位为1,则代表该键被按过
如果返回的这个数据的二进制位的最低位为0,则代表该键没被按过
如果我们要判断⼀个键是否被按过,可以检测GetAsyncKeyState返回值的最低值是否为1
#define KEY_PRESS(VK) ( (GetAsyncKeyState(VK) & 0x1) ? 1 : 0 )
举个例子

虚拟键值表如下
https://learn.microsoft.com/zh-cn/windows/win32/inputdev/virtual-key-codes
在游戏地图上,我们打印墙体使用宽字符:□,打印蛇使用宽字符●,打印食物使用宽字符★普通的字符是占一个字节的,这类宽字符是占用2个字节。这里再简单的讲一下C语言的国际化特性相关的知识,过去C语言并不适合非英语国家(地区)使用。C语言最初假定字符都是自己的。但是这些假定并不是在世界的任何地方都适用。
C语言字符默认是采用ASCII编码的,ASCII字符集采用的是单字节编码,且只使用了单字节中的低7位,最高位是没有使用的,可表示为0xxxxxxx;可以看到,ASCII字符集共包含128个字符,在英语国家中,128个字符是基本够用的,但是,在其他国家语言中,比如,在法语中,字母上方有注音符号,它就无法用 ASCII 码表示。于是,一些欧洲国家就决定,利用字节中闲置的最高位编入新的符号。比如,法语中的é的编码为130(⼆进制10000010)。这样一来,这些欧洲国家使用的编码体系,可以表示最多256个符号。但是,这里又出现了新的问题。不同的国家有不同的字母,因此,哪怕它们都使用256个符号的编码方式,代表的字母却不一样。比如,130在法语编码中代表了é,在希伯来语编码中却代表了字母Gimel, 在俄语编码中又会代表另一个符号。但是不管怎样,所有这些编码方式中,0--127表示的符号是一样的,不一样的只是128--255的这一段。至于亚洲国家的文字,使用的符号就更多了,汉字就多达10万左右。一个字节只能表示256种符号,肯定是不够的,就必须使用多个字节表达一个符号。比如,简体中文常见的编码方式是 GB2312,使用两个字节表示一个汉字,所以理论上最多可以表 256 x 256 = 65536 个符号。
后来为了使C语言适应国际化,C语言的标准中不断加入了国际化的支持。比如:加入和宽字符的类型wchar_t 和宽字符的输入和输出函数,加入和头文件 ,其中提供了允许程序员针对特定地区(通常是国家或者说某种特定语言的地理区域)调整程序行为的函数。
通过修改地区,程序可以改变它的行为来适应世界的不同区域。但地区的改变可能会影响库的许多部分,其中一部分可能是我们不希望修改的。所以C语言支持针对不同的类项进行修改,下面的一个宏,指定一个类项:
使用setlocale函数需要包含头文件
char* setlocale (int category, const char* locale);
setlocale(LC_ALL, "C");
setlocale(LC_ALL, "");//切换本地环境
扩展:
- #include
-
- int main()
- {
- char* loc;
- loc = setlocale(LC_ALL, NULL);
- printf("默认的本地信息:%s\n", loc);
-
- loc = setlocale(LC_ALL, "");
- printf("设置后的本地信息:%s\n", loc);
- return 0;
- }
运行结果:

那如果想在屏幕上打印宽字符,怎么打印呢?宽字符的字面量必须加上前缀L,否则C语言会把字面量当作窄字符类型处理。前缀L在单引号前面,表示宽字符,宽字符的打印使用wprintf,对应wprintf()的占位符为%lc;在双引号前面,表示宽字符串,对应wprintf()的占位符为%ls
- #include
- #include
-
- int main()
- {
- setlocale(LC_ALL, "");
- wchar_t ch1 = L'中';
- wchar_t ch2 = L'国';
- wchar_t ch3 = L'□';
- wchar_t ch4 = L'☆';
-
- printf("%c%c\n", 'a', 'b');
-
- wprintf(L"%lc\n", ch1);
- wprintf(L"%lc\n", ch2);
- wprintf(L"%lc\n", ch3);
- wprintf(L"%lc\n", ch4);
- return 0;
- }
运行结果:

- typedef struct SnakeNode
- {
- int x;
- int y;
- struct SnakeNode* next;
- }SnakeNode, * pSnakeNode;

- typedef struct Snake //定义贪吃蛇
- {
- pSnakeNode pSnake;//维护整条蛇的指针
- pSnakeNode pFood;//维护⻝物的指针
- enum DIRECTION Dir;//蛇头的⽅向默认是向右
- enum GAME_STATUS Status;//游戏状态
- int Socre;//当前获得分数
- int foodWeight;//默认每个⻝物10分
- int SleepTime;//每⾛⼀步休眠时间(蛇休眠的时间,休眠的时间越短,蛇的速度越快,休眠的时间越长,蛇的速度越慢)
- }Snake, * pSnake;
蛇的方向,可以一一列举,使用枚举
- //方向
- enum DIRECTION
- {
- UP = 1,
- DOWN,
- LEFT,
- RIGHT
- };
游戏状态,可以一一列举,使用枚举
- //游戏状态
- enum GAME_STATUS
- {
- OK,//正常运⾏
- KILL_BY_WALL,//撞墙
- KILL_BY_SELF,//咬到⾃⼰
- ESC//强制退出游戏
- };

首先我们先创建3个文件
snake.h ----> 贪吃蛇游戏中类型的声明,函数的声明
snake.c ----> 函数的实现
test.c ----> 贪吃蛇游戏的测试
游戏主逻辑(test.c)
- #include "snake.h"
-
- void test()
- {
- srand((unsigned int)time(NULL));
-
- int ch = 0;
- do
- {
- Snake ps = { 0 };
- //游戏开始前的初始化
- GameStart(&ps);
- //游戏玩的过程
- GameRun(&ps);
- //游戏结束
- GameEnd(&ps);
- SetPos(16,13);
- printf("再来一局吗?(Y/N):");
- scanf(" %c", &ch);
- } while (ch == 'Y' || ch == 'y');
- }
-
- int main()
- {
- //适配本地中文环境
- setlocale(LC_ALL, "");
- test();
- SetPos(0, 27);
- return 0;
- }
- //游戏开始
- void GameStart(pSnake ps)
- {
- //设置控制台的信息,窗口大小,窗口名
- system("mode con cols=100 lines=30");
- system("title 贪吃蛇");
- //隐藏光标
- HANDLE hOutput = NULL;
- hOutput = GetStdHandle(STD_OUTPUT_HANDLE);
- CONSOLE_CURSOR_INFO CursorInfo;
- GetConsoleCursorInfo(hOutput, &CursorInfo);
- CursorInfo.bVisible = false;
- SetConsoleCursorInfo(hOutput, &CursorInfo);
- //打印欢迎信息
- WelcomeToGame();
- }
首先我们要让窗口大小100行,30列,有人是不是会疑惑为什么不是58行,27列,我们不是之前给的图片就是这样的嘛?
那是因为这只是地图的大小,地图大小外还有提示的信息,就比如下图一样,所以我们给的大小就会比地图大小还要大一些
效果图:


要实现这个效果图,首先肯定要用到COORD和SetConsoleCursorPosition来设置光标位置
- //定位光标位置
- void SetPos(int x, int y)
- {
- COORD pos = { x,y };
- HANDLE hOutput = NULL;
- hOutput = GetStdHandle(STD_OUTPUT_HANDLE);
- //在屏幕上设置指定光标位置
- SetConsoleCursorPosition(hOutput, pos);
- }
接下来就是打印欢迎信息
- //打印欢迎信息
- void WelcomeToGame()
- {
- //欢迎信息
- SetPos(35,10);
- printf("欢迎来带贪吃蛇小游戏\n");
- SetPos(38, 20);
- //暂停
- system("pause");
- //清屏
- system("cls");
- //功能介绍
- SetPos(15, 10);
- printf("用 ↑ . ↓ . ← . → 来控制蛇的移动,F3是加速,F4是减速");
- SetPos(15, 11);
- printf("加速将得到更高的分数。");
- SetPos(38, 20);
- system("pause");
- system("cls");
- }
#define WALL L'□'
打印墙体代码:
- //绘制地图
- void CreateMap()
- {
- SetPos(0, 0);
- int i = 0;
- //一个宽字符占2位,所以i加的是2
- //i到56就行了,因为输出WALL就能把56个57空间给占了
- //上框框
- 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);
- }
- //左框框
- //这里i只要加1的原因是宽字符和正常字符的高度是一样的
- //只有宽字符和正常字符的宽度有差2倍
- for (i = 1; i < 26; i++)
- {
- SetPos(0, i);
- wprintf(L"%lc", WALL);
- }
- //右框框
- for (i = 1; i < 26; i++)
- {
- SetPos(56, i);
- wprintf(L"%lc", WALL);
- }
- }
效果图:

我们可以按照下面这张图来进行初始化蛇身与食物
蛇最开始长度为5节,每节对应链表的一个节点,蛇身的每一个节点都有自己的坐标。
#define BODY L'●'

1.打印蛇身
- //初始化蛇身
- //ps是维护整条蛇的地址
- void InitSnake(pSnake ps)
- {
- pSnakeNode cur = NULL;
- //按照图片上的指示创建5个节点
- int i = 0;
- for (i = 0; i < 5; i++)
- {
- cur = (pSnakeNode)malloc(sizeof(SnakeNode));
- if (cur == NULL)
- {
- perror("InitSnake():malloc()");
- return;
- }
- cur->x = POS_X + 2 * i;
- cur->y = POS_Y;
- cur->next = NULL;
- //创建好后就进行头插
- cur->next = ps->pSnake;
- ps->pSnake = cur;
- }
我把行和列封装成一个宏,这样的好处是方便后续修改
- #define POS_X 24
- #define POS_Y 5
头插法的解析

2.创建好后打印蛇身
- //创建好后就打印蛇的身体
- while (cur)
- {
- SetPos(cur->x, cur->y);
- wprintf(L"%lc", BODY);
- cur = cur->next;
- }
3.初始化贪吃蛇的其他数据
- //初始化贪吃蛇的数据
- ps->SleepTime = 200;
- ps->Socre = 0;
- ps->Status = OK;
- ps->Dir = RIGHT;
- ps->foodWeight = 10;
- ps->pFood = NULL;
4.初始化蛇身总代码
- //初始化蛇身
- void InitSnake(pSnake ps)
- {
- pSnakeNode cur = NULL;
- //按照图片上的指示创建5个节点
- int i = 0;
- for (i = 0; i < 5; i++)
- {
- cur = (pSnakeNode)malloc(sizeof(SnakeNode));
- if (cur == NULL)
- {
- perror("InitSnake():malloc()");
- return;
- }
- cur->x = POS_X + 2 * i;
- cur->y = POS_Y;
- cur->next = NULL;
- //创建好后就进行头插
- cur->next = ps->pSnake;
- ps->pSnake = cur;
- }
- //创建好后就打印蛇的身体
- while (cur)
- {
- SetPos(cur->x, cur->y);
- wprintf(L"%lc", BODY);
- cur = cur->next;
- }
- //初始化贪吃蛇的数据
- ps->SleepTime = 200;
- ps->Socre = 0;
- ps->Status = OK;
- ps->Dir = RIGHT;
- ps->foodWeight = 10;
- ps->pFood = NULL;
- }
由于打印食物宽字符L'★'后续可能会多次用到,所以我们把它封装成一个宏
#define FOOD L'★'
1.创建食物坐标
- int x = 0;
- int y = 0;
- agin:
- //使食物坐标必须要在墙内,并且x的坐标必须要是2的倍数
- do
- {
- x = rand() % 53 + 2;
- y = rand() % 25 + 1;
- } while (x % 2 != 0);
- //判断坐标是否在蛇的身上
- pSnakeNode cur = ps->pSnake;
- while (cur)
- {
- if (x == cur->x && y == cur->y)
- {
- goto agin;
- }
- cur = cur->next;
- }
x = rand() % 53 + 2;
rand() % 53生成范围是0~52,后面加2,所以x的范围则是2~54
y = rand() % 25 + 1;rand() % 25 + 1;生成范围是0~24,后面加1,所以y的范围则是1到25
首先我们已经保证生成的坐标是在墙内
然后我们还要判断x是否是2的倍数,所以我们用了个do-while循环来进行判断
后续我们又用了while循环来判断生成的食物节点是否与蛇身的某个节点重合,如果一样,我们就用goto语句来进行跳转,使坐标重新生成
使用rand(),我们需要包含头文件 为了防止生成的随机数与后续再次执行的程序一样,我用了srand((unsigned int)time(NULL));来修改种子,如果有不懂srand和time的同学可以看我之前写的博客(猜数字游戏),然后使用srand函数需要包含头文件
;使用time需要包含头文件
走到这一步我们生成的x与y坐标就已经符合要求了,接下来就是生成一个节点(pFood),让x与y赋值给pFood中的x与y,最后让维护蛇指针内的pFood指针来指向这个节点
- pSnakeNode pFood = (pSnakeNode)malloc(sizeof(SnakeNode));
- if (pFood == NULL)
- {
- perror("CreateFood()::malloc()");
- return;
- }
- pFood->x = x;
- pFood->y = y;
- SetPos(x, y);
- wprintf(L"%lc", FOOD);
- ps->pFood = pFood;
创建食物总代码
- //创建食物
- void CreateFood(pSnake ps)
- {
- int x = 0;
- int y = 0;
- agin:
- //使食物坐标必须要在墙内,并且x的坐标必须要是2的倍数
- do
- {
- x = rand() % 53 + 2;
- y = rand() % 25 + 1;
- } while (x % 2 != 0);
- //判断坐标是否在蛇的身上
- pSnakeNode cur = ps->pSnake;
- while (cur)
- {
- if (x == cur->x && y == cur->y)
- {
- goto agin;
- }
- cur = cur->next;
- }
- pSnakeNode pFood = (pSnakeNode)malloc(sizeof(SnakeNode));
- if (pFood == NULL)
- {
- perror("CreateFood()::malloc()");
- return;
- }
- pFood->x = x;
- pFood->y = y;
- SetPos(x, y);
- wprintf(L"%lc", FOOD);
- ps->pFood = pFood;
- }
首先我们再次看一遍游戏流程设计

首先我们可以看到程序运行起来后右侧的帮助信息

因为我们设置游戏窗口的时候设置的是100列,30行,所以设置帮助信息我是一开始是把光标定位到62列17行,大家可以按自己的想法来,不一定要和我一样
- //打印帮助信息
- void PrintHelpInfo()
- {
- SetPos(62, 17);
- printf("1.不能穿墙,不能咬到自己");
- SetPos(62, 18);
- printf("2.用↑.↓.←.→分别控制蛇的移动。");
- SetPos(62, 19);
- printf("3.F3是加速,F4是减速");
- SetPos(62, 20);
- printf("ESC:退出游戏 SPACE:暂停游戏");
- }
首先,我们需要知道每个键的虚拟键值,如下:
https://learn.microsoft.com/zh-cn/windows/win32/inputdev/virtual-key-codes
然后我整理出了本程序需要用到的虚拟键值

然后我们就要判断我们是否有按过这些键,就可以用到前面GetAsyncKeyState中定义的宏
#define KEY_PRESS(VK) ( (GetAsyncKeyState(VK) & 0x1) ? 1 : 0 )
游戏玩的过程总代码
- //游戏玩的过程
- void GameRun(pSnake ps)
- {
- //打印帮助信息
- PrintHelpInfo();
- do
- {
- //打印当前得分
- SetPos(62, 10);
- printf("得分:%d", ps->Socre);
- SetPos(62, 11);
- printf("每个食物得分:%-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_SPACE))
- {
- //进行休眠
- pause();
- }
- else if (KEY_PRESS(VK_F3))
- {
- //加速
- if (ps->SleepTime >= 50)
- {
- ps->SleepTime -= 30;
- ps->foodWeight += 2;
- }
- }
- else if (KEY_PRESS(VK_F4))
- {
- //减速
- if (ps->foodWeight > 2)
- {
- ps->SleepTime += 30;
- ps->foodWeight -= 2;
- }
- }
- else if (KEY_PRESS(VK_ESCAPE))
- {
- //按ESC键退出
- ps->Status = ESC;
- break;
- }
- //蛇每走一步都需要休眠,休眠时间越短,蛇移动速度就越快
- Sleep(ps->SleepTime);
- SnakeMove(ps);
- //判断是否撞墙了
- KillByWall(ps);
- //判断是否撞到自己了
- KillBySelf(ps);
- } while (ps->Status == OK);
- }
当我们按下了上下左右的时候,我们还要与当前蛇行走的方向进行判断
注意事项(打印每个食物得分):
打印每个食物的得分有个小细节就是要用%2d(或者%-2d,%3d,%-4d都可以),就是不要用%d来打印,因为一开始默认每个食物的得分是10分,后续我们是可以用F3和F4来控制蛇移动的速度,速度决定每吃一个食物所得分数。如果我们速度慢下来了,食物所得分数就会变少,由原来的10分变成8分或者更低,但因为一开始我们默认打印了10分,然后减了一次速度按理来说是要打印8分,但用%d打印的结果是80,原因是10后面的0没有被覆盖掉
当然我们在蛇身移动前我们可以让蛇先休眠一下
蛇身移动的主要思想:
移动过程:
向上和向下

- 向上走:首先创建的新节点是在蛇头上面,由图可以看出新节点y坐标就是蛇头坐标y减1,而新节点x坐标则是和蛇头x坐标一样
- 向下走:首先创建的新节点是在蛇头下面,由图可以看出新节点y坐标就是蛇头坐标y加1,而新节点x坐标则是和蛇头x坐标一样
- 向左走:首先创建的新节点是在蛇头左边,由图可以看出新节点x坐标是蛇头节点x坐标减2(减2的原因是宽字符打印宽度占2位),新节点y坐标则是和蛇头节点y坐标一样
- 向右走:首先创建的新节点是在蛇头右边,由图可以看出新节点x坐标是蛇头节点x坐标加2(加2的原因是宽字符打印宽度占2位),新节点y坐标则是和蛇头节点y坐标一样
- //蛇移动过程
- void SnakeMove(pSnake ps)
- {
- pSnakeNode pNextNode = (pSnakeNode)malloc(sizeof(SnakeNode));
- if (pNextNode == NULL)
- {
- perror("SnakeMove()::malloc()");
- return;
- }
- switch (ps->Dir)
- {
- case UP:
- pNextNode->x = ps->pSnake->x;
- pNextNode->y = ps->pSnake->y - 1;
- break;
- case DOWN:
- pNextNode->x = ps->pSnake->x;
- pNextNode->y = ps->pSnake->y + 1;
- break;
- case LEFT:
- pNextNode->x = ps->pSnake->x - 2;
- pNextNode->y = ps->pSnake->y;
- break;
- case RIGHT:
- pNextNode->x = ps->pSnake->x + 2;
- pNextNode->y = ps->pSnake->y;
- break;
- }
- //移动的过程中,判断是否遇到食物
- if (NextIsFood(pNextNode, ps))
- {
- //遇到食物
- EatFood(pNextNode, ps);
- }
- else
- {
- //没遇到食物
- NotEatFood(pNextNode, ps);
- }
- }
当然在蛇移动的过程中,可能移动的下一个节点就是食物,所以我们还要判断是否遇到了食物


-
- //判断移动过程中是否碰到食物
- int NextIsFood(pSnakeNode pNextNode, pSnake ps)
- {
- return (pNextNode->x == ps->pFood->x && pNextNode->y == ps->pFood->y);
- }
相等返回1,不相等返回0
如果返回值是1,则我们遇到了食物,那么我们就把新节点头插到贪吃蛇身上,然后打印蛇身
同时,我们之前定义食物的时候还动态malloc了一块空间,我们既然用新节点的空间头插到贪吃蛇上面,那我们就应该将原本食物的节点给销毁(free)
吃掉了食之后,我们原先在地图上的食物就被覆盖了,那么此时我们就应该再创建一个食物
同时,我们吃掉了一个食物之后,我们的分数也应该变高,就让原先的分数加上一个食物的分数
- //下一步要走的位置处就是食物,就吃掉食物
- void EatFood(pSnakeNode pNextNode, pSnake ps)
- {
- pNextNode->next = ps->pSnake;
- ps->pSnake = pNextNode;
- pSnakeNode cur = ps->pSnake;
- ps->Socre += ps->foodWeight;
- //打印蛇身
- while (cur)
- {
- SetPos(cur->x, cur->y);
- wprintf(L"%lc", BODY);
- cur = cur->next;
- }
- free(ps->pFood);
- CreateFood(ps);
- }
首先我不知道看到这里的小伙伴会不会有这个疑问:
我们知道食物和新节点是不同的地址(两次malloc开辟出来的),但是他们两个空间重合了(食物和下一个节点在地图上存放空间重合),所以会不会不理解释放食物为什么不会把下一个节点空间也释放了?
原因:
创建出来的下一个节点和食物这个节点只是x和y坐标一样,节点是完全不同的两个节点,所以释放食物对创建出来的下一个节点没有影响
当下一个节点不是食物的时候,我们就先将下一个节点头插到贪吃蛇上面
同时我们需要知道,本来贪吃蛇是已经被打印出来了的(初始化蛇身的时候),所以我们只需要将新的头节点打印出来,同时将尾节点打印成空格并释放,我们就能在视觉上达到贪吃蛇走一步的效果
至于找到尾结点对我们来说可是很轻松的,用while循环就能办到
注意:SetPos到尾结点位置之后要打印两个空格,注意,是两个空格,因为尾结点宽字符占2位
- //pSnakeNode pNextNode 是下⼀个节点的地址
- //pSnake ps 维护蛇的指针
- //下一步要走的位置处不是食物,就不吃食物
- void NotEatFood(pSnakeNode pNextNode, pSnake ps)
- {
- pNextNode->next = ps->pSnake;
- ps->pSnake = pNextNode;
- 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(" ");
- free(cur->next);
- cur->next = NULL;
- }

我不知道有没有小伙伴没看我前面介绍有这一个疑问,反正当时我是有这个疑问的:
原因是:这里就是不进行打印的,使用的是上一次打印留下的图形,走到cur->next->next是为了释放下一个节点,所以这个函数没有进行清屏,而是用printf(" ");来把最后一个图形覆盖掉
判断蛇是否撞到墙,我们只需要判断贪吃蛇的头节点是否在墙壁所圈定的范围之内,如果不在这个范围内,那就证明蛇已经撞到墙了
接着,我们需要将游戏的状态更改为 KILL_BY_WALL
当蛇向后走了一步时,判断到状态不为 OK,就会跳出循环,游戏结束

- //判断是否撞墙了
- 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;
- }
我们要判断蛇是否会撞到自己,我们只需要将头节点和蛇身的每一个坐标一一比对,当发现有相同的时候,就说明蛇已经咬到自己了
接着,我们需要将游戏的状态更改为 KILL_BY_SELF
当蛇向后走了一步时,判断到状态不为 OK,就会跳出循环,游戏结束
- //检测是否撞自己
- 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;
- return;
- }
- cur = cur->next;
- }
- }
游戏状态不再是OK(游戏继续)的时候,要告知游戏结束的原因,并且释放蛇身节点与食物节点。
至于删除蛇身,我们可以定义两个指针,一个指向要删除的节点,一个指向下一个节点
这是因为如果我们将该节点的空间释放掉之后,我们就找不到下一个节点了,所以我们才需要两个节点,而循环的条件就是当指针cur指向 NULL 的时候,循环停止
在释放完之后,不忘释放食物的空间
- /游戏结束
- void GameEnd(pSnake ps)
- {
- SetPos(18, 10);
- switch (ps->Status)
- {
- case KILL_BY_WALL:
- printf("很遗憾撞墙了,游戏结束");
- break;
- case KILL_BY_SELF:
- printf("很遗憾撞到自身了,游戏结束");
- break;
- case ESC:
- printf("按了ESC键,正常退出");
- break;
- }
- //释放蛇身节点和食物节点
- pSnakeNode cur = ps->pSnake;
- pSnakeNode pNextNode = NULL;
- while (cur)
- {
- pNextNode = cur;
- cur = cur->next;
- free(pNextNode);
- }
- free(ps->pFood);
- ps = NULL;
- }
test.c
- #define _CRT_SECURE_NO_WARNINGS 1
- #include "snake.h"
-
- void test()
- {
- srand((unsigned int)time(NULL));
-
- int ch = 0;
- do
- {
- Snake ps = { 0 };
- //游戏开始前的初始化
- GameStart(&ps);
- //游戏玩的过程
- GameRun(&ps);
- //游戏结束
- GameEnd(&ps);
- SetPos(16,13);
- printf("再来一局吗?(Y/N):");
- scanf(" %c", &ch);
- } while (ch == 'Y' || ch == 'y');
- }
-
- int main()
- {
- //适配本地中文环境
- setlocale(LC_ALL, "");
- test();
- SetPos(0, 27);
- return 0;
- }
snake.h
- #pragma once
- #include
- #include
- #include
- #include
- #include
//随机数 - #include
-
- #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 )
-
- //定义蛇身节点
- typedef struct SnakeNode
- {
- int x;
- int y;
- struct SnakeNode* next;
- }SnakeNode, *pSnakeNode;
-
- //定义贪吃蛇
- typedef struct Snake
- {
- pSnakeNode pSnake;//维护整条蛇的指针
- pSnakeNode pFood;//维护食物的指针
- enum DIRECTION Dir;//蛇头的方向默认是向右
- enum GAME_STATUS Status;//游戏状态
- int Socre;//当前获得分数
- int foodWeight;//默认每个食物10分
- int SleepTime;//每走一步休眠时间
- }Snake, * pSnake;
-
- //定义蛇的方向
- enum DIRECTION
- {
- UP = 1,
- DOWN,
- LEFT,
- RIGHT
- };
-
- //游戏状态
- enum GAME_STATUS
- {
- OK,//正常运行
- KILL_BY_WALL,//撞墙
- KILL_BY_SELF,//咬到自己
- ESC, //按了ESC键退出,正常退出
- };
-
- //游戏开始前的准备
- void GameStart(pSnake ps);
- //打印欢迎信息
- void WelcomeToGame();
- //定位控制台光标位置
- void SetPos(int x, int y);
- //绘制地图
- void CreateMap();
- //初始化蛇身
- void InitSnake(pSnake ps);
- //创建食物
- void CreateFood(pSnake ps);
-
-
- //游戏运行的整个逻辑
- void GameRun(pSnake ps);
- //打印帮助信息
- void PrintHelpInfo();
- //蛇移动过程
- void SnakeMove(pSnake ps);
- //判断蛇头的下一步要走的位置处是否是食物
- int NextIsFood(pSnakeNode pNextNode, pSnake ps);
- //下一步要走的位置处就是食物,就吃掉食物
- void EatFood(pSnakeNode pNextNode, pSnake ps);
- //下一步要走的位置处不是食物,不吃食物
- void NotEatFood(pSnakeNode pNextNode, pSnake ps);
- //检测是否撞墙
- void KillByWall(pSnake ps);
- //检测是否撞自己
- void KillBySelf(pSnake ps);
-
-
- //游戏结束,处理善后工作
- void GameEnd(pSnake ps);
snake.c
- #define _CRT_SECURE_NO_WARNINGS 1
- #include "snake.h"
-
- //定位光标位置
- void SetPos(int x, int y)
- {
- COORD pos = { x,y };
- HANDLE hOutput = NULL;
- hOutput = GetStdHandle(STD_OUTPUT_HANDLE);
- //在屏幕上设置指定光标位置
- SetConsoleCursorPosition(hOutput, pos);
- }
-
- //打印欢迎信息
- void WelcomeToGame()
- {
- //欢迎信息
- SetPos(35,10);
- printf("欢迎来带贪吃蛇小游戏\n");
- SetPos(38, 20);
- //暂停
- system("pause");
- //清屏
- system("cls");
- //功能介绍
- SetPos(15, 10);
- printf("用 ↑ . ↓ . ← . → 来控制蛇的移动,F3是加速,F4是减速");
- SetPos(15, 11);
- printf("加速将得到更高的分数。");
- SetPos(38, 20);
- system("pause");
- system("cls");
- }
-
- //绘制地图
- void CreateMap()
- {
- SetPos(0, 0);
- int i = 0;
- //一个宽字符占2位,所以i加的是2
- //i到56就行了,因为输出WALL就能把56个57空间给占了
- //上框框
- 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);
- }
- //左框框
- //这里i只要加1的原因是宽字符和正常字符的高度是一样的
- //只有宽字符和正常字符的宽度有差2倍
- for (i = 1; i < 26; i++)
- {
- SetPos(0, i);
- wprintf(L"%lc", WALL);
- }
- //右框框
- for (i = 1; i < 26; i++)
- {
- SetPos(56, i);
- wprintf(L"%lc", WALL);
- }
-
- }
-
- //初始化蛇身
- void InitSnake(pSnake ps)
- {
- pSnakeNode cur = NULL;
- //按照图片上的指示创建5个节点
- int i = 0;
- for (i = 0; i < 5; i++)
- {
- cur = (pSnakeNode)malloc(sizeof(SnakeNode));
- if (cur == NULL)
- {
- perror("InitSnake():malloc()");
- return;
- }
- cur->x = POS_X + 2 * i;
- cur->y = POS_Y;
- cur->next = NULL;
- //创建好后就进行头插
- cur->next = ps->pSnake;
- ps->pSnake = cur;
- }
- //创建好后就打印蛇的身体
- while (cur)
- {
- SetPos(cur->x, cur->y);
- wprintf(L"%lc", BODY);
- cur = cur->next;
- }
- //初始化贪吃蛇的数据
- ps->SleepTime = 200;
- ps->Socre = 0;
- ps->Status = OK;
- ps->Dir = RIGHT;
- ps->foodWeight = 10;
- ps->pFood = NULL;
- }
-
- //创建食物
- void CreateFood(pSnake ps)
- {
- int x = 0;
- int y = 0;
- agin:
- //使食物坐标必须要在墙内,并且x的坐标必须要是2的倍数
- do
- {
- x = rand() % 53 + 2;
- y = rand() % 25 + 1;
- } while (x % 2 != 0);
- //判断坐标是否在蛇的身上
- pSnakeNode cur = ps->pSnake;
- while (cur)
- {
- if (x == cur->x && y == cur->y)
- {
- goto agin;
- }
- cur = cur->next;
- }
- pSnakeNode pFood = (pSnakeNode)malloc(sizeof(SnakeNode));
- if (pFood == NULL)
- {
- perror("CreateFood()::malloc()");
- return;
- }
- pFood->x = x;
- pFood->y = y;
- SetPos(x, y);
- wprintf(L"%lc", FOOD);
- ps->pFood = pFood;
- }
-
- //游戏开始
- void GameStart(pSnake ps)
- {
- //设置控制台的信息,窗口大小,窗口名
- system("mode con cols=100 lines=30");
- system("title 贪吃蛇");
- //隐藏光标
- HANDLE hOutput = NULL;
- hOutput = GetStdHandle(STD_OUTPUT_HANDLE);
- CONSOLE_CURSOR_INFO CursorInfo;
- GetConsoleCursorInfo(hOutput, &CursorInfo);
- CursorInfo.bVisible = false;
- SetConsoleCursorInfo(hOutput, &CursorInfo);
- //打印欢迎信息
- WelcomeToGame();
- //绘制地图
- CreateMap();
- //初始化蛇身
- InitSnake(ps);
- //创建食物
- CreateFood(ps);
- }
-
- //打印帮助信息
- void PrintHelpInfo()
- {
- SetPos(62, 17);
- printf("1.不能穿墙,不能咬到自己");
- SetPos(62, 18);
- printf("2.用↑.↓.←.→分别控制蛇的移动。");
- SetPos(62, 19);
- printf("3.F3是加速,F4是减速");
- SetPos(62, 20);
- printf("ESC:退出游戏 SPACE:暂停游戏");
- }
-
- //暂停过程
- void pause()
- {
- while (1)
- {
- Sleep(100);
- if (KEY_PRESS(VK_SPACE))
- {
- break;
- }
- }
- }
-
- //判断移动过程中是否碰到食物
- int NextIsFood(pSnakeNode pNextNode, pSnake ps)
- {
- return (pNextNode->x == ps->pFood->x && pNextNode->y == ps->pFood->y);
- }
-
- //下一步要走的位置处就是食物,就吃掉食物
- void EatFood(pSnakeNode pNextNode, pSnake ps)
- {
- pNextNode->next = ps->pSnake;
- ps->pSnake = pNextNode;
- pSnakeNode cur = ps->pSnake;
- ps->Socre += ps->foodWeight;
- //打印蛇身
- while (cur)
- {
- SetPos(cur->x, cur->y);
- wprintf(L"%lc", BODY);
- cur = cur->next;
- }
- free(ps->pFood);
- CreateFood(ps);
- }
-
- //下一步要走的位置处不是食物,不吃食物
- void NotEatFood(pSnakeNode pNextNode, pSnake ps)
- {
- pNextNode->next = ps->pSnake;
- ps->pSnake = pNextNode;
- 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(" ");
- free(cur->next);
- cur->next = NULL;
- }
-
- //判断是否撞墙了
- 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;
- }
-
- //检测是否撞自己
- 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;
- return;
- }
- cur = cur->next;
- }
- }
-
- //蛇移动过程
- void SnakeMove(pSnake ps)
- {
- pSnakeNode pNextNode = (pSnakeNode)malloc(sizeof(SnakeNode));
- if (pNextNode == NULL)
- {
- perror("SnakeMove()::malloc()");
- return;
- }
- switch (ps->Dir)
- {
- case UP:
- pNextNode->x = ps->pSnake->x;
- pNextNode->y = ps->pSnake->y - 1;
- break;
- case DOWN:
- pNextNode->x = ps->pSnake->x;
- pNextNode->y = ps->pSnake->y + 1;
- break;
- case LEFT:
- pNextNode->x = ps->pSnake->x - 2;
- pNextNode->y = ps->pSnake->y;
- break;
- case RIGHT:
- pNextNode->x = ps->pSnake->x + 2;
- pNextNode->y = ps->pSnake->y;
- break;
- }
- //移动的过程中,判断是否遇到食物
- if (NextIsFood(pNextNode, ps))
- {
- //遇到食物
- EatFood(pNextNode, ps);
- }
- else
- {
- //没遇到食物
- NotEatFood(pNextNode, ps);
- }
- }
-
- //游戏玩的过程
- void GameRun(pSnake ps)
- {
- //打印帮助信息
- PrintHelpInfo();
- do
- {
- //打印当前得分
- SetPos(62, 10);
- printf("得分:%d", ps->Socre);
- SetPos(62, 11);
- printf("每个食物得分:%-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_SPACE))
- {
- //进行休眠
- pause();
- }
- else if (KEY_PRESS(VK_F3))
- {
- //加速
- if (ps->SleepTime >= 50)
- {
- ps->SleepTime -= 30;
- ps->foodWeight += 2;
- }
- }
- else if (KEY_PRESS(VK_F4))
- {
- //减速
- if (ps->foodWeight > 2)
- {
- ps->SleepTime += 30;
- ps->foodWeight -= 2;
- }
- }
- else if (KEY_PRESS(VK_ESCAPE))
- {
- //按ESC键退出
- ps->Status = ESC;
- break;
- }
- //蛇每走一步都需要休眠,休眠时间越短,蛇移动速度就越快
- Sleep(ps->SleepTime);
- SnakeMove(ps);
- //判断是否撞墙了
- KillByWall(ps);
- //判断是否撞到自己了
- KillBySelf(ps);
- } while (ps->Status == OK);
- }
-
-
- //游戏结束
- void GameEnd(pSnake ps)
- {
- SetPos(18, 10);
- switch (ps->Status)
- {
- case KILL_BY_WALL:
- printf("很遗憾撞墙了,游戏结束");
- break;
- case KILL_BY_SELF:
- printf("很遗憾撞到自身了,游戏结束");
- break;
- case ESC:
- printf("按了ESC键,正常退出");
- break;
- }
- //释放蛇身节点和食物节点
- pSnakeNode cur = ps->pSnake;
- pSnakeNode pNextNode = NULL;
- while (cur)
- {
- pNextNode = cur;
- cur = cur->next;
- free(pNextNode);
- }
- free(ps->pFood);
- ps = NULL;
- }