文章目录
- c语言简单实现贪吃蛇(巨细详解,附完整代码)
- ==前言==
- 一、游戏效果及功能实现:
- 二、Win32 API介绍
- 三、贪吃蛇游戏设计与分析
- 四、具体代码实现
- 五、完整代码附上(注意有snake.h snake.cpp test.cpp 三个文件)
c语言简单实现贪吃蛇(巨细详解,附完整代码)
前言
本文的贪吃蛇小游戏是基于c语言简单实现的,能够做到移动、吃食物、加速减速、撞墙死亡、撞身死亡等简单的贪吃蛇功能。作者使用的是Visual Studio 2019。由于内容较为详细所以可能篇幅较长,建议可以直接从 [四、具体代码实现] 开始看每个功能/部分的实现代码,遇到问题可以试着阅览前面部分的详细内容,文中有部分函数链接,可以跳转到函数介绍详细了解。所有完整代码在 [五、完整代码]。
一、游戏效果及功能实现:
1、规则:
不能碰到自己、不能穿墙
使用↑、↓、←、→来控制蛇的移动。
F1为加速,F2为减速
ESC退出游戏,space暂停游戏
2、基本功能实现:
- 贪吃蛇地图的绘制
- 蛇吃食物的功能
- 蛇撞墙死亡
- 蛇撞自身死亡
- 计算得分
- 蛇身加速减速
- 暂停游戏、退出游戏
3、技术要点
C语言函数、枚举、结构体、动态内存管理、预处理指令、链表、Win32 API等
4、实现思路
大概介绍下整体的一些实现思路:
-
通过在Win32 API中的一些函数实现在控制台上打印图案、定位光标和通过键盘控制贪吃蛇的移动,其中由于宽字符占两个字节,因此要将x轴上的坐标全部统一为偶数,即统一使用2、4、6、8等偶数的坐标避免打印重复和判定错误等,y轴坐标正常设置即可;
-
使用双向链表来维护贪吃蛇,在此基础上使用头插和尾删+打印图案的方式实现蛇的移动,蛇吃食物、判定死亡也是基于双向链表来实现;
-
使用rand()函数生成随机数转化为随即坐标来随机生成食物。
5、游戏效果呈现
二、Win32 API介绍
1、简单介绍
Windows 这个多作业系统除了协调应用程序的执行、分配内存、管理资源之外, 它同时也是⼀个很大的服务中心,调用这个服务中心的各种服务(每⼀种服务就是⼀个函数),可以帮应用程序达到开启视窗、描绘图形、使用周边设备等目的,由于这些函数服务的对象是应⽤程序(Application), 所以便 称之为 Application Programming Interface,简称 API 函数。WIN32 API也就是Microsoft Windows 32位平台的应用程序编程接口。
2、控制台程序(Console)
平时我们在写代码的时候运行的窗口就是控制台程序
我们可以使用cmd命令来设置控制台窗口的长度和宽度
cmd命令窗口打开方式:
windows搜索–>cmd–>右键管理员身份运行
控制cmd控制台长宽度命令
mode con cols=100 lines=100 //将行和列的长度设置为100
title命令(命名)
title 贪吃蛇 //给该控制台程序命名为贪吃蛇。
3、vs中的控制台窗口(调试控制台)
这里我用的是vs2019,所以我就以vs为例
当我们正常运行一个程序,程序运行结束的时候会有一个窗口,即为调试控制台窗口,如下图所示:
需要注意,有些默认程序运行结果的窗口可能是这样的,这算是一种终端,需要在设置里面调整为Windows控制台主机,具体操作为:点击下图加号旁边的箭头—设置—默认终端应用程序—Windows控制台主机或让windows决定,保存并重新运行。
4、设置控制台相关属性
system函数执行系统命令
#include<stdio.h>
#include<stdlib.h> //system函数的头文件
int main()
{
system("mode con cols=100 lines=50");//设置控制台窗口长宽
system("title 贪吃蛇");//设置控制台窗口的名字
//注:当程序结束后,名字就返回初始化的名字,只在运行中才能看得到这个名字。
return 0;
}
5、控制台屏幕上的坐标COORD
控制台窗口的每个点都有坐标,坐标轴的位置可以按照下图理解:
以左上角为原点,x轴向右递增,y轴向下递减
COORD 是Windows API中定义的⼀个结构体,表示⼀个字符在控制台屏幕上的坐标,头文件为windows.h,该结构体代码参考如下:
typedef struct _COORD {
SHORT X;
SHORT Y;
} COORD, *PCOORD;
使用COORD定义坐标:
COORD pos1={0,0};
COORD pos2={2,5};
6、GetStdHandle 函数
GetStdHandle 函数
GetStdHandle是⼀个Windows API函数。它用于从⼀个特定的标准设备(标准输入、标准输出或标准错误)中取得⼀个句柄(用来标识不同设备的数值),使用这个句柄可以操作设备。
GetStdHandle函数的头文件是window.h 返回值也是一个句柄
参数:
值 | 含义 |
---|---|
STD_INPUT_HANDLE | 标准输入句柄 |
STD_OUTPUT_HANDLE | 标注输出句柄 |
STD_ERROR_HANDLE | 标准错误句柄 |
注:只有这三个参数,每个参数用来获取不同的句柄,现在为实现获得屏幕的标准输出句柄,所以要给函数传STD_OUTPUT_HANDLE
HANDLE houtput = GetStdHandle(STD_OUTPUT_HANDLE);
//获得标准输出设备的句柄。
//使用HANDLE类型变量 houtput来接收值,GetStdHandle函数返回值的本质是一个指针,所以可以提前定义HANDLE houtput=NULL再接收
7、GetConsoleCursorInfo 函数(检索光标大小和可见性)
GetConsoleCursorInfo 函数
检索有关指定控制台屏幕缓冲区的光标大小和可见性的信息
语法:
BOOL WINAPI GetConsoleCursorInfo(
HANDLE hConsoleoutput
PCONSOLE_CURSOR_INFO lpConsoleCursorInfo
);
//PCONSOLE_CURSOR_INFO是指向CONSOLE_CURSOR_INFO结构的指针,该结构接收有关主机游标(光标)的信息,该结构体内容见下文。
举例:
HANDLE hOutput = NULL;
//获取标准输出的句柄(⽤来标识不同设备的数值)
hOutput = GetStdHandle(STD_OUTPUT_HANDLE);
CONSOLE_CURSOR_INFO CursorInfo;
GetConsoleCursorInfo(hOutput, &CursorInfo);//获取控制台光标信息
CONSOLE_CURSOR_INFO
这个结构体包含有关控制台光标的信息
typedef struct _CONSOLE_CURSOR_INFO {
DWORD dwSize;
BOOL bVisible;
} CONSOLE_CURSOR_INFO, *PCONSOLE_CURSOR_INFO;
dwSize是由光标填充的字符单元格的百分比,值为1到100之间,光标外观会变化,填充对应百分比的部分,如图可见光标占正常光标的25%,即四分之一。
bVisible,游标(光标)的可见性。 如果光标可见,则此成员为 TRUE,如果像设置为不可见,可以直接给他赋值为false,注意要加上bool.h的头文件
但是我们现在还不知道如何设置这些占比和可见性的相关信息,那么再看接下来的这个函数
8、SetConsoleCursorInfo 函数(设置光标大小和可见性)
SetConsoleCursorInfo 函数
设置指定控制台屏幕缓冲区的光标的大小和可见性。
语法:
BOOL WINAPI GetConsoleCursorInfo(
_In_ HANDLE hConsoleOutput,
_Out_ PCONSOLE_CURSOR_INFO lpConsoleCursorInfo
);
举例:
HANDLE hOutput = NULL;
//获取标准输出的句柄(⽤来标识不同设备的数值)
hOutput = GetStdHandle(STD_OUTPUT_HANDLE);
CONSOLE_CURSOR_INFO cursor_info = {0};
//定义一个光标信息的结构体
GetConsoleCursorInfo(houtput, &cursor_info);
//获取和houtput句柄相关的控制台上的光标信息,存放在cursor_Info中
cursor_info.dwSize=50;
//修改光标的占比(这里是百分之五十)
cursor_info.bVisible = false;
//将光标修改为不可见,看自己想不想要
SetConsoleCursorInfo(houtput,&cursor_info);
//设置和houtput句柄相关的控制台光标信息
system("pause");
//执行结果显示出的应为占一半的光标(没加bVisible=false的语句的结果)
9、SetConsoleCursorPosition 函数(设置光标坐标位置)
SetConsoleCursorPosition 函数
设置指定控制台屏幕缓冲区中的光标位置,我们将想要设置的坐标信息放在COORD类型的pos中,调用 SetConsoleCursorPosition函数将光标位置设置到指定的位置。
语法:
BOOL WINAPI SetConsoleCursorPosition(
_In_ HANDLE hConsoleOutput,
_In_ COORD dwCursorPosition
);
举例让光标在指定位置闪烁:
HANDLE houtput = NULL:
houtput = GetStdHandle(STD_OUTPUT_HANDLE);
//获得标准输出设备的句柄
COORD pos = {10,20};
SetConsoleCursorPosition(houtput,pos);
system("pause");
效果如图:
注:光标实际上是在“请”字的前面,但是因为打印了请按…这句话,所以到了后面去。将system(“pause”);这句话注释掉就会在请字前面了。
10、GetAsyncKeyState函数(获取键盘虚拟键值)
GetAsyncKeyState函数
获取按键情况,GetAsyncKeyState的函数原型如下:
SHORT GetAsyncKeyState(
int vKey
);
将键盘上每个键的虚拟键值传递给函数,函数通过返回值来分辨按键的状态。
GetAsyncKeyState 的返回值是short类型,在上⼀次调用GetAsyncKeyState 函数后,如果 返回的16位的short数据中,最高位是1,说明按键的状态是按下,如果最高是0,说明按键的状态是抬起;如果最低位被置为1则说明,该按键被按过,否则为0。 如果我们要判断⼀个键是否被按过,可以检测GetAsyncKeyState返回值的最低值是否为1
#define KEY_PRESS(VK) ( (GetAsyncKeyState(VK) & 0x1) ? 1 : 0 )
//结果是1,表示按过,结果是0,表示未按过
//vk是指要检测的键位的虚拟键代码
//将返回值的short类型数据和十六进制下的1(即0x1,其实直接写1也行)取&,用返回的真/假来判断是否按下,然后将它封装为宏
检测键盘输入的内容:虚拟键代码
通过键盘的每个键位对应的值来判断(可以理解为一种对应关系),参考虚拟键代码
有啥用?
讲了这么多,那么这些东西到底有什么用呢?
想想我们在贪吃蛇游戏中,我们需要有一个图形化的界面展示,蛇需要通过键盘输入的指令来移动,光标为了美观也需要隐藏,那么这些就会用到如上的这些函数了,这个小游戏不需要对这些函数有太深入的了解,只需要学会如何使用即可。
三、贪吃蛇游戏设计与分析
1、地图/界面
在游戏地图上,我们打印墙体使用宽字符:□,打印蛇使用宽字符■,打印食物使用宽字符★ 。普通的字符是占⼀个字节的,这类宽字符是占用2个字节。单字节字符类型是char,宽字符类型是wchar_t
<locale.h>本地化
<locale.h>提供的函数用于控制C标准库中对于不同的地区会产生不⼀样行为的部分,下面是一些简单的介绍。
C语言字符默认是采用ASCII编码的,ASCII字符集采⽤的是单字节编码,且只使用了单字节中的低7 位,最高位是没有使⽤的,可表⽰为0xxxxxxxx;可以看到,ASCII字符集共包含128个字符,在英语 国家中,128个字符是基本够⽤的,但是,在其他国家语⾔中,比如,在法语中,字母上方有注音符号,它就无法用 ASCII 码表示。于是,⼀些欧洲国家就决定,利用字节中闲置的最高位编入新的符号。比如,法语中的é的编码为130(⼆进制10000010)。这样⼀来,这些欧洲国家使⽤的编码体系,可以表示最多256个符号。但是,这里又出现了新的问题。不同的国家有不同的字母,因此,哪怕它们都使用256个符号的编码方式,代表的字母却不⼀样。比如,130在法语编码中代表了é,在希伯来语编码中却代表了字母Gimel ,在俄语编码中又会代表另⼀个符号。但是不管怎样,所有这些编码方式中,0–127表示的符号是⼀样的,不⼀样的只是128–255的这⼀段。 至于亚洲国家的文字,使用的符号就更多了,汉字就多达10万左右。⼀个字节只能表示256种符号, 肯定是不够的,就必须使用多个字节表达⼀个符号。比如,简体中文常见的编码方式是 GB2312,使用两个字节表示⼀个汉字,所以理论上最多可以表示256 x 256 = 65536 个符号。
在标准库中,依赖地区的部分有以下几项:
-
数字量的格式
-
货币量的格式
-
字符集
-
日期和时间的表示形式
类项
通过修改地区,程序可以改变它的行为来适应世界的不同区域。但地区的改变可能会影响库的许多部分,其中⼀部分可能是我们不希望修改的。所以C语言支持针对不同的类项进行修改,下面的⼀个宏,指定⼀个类项,详细参考
- LC_COLLATE
- LC_CTYPE
- LC_MONETARY
- LC_NUMERIC
- LC_TIME
- LC_ALL - 针对所有类项修改
setlocale函数
语法:
char* setlocale (int category, const char* locale);
用于修改当前地区,可以针对一个类项修改,也可以针对所有类项
setlocale第一个参数可以是前面说明的类项中的一个,那么每次只会影响一个类项,如果第一个参数是LC_ALL,就会影响所有的类项
C标准给第二个参数仅定义了2种可能的取值:“C”(正常的模式)和“”(本地模式)
setlocale(LC_ALL,"c");
当地区设置为“C”时,设置为c语言默认的模式,库函数正常实行
如果需要改变地区,需要用“”作为第二个参数,调用setlocale函数就可以切换到本地模式,例如切换到我们本地模式后就能够支持宽字符(如汉字)的输出等
setlocale(LC_ALL,"");
返回值
返回值是一个字符串指针,表示已经设置好的格式,如果调用失败,则返回空指针NULL
setlocale()可以用来查询当前地区,这时第二个参数设为NULL就可以了。
如下代码:
#include<locale.h>
int main()
{
char*loc;
loc=setlocale(LC_ALL,NULL);
printf("默认的本地信息: %s\n",loc);//打印结果:C
loc = setlocale(LC_ALL,"");
printf("设置后的本地信息:%s\n",loc);//打印结果:Chinese(Simplified)_China.936
return 0;
}
打印宽字符
那么该如何打印宽字符呢
首先需要使用setlocale函数(前文)更改到本地地区,才能够打印宽字符,打印宽字符需要在打印前面加上前缀L,否则C语言会把字面量当作窄字符类型处理,前缀L跟在单引号前面,表示宽字符,宽字符的打印使用wprintf,对应wprintf()的占位符为%lc;在双引号前面,表示宽字符对应wprintf()的占位符应为%ls。
例:
setlocale(LC_ALL, "");
wchar_t ch1 = L'★';
wprintf(L"%lc\n", ch1);
//打印结果: ★
wchar_t ch[] = L"你好";
wprintf(L"%ls\n", ch);
//打印结果: 你好
地图坐标
我们假设画一个27x58的地图(可以按照自己喜欢的比例,但是由于长一个字符位置的长比宽的长度要长许多,所以接近1:2的比例比较好,并且由于宽字符是占两个字符的宽度,我们将两个字符宽度和一个字符长度围成的一块看作一个点,所以x轴的长度最好是偶数)
示意图如下:
void CreateMap()//创建地图函数
{
wchar_t WALL = L'□';
setlocale(LC_ALL, "");//切换到本地地区
set_pos(0, 0);//定位到第一行的位置,开始循环打印
for (short i = 0; i < 58; i += 2) {//因为是宽字符,所以我这里是+=2;
wprintf(L"% lc", WALL);
}
set_pos(0, 26);//定位到最后一行的位置,开始循环打印
for (short i = 0; i < 58; i += 2) {
wprintf(L"% lc", WALL);
}
for (short i = 1; i < 26; i++) {//同理打印竖着的墙壁
set_pos(0, i);
wprintf(L"% lc", WALL);
set_pos(56, i);
wprintf(L"% lc", WALL);
}
set_pos(0, 28);//这行只是为了在测试的时候把程序运行结束的一段话挪到下面位置,不然会遮挡住最后一行的墙,整合到项目中自行删除
}
打印结果如下:
2、蛇身和食物
**注意事项:**由于游戏中的蛇身、食物、墙壁等都是宽字符,因此我们代码中的横坐标统一使用偶数,否则可能会出现食物的一半在墙里,或者蛇的一半在墙里等麻烦的情况。对于蛇,初始化的时候我们可以指定生成在地图正偏上方的位置,同时在蛇的运动中需要对蛇的前进点的出现和尾巴点的消失、蛇前进的间隔等做好维护。对于食物,应该注意在蛇身外的节点生成。
具体程序设计见下文。
3、数据结构设计
游戏运行过程中,蛇每吃一个食物,就会变长一截,上面讲过我们实现蛇的移动的方式是在前进的位置头插一个节点并打印,尾删尾节点并用空格覆盖住原来打印的图案,因此我们选择使用双向链表存储蛇的信息,这样相较于单链表不需要每次遍历链表来找到尾节点。蛇的每一节就是链表的每个节点,每个节点记录下蛇身在地图上的坐标,节点结构体定义如下:
typedef struct SnakeNode /*蛇链表节点*/ {
int x;
int y;
//坐标
struct SnakeNode* next;//指向下一个节点
struct SnakeNode* front;//指向上一个节点
}SnakeNode;
对于蛇本身,关于对它速度、方向、食物、食物分数、总分等的维护,为方便简洁,封装一个Snake的结构。
typedef struct Snake {
SnakeNode* pSnake;//维护整条蛇的指针,也是指向头节点的指针
SnakeNode* prear;//指向尾节点的指针
SnakeNode* pfood;//维护食物的指针
enum DIRECTION dir;//蛇头的⽅向
enum GAME_STATUS game_status;//游戏状态
int socre;//当前获得分数
int food_weight;//默认每个⻝物20分
int sleep_time;//每⾛⼀步休眠时间
}Snake;
四、具体代码实现
1、文件管理
为了便于管理和编辑,我这里创建了两个源文件和一个头文件,也可以按照自己习惯把snake.c和test.c的和snake.h的头文件和一些声明合并在一个文件中
test.c ——游戏的测试 (主函数、主程序)
snake.c——游戏的实现(编写函数)
snake.h——游戏函数的声明、类型的声明 (结构、函数的声明)
2、头文件的声明准备
define预处理
#define WALL L'□'
#define BODY L'■'
#define FOOD L'★'
#define POS_X 24
#define POS_Y 8
蛇的状态、游戏状态的枚举类型声明
enum DIRECTION {/*对蛇移动状态的枚举,给第一个赋值1,接下来的每个变量都会以此+1*/
UP = 1,
DOWN,
LEFT,
RIGHT
};
enum GAME_STATUS {/*枚举游戏运行状态*/
OK,//正常运行
KILL_BY_WALL,//撞墙死亡
KILL_BY_SELF,//撞蛇身死亡
END//正常退出/结束
};
3、控制台的定位(详细介绍见*[控制台屏幕上的坐标COORD]*)
为了简单化代码,我们把它封装成一个函数SetPos
void SetPos(short x, short y) /*定位光标位置*/ {
HANDLE houtput = NULL;
//获得标准输出设备的句柄
houtput = GetStdHandle(STD_OUTPUT_HANDLE);
COORD pos = { x,y };
//定位光标位置
SetConsoleCursorPosition(houtput, pos);
}
4、光标的隐藏(详细见*[GetStdHandle 函数]、[SetConsoleCursorInfo 函数 ]*)
为了在每次使用定位后和游戏中避免光标闪烁影响体验,我们先预先将光标设置为不可见:
void HideCursor() {/*隐藏光标*/
HANDLE houtput = GetStdHandle(STD_OUTPUT_HANDLE);
CONSOLE_CURSOR_INFO cursor_info = { 0 };
GetConsoleCursorInfo(houtput, &cursor_info);
cursor_info.bVisible = false;
SetConsoleCursorInfo(houtput, &cursor_info);
}
5、游戏界面初始化(欢迎界面、游戏说明界面、游戏准备界面)
一共有三个界面,每个界面之间可以使用system("pause")
来暂停进程
欢迎界面:
void CreateWelcome()/*创建欢迎界面*/
{
SetPos(16, 12);
printf("欢迎来到贪吃蛇小游戏!");
SetPos(0, 28);
}
效果如下:
游戏说明界面
void CreateSpeci() {/*打印说明页*/
SetPos(10, 10);
printf("请使用↑、↓、←、→键来控制蛇的移动。");
SetPos(19, 12);
printf("F1为加速,F2为减速");
SetPos(15, 14);
printf("ESC退出游戏,space暂停游戏");
SetPos(0, 28);
}
效果如下:
游戏准备界面
void CreateMap()//打印地图(周围的墙)
{
setlocale(LC_ALL, "");//切换到本地地区
SetPos(0, 0);
for (short i = 0; i < 58; i += 2) {
wprintf(L"% lc", WALL);
}//打印上面的墙体
SetPos(0, 26);
for (short i = 0; i < 58; i += 2) {
wprintf(L"% lc", WALL);
}//打印下面的墙体
for (short i = 1; i < 26; i++) {
SetPos(0, i);
wprintf(L"% lc", WALL);
SetPos(56, i);
wprintf(L"% lc", WALL);
}//打印左右墙体
SetPos(0, 28);//打印完后定位到地图下方,避免提示语挤兑地图
}
void CreateHelp() {/*打印游戏窗口中的说明信息*/
SetPos(66, 10);
printf("不允许碰墙,不允许碰蛇身");
SetPos(61, 14);
printf("请使用↑、↓、←、→键来控制蛇的移动");
SetPos(70, 16);
printf("F1为加速,F2为减速");
SetPos(60, 18);
printf("加速食物分值会变大,减速食物分值会变小");
SetPos(65, 20);
printf("ESC退出游戏,space暂停游戏");
}
效果如下:
6、蛇的初始化
默认出生时的蛇长度为5,因此构建有五个节点的双向链表来存储
void InitSnake(Snake* ps) {/*初始化贪吃蛇*/
SetPos(32, 5);
SnakeNode* cur = NULL;
for (int i = 0; i < 5; i++) {//创建初始化贪吃蛇的蛇身链表
cur = (SnakeNode*)malloc(sizeof(SnakeNode));
if (cur == NULL) {/*判断申请空间是否成功*/
perror("InitSnake()::malloc()");
return;
}
cur->next = NULL; cur->front = NULL;
cur->x = POS_X + i * 2;//提前预设好初始化蛇头所在的位置POS_X、POS_X
cur->y = POS_Y;//设置每个节点的坐标
//使用头插法创建双向链表
if (ps->pSnake == NULL) {
ps->pSnake = cur;
ps->prear = cur;
}
else {
cur->next = ps->pSnake;
ps->pSnake->front = cur;
ps->pSnake = cur;
}
}
cur = ps->pSnake;
while (cur) {
SetPos(cur->x, cur->y);
wprintf(L"%lc", BODY);
cur = cur->next;
}
ps->dir = RIGHT;//设置蛇运动的方向是向右。
ps->socre = 0;//设置目前得分
ps->food_weight = 20;//设置食物重量
ps->game_status = OK;//设置游戏状态
ps->sleep_time = 250;//设置每次运动间隔是两百五十毫秒
SetPos(0, 28);
}
7、食物的生成
rand()函数
详见rand
使用rand()函数生成随机数,在这里使用rand()函数头文件需要有stdlib.h time.h,同时需要先使用srand()函数
void CreateFood(Snake* ps) {/*创建食物*/
//x取值:2-54(且为偶数) y取值:1-25
int x, y;
again:
x = (rand() % 27 + 1) * 2;
y = rand() % 25 + 1;
//食物的生成位置不能够和蛇身的位置冲突
SnakeNode* cur = ps->pSnake;
while (cur) {
if (x == cur->x && y == cur->y) {
goto again;
}
cur = cur->next;
}//利用goto语句,如果生成食物的地址和蛇身链表某个位置地址重合,则返回重新生成
SnakeNode* pFood = (SnakeNode*)malloc(sizeof(SnakeNode));
if (pFood == NULL) {
perror("CreateFood()::malloc()"); return;
}//判断是否申请空间成功
pFood->front = pFood->next = NULL;
pFood->x = x; pFood->y = y;
ps->pfood = pFood;
SetPos(pFood->x, pFood->y);
wprintf(L"%lc", FOOD);
SetPos(0, 28);
}
在调用这个函数前,应该先使用srand()函数,才可使用CreateFood中的srand()
srand((unsigned int)time(NULL));//这个可以放在test.cpp中,不用封装
8、游戏的初始化
有了上述的诸多准备,我们就可以将跑程序前的所有准备工作做完,将它们整合到InitGame函数中
void InitGame(Snake* ps) {/*初始化游戏*/
system("mode con cols=105 lines=35");//设置窗口大小
system("title 贪吃蛇");//命名程序
HideCursor();//隐藏光标函数
CreateWelcome();//创建欢迎界面
system("pause");//是程序暂停直到键盘输入信息
system("cls");//清空屏幕
CreateSpeci();//创建说明界面
system("pause");
system("cls");
CreateMap();
CreateHelp();//创建游戏准备界面
InitSnake(ps);//初始化蛇
CreateFood(ps);//创建食物
SetPos(0, 28);
system("echo ------------------- 请按任意键开始游戏 -------------------&pause>nul");//暂停进程并打印内容
}
初始化后运行的效果就是先前展示的三组界面图。
RunGame的函数
以上一到八个函数都是游戏运行前的初始化准备工作,接下来就是运行游戏的几个关键函数。先来简要分析需要封装哪些功能。
在蛇的移动上,我们通过头插节点并打印蛇身和尾删节点并用空格覆盖的方式来实现视觉上的移动效果,因此封装两个函数HeadAppear和TailDisapp来实现。
在蛇吃食物方面,我们可以Snake结构下的dir和pSnake、pfood下的x、y坐标来判断是否吃到食物,因此可以封装一个NextIsFood函数。
在判定死亡上,有两种情况,一种是撞墙死亡,另一种是撞到自己,可以分别用坐标判定和便利链表每个节点的坐标的方式来判定,这里封装两个函数。
9、蛇的移动
蛇头出现
void HeadAppear(Snake* ps) {/*在蛇将要移动到的下一个位置打印出图案,并头插一个节点*/
SnakeNode* NewHead = (SnakeNode*)malloc(sizeof(SnakeNode));
//接下来判断蛇的运动方向来找到下一个位置
if (ps->dir == UP) {
NewHead->x = ps->pSnake->x;
NewHead->y = ps->pSnake->y - 1;
}
else if (ps->dir == DOWN) {
NewHead->x = ps->pSnake->x;
NewHead->y = ps->pSnake->y + 1;
}
else if (ps->dir == LEFT) {
NewHead->x = ps->pSnake->x - 2;
NewHead->y = ps->pSnake->y;
}
else if (ps->dir == RIGHT) {
NewHead->x = ps->pSnake->x + 2;
NewHead->y = ps->pSnake->y;
}
NewHead->front = NULL;
NewHead->next = ps->pSnake;
ps->pSnake->front = NewHead;
ps->pSnake = NewHead;
//头插新的节点
SetPos(NewHead->x, NewHead->y);
wprintf(L"%lc", BODY);
}
蛇尾消失
void TailDisapp(Snake* ps) {//尾删最后一个节点并用空格覆盖BODY图案
SnakeNode* tail = ps->prear;
SetPos(ps->prear->x, ps->prear->y);
printf(" ");
ps->prear = ps->prear->front;
ps->prear->next = NULL;
free(tail);
}
10、吃食物
判定食物
看着复杂,其实就是上下左右各个方向的情况分类讨论下
int NextIsFood(Snake* ps) {//判断要前进的下一个点有没有食物
if (ps->dir == UP && ps->pSnake->x == ps->pfood->x && ps->pSnake->y - 1 == ps->pfood->y)
return 1;
else if (ps->dir == DOWN && ps->pSnake->x == ps->pfood->x && ps->pSnake->y + 1 == ps->pfood->y)
return 1;
else if (ps->dir == LEFT && ps->pSnake->y == ps->pfood->y && ps->pSnake->x - 2 == ps->pfood->x)
return 1;
else if (ps->dir == RIGHT && ps->pSnake->y == ps->pfood->y && ps->pSnake->x + 2 == ps->pfood->x)
return 1;
return 0;
}
蛇移动、吃食物变长的逻辑
头出现,尾消失,吃食物,变长,这几步在蛇的移动和吃食物的过程中是需要有先后顺序的
吃食物的逻辑:
当蛇不吃食物时移动的时候需要头插尾删,但是吃到食物后身子变长了就相当于不需要使用TailDisapp函数来尾删并覆盖尾节点了,所以只需要用HeadAppear,这样就会达到变长的效果
移动的逻辑:
两个函数正确的调用顺序应该是先TailDisapp再HeadAppear。正常移动(不吃食物)的时候,看上去HeadAppear和TailDisapp函数谁在前没有区别,但是有一种特殊情况就会出现问题,示意图如下:
现在贪吃蛇走到如图所示的位置,红色是蛇头,箭头方向是运动方向,那么下一步按理来说会出现的情况应该是蛇头蛇尾接成环但是不碰,红色的头向左边移动一格,但是当我们先调用HeadAppear再调用TailDisapp的效果是这样的:
如图头的位置被覆盖掉了显示不出来,这是因为程序先执行了头插再执行了尾删,在头插执行完后头节点和尾节点的坐标是一样的,然后再用空格覆盖掉尾节点就是覆盖掉头节点,因此应该是先TailDisapp再HeadAppear的顺序
预期效果如图。
//以下是上述逻辑的代码,因为比较简洁黑,直接作为RunGame函数的一部分,没有封装
if (NextIsFood(ps)) {
free(ps->pfood);
HeadAppear(ps);
CreateFood(ps);
ps->socre += ps->food_weight;
}
else {
TailDisapp(ps);
HeadAppear(ps);
}
11、死亡判定
撞墙死亡
void KillByWall(Snake* ps) {//判定是否撞墙
if (ps->pSnake->x == 0 || ps->pSnake->x == 56 || ps->pSnake->y == 0 || ps->pSnake->y == 26) {
ps->game_status = KILL_BY_WALL;//撞墙了修改状态,退出RunGame函数的do while循环
SetPos(68, 5);
printf(" 你死亡了!游戏结束!");
SetPos(62, 7);
printf("-------- 你的得分是:%3d -------- ",ps->socre);
SetPos(ps->pSnake->x, ps->pSnake->y);
wprintf(L"%lc", L'×');//在撞墙位置打上x标记
}
}
撞到自己死亡
void KillBySelf(Snake* ps) {
SnakeNode* pcur = ps->pSnake->next;
while (pcur) {
if (pcur->x == ps->pSnake->x && pcur->y == ps->pSnake->y) {
ps->game_status = KILL_BY_SELF;
//修改状态,退出RunGame函数的do while循环
SetPos(68, 5);
printf(" 你死亡了!游戏结束!");
SetPos(62, 7);
printf("-------- 你的得分是:%3d -------- ", ps->socre);
SetPos(ps->pSnake->x, ps->pSnake->y);
wprintf(L"%lc", L'×');//在撞墙位置打上x标记
break;
}
pcur = pcur->next;
}
}
12、键盘操控相关
加速减速
通过Sleep()函数的参数值来调整速度,蛇走一步休眠一下再继续,因此休眠时间短,速度越快,可以修改sNake下的sleep_time来更改
//部分代码见下
else if (KEY_PRESS(VK_F1)) {//加速
if (ps->sleep_time > 50) {
ps->sleep_time -= 50;
ps->food_weight += 5;
}
}
else if (KEY_PRESS(VK_F2)) {//减速
if (ps->sleep_time < 400) {
ps->sleep_time += 50;
ps->food_weight -= 5;
}
}
空格暂停
单独封装一个pause函数,函数中通过循环使用Sleep函数实现暂停
void Pause() {
while (1) {
Sleep(300);
if (KEY_PRESS(VK_SPACE)) {
SetPos(0, 28);
printf(" ");//用来覆盖下面的提示语
break;
}
}
}
13、RunGame函数统合
void RunGame(Snake* ps) {
SetPos(0, 28);
printf(" ");
do {//通过Snake下的game_status作为循环条件
SetPos(62, 7);
printf("当前的得分:%3d", ps->socre);
SetPos(78, 7);
printf("当前食物的分数:%2d", ps->food_weight);
//打印得分
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_RIGHT) && ps->dir != LEFT) {
ps->dir = RIGHT;
}
else if (KEY_PRESS(VK_LEFT) && ps->dir != RIGHT) {
ps->dir = LEFT;
}
else if (KEY_PRESS(VK_SPACE)) {//暂停/继续
SetPos(0, 28);
printf("------------------- 请按空格键继续游戏 -------------------");
Pause();
}
else if (KEY_PRESS(VK_ESCAPE)) {//退出
ps->game_status = END;
break;
}
else if (KEY_PRESS(VK_F1)) {//加速
if (ps->sleep_time > 50) {
ps->sleep_time -= 50;
ps->food_weight += 5;
}
}
else if (KEY_PRESS(VK_F2)) {//减速
if (ps->sleep_time < 400) {
ps->sleep_time += 50;
ps->food_weight -= 5;
}
}
if (NextIsFood(ps)) {
free(ps->pfood);
HeadAppear(ps);
CreateFood(ps);
ps->socre += ps->food_weight;
}
else {
TailDisapp(ps);
HeadAppear(ps);
}
KillByWall(ps);
KillBySelf(ps);
Sleep(ps->sleep_time);
} while (ps->game_status == OK);
SetPos(0, 28);
}
五、完整代码附上(注意有snake.h snake.cpp test.cpp 三个文件)
snake.h
#pragma once
#include<stdio.h>
#include<stdlib.h>
#include<windows.h>
#include<locale.h>
#include<stdbool.h>
#include<time.h>
#define WALL L'□'
#define BODY L'■'
#define FOOD L'★'
#define POS_X 24
#define POS_Y 8
#define KEY_PRESS(VK) (( GetAsyncKeyState (VK) & 0x1 ) ? 1:0)
enum DIRECTION {/*对蛇移动状态的枚举,给第一个赋值1,接下来的每个变量都会以此+1*/
UP = 1,
DOWN,
LEFT,
RIGHT
};
enum GAME_STATUS {/*枚举游戏运行状态*/
OK,//正常运行
KILL_BY_WALL,//撞墙死亡
KILL_BY_SELF,//撞蛇身死亡
END//正常退出/结束
};
typedef struct SnakeNode /*蛇链表节点*/ {
int x;
int y;
//坐标
struct SnakeNode* next;//指向下一个节点
struct SnakeNode* front;//指向上一个节点
}SnakeNode;
typedef struct Snake {
SnakeNode* pSnake;//维护整条蛇的指针,也是指向头节点的指针
SnakeNode* prear;//指向尾节点的指针
SnakeNode* pfood;//维护食物的指针
enum DIRECTION dir;//蛇头的⽅向
enum GAME_STATUS game_status;//游戏状态
int socre;//当前获得分数
int food_weight;//默认每个⻝物20分
int sleep_time;//每⾛⼀步休眠时间
}Snake;
void SetPos(short x, short y);//定位地图
void HideCursor();/*隐藏光标*/
void Pause();//游戏暂停
void CreateWelcome();//打印欢迎界面
void CreateSpeci();//打印说明界面
void CreateHelp();//创建游戏窗口旁的信息提示
void CreateMap();//初始化地图
void CreateFood(Snake* ps);//创建食物
int NextIsFood(Snake* ps);//判断前进的下一个点是不是食物
void HeadAppear(Snake* ps);//打印出前进下一个点的图案并头插一个节点
void TailDisapp(Snake* ps);//尾删最后一个节点并用空格覆盖BODY图案
void InitSnake(Snake *ps);//初始化蛇
void InitGame(Snake* ps);//初始化游戏
void RunGame(Snake* ps);//运行游戏
void KillByWall(Snake* ps);//判定有无撞墙
void KillBySelf(Snake* ps);//判定有无撞自己
snake.cpp
#include"snake.h"
void SetPos(short x, short y) /*定位光标位置*/ {
HANDLE houtput = NULL;
//获得标准输出设备的句柄
houtput = GetStdHandle(STD_OUTPUT_HANDLE);
COORD pos = { x,y };
//定位光标位置
SetConsoleCursorPosition(houtput, pos);
}
void Pause() {
while (1) {
Sleep(300);
if (KEY_PRESS(VK_SPACE)) {
SetPos(0, 28);
printf(" ");
break;
}
}
}
void HideCursor() {/*隐藏光标*/
HANDLE houtput = GetStdHandle(STD_OUTPUT_HANDLE);
CONSOLE_CURSOR_INFO cursor_info = { 0 };
GetConsoleCursorInfo(houtput, &cursor_info);
cursor_info.bVisible = false;
SetConsoleCursorInfo(houtput, &cursor_info);
}
void CreateWelcome()/*创建欢迎界面*/
{
SetPos(16, 12);
printf("欢迎来到贪吃蛇小游戏!");
SetPos(0, 28);
}
void CreateSpeci() {/*打印说明页*/
SetPos(10, 10);
printf("请使用↑、↓、←、→键来控制蛇的移动。");
SetPos(19, 12);
printf("F1为加速,F2为减速");
SetPos(15, 14);
printf("ESC退出游戏,space暂停游戏");
SetPos(0, 28);
}
void CreateHelp() {/*打印游戏窗口中的说明信息*/
SetPos(66, 10);
printf("不允许碰墙,不允许碰蛇身");
SetPos(61, 14);
printf("请使用↑、↓、←、→键来控制蛇的移动");
SetPos(70, 16);
printf("F1为加速,F2为减速");
SetPos(60, 18);
printf("加速食物分值会变大,减速食物分值会变小");
SetPos(65, 20);
printf("ESC退出游戏,space暂停游戏");
}
void CreateMap()//打印地图(周围的墙)
{
setlocale(LC_ALL, "");//切换到本地地区
SetPos(0, 0);
for (short i = 0; i < 58; i += 2) {
wprintf(L"% lc", WALL);
}//打印上面的墙体
SetPos(0, 26);
for (short i = 0; i < 58; i += 2) {
wprintf(L"% lc", WALL);
}//打印下面的墙体
for (short i = 1; i < 26; i++) {
SetPos(0, i);
wprintf(L"% lc", WALL);
SetPos(56, i);
wprintf(L"% lc", WALL);
}//打印左右墙体
SetPos(0, 28);//打印完后定位到地图下方,避免提示语挤兑地图
}
void CreateFood(Snake* ps) {/*创建食物*/
//x取值:2-54(且为偶数) y取值:1-25
int x, y;
again:
x = (rand() % 27 + 1) * 2;
y = rand() % 25 + 1;
//食物的生成位置不能够和蛇身的位置冲突
SnakeNode* cur = ps->pSnake;
while (cur) {
if (x == cur->x && y == cur->y) {
goto again;
}
cur = cur->next;
}//利用goto语句,如果生成食物的地址和蛇身链表某个位置地址重合,则返回重新生成
SnakeNode* pFood = (SnakeNode*)malloc(sizeof(SnakeNode));
if (pFood == NULL) {
perror("CreateFood()::malloc()"); return;
}//判断是否申请空间成功
pFood->front = pFood->next = NULL;
pFood->x = x; pFood->y = y;
ps->pfood = pFood;
//将创建的食物和Snake结构体联系起来
SetPos(pFood->x, pFood->y);
wprintf(L"%lc", FOOD);
SetPos(0, 28);
}
int NextIsFood(Snake* ps) {//判断要前进的下一个点有没有食物
if (ps->dir == UP && ps->pSnake->x == ps->pfood->x && ps->pSnake->y - 1 == ps->pfood->y)
return 1;
else if (ps->dir == DOWN && ps->pSnake->x == ps->pfood->x && ps->pSnake->y + 1 == ps->pfood->y)
return 1;
else if (ps->dir == LEFT && ps->pSnake->y == ps->pfood->y && ps->pSnake->x - 2 == ps->pfood->x)
return 1;
else if (ps->dir == RIGHT && ps->pSnake->y == ps->pfood->y && ps->pSnake->x + 2 == ps->pfood->x)
return 1;
return 0;
}
void HeadAppear(Snake* ps) {/*在蛇将要移动到的下一个位置打印出图案,并头插一个节点*/
SnakeNode* NewHead = (SnakeNode*)malloc(sizeof(SnakeNode));
//接下来判断蛇的运动方向来找到下一个位置
if (ps->dir == UP) {
NewHead->x = ps->pSnake->x;
NewHead->y = ps->pSnake->y - 1;
}
else if (ps->dir == DOWN) {
NewHead->x = ps->pSnake->x;
NewHead->y = ps->pSnake->y + 1;
}
else if (ps->dir == LEFT) {
NewHead->x = ps->pSnake->x - 2;
NewHead->y = ps->pSnake->y;
}
else if (ps->dir == RIGHT) {
NewHead->x = ps->pSnake->x + 2;
NewHead->y = ps->pSnake->y;
}
NewHead->front = NULL;
NewHead->next = ps->pSnake;
ps->pSnake->front = NewHead;
ps->pSnake = NewHead;
//头插新的节点
SetPos(NewHead->x, NewHead->y);
wprintf(L"%lc", BODY);
}
void TailDisapp(Snake* ps) {//尾删最后一个节点并用空格覆盖BODY图案
SnakeNode* tail = ps->prear;
SetPos(ps->prear->x, ps->prear->y);
printf(" ");
ps->prear = ps->prear->front;
ps->prear->next = NULL;
free(tail);
}
void InitSnake(Snake* ps) {/*初始化贪吃蛇*/
SetPos(32, 5);
SnakeNode* cur = NULL;
for (int i = 0; i < 5; i++) {//创建初始化贪吃蛇的蛇身链表
cur = (SnakeNode*)malloc(sizeof(SnakeNode));
if (cur == NULL) {/*判断申请空间是否成功*/
perror("InitSnake()::malloc()");
return;
}
cur->next = NULL; cur->front = NULL;
cur->x = POS_X + i * 2;//提前预设好初始化蛇头所在的位置POS_X、POS_X
cur->y = POS_Y;//设置每个节点的坐标
//使用头插法创建双向链表
if (ps->pSnake == NULL) {
ps->pSnake = cur;
ps->prear = cur;
}
else {
cur->next = ps->pSnake;
ps->pSnake->front = cur;
ps->pSnake = cur;
}
}
cur = ps->pSnake;
while (cur) {
SetPos(cur->x, cur->y);
wprintf(L"%lc", BODY);
cur = cur->next;
}
ps->dir = RIGHT;//设置蛇运动的方向是向右。
ps->socre = 0;//设置目前得分
ps->food_weight = 20;//设置食物重量
ps->game_status = OK;//设置游戏状态
ps->sleep_time = 250;//设置每次运动间隔是两百五十毫秒
SetPos(0, 28);
}
void KillByWall(Snake* ps) {
if (ps->pSnake->x == 0 || ps->pSnake->x == 56 || ps->pSnake->y == 0 || ps->pSnake->y == 26) {
ps->game_status = KILL_BY_WALL;
SetPos(68, 5);
printf(" 你死亡了!游戏结束!");
SetPos(62, 7);
printf("-------- 你的得分是:%3d -------- ",ps->socre);
SetPos(ps->pSnake->x, ps->pSnake->y);
wprintf(L"%lc", L'×');
}
}
void KillBySelf(Snake* ps) {
SnakeNode* pcur = ps->pSnake->next;
while (pcur) {
if (pcur->x == ps->pSnake->x && pcur->y == ps->pSnake->y) {
ps->game_status = KILL_BY_SELF;
SetPos(68, 5);
printf(" 你死亡了!游戏结束!");
SetPos(62, 7);
printf("-------- 你的得分是:%3d -------- ", ps->socre);
SetPos(ps->pSnake->x, ps->pSnake->y);
wprintf(L"%lc", L'×');
break;
}
pcur = pcur->next;
}
}
void InitGame(Snake* ps) {/*初始化游戏*/
system("mode con cols=105 lines=35");//设置窗口大小
system("title 贪吃蛇");//命名程序
HideCursor();//隐藏光标函数
CreateWelcome();//创建欢迎界面
system("echo ------------------ 请按 → 继续 ------------------&pause>nul");
//是程序暂停直到键盘输入信息
system("cls");//清空屏幕
CreateSpeci();//创建说明界面
system("echo ------------------ 请按 → 继续 ------------------&pause>nul");
system("cls");
CreateMap();
CreateHelp();//创建游戏准备界面
InitSnake(ps);//初始化蛇
CreateFood(ps);//创建食物
SetPos(0, 28);
system("echo ------------------- 请按任意键开始游戏 -------------------&pause>nul");
}
void RunGame(Snake* ps) {
SetPos(0, 28);
printf(" ");
do {
SetPos(62, 7);
printf("当前的得分:%3d", ps->socre);
SetPos(78, 7);
printf("当前食物的分数:%2d", ps->food_weight);
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_RIGHT) && ps->dir != LEFT) {
ps->dir = RIGHT;
}
else if (KEY_PRESS(VK_LEFT) && ps->dir != RIGHT) {
ps->dir = LEFT;
}
else if (KEY_PRESS(VK_SPACE)) {//暂停/继续
SetPos(0, 28);
printf("------------------- 请按空格键继续游戏 -------------------");
Pause();
}
else if (KEY_PRESS(VK_ESCAPE)) {//退出
ps->game_status = END;
break;
}
else if (KEY_PRESS(VK_F1)) {//加速
if (ps->sleep_time > 50) {
ps->sleep_time -= 50;
ps->food_weight += 5;
}
}
else if (KEY_PRESS(VK_F2)) {//减速
if (ps->sleep_time < 400) {
ps->sleep_time += 50;
ps->food_weight -= 5;
}
}
if (NextIsFood(ps)) {
free(ps->pfood);
HeadAppear(ps);
CreateFood(ps);
ps->socre += ps->food_weight;
}
else {
TailDisapp(ps);
HeadAppear(ps);
}
KillByWall(ps);
KillBySelf(ps);
Sleep(ps->sleep_time);
} while (ps->game_status == OK);
SetPos(0, 28);
}
test.cpp
#include"snake.h"
void test()/*完成的是游戏的测试逻辑*/
{
Snake snake = { 0 };
//创建贪吃蛇
InitGame(&snake);
/*初始化游戏:欢迎界面、游戏介绍、地图初始化*/
RunGame(&snake);
//运行游戏
}
int main()
{
setlocale(LC_ALL, "");
srand((unsigned int)time(NULL));
test();
return 0;
}