做网站的流程百科,网站开发项目合同书,北京公司注册虚拟地址,网站备案要关多久文章目录 c语言简单实现贪吃蛇#xff08;巨细详解#xff0c;附完整代码#xff09;前言一、游戏效果及功能实现#xff1a;1、规则#xff1a;2、基本功能实现#xff1a;3、技术要点4、实现思路5、游戏效果呈现 二、Win32 API介绍1、简单介绍2、控制台程序#xff08… 文章目录 c语言简单实现贪吃蛇巨细详解附完整代码前言一、游戏效果及功能实现1、规则2、基本功能实现3、技术要点4、实现思路5、游戏效果呈现 二、Win32 API介绍1、简单介绍2、控制台程序Consolecmd命令窗口打开方式控制cmd控制台长宽度命令title命令命名 3、vs中的控制台窗口调试控制台4、设置控制台相关属性system函数执行系统命令 5、控制台屏幕上的坐标COORD6、GetStdHandle 函数7、GetConsoleCursorInfo 函数检索光标大小和可见性CONSOLE_CURSOR_INFO 8、SetConsoleCursorInfo 函数设置光标大小和可见性9、SetConsoleCursorPosition 函数设置光标坐标位置10、GetAsyncKeyState函数获取键盘虚拟键值检测键盘输入的内容*虚拟键代码*有啥用 三、贪吃蛇游戏设计与分析1、地图/界面locale.h本地化类项setlocale函数打印宽字符地图坐标 2、蛇身和食物3、数据结构设计 四、具体代码实现1、文件管理2、头文件的声明准备define预处理蛇的状态、游戏状态的枚举类型声明 3、控制台的定位详细介绍见*[控制台屏幕上的坐标COORD]*4、光标的隐藏详细见*[GetStdHandle 函数]、[SetConsoleCursorInfo 函数 ]*5、游戏界面初始化*欢迎界面、游戏说明界面、游戏准备界面*欢迎界面游戏说明界面游戏准备界面 6、蛇的初始化7、食物的生成rand()函数 8、游戏的初始化RunGame的函数9、蛇的移动蛇头出现 10、吃食物判定食物蛇移动、吃食物变长的逻辑 11、死亡判定撞墙死亡撞到自己死亡 12、键盘操控相关加速减速空格暂停 13、RunGame函数统合 五、完整代码附上注意有snake.h snake.cpp test.cpp 三个文件snake.hsnake.cpptest.cpp c语言简单实现贪吃蛇巨细详解附完整代码
前言
本文的贪吃蛇小游戏是基于c语言简单实现的能够做到移动、吃食物、加速减速、撞墙死亡、撞身死亡等简单的贪吃蛇功能。作者使用的是Visual Studio 2019。由于内容较为详细所以可能篇幅较长建议可以直接从 [四、具体代码实现] 开始看每个功能/部分的实现代码遇到问题可以试着阅览前面部分的详细内容文中有部分函数链接可以跳转到函数介绍详细了解。所有完整代码在 [五、完整代码]。gitee代码仓库贪吃蛇
一、游戏效果及功能实现
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 cols100 lines100 //将行和列的长度设置为100title命令命名
title 贪吃蛇 //给该控制台程序命名为贪吃蛇。3、vs中的控制台窗口调试控制台 这里我用的是vs2019所以我就以vs为例 当我们正常运行一个程序程序运行结束的时候会有一个窗口即为调试控制台窗口如下图所示 需要注意有些默认程序运行结果的窗口可能是这样的这算是一种终端需要在设置里面调整为Windows控制台主机具体操作为点击下图加号旁边的箭头—设置—默认终端应用程序—Windows控制台主机或让windows决定保存并重新运行。 4、设置控制台相关属性
system函数执行系统命令
#includestdio.h
#includestdlib.h //system函数的头文件
int main()
{system(mode con cols100 lines50);//设置控制台窗口长宽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 houtputNULL再接收7、GetConsoleCursorInfo 函数检索光标大小和可见性
GetConsoleCursorInfo 函数 检索有关指定控制台屏幕缓冲区的光标大小和可见性的信息
语法
BOOL WINAPI GetConsoleCursorInfo(HANDLE hConsoleoutputPCONSOLE_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.dwSize50;
//修改光标的占比这里是百分之五十
cursor_info.bVisible false;
//将光标修改为不可见看自己想不想要
SetConsoleCursorInfo(houtput,cursor_info);
//设置和houtput句柄相关的控制台光标信息
system(pause);
//执行结果显示出的应为占一半的光标没加bVisiblefalse的语句的结果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_COLLATELC_CTYPELC_MONETARYLC_NUMERICLC_TIMELC_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就可以了。
如下代码
#includelocale.h
int main()
{char*loc;locsetlocale(LC_ALL,NULL);printf(默认的本地信息 %s\n,loc);//打印结果Cloc setlocale(LC_ALL);printf(设置后的本地信息:%s\n,loc);//打印结果Chinese(Simplified)_China.936return 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的地图可以按照自己喜欢的比例但是由于长一个字符位置的长比宽的长度要长许多所以接近12的比例比较好并且由于宽字符是占两个字符的宽度我们将两个字符宽度和一个字符长度围成的一块看作一个点所以x轴的长度最好是偶数
示意图如下 void CreateMap()//创建地图函数
{ wchar_t WALL L□;setlocale(LC_ALL, );//切换到本地地区set_pos(0, 0);//定位到第一行的位置开始循环打印for (short i 0; i 58; i 2) {//因为是宽字符所以我这里是2wprintf(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_Xcur-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-25int 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 cols105 lines35);//设置窗口大小system(title 贪吃蛇);//命名程序HideCursor();//隐藏光标函数CreateWelcome();//创建欢迎界面system(pause);//是程序暂停直到键盘输入信息system(cls);//清空屏幕CreateSpeci();//创建说明界面system(pause);system(cls);CreateMap();CreateHelp();//创建游戏准备界面InitSnake(ps);//初始化蛇CreateFood(ps);//创建食物SetPos(0, 28);system(echo ------------------- 请按任意键开始游戏 -------------------pausenul);//暂停进程并打印内容
}初始化后运行的效果就是先前展示的三组界面图。 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
#includestdio.h
#includestdlib.h
#includewindows.h
#includelocale.h
#includestdbool.h
#includetime.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
#includesnake.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-25int 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_Xcur-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 cols105 lines35);//设置窗口大小system(title 贪吃蛇);//命名程序HideCursor();//隐藏光标函数CreateWelcome();//创建欢迎界面system(echo ------------------ 请按 → 继续 ------------------pausenul);//是程序暂停直到键盘输入信息system(cls);//清空屏幕CreateSpeci();//创建说明界面system(echo ------------------ 请按 → 继续 ------------------pausenul);system(cls);CreateMap();CreateHelp();//创建游戏准备界面InitSnake(ps);//初始化蛇CreateFood(ps);//创建食物SetPos(0, 28);system(echo ------------------- 请按任意键开始游戏 -------------------pausenul);
}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
#includesnake.hvoid test()/*完成的是游戏的测试逻辑*/
{Snake snake { 0 };//创建贪吃蛇InitGame(snake);/*初始化游戏欢迎界面、游戏介绍、地图初始化*/RunGame(snake);//运行游戏
} int main()
{setlocale(LC_ALL, );srand((unsigned int)time(NULL));test();return 0;
}