When the night gets dark,remember that the Sun is also a star.
当夜幕降临时,请记住太阳也是一颗星星。
------------《去月球海滩篇》
目录
[2.1 控制台窗口大小设置](#2.1 控制台窗口大小设置)
[2.2 命令行窗口的名称的变更](#2.2 命令行窗口的名称的变更)
[2.3 vs平台下的调试窗口的设置](#2.3 vs平台下的调试窗口的设置)
[2.4 隐藏光标](#2.4 隐藏光标)
[2.5 控制光标的位置](#2.5 控制光标的位置)
[2.6 获取键盘的值的情况](#2.6 获取键盘的值的情况)
[2.7 字符问题](#2.7 字符问题)
[3.1 游戏开始前的准备 GameStart函数](#3.1 游戏开始前的准备 GameStart函数)
[3.1.1 设置好控制台界面的行和列,隐藏控制台光标](#3.1.1 设置好控制台界面的行和列,隐藏控制台光标)
[3.1.2 打印欢迎界面](#3.1.2 打印欢迎界面)
[3.1.3 绘制地图](#3.1.3 绘制地图)
[3.1.4 初始化蛇身](#3.1.4 初始化蛇身)
[3.1.5 创建食物](#3.1.5 创建食物)
[3.2 游戏运行 GameRun函数](#3.2 游戏运行 GameRun函数)
[3.2.1 打印帮助信息](#3.2.1 打印帮助信息)
[3.2.2 判断玩家按下的按键](#3.2.2 判断玩家按下的按键)
[3.2.3 蛇的移动](#3.2.3 蛇的移动)
[3.2.3.1 NextFood](#3.2.3.1 NextFood)
[3.2.3.2 EatFood](#3.2.3.2 EatFood)
[3.2.3.3 Kill_Wall](#3.2.3.3 Kill_Wall)
[3.2.3.4 Kill_Self](#3.2.3.4 Kill_Self)
[3.2.4 .Sleep函数、循环移动](#3.2.4 .Sleep函数、循环移动)
[3.3 贪吃蛇游戏结束的善后](#3.3 贪吃蛇游戏结束的善后)
[3.4 再来一局,不言弃!!](#3.4 再来一局,不言弃!!)
一、《贪吃蛇》游戏介绍
也许正在看的你玩过一款名为《贪吃蛇》的小游戏,《贪食蛇》中玩家控制一条不断移动的蛇,在屏幕上吃掉出现的食物。每吃掉一个食物,蛇的身体就会变长。游戏的目标是尽可能长时间地生存下去,同时避免蛇头撞到自己的身体或屏幕边缘。玩家需要灵活操作,利用策略在有限的空间内避免碰撞,挑战高分。
通过对经典的《贪吃蛇》的简单介绍,可以知道这篇文章中实现的贪吃蛇选哟实现以下的几点:
- 通过按方向键上下左右,来改变蛇的移动方向
- 通过按F3键实现蛇的加速行进,按F4键可以降低蛇的移速。
- 按空格键可实现暂停,暂停后按任意键继续游戏。
- 按Esc键可直接退出游戏。
除此之外,本游戏还拥有计分系统,可保存玩家的历史最高记录。和食物分值显示,蛇的移速的不同可以改变食物的分值。
二、WIN32部分接口简单介绍
在贪吃蛇的实现过程当中,会使用部分WIN32的部分接口,此处简单介绍贪吃蛇的实现过程当中会使用到的API的功能以及简单说明:
2.1 控制台窗口大小设置
mode con cols=100 lines=30 #控制窗口,cols为行长度,lines为列行数
按住Win键+R键,打开Windows命令运行框,输入:cmd---------打开Windows控制台命令窗口,输入该指令,就可以调整窗口的大小了,效果如下:
回车运行后,控制台窗口的大小被修改了,通过观察窗口的属性值与我们设定一致
2.2 命令行窗口的名称的变更
命令行窗口的名称的变更,可以如下通过命令的方式来更改:
title 贪吃蛇#更改命令行窗口的名称
效果如下:

2.3 vs平台下的调试窗口的设置
在C语言中,需要使用system接口来改变终端 窗口的大小 以及 窗口名称,使用system 接口需要包含 stdlib.h 头文件,例如下面代码:
#include<stdio.h>
#include<stdlib.h>//使用system接口的头文件
int main()
{
system("title 贪吃蛇");//将命令行窗口的名字更改为需要的名字
system("mode con cols=100 lines=30");//设置命令行窗口的大小
return 0;
}
2.4 隐藏光标
终端可以看作尾一个坐标系,左上角为坐标原点,向右为x轴,向下位y轴,如下图所示:

在windows窗口上描述一个坐标需要使用一个windows API中定义的一个结构体 COORD,表示一个字符在控制台屏幕缓冲区上的坐标,在C语言中,我们需要包含 windows.h 头文件才能使用,使用实例如下:
#include<stdio.h>
#include<windows.h>//调用该api需要的头文件
#include<stdlib.h>
int main()
{
COORD pos = { 10, 10 };//使用第一个参数为行,第二参数为列
return 0;
}
为了实现光标隐藏,我们需要先调用 GetStdHandle 函数来获取标准输出句柄,使用这个句柄可以操作设备。
HANDLE output = NULL;//HANDLE为结构体指针类型
//获取标准输出句柄来表示不同设备的数值
output = GetStdHandle(STD_OUTPUT_HANDLE);
标准输出通常是指控制台窗口中打印文本的位置。通过使用GetStdHandle函数,可以获取表示标准输出的句柄,以便可以对其进行操作。函数的参数STD_OUTPUT_HANDLE是一个宏,它表示标准输出句柄的常量值。通过将此参数传递给GetStdHandle函数,可以获得标准输出的句柄。获取标准输出句柄后,可以使用其他Windows API函数来更改控制台的输出属性、位置和文本颜色等,以实现更复杂的控制台输出。
要隐藏光标,需要先获得一个光标信息,上面我们已经获取了标准输出相关设备的句柄,接下来我们创建 CONSOLE_CORSOR_INFO 结构体对象(接收 有关主机光标信息的结构体),再调用 GetConsoleCursorInfo 函数来获得光标信息:
#include<stdio.h>
#include<windows.h>//调用win32 api所需要的头文件
int main()
{
HANDLE output = NULL;
//获取标准输出句柄来表示不同设备的数值
output = GetStdHandle(STD_OUTPUT_HANDLE);
CONSOLE_CURSOR_INFO cursor_info;
GetConsoleCursorInfo(output, &cursor_info);//获取光标的信息
return 0;
}
CONSOLE_CURSOR_INFO这个结构体包含了控制台光标信息:
typedef struct _CONSOLE_CURSOLE_INFO {
DWORD dwSize;
BOOL bVisible;
}CONSOLE_CURSOR_INFO, *PCONSOLE_CURSOR_INFO;
- dwSize参数,由光标填充的字符单元格的百分比。值范围为1到100。光标外观会变化,范围从完全填充单元格到单元底部的水平线条。
- bVisible 参数,设置光标的可见性,默认尾true;如果光标不可见,设置为false。
我们调用结构体的第二个参数设置为false(注意:C语言要包含 stdbool.h 头文件才能使用布尔类型)
GetConsoleCursorInfo 函数介绍

然后再调用SetConsoleCursorInfo 函数来设置更改的光标信息。
#include<stdio.h>
#include<stdbool.h>
#include<windows.h>
int main()
{
HANDLE output = NULL;
//获取标准输出句柄来表示不同设备的数值
output = GetStdHandle(STD_OUTPUT_HANDLE);
CONSOLE_CURSOR_INFO cursor_info;
GetConsoleCursorInfo(output, &cursor_info);//获取光标的信息
cursor_info.bVisible = false;
SetConsoleCursorInfo(output, &cursor_info);//设置更改信息
int ch = getchar();
putchar(ch);
return 0;
}
使用getchar putchar来输入输出信息,检测是否隐藏光标成功

下面是光标未被隐藏的结果,输入字符或者数字后,末尾会有一个光标在闪烁:

2.5 控制光标的位置
设置终端光标输出位置,我们首先要获取想要输出位置的坐标,上面我们介绍了COORD结构体,用来设置位置坐标。获取完坐标之后,我们可以调用 SetConsoleCorsorPosition 函数将光标位置设置到获取的坐标位置
BOOL SetConsoleCorsorPosition{
HANDLE output;//句柄
COORD pos;//位置
};
通过这个接口就可以将光标输出的信息放在想要的位置上了:
#include<stdio.h>
#include<stdbool.h>
#include<windows.h>
int main()
{
HANDLE output = NULL;
//获取标准输出句柄来表示不同设备的数值
output = GetStdHandle(STD_OUTPUT_HANDLE);
COORD pos = { 20, 20 };
SetConsoleCursorPosition(output, pos);
int ch = getchar();
putchar(ch);
return 0;
}
考虑到贪吃蛇中会频繁的改变贯标的位置,不妨把这个接口封装为一个函数,来完成光标定位的功能:
//定位光标位置
void SearchLocal(int x,int y) {
//获取控制台的句柄,控制权限,HANDLE 结构体指针
HANDLE handle = GetStdHandle(STD_OUTPUT_HANDLE);
COORD pos = { x,y };
// 获取设备句柄,传入坐标COORD
SetConsoleCursorPosition(handle, pos);
}
2.6 获取键盘的值的情况
控制贪吃蛇的移动,加速或者减速,我们都是通过键盘按下对应的按键进行控制,那系统是如何知道我们按下的按键是哪一个呢?
使用 GetAsyncKeyState 函数来获取按键情况,此函数函数原型如下:
SHORT GetAsyncKeyState(int vKey);
将键盘上的键值传给函数,通过函数返回值来判断按键的状态。GetAsyncKeyState 返回值是short类型,在上一次调用此函数后,如果返回的16位的short数据中,最高位是1,说明按键的状态是按下,如果最高位是0,说明按键的状态是抬起;如果最低位被置为1,则说明该按键被按过,否则位0。
如果我们要判断按键是否被按过,只需要判断返回值最低值是否为1即可,我们可以按位与上0x1来获取最低位的值,那么我们就可这样来编写函数:
//检测按键是否被按过,间返回的虚拟值按位与上1,相同为1,相异为0,进行检测
//封装为一个宏
#define KEY_PRESS(vk) (GetAsyncKeyState(vk)&0x1?1:0)
前面提出的系统是如何判断我们按下的按键爱是哪一个的疑问,我们可以通过虚拟键码来判断是不同按键的不同状态,这样就可以实现一些按键响应的功能了。 Virtual-Key 代码 (Winuser.h) - Win32 apps | Microsoft Learnhttps://learn.microsoft.com/zh-cn/windows/win32/inputdev/virtual-key-codes
本文贪吃蛇中需要用到的虚拟键值如下:

2.7 字符问题
我们在打印蛇身和墙体的时候,总不能用数字或者字符来表示吧。所以需要用到特殊字符------宽字符。宽字符的长度为2字节,因为不同地区的语言不同,计算机中描述的方式也不太一样,普通的单字节字符并不适合我们的地区,因此C语言加入了宽字符(字符类型:wchar_t 需要包含 locale.h 头文件)允许程序员针对特定地区调整程序行为函数。
类项: 通过修改地区,程序可以改变它的行为来适应世界的不同区域。但是地区改变可能会影响库的许多部分,其中一部分可能是我们不希望修改的,所以C语言针对不同类型的类项进行修改,下面的一个宏指定一个类项:
- LC_COLLATE:影响字符串比较函数
- LC_CTYPE:影响字符处理函数行为
- LC_MONETARY:影响货币格式
- LC_NUMERIC:影响printf()函数
- LC_TIME:影响时间格式
- LC_ALL :针对所有类项修改,将以上所有类别设定为给定的语言环境
使用 setlocale 函数修改类项:
char* setlocale(int category, const char* locale);
函数的第一个参数可以是前面说明的类项中的一个,那么每次只会影响一个类项,如果第一个参数是LC_ALL,就会影响所有类项。C标准给第二个参数定义了2种可能取值:"C"(正常模式)和""(本地模式) 在任意程序执行开始,默认隐式调用:
setlocale(LC_ALL, "C");
//我们需要切换到本地环境输出字符,所以:
setlocale(LC_ALL, " ");//这种模式下程序会适应本地环境,切换后程序就支持宽字符的打印了
想要打印宽字符也是与普通打印不同的,宽字符字面量前必须加上L ,否则C语言就会将其当为窄字符,且占位符应当为"%lc ",和"%ls",才可正常打印宽字符。此处打印宽字符用到了wprintf函数,这里简单介绍一下:
wprintf 是C语言中用于宽字符输出的函数,它类似于 printf ,但是用于处理宽字符(wchar_t)和宽字符串(wchar_t*)。这个函数定义在 wchar.h 头文件中,并且可以用来将格式化的数据打印到标准输出。
格式说明符:
在 wprintf 函数中,格式说明符的使用与 printf 函数相似。例如,%lc 用于宽字符,%ls 用于宽字符串。与 printf 不同的是,wprintf 需要使用 L 前缀来指定宽字符串字面量
#include<stdio.h>
#include<locale.h>
int main()
{
setlocale(LC_ALL, "");
wchar_t ch[] = L"你好哇";
wprintf(L"%ls\n", ch);
return 0;
}
运行结果如下:

三、贪吃蛇的实现
贪吃蛇流程简述:贪吃蛇游戏分为三个大步骤,游戏开始,游戏运行,游戏结束,我们分别用三个函数来完成。
//贪吃蛇游戏界面初始化
GameStart(&snake);
//贪吃蛇游戏运行
GameRun(&snake);
//贪吃蛇游戏结束的善后
GameEnd(&snake);
在设计游戏之前,我们需要先写出蛇的相关信息,例如:蛇移动的方向,蛇的状态,食物的分数,当前游戏总分等......我们将这些信息用结构体和枚举列出:
//贪吃蛇游戏状态
enum GAMSTATUS {
OK=1, //正常运行
EXIT, //按ESC键退出
KILL_BY_WALL, //撞墙了!
KILL_BY_SELF //咬到自己了!
};
//贪吃蛇行走方向
enum SNAKEWAY {
UP = 1, //前进
DOWN, //向下
LEFT, //左拐
RIGHT //右拐
};
//贪吃蛇,蛇身节点的定义
typedef struct SnakeNode {
//节点的位置
int x;
int y;
//下一个蛇身节点
struct SnakeNode* next;
}SnakeNode;
//整局游戏贪吃蛇的维护
typedef struct Snake {
//贪吃蛇,蛇头指针,方便找到贪吃蛇的位置/维护整条蛇的指针
SnakeNode* SnakePhead;
//指向食物的指针
SnakeNode* FoodNode;
//玩家当前得分
int Scour;
//每个食物的得分
int FoofScour;
//蛇休眠的时间,休眠的时间越短,蛇的速度越快,休眠的时间月长,蛇的速度越慢
int SleepTime;
//贪吃蛇游戏状态
enum GAMSTATUS Status;
//贪吃蛇行进的方向
enum SNAKEWAY way;
}Snake;
3.1 游戏开始前的准备 GameStart函数
下面是GameStart函数:
void GameStart(Snake* ps) {
//设定控制台屏幕大小
system("mode con cols=100 lines=30");
//设置控制台标题
system("title 贪吃蛇");
//设定光标,进行隐藏
//获取控制台句柄
HANDLE handle = GetStdHandle(STD_OUTPUT_HANDLE);
//创建结构体来接收控制台光标的数据
//有两个参数:dwSize 光标占空比; bVisible 光标可见度 默认true
CONSOLE_CURSOR_INFO cursor_info = { 0 };
GetConsoleCursorInfo(handle, &cursor_info);
//隐藏光标
cursor_info.bVisible = false;
//将修改的光标属性值传给控制台进行控制,参数类型跟GetConsoleCursorInfo一样
SetConsoleCursorInfo(handle, &cursor_info);
//游戏开始欢迎界面
welcome();
//绘制地图
GameMap();
//贪吃蛇初始化
InitSnake(ps);
//创建食物
CreatFood(ps);
}
接下来我们来逐一来了解一下GameStaert的答题流程,以及它实现了哪些功能。
3.1.1 设置好控制台界面的行和列,隐藏控制台光标
在这个大块中,我们首先需要设置好控制台界面的行和列,隐藏控制台光标:

3.1.2 打印欢迎界面
其次打印欢迎界面,用SearchLocal函数来找到合适的位置,使界面变得美观,期间使用了system("cls")来进行清屏操作:
//初始化贪吃蛇界面
void welcome() {
//将光标定位到界面的中间位置
SearchLocal(35, 10);
printf("欢迎来到贪吃蛇小游戏\n");
SearchLocal(38, 20);
system("pause");
system("cls");
//功能介绍
SearchLocal(15, 10);
printf("用↑ . ↓ . ← . → 来控制蛇的移动,F3为加速按键,F4为减速\n");
SearchLocal(35, 12);
printf("加速能够获得更高的分数!!!!");
SearchLocal(38, 20);
system("pause");
system("cls");
}
界面展示如下:


3.1.3 绘制地图
接着,绘制地图,墙壁的范围在控制台窗口的范围如图所示:

如果想在控制台的窗口中指定位置输出信息,我们需要得知该位置的坐标,所以⾸先介绍⼀下控制台窗口的坐标知识。
在控制台窗口中,行和列的大小也是不一样的:

可以看到,在控制台窗口中,行明显要比列更大,那在我们打印墙壁时,就需要用到宽字符:'□',宽字符与普通字符不同,一个宽字符的大小是两个字节,并且一个宽字符在窗口中占一行两列(两个坐标的大小),而一个普通字符的大小是一个字节,一个普通字符在窗口中占一行一列(一个坐标的大小),打印出来会更加美观。
这里我们把墙壁的行列定义为宏,后面可以根据自己的需要修改墙壁的大小,本文用到的是27行,58列的墙壁
#define ROW 26 //游戏区行数
#define COL 56 //游戏区列数

这里列的偏移值每次加2,是因为宽字符的x轴的密度与y轴的密度不一致,而设置每次偏移2,这样打印下一个宽字符的时候不会重叠

//绘制地图
void GameMap() {
//上
SearchLocal(0, 0);
for (int i = 0; i < COL; i += 2) {
wprintf(L"%c", WALL);
}
//左
for (int i = 1; i < ROW; i++) {
SearchLocal(0, i);
wprintf(L"%lc", WALL);
}
//右
for (int i = 1; i < ROW; i++) {
SearchLocal(COL-1, i);
wprintf(L"%lc", WALL);
}
//下
SearchLocal(0, ROW-1);
for (int i = 0; i < COL; i += 2) {
wprintf(L"%lc", WALL);
}
}
3.1.4 初始化蛇身
在这里,得用到数据结构中的链表,每个蛇身视为一个节点(游戏开始时有5个节点),将这5个节点的坐标相邻(利用头插法初始化),依次打印(最后要初始化一下蛇的属性):
初始蛇头的位置:
#define Origin_x 24 // 初始蛇头的x坐标
#define Origin_y 5 // 初始蛇头的y坐标
//打印蛇身
void SnakePrint(Snake* ps) {
SnakeNode* pcur = ps->SnakePhead;
while (pcur) {
SearchLocal(pcur->x, pcur->y);
wprintf(L"%lc", BODY);
pcur = pcur->next;
}
}
//初始化蛇
void InitSnake(Snake*ps) {
//创建5个蛇身节点
SnakeNode* pcur = NULL;
for (int i = 0; i < 5; i++) {
pcur = (SnakeNode*)malloc(sizeof(SnakeNode));
if (pcur == NULL) {
perror("malloc");
return;
//exit(1)
}
//头插
pcur->x = Origin_x+2*i;
pcur->y = Origin_y;
pcur->next = NULL;
if (ps->SnakePhead == NULL) {
ps->SnakePhead = pcur;
}
else {
pcur->next = ps->SnakePhead;
ps->SnakePhead = pcur;
}
}
//打印蛇身
SnakePrint(ps);
//初始化
ps->Scour = 0;
ps->FoodNode = NULL;
ps->FoofScour = 10;
ps->SleepTime = 200;
ps->Status = OK;
ps->way = RIGHT;
}
3.1.5 创建食物
将食物也当成一个普通的SnakeNode节点,因为食物也具有和蛇身节点相同的属性,只不过它的next指针永远为空,而它的x和y坐标是随机生成的,所以这里我们需要用到rand函数来生成随机数,不过生成的随机数也需要具备如下特点:
- x坐标的值为2~54,y坐标点值为1~25(不能在墙壁上或墙壁外)
- 必须为2的倍数(保证蛇可以吃到食物)
- 不能和蛇身的坐标重合
确定好以上条件后,我们就可以将食物打印在地图上了。
//创建食物
void CreatFood(Snake* ps) {
//随机生成食物的坐标(x,y)
//1.不能超过墙,且不能重叠 x:2~54 -- 52+2 y:1~24 -- 23+1
//2.食物不能与蛇身重叠,遍历蛇身
//创建食物节点
SnakeNode* FoodNode = (SnakeNode*)malloc(sizeof(SnakeNode));
if (FoodNode == NULL) {
perror("creat FoodNode fail");
return;
}
int x = 0;
int y = 0;
again:
do {
x = rand() % (COL-4) + 2;
y = rand() % (ROW-2) + 1;
} while (x % 2 != 0); //1.不能超过墙,且不能重叠
//2.食物不能与蛇身重叠,遍历蛇身
SnakeNode* pcur = ps->SnakePhead;
while (pcur) {
if (x == pcur->x && y == pcur->y) {
goto again;
}
pcur = pcur->next;
}
//真正创建了属于食物的坐标
FoodNode->x = x;
FoodNode->y = y;
FoodNode->next = NULL;
ps->FoodNode = FoodNode;
//打印食物
SearchLocal(x,y);
wprintf(L"%lc", FOOD);
}
由于食物是随机生成的(在蛇可爬行区域内),而食物有几率生成到蛇的身上,所以每生成一个食物节点后,得遍历整条蛇;如果重叠,就重新生成一个食物,直到不重叠为止。此处用到了again:,这里简略的介绍一下:
在 C 语言中, again: 不是一个关键字或预定义标识符,它只是一个标识符的名称,可以用作标签(label)。 在 C 语言中,标签通常用于在嵌套循环或 switch 语句中进行无条件跳转,从而提高代码的可读性和灵活性。 例如,使用 again: 标签可以在循环中使用 goto 语句来实现无限循环。 但是,使用 goto 语句会使代码变得难以理解和维护,因此应该谨慎使用。
3.2 游戏运行 GameRun函数
下面是 GameRun函数:
//贪吃蛇游戏运行
void GameRun(Snake* ps) {
//打印操作介绍
PrintOption();
do{
//打印当前得分情况
SearchLocal(65, 11);
printf("当前得分:%d",ps->Scour);
SearchLocal(78, 11);
printf("当前食物分值:%02d", ps->FoofScour); //每次移动分值可能变化放到循环中
//检测按键是否按过
if (KEY_PRESS(VK_UP)&& ps->way !=DOWN) {
ps->way = UP;
}
else if(KEY_PRESS(VK_DOWN) && ps->way != UP) {
ps->way = DOWN;
}
else if (KEY_PRESS(VK_LEFT) && ps->way != RIGHT) {
ps->way = LEFT;
}
else if (KEY_PRESS(VK_RIGHT) && ps->way != LEFT) {
ps->way = RIGHT;
}
else if (KEY_PRESS(VK_ESCAPE)) {
ps->way = EXIT;
break;
}
//空格进入暂停,死循环睡眠,在次按下空格进行游戏
else if (KEY_PRESS(VK_SPACE)) {
pause();
}
//F3加速,睡眠时间减少,食物分值增加,限制条件睡眠时间不为负数
else if (KEY_PRESS(VK_F3)) {
//时间不能为负,每次-30,至多减到20;
if (ps->SleepTime > 20) {
ps->SleepTime -= 30;
ps->FoofScour += 2;
}
}
//F4减速,睡眠时间增加,食物分值减少,限制条件食物分值不为0
else if (KEY_PRESS(VK_F4)) {
if (ps->FoofScour > 2) {
ps->SleepTime += 30;
ps->FoofScour -= 2;
}
}
//贪吃蛇开始移动
SnakeMove(ps);
//睡眠一下
Sleep(ps->SleepTime);
} while (ps->Status == OK);
}
3.2.1 打印帮助信息
游戏开始后的帮助信息进行打印:

将光标移到指定位置(如果更改墙体的大小,帮助信息的位置光标需要做对应的修改,还有如果超出了窗口大小,可以调整窗口的大小),打印即可:
//打印帮助信息
void PrintOption() {
SearchLocal(63, 17);
printf("用↑ . ↓ . ← . → 来控制蛇的移动");
SearchLocal(63, 18);
printf("space(空格)为暂停/回到游戏按键");
SearchLocal(68, 19);
printf("F3为加速按键,F4为减速");
SearchLocal(72, 20);
printf("ESC为退出按键");
SearchLocal(63, 21);
printf("注意:不能撞墙,不能咬到自己!!!");
}
3.2.2 判断玩家按下的按键
接下来,玩家会通过按方向键(↑ . ↓ . ← . →)以及F3、F4来控制蛇的移动,我们就需要用到我们上面所定义好的宏,传入各个键的虚拟键值,利用if else语句来判断玩家所按下的键,但是需要注意的是:如果蛇正在向下移动,则蛇的移动方向不能突然变为向上,否则会撞到自己(即向下移动时,按上方向键无效),左右上方向时同理:
//休眠函数
void pause() {
while (1) {
Sleep(200);
if (KEY_PRESS(VK_ESCAPE)) {
break;
}
}
}

3.2.3 蛇的移动
可以把下一步要移动到的坐标当做一个SnakeNode节点,malloc出一个新节点(这里命名为pnext),再根据蛇的方向设置这个节点的x,y值,接着,我们需要判断移动到的下一个坐标是不是食物,如果是食物则头插进蛇体中(不要忘记释放掉pnext节点(因为食物节点和pnext的位置是重复的));如果不是食物,也需要将pnext节点头插到蛇体中,要将最后一个节点用" "进行覆盖,不让这个节点出现在屏幕上。然后接着判断这次移动是否撞到了墙或者自己。
//贪吃蛇开始移动
void SnakeMove(Snake*ps){
//创建指针记录蛇移动的下一个位置
SnakeNode* pnext = (SnakeNode*)malloc(sizeof(SnakeNode));
if (pnext == NULL) {
perror("SnakeMove::malloc");
return;
}
pnext->next = NULL;
//记录蛇移动的下一个位置
switch (ps->way){
case UP:
pnext->x = ps->SnakePhead->x;
pnext->y = ps->SnakePhead->y - 1;
break;
case DOWN:
pnext->x = ps->SnakePhead->x;
pnext->y = ps->SnakePhead->y + 1;
break;
case LEFT:
pnext->x = ps->SnakePhead->x-2;
pnext->y = ps->SnakePhead->y;
break;
case RIGHT:
pnext->x = ps->SnakePhead->x + 2;
pnext->y = ps->SnakePhead->y;
break;
}
//判断下一个位置是否为食物
//是食物,吃掉 --- 头插
if (NextFood(ps,pnext)) {
EatFood(ps, pnext);
}
//不是食物,头插删除尾节点
else {
NotEatFood(ps, pnext);
}
//打印蛇身
SnakePrint(ps);
//检测是否穿墙
Kill_Wall(ps);
//检测是否咬到自己
Kill_Self(ps);
}
3.2.3.1 NextFood
判断下一个节点的坐标是否与食物的节点的坐标一致,是则返回1;反之返回0。
//下一个位置是否为食物;
int NextFood(Snake* ps, SnakeNode* pnext) {
if (pnext->x == ps->FoodNode->x && pnext->y == ps->FoodNode->y) {
return 1;
}
else{
return 0;
}
}
3.2.3.2 EatFood
//吃食物
void EatFood(Snake* ps, SnakeNode* pnext) {
pnext->next = ps->SnakePhead;
ps->SnakePhead = pnext;
//得分增加
ps->Scour += ps->FoofScour;
//释放旧的食物
free(ps->FoodNode);
ps->FoodNode = NULL;
//重新创建食物
CreatFood(ps);
}
3.2.3.3 Kill_Wall
//检测是否穿墙
void Kill_Wall(Snake* ps) {
if (ps->SnakePhead->x == 0 ||
ps->SnakePhead->x == (COL-1) ||
ps->SnakePhead->y == 0 ||
ps->SnakePhead->y == (ROW-1))
{
ps->Status = KILL_BY_WALL;
}
}
3.2.3.4 Kill_Self
//检测是否咬到自己
void Kill_Self(Snake* ps) {
SnakeNode* pcur = ps->SnakePhead->next;
while (pcur) {
if (ps->SnakePhead->x == pcur->x && ps->SnakePhead->y == pcur->y) {
ps->Status = KILL_BY_SELF;
return;
}
pcur = pcur->next;
}
}
3.2.4 .Sleep函数、循环移动
需要一段时间来间断一下蛇体的移动,并判断蛇的状态进行循环:

此处用到了Sleep函数,依旧简略的介绍一番:
Sleep()
函数是 Windows 平台下的一个 API,定义在**windows.h
** 头文件中。其功能是挂起(暂停)当前线程的执行一段时间。输入参数是毫秒数。
3.3 贪吃蛇游戏结束的善后
打印出提示信息告诉玩家游戏失败的原因,最后,因为蛇身的节点是我们动态malloc开辟的,为了避免内存泄露,我们需要一一free掉。
//贪吃蛇游戏结束的善后
void GameEnd(Snake* ps) {
SearchLocal(15, 10);
//打印结束状态
switch (ps->Status) {
case EXIT:
printf("正常退出,游戏结束了");
break;
case KILL_BY_WALL:
printf("很遗憾,游戏结束,你撞到墙了!!!");
break;
case KILL_BY_SELF:
printf("不小心咬到自己了,游戏结束了!!!");
break;
}
//释放空间
SnakeNode* pcur = ps->SnakePhead;
SnakeNode* del = NULL;
while (pcur) {
del = pcur->next;
free(pcur);
pcur = del;
}
free(ps->FoodNode);
ps = NULL;
}
3.4 再来一局,不言弃!!
游戏失败后,为了使游戏更完整,设置循环提示玩家是否再来一局,这里用do while循环来实现:
void snakegame() {
int ch = 0;
do {
//创建贪吃蛇
Snake snake = { 0 };
//贪吃蛇游戏界面初始化
GameStart(&snake);
//贪吃蛇游戏运行
GameRun(&snake);
//贪吃蛇游戏结束的善后
GameEnd(&snake);
SearchLocal(15, 8);
printf("要再来一局吗?? Y/N:");
ch = getchar();
getchar();// 清理\n
SearchLocal(0, 27);
} while (ch == 'Y' || ch == 'y');
}