前言
本篇博客我们来实现一个小游戏项目——贪吃蛇,相信肯定很多人都玩过,那么整个贪吃蛇是怎么实现出来的那,这个项目用到了很多方面的知识:C语言函数、枚举、结构体、动态内存管理、预处理指令、链表、Win32 API等。我们就通过这篇博客一步一步去分析,实现贪吃蛇。
? 个人主页:小张同学zkf
⏩ 文章专栏:数据结构 C语言
若有问题 评论区见?
?欢迎大家点赞?收藏⭐文章
目录
1.游戏背景
2.Win32 API介绍
2.1Win32 API
2.2控制台程序
2.3控制台屏幕上的坐标COORD
2.4GetStdHandle
2.5GetConsoleCursorInfo
2.6CONSOLE_CURSOR_INFO
2.7SetConsoleCursorInfo
2.8SetConsoleCursorPosition
2.9GetAsyncKeyState
3.贪吃蛇游戏思路
3.1游戏窗口
3.1.1本地化
3.1.2类项
3.1.3setlocale函数
3.1.4宽字符的打印
3.1.5地图坐标
3.2蛇身和食物
3.3数据结构设计
3.4游戏流程设计
4.核心逻辑分析
4.1游戏主逻辑
4.2 游戏开始(GameStart)
4.2.1打印欢迎界面
4.2.2创建地图
4.2.3初始化蛇身
4.2.4创建第一个食物
4.3游戏运行(GameRun)
4.3.1KEY_PRESS
4.3.2PrintHelpInfo
4.3.3蛇身移动(SnakeMove)
4.3.3.1NextIsFood
4.3.3.2 EatFood
4.3.3.3NoFood
4.3.3.4KillByWall
4.3.3.5KillBySelf
4.4游戏结束
5.参考代码
1.游戏背景
贪吃蛇是一款休闲益智类游戏,有PC和手机等多平台版本。既简单又耐玩。该游戏通过控制蛇头方向吃食物,从而使得蛇变得越来越长,贪吃蛇是久负盛名的游戏,它也和俄罗斯方块,扫雷等游戏位列经典游戏的行列。
2.Win32 API介绍
实现贪吃蛇不仅需要C语言与数据结构的一些知识,还要会使用到的一些Win32 API知识,接下来我们就看一下需要用到哪些知识
2.1Win32 API
Windows 这个多作业系统除了协调应用程序的执行、分配内存、管理资源之外, 它同时也是一个很大的服务中心,调用这个服务中心的各种服务(每一种服务就是一个函数),可以帮应用程序达到开启视窗、描绘图形、使用周边设备等目的,由于这些函数服务的对象是应用程序(Application), 所以便称之为 Application Programming Interface,简称 API 函数 。 WIN32 API也就是Microsoft Window32位平台的应用程序编程接口 。
2.2控制台程序
平常我们运行起来的黑框程序其实就是控制台程序 我们可以使用cmd命令来设置控制台窗口的长宽:设置控制台窗口的大小,30行,100列 mode con cols= 100 lines= 30通过这个命令可以把屏幕控制在100列的长度,30行宽度
参考:mode命令
也可以通过命令设置控制台窗口的名字title 贪吃蛇
参考:title命令
这些能在控制台窗口执行的命令,也可以调用C语言函数system来执行。例如:
# include <stdio.h> int main () { // 设置控制台窗⼝的⻓ 宽:设置控制台窗⼝的⼤⼩, 30 ⾏, 100 列 system( "mode con cols=100 lines=30" ); // 设置 cmd 窗⼝名称 system( "title 贪吃蛇 " ); return 0 ; }
2.3控制台屏幕上的坐标COORD
COORD 是Windows API中定义的一个结构体 ,表示一个字符在控制台屏幕幕缓冲区上的坐标,坐标系(0,0) 的原点位于缓冲区的顶部左侧单元格。COORD类型的声明:
typedef struct _ COORD { SHORT X; SHORT Y; } COORD, *PCOORD;给坐标赋值:
COORD pos = { 10 , 15 };2.4GetStdHandle
GetStdHandle是一个Windows API函数。它用于从一个特定的标准设备(标准输入、标准输出或标准错误)中取得一个 句柄 (用来标识不同设备的数值),使用这个句柄可以操作设备。参考:GetStdHandle
HANDLE GetStdHandle (DWORD nStdHandle);实例:
HANDLE hOutput = NULL ; // 获取标准输出的句柄 ( ⽤来标识不同设备的数值 ) hOutput = GetStdHandle(STD_OUTPUT_HANDLE);2.5GetConsoleCursorInfo
这个也是API函数,作用是检索有关指定控制台屏幕缓冲区的光标大小和可见性的信息
BOOL WINAPI GetConsoleCursorInfo ( HANDLE hConsoleOutput, PCONSOLE_CURSOR_INFO lpConsoleCursorInfo ); PCONSOLE_CURSOR_INFO 是指向 CONSOLE_CURSOR_INFO 结构的指针,该结构接收有关主机游标(光标)的信息
参考:GetConsoleCursorInfo
实例
HANDLE hOutput = NULL ; // 获取标准输出的句柄 ( ⽤来标识不同设备的数值 ) hOutput = GetStdHandle (STD_OUTPUT_HANDLE); CONSOLE_CURSOR_INFO CursorInfo; GetConsoleCursorInfo (hOutput, &CursorInfo); // 获取控制台光标信息2.6CONSOLE_CURSOR_INFO
这个结构体,包含有关控制台光标的信息
typedef struct _ CONSOLE_CURSOR_INFO { DWORD dwSize; BOOL bVisible; } CONSOLE_CURSOR_INFO, *PCONSOLE_CURSOR_INFO; 这个结构体的俩成员 • dwSize,由光标填充的字符单元格的百分比。 此值介于1到100之间。 光标外观会变化,范围从完全填充单元格到单元底部的水平线条。 • bVisible,游标的可见性。 如果光标可见,则此成员为TRUE。 CursorInfo.bVisible = false ; // 隐藏控制台光标 参考: CONSOLE_CURSOR_INFO2.7SetConsoleCursorInfo
设置指定控制台屏幕缓冲区的光标的大小和可见性。
BOOL WINAPI SetConsoleCursorInfo ( HANDLE hConsoleOutput, const CONSOLE_CURSOR_INFO *lpConsoleCursorInfo ); 实例: HANDLE hOutput = GetStdHandle (STD_OUTPUT_HANDLE); // 影藏光标操作 CONSOLE_CURSOR_INFO CursorInfo; GetConsoleCursorInfo (hOutput, &CursorInfo); // 获取控制台光标信息 CursorInfo.bVisible = false ; // 隐藏控制台光标 SetConsoleCursorInfo (hOutput, &CursorInfo); // 设置控制台光标状态参考:SetConsoleCursorInfo
2.8SetConsoleCursorPosition
设置指定控制台屏幕缓冲区中的光标位置,我们将想要设置的坐标信息放在COORD类型的pos中,调用SetConsoleCursorPosition函数将光标位置设置到指定的位置。 BOOL WINAPI SetConsoleCursorPosition ( HANDLE hConsoleOutput, COORD pos );参考:SetConsoleCursorPosition
实例:
COORD pos = { 10 , 5 }; HANDLE hOutput = NULL ; // 获取标准输出的句柄 ( ⽤来标识不同设备的数值 ) hOutput = GetStdHandle (STD_OUTPUT_HANDLE); // 设置标准输出上光标的位置为 pos SetConsoleCursorPosition (hOutput, pos);SetPos: 封装一个设置光标位置的函数
/ 设置光标的坐标 void SetPos ( short x, short y) { COORD pos = { x, y }; HANDLE hOutput = NULL ; // 获取标准输出的句柄 ( ⽤来标识不同设备的数值 ) hOutput = GetStdHandle (STD_OUTPUT_HANDLE); // 设置标准输出上光标的位置为 pos SetConsoleCursorPosition (hOutput, pos); }2.9GetAsyncKeyState
这个函数就是获取按键情况,GetAsyncKeyState的函数原型如下: SHORT GetAsyncKeyState ( int vKey );参考:GetAsyncKeyState
将键盘上每个键的虚拟键值传递给函数,函数通过返回值来分辨按键的状态。 GetAsyncKeyState 的返回值是short类型,在上一次调用 GetAsyncKeyState 函数后,如果 返回的16位的short数据中,最高位是1,说明按键的状态是按下,如果最高是0,说明按键的状态是抬起;如果最低位被置为1则说明,该按键被按过,否则为0。如果我们要判断一个键是否被按过,可以检测GetAsyncKeyState返回值的最低值是否为1 对与贪吃蛇来说,按过就行了,所以我们只需要确定键位按过,即判断返回值最低位是否是1 # define KEY_PRESS(VK) ( (GetAsyncKeyState(VK) & 0x1) ? 1 : 0 )我们可以定一个宏专门判断最低位是否是1
参考:虚拟键码(Winuser.h) - Win32 apps
实例:检测数字键 # include <stdio.h> # include <windows.h> 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 ; }3.贪吃蛇游戏思路
3.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,使用两个字节表示一个汉字,所以理论上最多可以表示 256 x 256 = 65536 个符号。 后来为了使C语言适应国际化,C语言的标准中不断加入了国际化的支持。比如:加入了宽字符的类型wchar_t 和宽字符的输入和输出函数,加入了<locale.h>头文件,其中提供了允许程序员针对特定地区(通常是国家或者说某种特定语言的地理区域)调整程序行为的函数。
3.1.1<locale.h>本地化
<locale.h>提供的函数用于控制C标准库中对于不同的地区会产生不一样行为的部分。 在标准中,依赖地区的部分有以下几项: • 数字量的格式 • 货币量的格式 • 字符集 • 日 期和时间的表示形式3.1.2类项
通过修改地区,程序可以改变它的行为来适应世界的不同区域。但地区的改变可能会影响库的许多部分,其中一部分可能是我们不希望修改的。所以C语言支持针对不同的类项进行修改,下面的一个宏, 指定一个类项: • LC_COLLATE:影响字符串比较函数 strcoll() 和 strxfrm() 。 • LC_CTYPE:影响字符处理函数的行为。 • LC_MONETARY:影响货币格式。 • LC_NUMERIC:影响 printf() 的数字格式。 • LC_TIME:影响时间格式 strftime() 和 wcsftime() 。 • LC_ALL - 针对所有类项修改,将以上所有类别设置为给定的语言环境详细介绍:https://learn.microsoft.com/zh-cn/cpp/c-runtime-library/reference/setlocale-wsetlocale?view=msvc-170
3.1.3setlocale函数
char * setlocale ( int category, const char * locale); setlocale 函数用于修改当前地区,可以针对一个类项修改,也可以针对所有类项。 setlocale 的第一个参数可以是前面说明的类项中的一个,那么每次只会影响一个类项,如果第一个参数是LC_ALL,就会影响所有的类项。 C标准给第二个参数仅定义了2种可能取值: "C" (正常模式)和 " " (本地模式)。 在任意程序执行开始,都会隐藏式执行调用: setlocale (LC_ALL, "C" ); 当地区设置为"C"时,库函数按正常方式执行,小数点是一个点。 当程序运行起来后想改变地区,就只能显示调用setlocale函数。用" "作为第2个参数,调用setlocale函数就可以切换到本地模式,这种模式下程序会适应本地环境。 比如: 切换到我们的本地模式后就支持宽字符(汉字)的输出等 。 setlocale (LC_ALL, " " ); // 切换到本地环境3.1.4宽字符的打印
那如果想在屏幕上打印宽字符,怎么打印呢? 宽字符的字面量必须加上前缀“L”,否则 C 语言会把字面量当作窄字符类型处理。前缀“L”在单引 号前面,表示宽字符,对应 wprintf() 的占位符为 %lc ;在双引号前面,表示宽字符串,对应 wprintf() 的占位符为 %ls 。 # include <stdio.h> # include <locale.h> int main () { setlocale (LC_ALL, "" ); wchar_t ch1 = L' ● ' ; wchar_t ch2 = L' ★ ' ; printf ( "%c%c\n" , 'a' , 'b' ); wprintf ( L"%lc\n" , ch1); wprintf ( L"%lc\n" , ch2); return 0 ; }这样一些好看的图案就可以在屏幕上打印了,宽字符占两个字节
3.1.5地图坐标
我们假设实现一个棋盘27行,58列的棋盘(行和列可以根据自己的情况修改),再围绕地图画出墙,如下:
这样我们根据坐标就可以将墙给表示出来了
3.2蛇身和食物
初始化状态,假设蛇的长度是5,蛇身的每个节点是●,在固定的一个坐标处,比如(24, 5)处开始出现蛇,连续5个节点。 注意:蛇的每个节点的x坐标必须是2个倍数,否则可能会出现蛇的一个节点有一半儿出现在墙体中,另外一般在墙外的现象,坐标不好对齐!!! 关于食物,就是在墙体内随机生成一个坐标(x坐标必须是2的倍数),坐标不能和蛇的身体重合,然后打印★。3.3数据结构设计
在游戏运行的过程中,蛇每次吃一个食物,蛇的身体就会变长一节,如果我们 使用链表存储蛇的信 息,那么蛇的每一节其实就是链表的每个节点 。每个节点只要记录好蛇身节点在地图上的坐标就行,所以蛇节点结构如下: typedef struct SnakeNode { int x; int y; struct SnakeNode * next; }SnakeNode, * pSnakeNode; 要管理整条贪吃蛇,我们再封装一个Snake的结构来维护整条贪吃蛇,那我们想象一下,这个蛇有关的信息有哪些:整条蛇指针,食物指针,蛇头方向,游戏状态,总分,每一个食物分,每一步的休眠时间,大概有这些,代码如下: typedef struct Snake { pSnakeNode _pSnake; // 维护整条蛇的指针 pSnakeNode _pFood; // 维护⻝物的指针 enum DIRECTION _Dir; // 蛇头的⽅向 , 默认是向右 enum GAME_STATUS _Status; // 游戏状态 int _Socre; // 游戏当前获得分数 int _foodWeight; // 默认每个⻝物 10 分 int _SleepTime; // 每⾛⼀步休眠时间 }Snake, * pSnake;蛇的方向,分为上,下,左,右可以列举,使用枚举
// ⽅向 enum DIRECTION { UP = 1 , DOWN, LEFT, RIGHT };游戏状态,分为正常运行,撞墙,咬到自己,正常结束可以列举,使用枚举
// 游戏状态 enum GAME_STATUS { OK, // 正常运⾏ KILL_BY_WALL, // 撞墙 KILL_BY_SELF, // 咬到⾃⼰ END_NOMAL // 正常结束 };3.4游戏流程设计
4.核心逻辑分析
4.1游戏主逻辑
首先,程序开始就设置程序支持本地模式,然后进入游戏的主逻辑,有了主逻辑,才能有了明确的方向。 主逻辑分为3个过程: • 游戏开始(GameStart)完成游戏的初始化 • 游戏运行(GameRun)完成游戏运行逻辑的实现 • 游戏结束(GameEnd)完成游戏结束的说明,实现资源释放 # include <locale.h> void test () { int ch = 0 ; srand (( unsigned int ) time ( NULL )); do { Snake snake = { 0 }; GameStart (&snake); GameRun (&snake); GameEnd (&snake); SetPos ( 20 , 15 ); printf ( " 再来⼀局吗? (Y/N):" ); ch = getchar (); getchar (); // 清 理 \n } while (ch == 'Y' ); SetPos ( 0 , 27 ); } int main () { // 修改当前地区为本地模式,为了⽀持中⽂宽字符的打印 setlocale (LC_ALL, "" ); // 测试逻辑 test (); return 0 ; }注意:getchar()是获取字符信息,若此刻你没有输出任何字符,相当于运行上的一个暂停。
4.2 游戏开始(GameStart)
这个模块完成游戏的初始化任务: • 控制台窗口大小的设置 • 控制台窗口名字的设置 • 鼠 标光标的隐藏 • 打印欢迎界面 • 创建地图 • 初始化第蛇 • 创建第一个食物 有了上面的API函数的了解,这些就简单写了 void GameStart (pSnake ps) { // 设置控制台窗⼝的⼤⼩, 30 ⾏, 100 列 //mode 为 DOS 命令 system ( "mode con cols=100 lines=30" ); // 设置 cmd 窗⼝名称 system ( "title 贪吃蛇 " ); // 获取标准输出的句柄 ( ⽤来标识不同设备的数值 ) HANDLE hOutput = GetStdHandle (STD_OUTPUT_HANDLE); // 影藏光标操作 CONSOLE_CURSOR_INFO CursorInfo; GetConsoleCursorInfo (hOutput, &CursorInfo); // 获取控制台光标信息 CursorInfo.bVisible = false ; // 隐藏控制台光标 SetConsoleCursorInfo (hOutput, &CursorInfo); // 设置控制台光标状态 // 打印欢迎界⾯ WelcomeToGame (); // 打印地图 CreateMap (); // 初始化蛇 InitSnake (ps); // 创造第⼀个⻝物 CreateFood (ps); }
4.2.1打印欢迎界面
在游戏正式开始之前,做一些功能提醒
void WelcomeToGame () { SetPos ( 40 , 15 ); printf ( " 欢迎来到贪吃蛇⼩游戏 " ); SetPos ( 40 , 25 ); // 让按任意键继续的出现的位置好看点 system ( "pause" ); system ( "cls" ); SetPos ( 25 , 12 ); printf ( " ⽤ ↑ . ↓ . ← . → 分别控制蛇的移动, F3 为加速, F4 为减速 \n" ); SetPos ( 25 , 13 ); printf ( " 加速将能得到更⾼的分数。 \n" ); SetPos ( 40 , 25 ); // 让按任意键继续的出现的位置好看点 system ( "pause" ); system ( "cls" ); }
system("pause")这个就是按任意键继续的功能与system("cls")(清空屏幕的功能)一连用就达到了切换页面的效果。
4.2.2创建地图
创建地图就是将墙打印出来,因为是宽字符打印,所有使用wprintf函数,打印格式串前使用L
打印地图的关键是要算好坐标,才能在想要的位置打印墙体。 墙体打印的宽字符: # define WALL L' □ '易错点:就是坐标的计算
上:(0,0)到(56,0) 下:(0,26)到(56,26) 左:(0,1)到(0,25) 右:(56,1)到(56,25)创建地图函数CreateMap
void CreateMap () { int i = 0 ; // 上 (0,0)-(56, 0) SetPos ( 0 , 0 ); for (i = 0 ; i < 58 ; i += 2 ) { wprintf ( L"%c" , WALL); } // 下 (0,26)-(56, 26) SetPos ( 0 , 26 ); for (i = 0 ; i < 58 ; i += 2 ) { wprintf ( L"%c" , WALL); } // 左 //x 是 0 , y 从 1 开始增⻓ for (i = 1 ; i < 26 ; i++) { SetPos ( 0 , i); wprintf ( L"%c" , WALL); } //x 是 56 , y 从 1 开始增⻓ for (i = 1 ; i < 26 ; i++) { SetPos ( 56 , i); wprintf ( L"%c" , WALL); } }4.2.3初始化蛇身
蛇最开始长度为5节,每节对应链表的一个节点,蛇身的每一个节点都有自己的坐标。 创建5个节点,然后将每个节点存放在链表中进行管理。创建完蛇身后,将蛇的每一节打印在屏幕上。 • 蛇的初始位置从 (24,5) 开始。 再设置当前游戏的状态,蛇移动的速度,默认的方向,初始成绩,每个食物的分数。 • 游戏状态是:OK • 蛇的移动速度:200毫秒 • 蛇的默认方向:RIGHT • 初始成绩:0 • 每个食物的分数:10 蛇身打印的宽字符: # define BODY L' ● 初始化蛇身函数:InitSnake void InitSnake (pSnake ps) { pSnakeNode cur = NULL ; int i = 0 ; // 创建蛇⾝节点,并初始化坐标 // 头插法 for (i = 0 ; i < 5 ; i++) { // 创建蛇⾝的节点 cur = (pSnakeNode) malloc ( sizeof (SnakeNode)); if (cur == NULL ) { perror ( "InitSnake()::malloc()" ); return ; } // 设置坐标 cur->next = NULL ; cur->x = POS_X + i * 2 ; cur->y = POS_Y; // 头插法 if (ps->_pSnake == NULL ) { ps->_pSnake = cur; } else { cur->next = ps->_pSnake; ps->_pSnake = cur; } } // 打印蛇的⾝体 cur = ps->_pSnake; while (cur) { SetPos (cur->x, cur->y); wprintf ( L"%lc" , BODY); cur = cur->next; } // 初始化贪吃蛇数据 ps->_SleepTime = 200 ; ps->_Socre = 0 ; ps->_Status = OK; ps->_Dir = RIGHT; ps->_foodWeight = 10 ; } 对于这个初始化蛇身的函数,我们要用到单链表的头插,将蛇通过单链表的方式连接到一起,然后一个个遍历,打印宽字符就行了4.2.4创建第一个食物
• 先随机生成食物的坐标 ◦ x坐标必须是2的倍数 ◦ 食 物的坐标不能和蛇身每个节点的坐标重复 • 创建食物节点,打印食物 食物打印的宽字符: # define FOOD L' ★ ' 创建食物的函数:CreateFood void CreateFood (pSnake ps) { int x = 0 ; int y = 0 ; again: // 产⽣的 x 坐标应该是 2 的倍数,这样才可能和蛇头坐标对⻬。 do { x = rand () % 53 + 2 ; y = rand () % 25 + 1 ; } while (x % 2 != 0 ); pSnakeNode cur = ps->_pSnake; // 获取指向蛇头的指针 // ⻝物不能和蛇⾝冲突 while (cur) { if (cur->x == x && cur->y == y) { goto again; } cur = cur->next; } pSnakeNode pFood = (pSnakeNode) malloc ( sizeof (SnakeNode)); // 创建⻝物 if (pFood == NULL ) { perror ( "CreateFood::malloc()" ); return ; } else { pFood->x = x; pFood->y = y; SetPos (pFood->x, pFood->y); wprintf ( L"%c" , FOOD); ps->_pFood = pFood; } }生成食物的函数,用rand随机生成但要注意不要越过墙的坐标范围,而且不能随机生成到蛇身上,也就是说随机坐标要有这俩判断条件,我们这个代码,中间生成的随机值与蛇身重合,就可以用goto语句来重新来一遍循环,将食物节点下一个next值置为空,别忘了,将食物节点,储存到蛇的结构体中
4.3游戏运行(GameRun)
游戏运行期间,右侧打印帮助信息,提示玩家,坐标开始位置(64, 15) 根据游戏状态检查游戏是否继续,如果是状态是OK,游戏继续,否则游戏结束。 如果游戏继续,就是检测按键情况,确定蛇下一步的方向,或者是否加速减速,是否暂停或者退出游戏。 需要的虚拟按键的罗列: • 上:VK_UP • 下:VK_DOWN • 左:VK_LEFT • 右:VK_RIGHT • 空格:VK_SPACE • ESC:VK_ESCAPE • F3:VK_F3 • F4:VK_F4确定了蛇的方向和速度,蛇就可以移动了。
void GameRun (pSnake ps) { // 打印右侧帮助信息 PrintHelpInfo (); do { SetPos ( 64 , 10 ); printf ( " 得分: %d " , ps->_Socre); printf ( " 每个⻝物得分: %d 分 " , ps->_foodWeight); 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_LEFT) && ps->_Dir != RIGHT) { ps->_Dir = LEFT; } else if ( KEY_PRESS (VK_RIGHT) && ps->_Dir != LEFT) { ps->_Dir = RIGHT; } else if ( KEY_PRESS (VK_SPACE)) { pause (); } else if ( KEY_PRESS (VK_ESCAPE)) { ps->_Status = END_NOMAL; break ; } else if ( KEY_PRESS (VK_F3)) { if (ps->_SleepTime >= 80 ) { ps->_SleepTime -= 30 ; ps->_foodWeight += 2 ; // ⼀个⻝物分数最⾼是 20 分 } } else if ( KEY_PRESS (VK_F4)) { if (ps->_SleepTime < 320 ) { ps->_SleepTime += 30 ; ps->_foodWeight -= 2 ; // ⼀个⻝物分数最低是 2 分 } } // 蛇每次⼀定之间要休眠的时间,时间短,蛇移动速度就快 Sleep (ps->_SleepTime); SnakeMove (ps); } while (ps->_Status == OK); }这个函数我们根据虚拟键位值返回的值判断方向,但我们在玩贪吃蛇时,假如蛇方向在上,你不能按下的键,与它方向相反的键你按了不管用,改变不了蛇的状态,在加速减速中,我们通过控制睡眠时间长短,来控制蛇的速度,此外我们还需要确定蛇移动函数,注意:这些信息一定是在游戏正常运行时才能出现的
4.3.1KEY_PRESS
检测按键状态,我们封装了⼀个宏
# define KEY_PRESS(VK) ((GetAsyncKeyState(VK)&0x1) ? 1 : 0)4.3.2PrintHelpInfo
我们可以在游戏运行时,右边放些提示信息,来方便我们玩游戏
void PrintHelpInfo () { // 打印提⽰信息 SetPos ( 64 , 15 ); printf ( " 不能穿墙,不能咬到⾃⼰ \n" ); SetPos ( 64 , 16 ); printf ( " ⽤ ↑ . ↓ . ← . → 分别控制蛇的移动 ." ); SetPos ( 64 , 17 ); printf ( "F3 为加速, F4 为减速 \n" ); SetPos ( 64 , 18 ); printf ( "ESC :退出游戏 .space :暂停游戏 ." ); }4.3.3蛇身移动(SnakeMove)
上面游戏运行过程中我们不是需要定义一个蛇移动的函数嘛
先创建下一个节点,根据移动方向和蛇头的坐标,蛇移动到下一个位置的坐标。 确定了下一个位置后,看下一个位置是否是食物(NextIsFood),是食物就做吃食物处理 (EatFood),如果不是食物则做前进一步的处理(NoFood)。 蛇身移动后,判断此次移动是否会造成撞墙(KillByWall)或者撞上自己蛇身(KillBySelf),从而影响游戏的状态。 那NextIsFood,EatFood,NoFood,KillByWall,KillBySelf函数我们都需要一一定义 void SnakeMove (pSnake ps) { // 创建下⼀个节点 pSnakeNode pNextNode = (pSnakeNode) malloc ( sizeof (SnakeNode)); if (pNextNode == NULL ) { perror ( "SnakeMove()::malloc()" ); return ; } // 确定下⼀个节点的坐标,下⼀个节点的坐标根据,蛇头的坐标和⽅向确定 switch (ps->_Dir) { case UP: { pNextNode->x = ps->_pSnake->x; pNextNode->y = ps->_pSnake->y - 1 ; } break ; case DOWN: { pNextNode->x = ps->_pSnake->x; pNextNode->y = ps->_pSnake->y + 1 ; } break ; case LEFT: { pNextNode->x = ps->_pSnake->x - 2 ; pNextNode->y = ps->_pSnake->y; } break ; case RIGHT: { pNextNode->x = ps->_pSnake->x + 2 ; pNextNode->y = ps->_pSnake->y; } break ; } // 如果下⼀个位置就是⻝物 if ( NextIsFood (pNextNode, ps)) { EatFood (pNextNode, ps); } else // 如果没有⻝物 { NoFood (pNextNode, ps); } KillByWall (ps); KillBySelf (ps); }4.3.3.1NextIsFood
假如下一个坐标是食物,我们需要返回1或0,1就代表是食物,0就代表不是食物,方便If判断
//pSnakeNode psn 是下⼀个节点的地址 //pSnake ps 维护蛇的指针 int NextIsFood (pSnakeNode psn, pSnake ps) { return (psn->x == ps->_pFood->x) && (psn->y == ps->_pFood->y); }4.3.3.2 EatFood
这个就是吃掉事物的函数,假如下一个坐标是食物,我们吃掉它,我们蛇身需要增长,我们想一下,我们可以直接用食物的节点头插到我们蛇身,成为我们新蛇头节点,这样就可以让蛇身增长了
吃完之后,得需要再打印一遍蛇,吃掉的话,总分就会加食物分,我们不要忘记
//pSnakeNode psn 是下⼀个节点的地址 //pSnake ps 维护蛇的指针 void EatFood (pSnakeNode psn, pSnake ps) { // 头插法 psn->next = ps->_pSnake; ps->_pSnake = psn; // 打印蛇 pSnakeNode cur = ps->_pSnake; while (cur) { SetPos (cur->x, cur->y); wprintf ( L"%c" , BODY); cur = cur->next; } ps->_Socre += ps->_foodWeight; // 释放⻝物节点 free (ps->_pFood); // 创建新的⻝物 CreateFood (ps); }4.3.3.3NoFood
这个函数就是下一步不是食物时,将下一个节点头插入蛇的身体,并将之前蛇身最后一个节点打印为空格,释放掉蛇身的最后一个节点。 易错点: 这里最容易错误的是,释放最后一个结点后,还得将指向在最后一个结点的指针改为NULL, 保证蛇尾打印可以正常结束,不会越界访问。 相当于整体思路:先头插->再打印蛇(不打印最后一个节点)->再打印空格->最后释放掉最后一个节点空间 //pSnakeNode psn 是下⼀个节点的地址 //pSnake ps 维护蛇的指针 void NoFood (pSnakeNode psn, pSnake ps) { // 头插法 psn->next = ps->_pSnake; ps->_pSnake = psn; // 打印蛇 pSnakeNode cur = ps->_pSnake; { SetPos (cur->x, cur->y); wprintf ( L"%c" , BODY); cur = cur->next; } // 最后⼀个位置打印空格,然后释放节点 SetPos (cur->next->x, cur->next->y); printf ( " " ); free (cur->next); cur->next = NULL ; }4.3.3.4KillByWall
判断蛇头的坐标是否和墙的坐标冲突函数
这个函数目的就在于若蛇头坐标与墙坐标重叠就将游戏状态变为因撞墙而结束
//pSnake ps 维护蛇的指针 int KillByWall (pSnake ps) { if ((ps->_pSnake->x == 0 ) || (ps->_pSnake->x == 56 ) || (ps->_pSnake->y == 0 ) || (ps->_pSnake->y == 26 )) { ps->_Status = KILL_BY_WALL; return 1 ; } return 0 ; }4.3.3.5KillBySelf
判断蛇头的坐标是否和蛇身体的坐标冲突,若冲突,就将游戏状态变为因撞到蛇身而结束
/pSnake ps 维护蛇的指针 int KillBySelf (pSnake ps) { pSnakeNode cur = ps->_pSnake->next; while (cur) { if ((ps->_pSnake->x == cur->x) && (ps->_pSnake->y == cur->y)) ps->_Status = KILL_BY_SELF; return 1 ; } cur = cur->next; } return 0 ; }
4.4游戏结束
游戏状态不再是OK(游戏继续)的时候,要告知游戏结束的原因,并且释放蛇身节点。
void GameEnd (pSnake ps) { pSnakeNode cur = ps->_pSnake; SetPos ( 24 , 12 ); switch (ps->_Status) { case END_NOMAL: printf ( " 您主动退出游戏 \n" ); break ; case KILL_BY_SELF: printf ( " 您撞上⾃⼰了 , 游戏结束 !\n" ); break ; case KILL_BY_WALL: printf ( " 您撞墙了 , 游戏结束 !\n" ); break ; } // 释放蛇⾝的节点 while (cur) { pSnakeNode del = cur; cur = cur->next; free (del); } }5.参考代码
test.cpp
#include "Snake.h" #include <locale.h> void test() { int ch = 0; srand((unsigned int)time(NULL)); do { Snake snake = { 0 }; GameStart(&snake); GameRun(&snake); GameEnd(&snake); SetPos(20, 15); printf("再来⼀局吗?(Y/N):"); ch = getchar(); getchar();//清理\n } while (ch == 'Y' || ch == 'y'); SetPos(0, 27); }int main(){ //修改当前地区为本地模式,为了⽀持中⽂宽字符的打印 setlocale(LC_ALL, ""); //测试逻辑 test(); return 0;}
snake.h #pragma once#include <windows.h>#include <time.h>#include <stdio.h>#define KEY_PRESS(VK) ((GetAsyncKeyState(VK)&0x1) ? 1 : 0)//⽅向enum DIRECTION{ UP = 1, DOWN, LEFT, RIGHT};//游戏状态enum GAME_STATUS{ OK,//正常运⾏ KILL_BY_WALL,//撞墙 KILL_BY_SELF,//咬到⾃⼰ END_NOMAL//正常结束};#define WALL L'□'#define BODY L'●' //★○●◇◆□■#define FOOD L'★' //★○●◇◆□■//蛇的初始位置#define POS_X 24#define POS_Y 5//蛇⾝节点typedef struct SnakeNode{ int x; int y; struct SnakeNode* next;}SnakeNode, * pSnakeNode;typedef struct Snake{ pSnakeNode _pSnake;//维护整条蛇的指针 pSnakeNode _pFood;//维护⻝物的指针 enum DIRECTION _Dir;//蛇头的⽅向默认是向右 enum GAME_STATUS _Status;//游戏状态 int _Socre;//当前获得分数 int _foodWeight;//默认每个⻝物10分 int _SleepTime;//每⾛⼀步休眠时间}Snake, * pSnake;//游戏开始前的初始化void GameStart(pSnake ps);//游戏运⾏过程void GameRun(pSnake ps);//游戏结束void GameEnd(pSnake ps);//设置光标的坐标void SetPos(short x, short y);//欢迎界⾯void WelcomeToGame();//打印帮助信息void PrintHelpInfo();//创建地图void CreateMap();//初始化蛇void InitSnake(pSnake ps);//创建⻝物void CreateFood(pSnake ps);//暂停响应void pause();//下⼀个节点是⻝物int NextIsFood(pSnakeNode psn, pSnake ps);//吃⻝物void EatFood(pSnakeNode psn, pSnake ps);//不吃⻝物void NoFood(pSnakeNode psn, pSnake ps);//撞墙检测int KillByWall(pSnake ps);//撞⾃⾝检测int KillBySelf(pSnake ps);//蛇的移动void SnakeMove(pSnake ps);//游戏初始化void GameStart(pSnake ps);//游戏运⾏void GameRun(pSnake ps);//游戏结束void GameEnd(pSnake ps);
snake.cpp #include "Snake.h"//设置光标的坐标void SetPos(short x, short y){ COORD pos = { x, y }; HANDLE hOutput = NULL;//获取标准输出的句柄(⽤来标识不同设备的数值) hOutput = GetStdHandle(STD_OUTPUT_HANDLE); //设置标准输出上光标的位置为pos SetConsoleCursorPosition(hOutput, pos);}void WelcomeToGame(){ SetPos(40, 15); printf("欢迎来到贪吃蛇⼩游戏"); SetPos(40, 25);//让按任意键继续的出现的位置好看点 system("pause"); system("cls"); SetPos(25, 12); printf("⽤ ↑ . ↓ . ← . → 分别控制蛇的移动, F3为加速,F4为减速\n"); SetPos(25, 13); printf("加速将能得到更⾼的分数。\n"); SetPos(40, 25);//让按任意键继续的出现的位置好看点 system("pause"); system("cls");}void CreateMap(){ int i = 0; //上(0,0)-(56, 0) SetPos(0, 0); for (i = 0; i < 58; i += 2) { wprintf(L"%c", WALL); } //下(0,26)-(56, 26) SetPos(0, 26); for (i = 0; i < 58; i += 2) { wprintf(L"%c", WALL); } //左 //x是0,y从1开始增⻓ for (i = 1; i < 26; i++) { SetPos(0, i); wprintf(L"%c", WALL); } //x是56,y从1开始增⻓ for (i = 1; i < 26; i++) { SetPos(56, i);wprintf(L"%c", WALL); } } void InitSnake(pSnake ps) { pSnakeNode cur = NULL; int i = 0; //创建蛇⾝节点,并初始化坐标 //头插法 for (i = 0; i < 5; i++) { //创建蛇⾝的节点 cur = (pSnakeNode)malloc(sizeof(SnakeNode)); if (cur == NULL) { perror("InitSnake()::malloc()"); return; } //设置坐标 cur->next = NULL; cur->x = POS_X + i * 2; cur->y = POS_Y; //头插法 if (ps->_pSnake == NULL) { ps->_pSnake = cur; } else { cur->next = ps->_pSnake; ps->_pSnake = cur; } } //打印蛇的⾝体 cur = ps->_pSnake; while (cur) { SetPos(cur->x, cur->y); wprintf(L"%c", BODY); cur = cur->next; }//初始化贪吃蛇数据 ps->_SleepTime = 200; ps->_Socre = 0; ps->_Status = OK; ps->_Dir = RIGHT; ps->_foodWeight = 10; } void CreateFood(pSnake ps) { int x = 0; int y = 0; again: //产⽣的x坐标应该是2的倍数,这样才可能和蛇头坐标对⻬。 do { x = rand() % 53 + 2; y = rand() % 25 + 1; } while (x % 2 != 0); pSnakeNode cur = ps->_pSnake;//获取指向蛇头的指针 //⻝物不能和蛇⾝冲突 while (cur) { if (cur->x == x && cur->y == y) { goto again; } cur = cur->next; } pSnakeNode pFood = (pSnakeNode)malloc(sizeof(SnakeNode)); //创建⻝物 if (pFood == NULL) { perror("CreateFood::malloc()"); return; } else { pFood->x = x; pFood->y = y; SetPos(pFood->x, pFood->y); wprintf(L"%c", FOOD); ps->_pFood = pFood;}}void PrintHelpInfo(){ //打印提⽰信息 SetPos(64, 15); printf("不能穿墙,不能咬到⾃⼰\n"); SetPos(64, 16); printf("⽤↑.↓.←.→分别控制蛇的移动."); SetPos(64, 17); printf("F3 为加速,F4 为减速\n"); SetPos(64, 18); printf("ESC :退出游戏.space:暂停游戏.");}void pause()//暂停{ while (1) { Sleep(300); if (KEY_PRESS(VK_SPACE)) { break; } }}//pSnakeNode psn 是下⼀个节点的地址//pSnake ps 维护蛇的指针int NextIsFood(pSnakeNode psn, pSnake ps){ return (psn->x == ps->_pFood->x) && (psn->y == ps->_pFood->y);}//pSnakeNode psn 是下⼀个节点的地址//pSnake ps 维护蛇的指针void EatFood(pSnakeNode psn, pSnake ps){ //头插法 psn->next = ps->_pSnake; ps->_pSnake = psn; pSnakeNode cur = ps->_pSnake; //打印蛇 while (cur){ SetPos(cur->x, cur->y); wprintf(L"%c", BODY); cur = cur->next; } ps->_Socre += ps->_foodWeight; free(ps->_pFood); CreateFood(ps);}//pSnakeNode psn 是下⼀个节点的地址//pSnake ps 维护蛇的指针void NoFood(pSnakeNode psn, pSnake ps){ //头插法 psn->next = ps->_pSnake; ps->_pSnake = psn; pSnakeNode cur = ps->_pSnake; //打印蛇 while (cur->next->next) { SetPos(cur->x, cur->y); wprintf(L"%c", BODY); cur = cur->next; } //最后⼀个位置打印空格,然后释放节点 SetPos(cur->next->x, cur->next->y); printf(" "); free(cur->next); cur->next = NULL;}//pSnake ps 维护蛇的指针int KillByWall(pSnake ps){ if ((ps->_pSnake->x == 0) || (ps->_pSnake->x == 56) || (ps->_pSnake->y == 0) || (ps->_pSnake->y == 26)) { ps->_Status = KILL_BY_WALL; return 1; } return 0;}//pSnake ps 维护蛇的指针int KillBySelf(pSnake ps){ pSnakeNode cur = ps->_pSnake->next; while (cur) { if ((ps->_pSnake->x == cur->x) && (ps->_pSnake->y == cur->y)) { ps->_Status = KILL_BY_SELF; return 1; } cur = cur->next; } return 0;}void SnakeMove(pSnake ps){ //创建下⼀个节点 pSnakeNode pNextNode = (pSnakeNode)malloc(sizeof(SnakeNode)); if (pNextNode == NULL) { perror("SnakeMove()::malloc()"); return; } //确定下⼀个节点的坐标,下⼀个节点的坐标根据,蛇头的坐标和⽅向确定 switch (ps->_Dir) { case UP: { pNextNode->x = ps->_pSnake->x; pNextNode->y = ps->_pSnake->y - 1; } break; case DOWN: { pNextNode->x = ps->_pSnake->x; pNextNode->y = ps->_pSnake->y + 1; } break; case LEFT: { pNextNode->x = ps->_pSnake->x - 2; pNextNode->y = ps->_pSnake->y;} break; case RIGHT: { pNextNode->x = ps->_pSnake->x + 2; pNextNode->y = ps->_pSnake->y; } break; } //如果下⼀个位置就是⻝物 if (NextIsFood(pNextNode, ps)) { EatFood(pNextNode, ps); } else//如果没有⻝物 { NoFood(pNextNode, ps); } KillByWall(ps); KillBySelf(ps);}void GameStart(pSnake ps){ //设置控制台窗⼝的⼤⼩,30⾏,100列 //mode 为DOS命令 system("mode con cols=100 lines=30"); //设置cmd窗⼝名称 system("title 贪吃蛇"); //获取标准输出的句柄(⽤来标识不同设备的数值) HANDLE hOutput = GetStdHandle(STD_OUTPUT_HANDLE); //影藏光标操作 CONSOLE_CURSOR_INFO CursorInfo; GetConsoleCursorInfo(hOutput, &CursorInfo);//获取控制台光标信息 CursorInfo.bVisible = false; //隐藏控制台光标 SetConsoleCursorInfo(hOutput, &CursorInfo);//设置控制台光标状态 //打印欢迎界⾯ WelcomeToGame(); //打印地图 CreateMap();//初始化蛇 InitSnake(ps); //创造第⼀个⻝物 CreateFood(ps);}void GameRun(pSnake ps){ //打印右侧帮助信息 PrintHelpInfo(); do { SetPos(64, 10); printf("得分:%d ", ps->_Socre); printf("每个⻝物得分:%d分", ps->_foodWeight); 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_LEFT) && ps->_Dir != RIGHT) { ps->_Dir = LEFT; } else if (KEY_PRESS(VK_RIGHT) && ps->_Dir != LEFT) { ps->_Dir = RIGHT; } else if (KEY_PRESS(VK_SPACE)) { pause(); } else if (KEY_PRESS(VK_ESCAPE)) { ps->_Status = END_NOMAL; break; } else if (KEY_PRESS(VK_F3)) { if (ps->_SleepTime >= 50) { ps->_SleepTime -= 30; ps->_foodWeight += 2;} } else if (KEY_PRESS(VK_F4)) { if (ps->_SleepTime < 350) { ps->_SleepTime += 30; ps->_foodWeight -= 2; if (ps->_SleepTime == 350) { ps->_foodWeight = 1; } } } //蛇每次⼀定之间要休眠的时间,时间短,蛇移动速度就快 Sleep(ps->_SleepTime); SnakeMove(ps); } while (ps->_Status == OK);}void GameEnd(pSnake ps){ pSnakeNode cur = ps->_pSnake; SetPos(24, 12); switch (ps->_Status) { case END_NOMAL: printf("您主动退出游戏\n"); break; case KILL_BY_SELF: printf("您撞上⾃⼰了 ,游戏结束!\n"); break; case KILL_BY_WALL: printf("您撞墙了,游戏结束!\n"); break; } //释放蛇⾝的节点 while (cur) { pSnakeNode del = cur; cur = cur->next; free(del); }}
结束语
贪吃蛇博客就总结完了,有什么问题,欢迎各位大佬评论,总的来说,结合了C语言知识,数据结构知识,API函数方面有关函数等,只要思路顺清楚还是比较简单的。
OK,感谢观看!!!