Win32 API介绍:
在写贪吃蛇这款游戏时需要用到一些有关Win32 API的知识, 接下来我会将设计到的知识点列举并讲解:
首先我们先了解一下Win32 API是什么,Windows这个多作业系统除了协调应⽤程序的执⾏、分配内存、管理资源之外,它同时也是⼀个很⼤ 的服务中⼼,调⽤这个服务中⼼的各种服务(每⼀种服务就是⼀个函数),可以帮应⽤程序达到开启 视窗、描绘图形、使⽤周边设备等⽬的,由于这些函数服务的对象是应⽤程序(Application),所以便 称之为Application Programming Interface,简称API函数。WIN32 API也就是Microsoft Windows 32位平台的应⽤程序编程接⼝。
控制台程序:
控制台就是我们在运行代码时出现的那个窗口:
有些人的可能是下面这张图的样子:
这个是无法实现贪吃蛇的,我们需要进行设置上的修改,改成第一个图的样子即可,具体步骤如下:
在更改完设置后我们就可以尝试对控制台进行一些操作了,比如说我们在玩贪吃蛇游戏的时候需要一个大小合适的游戏窗口,并且将控制台的标题改成“贪吃蛇”,这样会让我们写出来的游戏更加完美,接下来我来介绍一下这些命令:
1.mode命令:
(参考链接mode | Microsoft Learn )
代码:
system("mode con cols=30 lines=30");
演示效果:
我们把列和行的大小都设置为30,这样只是让效果更加明显,在实际使用时需要我们不断去尝试运行去找到一个合适的大小。
2.title命令
参考链接(title | Microsoft Learn)
代码:
system("title 贪吃蛇");
演示效果:
这里我使用了“pause”命令,它会使代码暂停下来,如果不加这个命令的话,我们不会看到效果,因为在程序运行结束时,title也会结束,我们不妨看一下没有这个命令时的效果:
控制台屏幕上的坐标COORD:
参考链接(COORD 结构 - Windows Console | Microsoft Learn)
COORD是Windows API中定义的⼀个结构体,表⽰⼀个字符在控制台屏幕幕缓冲区上的坐标,坐标系(0,0)的原点位于缓冲区的顶部左侧单元格。
COORD的类型:
typedef struct _COORD {
SHORT X;
SHORT Y;
} COORD, *PCOORD;
控制台其实是有坐标的,但它会与我们平常认识的有些许不同,如下图所示:
需要注意的是x和y坐标的单位长度是不一样的,大概就是1 :2的比例,如下图所示:
在打印东西的时候,总会在光标处打印,打印一下光标就会向后移动一下,在我们实现贪吃蛇的时候会需要在特定的位置进行打印,那就需要将光标放在指定的位置,也就是上面所讲的坐标,接下来我就介绍一下如何实现在指定位置打印的功能。
GetStdHandle 函数:
参考链接(GetStdHandle 函数 - Windows Console | Microsoft Learn)
仔细观察运行窗口会发现有个光标在一直闪烁,设想一下,在使用贪吃蛇时总会有一个光标在闪来闪去,这会非常影响游戏体验,那我们就要想办法将光标隐藏掉。
怎么隐藏呢?这就需要我们获得光标的一个控制权,那光标的控制权又在哪里,这就需要我们设想一下炒菜的这么一个情景,在颠锅的时候我们需要手持锅的把手,再回到代码的世界里,控制台就是这口大锅,GetStdHandle就是获得控制台控制权的一个函数,这里我们将这种控制权叫做句柄。
有了这样的思考,这个函数的定义也就易于理解了。
GetStdHandle是⼀个Windows API函数。它⽤于从⼀个特定的标准设备(标准输⼊、标准输出或标 准错误)中取得⼀个句柄(⽤来标识不同设备的数值),使⽤这个句柄可以操作设备。
函数声明:(并不需要我们自己声明,写出来只是为了便于理解)
不难看出函数的返回类型是HANDLE,它是一个指向句柄的指针,如果获取失败,就会返回空指针;
HANDLE GetStdHandle(DWORD nStdHandle);
函数的参数只有一个,这种类型的参数只有三种,只需选择需要的然后copy到代码中即可。
示例:
HANDLE hOutput = NULL;
//获取标准输出的句柄(⽤来标识不同设备的数值)
hOutput = GetStdHandle(STD_OUTPUT_HANDLE);
if (hOutput == NULL)//如果获取失败,直接退出程序并打印错误信息
{
perror("GetStdHandle");
exit(1);
}
GetConsoleCursorInfo 函数:
参考链接(GetConsoleCursorInfo 函数 - Windows Console | Microsoft Learn)
既然已经拿到句柄,我们就可以进行操作了,怎么操作呢?这还需要借助一个函数。
GetConsoleCursorInfo的作用就是检索有关指定控制台屏幕缓冲区的光标⼤⼩和可⻅性的信息。
语法:
可见函数有两个参数,一个就是我们使用 GetStdHandle获得的句柄,
BOOL WINAPI GetConsoleCursorInfo(
_In_ HANDLE hConsoleOutput,
_Out_ PCONSOLE_CURSOR_INFO lpConsoleCursorInfo
);
PCONSOLE_CURSOR_INFO 是指向 CONSOLE_CURSOR_INFO 结构的指针,该结构接收有关主机游标
另一个是是指向 CONSOLE_CURSOR_INFO 结构的指针,该结构接收有关主机游标,它包含两个类型的变量,一个是由游标填充的字符单元的百分比,另一个是光标的显示与隐藏。
typedef struct _CONSOLE_CURSOR_INFO {
DWORD dwSize;
BOOL bVisible;
} CONSOLE_CURSOR_INFO, *PCONSOLE_CURSOR_INFO;
示例:
HANDLE hOutput = NULL;
//获取标准输出的句柄(⽤来标识不同设备的数值)
hOutput = GetStdHandle(STD_OUTPUT_HANDLE);
CONSOLE_CURSOR_INFO CursorInfo;
GetConsoleCursorInfo(hOutput, &CursorInfo);//获取控制台光标信息
CursorInfo.bVisible = false;
我们来看一下效果:发现光标并没有消失,要想实现对光标的设置,还需要调用一个函数。
SetConsoleCursorInfo函数:
参考链接(SetConsoleCursorInfo 函数 - Windows Console | Microsoft Learn)
设置指定控制台屏幕缓冲区的光标的⼤⼩和可⻅性。
语法:SetConsoleCursorInfo函数的参数与GetConsoleCursorInfo 函数的参数类型相同 。
BOOL WINAPI SetConsoleCursorInfo(
_In_ HANDLE hConsoleOutput,
_In_ const CONSOLE_CURSOR_INFO *lpConsoleCursorInfo
);
示例:
HANDLE hOutput = GetStdHandle(STD_OUTPUT_HANDLE);
//影藏光标操作
CONSOLE_CURSOR_INFO CursorInfo;
GetConsoleCursorInfo(hOutput, &CursorInfo);//获取控制台光标信息
CursorInfo.bVisible = false; //隐藏控制台光标
SetConsoleCursorInfo(hOutput, &CursorInfo);//设置控制台光标状态
效果图:
SetConsoleCursorPosition 函数:
参考链接(SetConsoleCursorPosition 函数 - Windows Console | Microsoft Learn)
在隐藏好光标后,就可以尝试将光标放置到指定坐标上了,这就需要借助SetConsoleCursorPosition 函数。
语法:参数有两个,分别是获得的句柄和创建的COORD类型的变量。
BOOL WINAPI SetConsoleCursorPosition(
_In_ HANDLE hConsoleOutput,
_In_ COORD dwCursorPosition
);
示例:
COORD pos = { 10, 5};
HANDLE hOutput = NULL;
//获取标准输出的句柄(⽤来标识不同设备的数值)
hOutput = GetStdHandle(STD_OUTPUT_HANDLE);
//设置标准输出上光标的位置为pos
SetConsoleCursorPosition(hOutput, pos);
效果图:
学到这里,我们就可以尝试去封装一个函数来控制光标的位置,具体代码如下:
//设置光标的坐标
void SetPos(short x, short y)
{
COORD pos = { x, y };
HANDLE hOutput = NULL;
//获取标准输出的句柄(⽤来标识不同设备的数值)
hOutput = GetStdHandle(STD_OUTPUT_HANDLE);
//设置标准输出上光标的位置为pos
SetConsoleCursorPosition(hOutput, pos);
}
这样我们就可以通过调用该函数并传入想要设置的坐标就可以实现这一功能。
GetAsyncKeyState函数:
参考链接(getAsyncKeyState 函数 (winuser.h) - Win32 apps | Microsoft Learn)
思考一下,在玩贪吃蛇游戏的时候,我们会通过按相应的按键来控制蛇的移动,这种功能是如何实现的?接下来就介绍一下实现这个功能的函数。
GetAsyncKeyState函数会获取按键情况,我们需要知道的一点是键盘上的所有按键都有一个专属的虚拟值,将键盘上每个键的虚拟键值传递给函数,函数通过返回值来分辨按键的状态。GetAsyncKeyState 的返回值是short类型,在上⼀次调⽤ GetAsyncKeyState 函数后,如果 返回的16位的short数据中,最⾼位是1,说明按键的状态是按下,如果最⾼是0,说明按键的状态是抬 起;如果最低位被置为1则说明,该按键被按过,否则为0。
如果我们要判断⼀个键是否被按过,可以检测GetAsyncKeyState返回值的最低值是否为1。
语法:
SHORT GetAsyncKeyState(
int vKey
);
示例:
这里我用了一个宏定义,宏的内容就是( (GetAsyncKeyState(VK) & 0x1) ? 1 : 0 ),0x1其实就等价于十进制中的1,写成二进制为0000 0000 0000 0000 0000 0000 0000 0001,与1进行&得到的是最后一位是否是1。这样可以让代码看起来更加简洁。
#include <stdio.h>
#include <windows.h>
#define KEY_PRESS(VK) ( (GetAsyncKeyState(VK) & 0x1) ? 1 : 0 )
int main()
{
while (1)
{
if (KEY_PRESS(0x30))
{
printf("0\n");
}
else if (KEY_PRESS(0x31))
{
printf("1\n");
}
else if (KEY_PRESS(0x32))
{
printf("2\n");
}
else if (KEY_PRESS(0x33))
{
printf("3\n");
}
else if (KEY_PRESS(0x34))
{
printf("4\n");
}
else if (KEY_PRESS(0x35))
{
printf("5\n");
}
else if (KEY_PRESS(0x36))
{
printf("6\n");
}
else if (KEY_PRESS(0x37))
{
printf("7\n");
}
else if (KEY_PRESS(0x38))
{
printf("8\n");
}
else if (KEY_PRESS(0x39))
{
printf("9\n");
}
}
return 0;
}
效果演示:
我们按键盘上的数字,按几控制台上就会打印几。
接下来我们步入正题,讲解一下贪吃蛇游戏的实现思路。
贪吃蛇游戏设计与分析:
联想一下我们自己玩游戏的时候,刚进入游戏它会打印一个欢迎界面,然后显示游戏规则,最后进入游戏,贪吃蛇的速度和方向通过我们的控制在一定的区域内吃食物,蛇头碰到自己或者撞到墙会直接结束游戏,游戏期间还可以加速和减速,加速吃到的食物分数会更高,减速吃到的食物分数会低一点,游戏期间可以暂停,直接退出。
那我们就以这个思路来写代码:(接下来的功能实现我都会以函数的形式进行书写,最终会有完整代码)
游戏运行前数据的初始化:
0.设置窗口的大小并隐藏光标:
这里就使用我们上面所讲的对光标的状态设置和控制台设置的知识点,写起来也会相对轻松一点。
代码:
//0.设置窗口的大小并隐藏光标
void Std_set()
{
system("mode con cols=100 lines=30");
system("title 贪吃蛇");
HANDLE houtput = NULL;
houtput = GetStdHandle(STD_OUTPUT_HANDLE);
CONSOLE_CURSOR_INFO con;
GetConsoleCursorInfo(houtput, &con);
con.bVisible = false;
SetConsoleCursorInfo(houtput, &con);
}
1.欢迎界面的打印:
只需要我们将光标放到合适的位置打印”欢迎进入贪吃蛇小游戏“,如果不进行位置调整 会很难看。大家可以对比一下:
显然是第二种比较美观,而位置的设置完全可以调用我们前面封装的函数,那么我们的欢迎界面也就完成了。
代码:
#include<stdio.h>
//设置光标的坐标
void SetPos(int x, int y)
{
COORD pos = { x, y };
HANDLE hOutput = NULL;
//获取标准输出的句柄(⽤来标识不同设备的数值)
hOutput = GetStdHandle(STD_OUTPUT_HANDLE);
//设置标准输出上光标的位置为pos
SetConsoleCursorPosition(hOutput, pos);
}
int main()
{
SetPos(35, 11);
printf("欢迎来到贪吃蛇小游戏。\n");
system("pause");
return 0;
}
2.规则说明界面的打印:
操作与欢迎界面的打印一模一样,只需要更换一下打印的内容即可,在这我还是进行一个演示并放置参考代码。
效果演示:
代码:
//2.规则说明界面
void Print_rule()
{
Set_Con(15, 10);
printf("请用↑. ↓. ←. →控制蛇移动的方向,F3为加速,F4为减速,加速会获得更高的分数。\n\n\n\n\n\n\n");
system("pause");
//system("cls");
}
设想一下,贪吃蛇的速度和方向通过我们的控制在一定的区域内吃食物,蛇头碰到自己或者撞到墙会直接结束游戏。
根据设想,我们就会有这么一个代码:
//游戏的初始化
void Set_Game(Snake* ppsnake)
{
//0.设置窗口的大小并隐藏光标
Std_set();
//1.欢迎界面
Print_wel();
//2.规则说明界面
Print_rule();
//3.绘制地图
Print_wall();
//4.绘制蛇
Print_snake(ppsnake);
//5.绘制食物
Print_food(ppsnake);
}
接下来就是地图的绘制
3.地图的绘制:
这里地图的大小就由我规定了,如有需要,可以自行更改。
地图其实也就是我们的墙体,但是墙体我想要用特殊的图案进行绘制,比如效果图是这样的:
这个图案可以在输入法中寻找,以下是具体步骤:
那我们进行实操:
代码:
//3.绘制地图
void Print_wall()
{
for (int i = 0; i < 29; i++)
{
wprintf(L"%lc", L'□');
}
Set_Con(0, 26);
for (int i = 0; i < 29; i++)
{
wprintf(L"%lc", L'□');
}
for (int i = 1; i < 26; i++)
{
Set_Con(0, i);
wprintf(L"%lc", L'□');
}
for (int i = 1; i < 26; i++)
{
Set_Con(56, i);
wprintf(L"%lc", L'□');
}
}
效果演示:
发现和我们预想的不太一样,这就涉及到另一个知识:
如果想在屏幕上打印宽字符,怎么打印呢?
宽字符的字⾯量必须加上前缀“L”,否则C语⾔会把字⾯量当作窄字符类型处理。前缀“L”在单引 号前⾯,表⽰宽字符,对应 wprintf() 的占位符为 %lc ;在双引号前⾯,表⽰宽字符串,对应 wprintf() 的占位符为 %ls 。
代码:
int main()
{
//适配到本地
setlocale(LC_ALL, "");
wchar_t ch = L'□';
wprintf(L"%lc", ch);
return 0;
}
本地化设置:
这⾥再简单的讲⼀下C语⾔的国际化特性相关的知识,过去C语⾔并不适合⾮英语国家(地区)使⽤。 C语⾔最初假定字符都是单字节的。但是这些假定并不是在世界的任何地⽅都适⽤。
C语⾔字符默认是采⽤ASCII编码的,ASCII字符集采⽤的是单字节编码,且只使⽤了单字节中的低7 位,最⾼位是没有使⽤的,可表⽰为0xxxxxxxx;可以看到,ASCII字符集共包含128个字符,在英语 国家中,128个字符是基本够⽤的,但是,在其他国家语⾔中,⽐如,在法语中,字⺟上⽅有注⾳符 号,它就⽆法⽤ASCII码表⽰。于是,⼀些欧洲国家就决定,利⽤字节中闲置的最⾼位编⼊新的符 号。⽐如,法语中的é的编码为130(⼆进制10000010)。这样⼀来,这些欧洲国家使⽤的编码体 系,可以表⽰最多256个符号。但是,这⾥⼜出现了新的问题。不同的国家有不同的字⺟,因此,哪 怕它们都使⽤256个符号的编码⽅式,代表的字⺟却不⼀样。⽐如,130在法语编码中代表了é,在希 伯来语编码中却代表了字⺟Gimel,在俄语编码中⼜会代表另⼀个符号。但是不管怎样,所有这 些编码⽅式中,0--127表⽰的符号是⼀样的,不⼀样的只是128--255的这⼀段。 ⾄于亚洲国家的⽂字,使⽤的符号就更多了,汉字就多达10万左右。⼀个字节只能表⽰256种符号, 肯定是不够的,就必须使⽤多个字节表达⼀个符号。⽐如,简体中⽂常⻅的编码⽅式是GB2312,使 ⽤两个字节表⽰⼀个汉字,所以理论上最多可以表⽰256x256=65536个符号。在俄语编码中⼜会代表另⼀个符号。但是不管怎样,所有这 些编码⽅式中,0--127表⽰的符号是⼀样的,不⼀样的只是128--255的这⼀段。 ⾄于亚洲国家的⽂字,使⽤的符号就更多了,汉字就多达10万左右。⼀个字节只能表⽰256种符号, 肯定是不够的,就必须使⽤多个字节表达⼀个符号。⽐如,简体中⽂常⻅的编码⽅式是GB2312,使 ⽤两个字节表⽰⼀个汉字,所以理论上最多可以表⽰256x256=65536个符号。
后来为了使C语⾔适应国际化,C语⾔的标准中不断加⼊了国际化的⽀持。⽐如:加⼊了宽字符的类型 wchar_t 和宽字符的输⼊和输出函数,加⼊了头⽂件,其中提供了允许程序员针对特定 地区(通常是国家或者说某种特定语⾔的地理区域)调整程序⾏为的函数。
在游戏地图上,我们打印墙体使⽤宽字符:□,打印蛇使⽤宽字符●,打印⻝物使⽤宽字符◆ 普通的字符是占⼀个字节的,这类宽字符是占⽤2个字节。
<locale.h>提供的函数⽤于控制C标准库中对于不同的地区会产⽣不⼀样⾏为的部分。
在标准中,依赖地区的部分
有以下⼏项:
• 数字量的格式
• 货币量的格式
• 字符集
• ⽇期和时间的表⽰形式
类项:
通过修改地区,程序可以改变它的⾏为来适应世界的不同区域。但地区的改变可能会影响库的许多部 分,其中⼀部分可能是我们不希望修改的。
所以C语⾔⽀持针对不同的类项进⾏修改,下⾯的⼀个宏, 指定⼀个类项:
• LC_COLLATE:影响字符串⽐较函数 strcoll() 和 strxfrm() 。
• LC_CTYPE:影响字符处理函数的⾏为。
• LC_MONETARY:影响货币格式。
• LC_NUMERIC:影响 printf() 的数字格式。
• LC_TIME:影响时间格式 strftime() 和 wcsftime() 。
• LC_ALL-针对所有类项修改,将以上所有类别设置为给定的语⾔环境。
setlocale函数:
参考链接(setlocale - C++ Reference)
语法:
char* setlocale (int category, const char* locale);
setlocale的第⼀个参数可以是前⾯说明的类项中的⼀个,那么每次只会影响⼀个类项,如果第⼀个参 数是LC_ALL,就会影响所有的类项。
C标准给第⼆个参数仅定义了2种可能取值:"C"(正常模式)和""(本地模式)。
了解之后就可以直接投入使用了:
代码:
#include<stdio.h>
#include<locale.h>
//3.绘制地图
void Print_wall()
{
for (int i = 0; i < 29; i++)
{
wprintf(L"%lc", L'□');
}
Set_Con(0, 26);
for (int i = 0; i < 29; i++)
{
wprintf(L"%lc", L'□');
}
for (int i = 1; i < 26; i++)
{
Set_Con(0, i);
wprintf(L"%lc", L'□');
}
for (int i = 1; i < 26; i++)
{
Set_Con(56, i);
wprintf(L"%lc", L'□');
}
}
int main()
{
//适配到本地
setlocale(LC_ALL, "");
Print_wall();
//system("pause");
return 0;
}
效果演示:
观察到最后一行没有打印出来,其实不然,只是下面的文字将其遮挡住了,只需加一个”pause“命令即可看到效果:
4.绘制蛇:
这里会涉及到链表的知识,在前面的文章中有讲过,那蛇其实就是一个链表而已,每个结点里面储存着各自结点的x, y坐标,用于蛇身的打印,需要注意的是需要将蛇头的结点储存起来,以便找到整条蛇。
所以我们需要创建一个蛇的结点类型,代码如下:
//蛇身的结点
typedef struct SnakeNode
{
//结点的坐标
short x;
short y;
//指向下一个结点
struct SnakeNode* next;
}snakeNode;
假设我们将蛇身的长度初始为5,增加结点的时候采用尾插的方式,蛇头的坐标为(24,5),
那么初始化蛇身的代码就可以是如下:
//4.绘制蛇
void Print_snake(Snake* ppsnake)
{
snakeNode* cur = NULL;
for (int i = 0; i < 5; i++)
{
cur = (snakeNode*)malloc(sizeof(snakeNode));
if (cur == NULL)
{
perror("malloc");
exit(1);
}
cur->x = 24 + 2 * i;
cur->y = 5;
cur->next = NULL;
//尾插
if (ppsnake->psnake == NULL)
{
ppsnake->psnake = cur;
}
else
{
cur->next = ppsnake->psnake;
ppsnake->psnake = cur;
}
}
cur = ppsnake->psnake;
//打印蛇身
while (cur)
{
Set_Con(cur->x, cur->y);
wprintf(L"%lc", L'●');
cur = cur->next;
}
}
当然,我们在绘制蛇的时候可以顺便将整个游戏的数据初始化一下,比如说游戏状态,蛇的初始方向,吃一个食物获得的分数,获得的总分数,蛇的速度(也就是两次打印之间的时间间隔)。这就需要我们写一些枚举和结构体,具体如下:
//蛇的方向
typedef enum DIRECTION {
UP = 1,
DOWN,
LEFT,
RIGHT,
}DIRECTION;
//游戏状态
typedef enum GAME_STATUS
{
//正常运行
REGULAR= 1,
//撞墙
OVER_BY_WALL,
//撞到自己
OVER_BY_SELF,
//正常退出
OVER_NORMAL
}GAME_STATUS;
//贪吃蛇
typedef struct Snake
{
//蛇的头结点
snakeNode* psnake;
//食物
snakeNode* pfood;
//蛇的方向
DIRECTION dir;
//蛇的速度
int sleep_time;//时间和速度成反比
//一个食物的分数
int foof_weight;
//总分数
int score;
//游戏状态
GAME_STATUS status;
}Snake;
绘制蛇的代码优化:
//4.绘制蛇
void Print_snake(Snake* ppsnake)
{
snakeNode* cur = NULL;
for (int i = 0; i < 5; i++)
{
cur = (snakeNode*)malloc(sizeof(snakeNode));
if (cur == NULL)
{
perror("malloc");
exit(1);
}
cur->x = 24 + 2 * i;
cur->y = 5;
cur->next = NULL;
//尾插
if (ppsnake->psnake == NULL)
{
ppsnake->psnake = cur;
}
else
{
cur->next = ppsnake->psnake;
ppsnake->psnake = cur;
}
}
cur = ppsnake->psnake;
//打印蛇身
while (cur)
{
Set_Con(cur->x, cur->y);
wprintf(L"%lc", L'●');
cur = cur->next;
}
//设置食物分数
ppsnake->foof_weight = 10;
//总分
ppsnake->score = 0;
//速度
ppsnake->sleep_time = 200;
//初始方向
ppsnake->dir = RIGHT;
//游戏状态
ppsnake->status = REGULAR;
}
5.绘制食物:
食物的结点类型与蛇的相同,只不过只有一个结点,但是该结点的坐标不能与蛇身相同并且坐标是随机的,关于产生随机值这个知识在前面扫雷的实现里有讲,所以这里就不细说了。
代码:
//5.绘制食物
void Print_food(Snake* ppsnake)
{
int x = 0;
int y = 0;
again:
do
{
x = rand() % 53 + 2;
y = rand() % 25 + 1;
} while (x % 2 != 0);
snakeNode* cur = ppsnake->psnake;
while (cur)
{
if (cur->x == x && cur->y == y)
{
goto again;
}
cur = cur->next;
}
snakeNode* p = (snakeNode*)malloc(sizeof(snakeNode));
if (p == NULL)
{
perror("malloc");
return;
}
p->x = x;
p->y = y;
p->next = NULL;
Set_Con(x, y);
wprintf(L"%lc", L'◆');
ppsnake->pfood = p;
}
游戏运行:
0.实现按键判断及反应:
将要判断的按键的虚拟值传入写的宏中即可,判断后进行相应的反应。
代码:
//暂停游戏
void PAUSE()
{
while (1)
{
Sleep(100);
if (KEY_PASS(VK_SPACE))
{
break;
}
}
}
void StartGame(Snake* ppsnake)
{
do {
if (KEY_PASS(VK_UP) && ppsnake->dir != DOWN)//向上
{
ppsnake->dir = UP;
}
else if (KEY_PASS(VK_DOWN) && ppsnake->dir != UP)//向下
{
ppsnake->dir = DOWN;
}
else if (KEY_PASS(VK_LEFT) && ppsnake->dir != RIGHT)//向左
{
ppsnake->dir = LEFT;
}
else if (KEY_PASS(VK_RIGHT) && ppsnake->dir != LEFT)//向右
{
ppsnake->dir = RIGHT;
}
else if (KEY_PASS(VK_SPACE))//暂停,也就是写一个死循环,永远处于休眠状态
{
PAUSE();
}
else if (KEY_PASS(VK_ESCAPE))//正常退出游戏
{
ppsnake->status = OVER_NORMAL;
}
else if (KEY_PASS(VK_F3))//加速
{
//对加速次数进行限制,防止蛇出现瞬移的情况
if (ppsnake->foof_weight < 18)
{
ppsnake->sleep_time -= 30;
ppsnake->foof_weight += 2;
}
}
else if (KEY_PASS(VK_F4))//减速
{
//与加速相同,要做次数限制
if (ppsnake->foof_weight > 2)
{
ppsnake->sleep_time += 30;
ppsnake->foof_weight -= 2;
}
}
Snakemove(ppsnake);//移动蛇
Sleep(ppsnake->sleep_time);//等价于蛇的速度
} while (ppsnake->status == REGULAR);
}
1.帮助信息的打印:
想要达到一个这样的效果:
我们只需要找到 合适的坐标,接着打印一下就可以了,可以把这个函数在游戏运行里调用,因为总分和每个食物的分数是要进行更新的。
添加此功能后的代码:
//暂停游戏
void PAUSE()
{
while (1)
{
Sleep(100);
if (KEY_PASS(VK_SPACE))
{
break;
}
}
}
//打印帮助信息
void Print_help()
{
Set_Con(64, 14);
printf("按ESC退出游戏");
Set_Con(64, 15);
printf("按F3加速,按F4减速");
}
void StartGame(Snake* ppsnake)
{
Print_help();
do {
Set_Con(64, 12);
printf("总分数:%d", ppsnake->score);
Set_Con(64, 13);
printf("当前每个食物的分数:%02d", ppsnake->foof_weight);
if (KEY_PASS(VK_UP) && ppsnake->dir != DOWN)//向上
{
ppsnake->dir = UP;
}
else if (KEY_PASS(VK_DOWN) && ppsnake->dir != UP)//向下
{
ppsnake->dir = DOWN;
}
else if (KEY_PASS(VK_LEFT) && ppsnake->dir != RIGHT)//向左
{
ppsnake->dir = LEFT;
}
else if (KEY_PASS(VK_RIGHT) && ppsnake->dir != LEFT)//向右
{
ppsnake->dir = RIGHT;
}
else if (KEY_PASS(VK_SPACE))//暂停,也就是写一个死循环,永远处于休眠状态
{
PAUSE();
}
else if (KEY_PASS(VK_ESCAPE))//正常退出游戏
{
ppsnake->status = OVER_NORMAL;
}
else if (KEY_PASS(VK_F3))//加速
{
//对加速次数进行限制,防止蛇出现瞬移的情况
if (ppsnake->foof_weight < 18)
{
ppsnake->sleep_time -= 30;
ppsnake->foof_weight += 2;
}
}
else if (KEY_PASS(VK_F4))//减速
{
//与加速相同,要做次数限制
if (ppsnake->foof_weight > 2)
{
ppsnake->sleep_time += 30;
ppsnake->foof_weight -= 2;
}
}
Snakemove(ppsnake);//移动蛇
Sleep(ppsnake->sleep_time);//等价于蛇的速度
} while (ppsnake->status == REGULAR);
}
接下来实现蛇的移动 ,蛇的移动其实就是每隔一定时间重新打印蛇,只不过这个时间很短,人的肉眼难以分辨罢了,所以我们只需要着力去实现Snakemove函数。
2.蛇的移动:
实现该函数的思路大概就是去创建一个新的结点,这个结点储存着蛇头的下一个位置结点,需要注意的一点是,防止蛇头只有一半接触到食物,效果如下,所以我们需要蛇身x坐标一直是2的倍数,接下来开始写代码。
代码:
//蛇的移动
void Snakemove(Snake* pp)
{
snakeNode* pnew = (snakeNode*)malloc(sizeof(snakeNode));
if (pnew == NULL)
{
perror("malloc");
return;
}
switch (pp->dir)
{
case UP:
pnew->x = pp->psnake->x;
pnew->y = pp->psnake->y - 1;
break;
case DOWN:
pnew->x = pp->psnake->x;
pnew->y = pp->psnake->y + 1;
break;
case LEFT:
pnew->x = pp->psnake->x - 2;
pnew->y = pp->psnake->y;
break;
case RIGHT:
pnew->x = pp->psnake->x + 2;
pnew->y = pp->psnake->y;
break;
}
}
3.判断下一个位置是否是食物:
在蛇移动的过程中,下一个结点可能会是食物,所以我们要做出相应的反应,大纲如下:
//蛇的移动
void Snakemove(Snake* pp)
{
snakeNode* pnew = (snakeNode*)malloc(sizeof(snakeNode));
if (pnew == NULL)
{
perror("malloc");
return;
}
switch (pp->dir)
{
case UP:
pnew->x = pp->psnake->x;
pnew->y = pp->psnake->y - 1;
break;
case DOWN:
pnew->x = pp->psnake->x;
pnew->y = pp->psnake->y + 1;
break;
case LEFT:
pnew->x = pp->psnake->x - 2;
pnew->y = pp->psnake->y;
break;
case RIGHT:
pnew->x = pp->psnake->x + 2;
pnew->y = pp->psnake->y;
break;
}
//判断下一个结点是不是食物
if (IF_FOOF(pnew, pp))
{
//是食物:
EatFood(pnew, pp);
pnew = NULL;
}
else
{
//不是食物:
NoFood(pnew, pp);
}
}
接下来我们来实现EatFood和NoFood这两个函数,只需要判断蛇移动的下一个结点的x,y坐标是否相同即可,所以在传参的时候需要将蛇头的下一个结点传过去。
EatFood函数:
如果下一个结点是食物,可以直接将其化作新的蛇头,正好蛇的长度加一,重新打印蛇身,在吃掉食物的同时,我们要再创建一个新的食物,并且在总分中加上食物的分数,具体代码如下:
//是食物:
void EatFood(snakeNode* pnew, Snake* pp)
{
//把新的结点加到蛇身上
pp->pfood->next = pp->psnake;
pp->psnake = pp->pfood;
snakeNode* cur = pp->psnake;
//释放新的结点
free(pnew);
pnew = NULL;
//重新打印蛇身
while (cur != NULL)
{
Set_Con(cur->x, cur->y);
wprintf(L"%lc", L'●');
cur = cur->next;
}
pp->score += pp->foof_weight;
//创建新的食物:
Print_food(pp);
}
NoFood函数:
如果下一个结点不是食物,我们需要把新的结点加到蛇的身上,让其成为新的蛇头,并且重新打印蛇身,需要注意的是原来蛇的尾巴需要释放掉,不然蛇会越来越长,不过在释放之前需要用到蛇的尾部结点去将原来尾部的位置用两个空格遮盖住,具体代码如下:
//不是食物:
void NoFood(snakeNode* pnew, Snake* pp)
{
//把新的结点加到蛇身上
pnew->next = pp->psnake;
pp->psnake = pnew;
snakeNode* cur = pp->psnake;
//重新打印蛇身
while (cur->next->next != NULL)
{
Set_Con(cur->x, cur->y);
wprintf(L"%lc", L'●');
cur = cur->next;
}
Set_Con(cur->next->x, cur->next->y);
printf(" ");
//释放尾部结点
free(cur->next);
cur->next = NULL;
}
判断完下一个结点是不是食物后还要判断是否撞墙和撞到自己,大纲如下:
//蛇的移动
void Snakemove(Snake* pp)
{
snakeNode* pnew = (snakeNode*)malloc(sizeof(snakeNode));
if (pnew == NULL)
{
perror("malloc");
return;
}
switch (pp->dir)
{
case UP:
pnew->x = pp->psnake->x;
pnew->y = pp->psnake->y - 1;
break;
case DOWN:
pnew->x = pp->psnake->x;
pnew->y = pp->psnake->y + 1;
break;
case LEFT:
pnew->x = pp->psnake->x - 2;
pnew->y = pp->psnake->y;
break;
case RIGHT:
pnew->x = pp->psnake->x + 2;
pnew->y = pp->psnake->y;
break;
}
//判断下一个结点是不是食物
if (IF_FOOF(pnew, pp))
{
//是食物:
EatFood(pnew, pp);
pnew = NULL;
}
else
{
//不是食物:
NoFood(pnew, pp);
}
//撞到墙
KillByWall(pp);
//撞到自己
KillBySelf(pp);
}
4.是否撞墙和撞到自己
接下来我们尝试去实现KillByWall和KillBySelf函数:
KillByWall函数
只需判断蛇头是否超出我们前面绘制地图时给出的范围,如果超过了就修改游戏状态即可,具体代码如下:
//撞到墙
void KillByWall(Snake* pp)
{
if (pp->psnake->x == 0 || pp->psnake->x == 56 || pp->psnake->y == 0 || pp->psnake->y == 26)
{
pp->status = OVER_BY_WALL;
}
}
KillBySelf函数:
将除蛇头以外的蛇身的每一个结点与蛇头的x,y坐标进行对比即可,具体代码如下:
//撞到自己
void KillBySelf(Snake* pp)
{
snakeNode* cur = pp->psnake->next;
while (cur)
{
if (cur->x == pp->psnake->x && cur->y == pp->psnake->y)
{
pp->status = OVER_BY_SELF;
break;
}
cur = cur->next;
}
}
到这里游戏的运行就已经完成了,但是我们还要对游戏结束后进行善后工作。
5.游戏结束善后工作:
首先要提示玩家游戏为什么结束,然后释放蛇的结点,具体代码如下:
//结束游戏
void EndGame(Snake* pp)
{
switch (pp->status)
{
case OVER_BY_SELF:
printf("撞到自己了,本局游戏结束!!!");
break;
case OVER_BY_WALL:
printf("撞到墙了,本局游戏结束!!!");
break;
case OVER_NORMAL:
printf("本局游戏已正常退出!!!");
break;
}
//释放结点
snakeNode* cur = pp->psnake;
while (cur)
{
snakeNode* del = cur;
cur = cur->next;
free(del);
}
}
细节补充:
在传参上我们需要注意因为我们在实现功能的时候对结构体中的内容进行了修改,所以要传结构体指针。
我们还可以在增加一些细节,比如使用文件操作来储存历史最高纪录,这方面的知识在通讯录的实现中讲过,感兴趣可以去看一下。
游戏所有代码:
到这里已经将所有功能都实现了,现在就将他们的合体展示出来,分为三个文件:
snake.h:
#pragma once
#include<Windows.h>
#include<stdio.h>
#include<stdbool.h>
#include<locale.h>
#include<stdlib.h>
#include<time.h>
#define KEY_PASS(vk) ((GetAsyncKeyState(vk)&1)?1:0)
//蛇身的结点
typedef struct SnakeNode
{
//结点的坐标
short x;
short y;
//指向下一个结点
struct SnakeNode* next;
}snakeNode;
//蛇的方向
typedef enum DIRECTION {
UP = 1,
DOWN,
LEFT,
RIGHT,
}DIRECTION;
//游戏状态
typedef enum GAME_STATUS
{
//正常运行
REGULAR= 1,
//撞墙
OVER_BY_WALL,
//撞到自己
OVER_BY_SELF,
//正常退出
OVER_NORMAL
}GAME_STATUS;
//贪吃蛇
typedef struct Snake
{
//蛇的头结点
snakeNode* psnake;
//食物
snakeNode* pfood;
//蛇的方向
DIRECTION dir;
//蛇的速度
int sleep_time;//时间和速度成反比
//一个食物的分数
int foof_weight;
//总分数
int score;
//游戏状态
GAME_STATUS status;
}Snake;
//游戏的初始化
void Set_Game(Snake* ppsnake);
//光标位置的设置
void Set_Con(int x, int y);
//0.设置窗口的大小并隐藏光标
void Std_set();
//1.欢迎界面
void Print_wel();
//2.规则说明界面
void Print_rule();
//3.绘制地图
void Print_wall();
//4.绘制蛇
void Print_snake(Snake* ppsnake);
//5.绘制食物
void Print_food(Snake* ppsnake);
//开始游戏
void StartGame(Snake* snake);
//判断下一个结点是不是食物
int IF_FOOF(snakeNode* pnew, Snake* pp);
//是食物:
void EatFood(snakeNode* pnew,Snake* pp);
//不是食物:
void NoFood(snakeNode* pnew, Snake* pp);
//撞到墙
void KillByWall(Snake* pp);
//撞到自己
void KillBySelf(Snake* pp);
//结束游戏
void EndGame(Snake* pp);
snake.c:
#include"snake.h"
//游戏的初始化
void Set_Game(Snake* ppsnake)
{
//0.设置窗口的大小并隐藏光标
Std_set();
//1.欢迎界面
Print_wel();
//2.规则说明界面
Print_rule();
//3.绘制地图
Print_wall();
//4.绘制蛇
Print_snake(ppsnake);
//5.绘制食物
Print_food(ppsnake);
}
//光标位置的设置
void Set_Con(int x, int y)
{
HANDLE output = NULL;
output = GetStdHandle(STD_OUTPUT_HANDLE);
COORD coor = { x, y };
SetConsoleCursorPosition(output, coor);
}
//0.设置窗口的大小并隐藏光标
void Std_set()
{
system("mode con cols=100 lines=30");
system("title 贪吃蛇");
HANDLE houtput = NULL;
houtput = GetStdHandle(STD_OUTPUT_HANDLE);
CONSOLE_CURSOR_INFO con;
GetConsoleCursorInfo(houtput, &con);
con.bVisible = false;
SetConsoleCursorInfo(houtput, &con);
}
//1.欢迎界面
void Print_wel()
{
Set_Con(40, 12);
printf("欢迎来到贪吃蛇小游戏!!!\n\n\n\n");
system("pause");
system("cls");
}
//2.规则说明界面
void Print_rule()
{
Set_Con(15, 10);
printf("请用↑. ↓. ←. →控制蛇移动的方向,F3为加速,F4为减速,加速会获得更高的分数。\n\n\n\n\n\n\n");
system("pause");
system("cls");
}
//3.绘制地图
void Print_wall()
{
for (int i = 0; i < 29; i++)
{
wprintf(L"%lc", L'□');
}
Set_Con(0, 26);
for (int i = 0; i < 29; i++)
{
wprintf(L"%lc", L'□');
}
for (int i = 1; i < 26; i++)
{
Set_Con(0, i);
wprintf(L"%lc", L'□');
}
for (int i = 1; i < 26; i++)
{
Set_Con(56, i);
wprintf(L"%lc", L'□');
}
}
//4.绘制蛇
void Print_snake(Snake* ppsnake)
{
snakeNode* cur = NULL;
for (int i = 0; i < 5; i++)
{
cur = (snakeNode*)malloc(sizeof(snakeNode));
if (cur == NULL)
{
perror("malloc");
exit(1);
}
cur->x = 24 + 2 * i;
cur->y = 5;
cur->next = NULL;
//尾插
if (ppsnake->psnake == NULL)
{
ppsnake->psnake = cur;
}
else
{
cur->next = ppsnake->psnake;
ppsnake->psnake = cur;
}
}
cur = ppsnake->psnake;
//打印蛇身
while (cur)
{
Set_Con(cur->x, cur->y);
wprintf(L"%lc", L'●');
cur = cur->next;
}
//设置食物分数
ppsnake->foof_weight = 10;
//总分
ppsnake->score = 0;
//速度
ppsnake->sleep_time = 200;
//初始方向
ppsnake->dir = RIGHT;
//游戏状态
ppsnake->status = REGULAR;
}
//5.绘制食物
void Print_food(Snake* ppsnake)
{
int x = 0;
int y = 0;
again:
do
{
x = rand() % 53 + 2;
y = rand() % 25 + 1;
} while (x % 2 != 0);
snakeNode* cur = ppsnake->psnake;
while (cur)
{
if (cur->x == x && cur->y == y)
{
goto again;
}
cur = cur->next;
}
snakeNode* p = (snakeNode*)malloc(sizeof(snakeNode));
if (p == NULL)
{
perror("malloc");
return;
}
p->x = x;
p->y = y;
p->next = NULL;
Set_Con(x, y);
wprintf(L"%lc", L'◆');
ppsnake->pfood = p;
}
//打印帮助信息
void Print_help()
{
Set_Con(64, 14);
printf("按ESC退出游戏");
Set_Con(64, 15);
printf("按F3加速,按F4减速");
}
//暂停游戏
void PAUSE()
{
while (1)
{
Sleep(100);
if (KEY_PASS(VK_SPACE))
{
break;
}
}
}
//判断下一个结点是不是食物
int IF_FOOF(snakeNode* pnew, Snake* pp)
{
return (pp->pfood->x == pnew->x && pp->pfood->y == pnew->y);
}
//是食物:
void EatFood(snakeNode* pnew, Snake* pp)
{
//把新的结点加到蛇身上
pp->pfood->next = pp->psnake;
pp->psnake = pp->pfood;
snakeNode* cur = pp->psnake;
//释放新的结点
free(pnew);
pnew = NULL;
//重新打印蛇身
while (cur != NULL)
{
Set_Con(cur->x, cur->y);
wprintf(L"%lc", L'●');
cur = cur->next;
}
pp->score += pp->foof_weight;
//创建新的食物:
Print_food(pp);
}
//不是食物:
void NoFood(snakeNode* pnew, Snake* pp)
{
//把新的结点加到蛇身上
pnew->next = pp->psnake;
pp->psnake = pnew;
snakeNode* cur = pp->psnake;
//重新打印蛇身
while (cur->next->next != NULL)
{
Set_Con(cur->x, cur->y);
wprintf(L"%lc", L'●');
cur = cur->next;
}
Set_Con(cur->next->x, cur->next->y);
printf(" ");
//释放尾部结点
free(cur->next);
cur->next = NULL;
}
//撞到墙
void KillByWall(Snake* pp)
{
if (pp->psnake->x == 0 || pp->psnake->x == 56 || pp->psnake->y == 0 || pp->psnake->y == 26)
{
pp->status = OVER_BY_WALL;
}
}
//撞到自己
void KillBySelf(Snake* pp)
{
snakeNode* cur = pp->psnake->next;
while (cur)
{
if (cur->x == pp->psnake->x && cur->y == pp->psnake->y)
{
pp->status = OVER_BY_SELF;
break;
}
cur = cur->next;
}
}
//蛇的移动
void Snakemove(Snake* pp)
{
snakeNode* pnew = (snakeNode*)malloc(sizeof(snakeNode));
if (pnew == NULL)
{
perror("malloc");
return;
}
switch (pp->dir)
{
case UP:
pnew->x = pp->psnake->x;
pnew->y = pp->psnake->y - 1;
break;
case DOWN:
pnew->x = pp->psnake->x;
pnew->y = pp->psnake->y + 1;
break;
case LEFT:
pnew->x = pp->psnake->x - 2;
pnew->y = pp->psnake->y;
break;
case RIGHT:
pnew->x = pp->psnake->x + 2;
pnew->y = pp->psnake->y;
break;
}
//判断下一个结点是不是食物
if (IF_FOOF(pnew, pp))
{
//是食物:
EatFood(pnew, pp);
pnew = NULL;
}
else
{
//不是食物:
NoFood(pnew, pp);
}
//撞到墙
KillByWall(pp);
//撞到自己
KillBySelf(pp);
}
//开始游戏
void StartGame(Snake* ppsnake)
{
Print_help();
do {
Set_Con(64, 12);
printf("总分数:%d", ppsnake->score);
Set_Con(64, 13);
printf("当前每个食物的分数:%02d", ppsnake->foof_weight);
if (KEY_PASS(VK_UP) && ppsnake->dir != DOWN)//向上
{
ppsnake->dir = UP;
}
else if (KEY_PASS(VK_DOWN) && ppsnake->dir != UP)//向下
{
ppsnake->dir = DOWN;
}
else if (KEY_PASS(VK_LEFT) && ppsnake->dir != RIGHT)//向左
{
ppsnake->dir = LEFT;
}
else if (KEY_PASS(VK_RIGHT) && ppsnake->dir != LEFT)//向右
{
ppsnake->dir = RIGHT;
}
else if (KEY_PASS(VK_SPACE))//暂停
{
PAUSE();
}
else if (KEY_PASS(VK_ESCAPE))//正常退出游戏
{
ppsnake->status = OVER_NORMAL;
}
else if (KEY_PASS(VK_F3))//加速
{
if (ppsnake->foof_weight < 18)
{
ppsnake->sleep_time -= 30;
ppsnake->foof_weight += 2;
}
}
else if (KEY_PASS(VK_F4))//减速
{
if (ppsnake->foof_weight > 2)
{
ppsnake->sleep_time += 30;
ppsnake->foof_weight -= 2;
}
}
Snakemove(ppsnake);
Sleep(ppsnake->sleep_time);
} while (ppsnake->status == REGULAR);
}
//结束游戏
void EndGame(Snake* pp)
{
switch (pp->status)
{
case OVER_BY_SELF:
printf("撞到自己了,本局游戏结束!!!");
break;
case OVER_BY_WALL:
printf("撞到墙了,本局游戏结束!!!");
break;
case OVER_NORMAL:
printf("本局游戏已正常退出!!!");
break;
}
//释放结点
snakeNode* cur = pp->psnake;
while (cur)
{
snakeNode* del = cur;
cur = cur->next;
free(del);
}
}
test.c:
#include"snake.h"
void test1()
{
char ch = 0;
do {
system("cls");
//创建贪吃蛇
Snake snake = { 0 };
//初始化游戏
//0.设置窗口的大小并隐藏光标
//1.欢迎界面
//2.规则说明界面
//3.绘制地图
//4.绘制蛇
//5.绘制食物
//6.设置游戏的相关信息
Set_Game(&snake);
//开始游戏
StartGame(&snake);
//结束游戏
EndGame(&snake);
Set_Con(20, 15);
printf("再来一局?(Y/N):");
ch = getchar();
while (getchar() != '\n');
} while (ch == 'Y' || ch == 'y');
Set_Con(0, 27);
}
int main()
{
//适配到本地
setlocale(LC_ALL, "");
srand((unsigned int)time(NULL));
//测试游戏
test1();
return 0;
}