贪吃蛇游戏
🥕个人主页:开敲🍉
🔥所属专栏:C语言🍓
🌼文章目录🌼
0. 前言
1. 游戏背景
2. 实现后游戏画面展示
3. 技术要求
4. Win32 API介绍
4.1 Win32 API
4.2 控制台程序
4.3 控制台屏幕上的光标
4.4 GetStdHandle
4.5 GetConsoleCursorInfo
4.5.1 CONSOLE_CURSOR_INFO
4.6 SetConsoleCursorPosition
4.7 GetAsyncKeyState
5. 贪吃蛇游戏设计与分析
5.1 地图
5.1.1 本地化
5.1.2 类项
5.1.3 setlocale函数
5.1.3 宽字符的打印
5.2 蛇身和食物
5.3 数据结构设计
5.4 游戏流程设计
6. 核心逻辑实现分析
6.1 游戏主逻辑
6.2 初始化游戏
6.3 游戏运行
6.3.1 调整贪吃蛇的移动
6.4 结束游戏
0. 前言
游戏实现的源码放在了:贪吃蛇游戏源码(VS编译环境)-CSDN博客 中,需要的可以自行拷贝。
1. 游戏背景
贪吃蛇是久负盛名的游戏,它和俄罗斯方块、扫雷等游戏位列经典游戏行列。游戏的玩法也非常简单,玩家操控一条蛇,通过不断地吃食物来延长自己的身体,如果途中蛇头撞到了墙壁或者自己的身体,游戏就失败了。
2. 实现后游戏画面展示
3. 技术要求
C语言库函数、枚举、结构体、动态内存管理、预处理指令、链表、Win32 API 等
4. Win32 API介绍
本次贪吃蛇的实现会用到一些Win32 API的知识,接下来我们一起学习一些Win32 API的知识。
4.1 Win32 API
Windows系统除了协调应用程序的执行、分配内存、管理资源之外,它同时也是⼀个很大的服务中心,调用这个服务中心的各种服务(每一种服务就是一个函数),可以帮应用程序达到开启视窗、描绘图形、使用周边设备等目的,由于这些函数服务的对象是应用程序(Application),所以便称之为ApplicationProgrammingInterface,简称API函数。
WIN32API也就是MicrosoftWindows32位平台的应用程序编程接口。
4.2 控制台程序
在我们电脑上搜索cmd后跳出来的黑框框就是控制台程序。
我们可以使用cmd命令来设置控制台窗口的长、宽:mode命令
mode con cols=100 lines=30 //将控制台窗口行数设为30,列数设为100
也可以通过命令设置控制台窗口的名字:title命令
这些能在控制台窗口执行的命令,也可以通过调用C语言库函数system来执行。例如:
4.3 控制台屏幕上的光标
COORD是Windows API中定义的一个结构体,表示控制台光标在控制台屏幕上的坐标,坐标系的原点(0,0)在缓冲区的控制台屏幕的左上角。
COORD类型的声明:
给坐标赋值:
COORD pos = {1,1};
4.4 GetStdHandle
GetStdHandle是⼀个Windows API函数。它用于从⼀个特定的标准设备(标准输入、标准输出或标准错误)中取得⼀个句柄(用来标识不同设备的数值),使用这个句柄可以操作设备。
HANDLE GetStdHandle(DWORD nStdHandle);//返回类型为HANDLE
使用实例:
HANDLE houtput = NULL; //创建一个HANDLE类型的变量
houtput = GetStdHandle(STD_OUTPUT_HANDLE) //这里GetStdHandle中的参数表示获取当前控制台窗口的句柄(可以理解为拿到了当前控制台窗口的地址,从而能够操作当前控制台窗口)
4.5 GetConsoleCursorInfo
用于检索指定控制台屏幕光标信息:
1 BOOL WINAPI GetConsoleCursorInfo(
2 HANDLE hConsoleOutput,
3 PCONSOLE_CURSOR_INFO lpConsoleCursorInfo
4 );
5 PCONSOLE_CURSOR_INFO 是指向 CONSOLE_CURSOR_INFO 结构的指针,该结构6 接收指定控制台屏幕光标
实例:
HANDLE hOutput = NULL;
//获取标准输出的句柄(⽤来标识不同设备的数值)
hOutput = GetStdHandle(STD_OUTPUT_HANDLE);
CONSOLE_CURSOR_INFO CursorInfo;//用于存放光标信息
GetConsoleCursorInfo(hOutput, &CursorInfo);//获取控制台光标信息
4.5.1 CONSOLE_CURSOR_INFO
这是个结构体类型,用于存放指定控制台屏幕光标的信息:
① dwSize,由光标填充的字符单元格的百分比。此值介于1到100之间。光标外观会变化,范围从完全填充单元格到单元底部的水平线条。
② bVisible,光标的可见性。如果光标可见,则此成员为TRUE。
1 CursorInfo.bVisible = false; //隐藏控制台光标
4.6 SetConsoleCursorPosition
设置指定控制台屏幕缓冲区中的光标位置,我们将想要设置的坐标信息放在COORD类型的pos中,调用SetConsoleCursorPosition函数将光标位置设置到指定的位置。
实例:
1 COORD pos = { 10, 5};//设置光标坐标
2 HANDLE hOutput = NULL;
3 //获取标准输出的句柄(用来标识不同设备的数值)
4 hOutput = GetStdHandle(STD_OUTPUT_HANDLE);
5 //设置标准输出上光标的位置为pos
6 SetConsoleCursorPosition(hOutput, pos);
封装一个设置光标的函数,以便实现贪吃蛇时能快速方便地设置光标位置:
1 //设置光标的坐标
2 void SetPos(short x, short y)
3 {
4 COORD pos = { x, y };//设置光标坐标
5 HANDLE hOutput = NULL;
6 //获取标准输出的句柄(用来标识不同设备的数值)
7 hOutput = GetStdHandle(STD_OUTPUT_HANDLE);
8 //设置标准输出上光标的位置为pos
9 SetConsoleCursorPosition(hOutput, pos);
10 }
4.7 GetAsyncKeyState
读取键盘按键情况,函数原型如下:
将键盘上每个键的虚拟键值传递给函数,函数通过返回值来分辨按键的状态。
GetAsyncKeyState 的返回值是short类型,在上⼀次调用 GetAsyncKeyState 函数后,如果
返回的16位的short数据中,最高位是1,说明按键的状态是按下,如果最高是0,说明按键的状态是抬起;如果最低位被置为1则说明,该按键被按过,否则为0。如果我们要判断⼀个键是否被按过,可以检测GetAsyncKeyState返回值的最低值是否为1。
这里我们可以使用一个宏来快速地判断某一案件是否被按过:
其中VK传的就是想要知道有没有被按过的键的虚拟键值,键盘各键位虚拟键值表如下:
虚拟键码 (Winuser.h) - Win32 apps | Microsoft Learn
实例:检测数字键
1 #include <stdio.h>
2 #include <windows.h>
3 int main()
4 {
5 while (1)
6 {
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;
}
5. 贪吃蛇游戏设计与分析
5.1 地图
我们最终的贪吃蛇大纲要是这个样子,那我们的地图如何布置呢?
这里不得不讲⼀下控制台窗口的⼀些知识,如果想在控制台的窗口中指定位置输出信息,我们得知道该位置的坐标,所以首先介绍⼀下控制台窗口普的坐标知识。
控制台窗口的坐标如下所示,横向的是X轴,从左向右依次增长,纵向是Y轴,从上到下依次增长。
在游戏地图上,我们打印墙体使用宽字符:'□',打印蛇使用宽字符'●',打印食物使用宽字符'★'
普通的字符是占⼀个字节的,这类宽字符是占用2个字节。
这里再简单的讲⼀下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个符号。后来为了使C语言适应国际化,C语言的标准中不断加入了国际化的支持。比如:加入了宽字符的类型wchar_t 和宽字符的输入和输出函数,加入了<locale.h>头文件,其中提供了允许程序员针对特定地区(通常是国家或者说某种特定语言的地理区域)调整程序行为的函数。
5.1.1 <locale.h>本地化
<locale.h>提供的函数用于控制C标准库中对于不同的地区会产生不⼀样行为的部分。
在标准中,依赖地区的部分有以下几项:
① 数字量的格式
② 货币量的格式
③ 字符集
④ 日期和时间的表示形式
5.1.2 类项
通过修改地区,程序可以改变它的行为来适应世界的不同区域。但地区的改变可能会影响库的许多部分,其中⼀部分可能是我们不希望修改的。所以C语言⽀持针对不同的类项进行修改,下面的⼀个宏,指定⼀个类项:
① LC_COLLATE:影响字符串比较函数 strcoll() 和 strxfrm() 。
② LC_CTYPE:影响字符处理函数的行为。
③ LC_MONETARY:影响货币格式。
④ LC_NUMERIC:影响 printf() 的数字格式。
⑤ LC_TIME:影响时间格式 strftime() 和 wcsftime() 。
⑥ LC_ALL - 针对所有类项修改,将以上所有类别设置为给定的语言环境。
每个类项的详细说明可以参考:setlocale,_wsetlocale | Microsoft Learn
5.1.3 setlocale函数
1 char* setlocale (int category, const char* locale);
setlocale函数用于修改当前地区,可以针对一个类项修改,也可以针对所有类项。
setlocale的第⼀个参数可以是前面说明的类项中的一个,那么每次只会影响一个类项,如果第⼀个参数是LC_ALL,就会影响所有的类项。
C标准给第二个参数仅定义了2种可能取值:"C"(正常模式)和""(本地模式)。
在任意程序执行开始,都会隐藏式执行调用:
1 setlocale(LC_ALL, "C");
当地区设置为"C"时,库函数按正常方式执行,小数点是⼀个点。
当程序运行起来后想改变地区,就只能显示调用setlocale函数。⽤""作为第2个参数,调用setlocale函数就可以切换到本地模式,这种模式下程序会适应本地环境。
比如:切换到我们的本地模式后就支持宽字符(汉字)的输出等。
1 setlocale(LC_ALL, " ");//切换到本地环境
5.1.3 宽字符的打印
那如果想在屏幕上打印宽字符,怎么打印呢?
宽字符的字面量必须加上前缀“L”,否则C语言会把字面量当作窄字符类型处理。前缀“L”在单引号前面,表示宽字符,对应 wprintf() 的占位符为 %lc ;在双引号前面,表示宽字符串,对应wprintf() 的占位符为 %ls 。
1 #include <stdio.h>
2 #include<locale.h>
3 int main(){
4 setlocale(LC_ALL, "");
5 wchar_t ch1 = L'●';6 wchar_t ch2 = L'■';
7 wchar_t ch3 = L'★';
8 wprintf(L"%lc\n", ch1);
9 wprintf(L"%lc\n", ch2);
10 wprintf(L"%lc\n", ch3);
11 return 0;
}
5.2 蛇身和食物
初始化状态,假设蛇的长度是5,蛇身的每个节点是●,在固定的⼀个坐标处,比如(24,5)处开始出现蛇,连续5个节点。
注意:蛇的每个节点的x坐标必须是2个倍数,否则可能会出现蛇的⼀个节点有⼀半出现在墙体中,另外⼀半在墙外的现象,坐标不好对齐。
关于食物,就是在墙体内随机生成⼀个坐标(x坐标必须是2的倍数),坐标不能和蛇的身体重合,然后打印★。
5.3 数据结构设计
在游戏运行的过程中,蛇每次吃⼀个食物,蛇的身体就会变长一节,如果我们使用链表存储蛇的信息,那么蛇的每⼀节其实就是链表的每个节点。每个节点只要记录好蛇身节点在地图上的坐标就行,所以蛇节点结构如下:
要管理整条贪吃蛇,我们再封装⼀个Snake的结构来维护整条贪吃蛇:
蛇的方向总共只有:上、下、左、右四个方向,因此我们可以使用枚举:
游戏的状态也无非就是:正常进行、撞到墙壁、撞到自己、正常退出四种状态,因此我们也可以使用枚举:
5.4 游戏流程设计
6. 核心逻辑实现分析
6.1 游戏主逻辑
程序开始就设置程序支持本地模式,然后进入游戏的主逻辑。
主逻辑分为3个过程:
① 游戏初始化(InitGame) 完成游戏的初始化
② 游戏运行(GameRun) 完成游戏运行逻辑的实现
③ 游戏结束(GameOver) 完成游戏结束后的善后工作(释放动态开辟的空间)
6.2 初始化游戏
这个模块所需要完成的任务:
① 控制台窗口大小的设置
② 控制台窗口名字的设置
③ 鼠标光标的隐藏
④ 打印欢迎界面
⑤ 创建地图
⑥ 初始化游戏开始时蛇的长度
⑦ 创建第一个食物
InitWelcome函数:
CreatGameMap函数:
InitSnake函数:
CreatFood函数:
6.3 游戏运行
游戏运行期间,右侧打印帮助信息,提示玩家,坐标开始位置(64,15)
根据游戏状态检查游戏是否继续,如果是状态是OK,游戏继续,否则游戏结束。
如果游戏继续,就是检测按键情况,确定蛇下一步的方向,或者是否加速减速,是否暂停或者退出游戏。
需要用到的虚拟键:
① 上:VK_UP
② 下:VK_DOWN
③ 左:VK_LEFT
④ 右:VK_RIGHT
⑤ W:0x57
⑥ A:0x41
⑦ S:0x53
⑧ D:0x44
确定了蛇的方向以后,就可以实现蛇移动的函数了:
NextNodeWhetherFood函数:
EatFood函数:
NotFood函数:
KillByWall函数:
KillBySelf函数:
6.3.1 调整贪吃蛇的移动
需要根据玩家按下的键来调整贪吃蛇移动的方向、速度:
Pause函数:
6.4 结束游戏
根据最终结束游戏时游戏的状态判断是因为什么而结束的游戏: