贪吃蛇小游戏的实现【C语言魅力时刻】_c#贪吃蛇小游戏实现关卡难度的控制-程序员宅基地

技术标签: 游戏程序  c语言  游戏专栏  开发语言  

前言:

在这里插入图片描述
大家都玩过贪吃蛇大作战吧,和扫雷,俄罗斯方块一样,作为世界上最负盛名且历史最为悠久的游戏之一,可以说,它几乎成为了人类游戏史上经典之作之一,从早期的贪吃蛇到现在的即时战略游戏,策略游戏,FPS游戏,MOBA游戏,TGA系列,角色扮演…游戏事业的发展几乎成为了最新科技的符号之一,程序是游戏的载体,那当我们已经掌握了C语言的很多知识之后,我们能否实现一个贪吃蛇游戏呢?下面让我们从无到有,来完整的书写一个C语言为基础的贪吃蛇游戏。

1.贪吃蛇游戏的大体实现思路:

任何一个游戏,我们首先都需要书写一个大体的游戏构建思路,对于一个游戏来说,常规的构建思路大致分为三个步骤(我们首先面向客户端来说):游戏选择界面(菜单),游戏运行,游戏结束。所以,我们由此需要构建的东西有:游戏的菜单窗口,游戏的地图窗口,以及配套的一些游戏提示性的话语和文字。但作为游戏的设计者,我们仅仅站在客户的角度是不够的,除了用户能看到的前端外,我们也要进行后端的处理。在后端,我们要处理的问题是:蛇如何按照我们的电脑按键输入去对应移动,如何控制蛇的速度,游戏运行方式如何判定,数据如何初始化,蛇吃掉食物后如何增加蛇的长度….针对这些问题,我大致将其整理为如下的一个游戏实现逻辑:
在这里插入图片描述
由上面的游戏逻辑,我们大致将我们的主程序分为三个阶段的函数来运行,第一阶段函数,第二阶段函数,第三阶段函数。
故我们的主程序就为如下:

//我这里将其放在void test()函数里,方便调试和测试
void game()
{
    
	//贪吃蛇游戏大概分为三个部分进行,故我们分为三个大函数来进行
	//1.游戏开始--初始化游戏的过程,将蛇自身结构体里面的数据进行初始化,地图的构造,蛇的出生点设置以及蛇的打印,游戏界面的生成等
	 GameStart(&snake);//传地址直接改变实参
	//2.游戏运行--游戏的正常运行过程
	 GameRun(&snake);
	//3.游戏结束--对游戏中的一些占用内存的资源进行释放回收和清理
	 GameEnd(&snake);
}
int main()
{
    
     test();
   return 0;
}

2.对象属性配置:

好,接下来让我们继续,当我们大致将大块分好后,我们就要创建我们的人物:蛇
在任何一门语言中,我们都有专门用来描述一个对象自身属性的自定义类型,在C语言中即是结构体,而在C++中则升级为了更为高级的类。故在C语言中,我们就用结构体来记录蛇的信息。
如下:

typedef struct SnakeNode
{
    
	//描述蛇身节点的坐标
	int x;
	int y;
	struct SnakeNode* next;
}SnakeNode,*pSnakeNode;//*pSnakeNode为这个结构体的指针重命名
typedef struct Snake
{
    
	pSnakeNode _pSnake;//指向贪吃蛇头节点的指针,我们在游戏过程中是操控蛇头进行游戏,利用蛇头来判定游戏状态,故控制蛇头很关键
	pSnakeNode _pFood;//指向食物的节点,本质上食物被蛇吃了后也算蛇的节点的一部分,故我们也用一个指针来管理
	int _Score;//累计的得分
	int _FoodWeight;//吃一个食物的分数
	int _SleepTime;//蛇的速度,本质上为一个延时函数,休眠的时间越长速度越慢,反之速度越快
	enum DIRECTION _Dir;//描述蛇的方向,由于蛇的方向固定,且每次只能按一个按键,故我们利用枚举体来处理,未来的Dir取值只可能是4个方向中的一种
	enum GAME_STATUS _Status;//游戏状态,判断蛇是撞墙还是自己吃到了自己还是按ESC键退出了
}Snake,*pSnake;

我们首先把蛇分为两个部分,由于我们的移动涉及到蛇的每一个节点,故我们首先对蛇的每一个节点构建一个结构体,同时我们要注意,我们在控制台屏幕上的运行,本质上类似我们数学上的平面直角坐标系,其大致的坐标系如下:
在这里插入图片描述
以控制台屏幕的左上角为0 0顶点为中心,向右为x轴向延展,向下为y轴向延展,而我们的每一个数据的打印,构建都是以这种坐标的形式展开的,其展开的方式就类似一个二维数组。故我们蛇的移动也是以坐标为前提的移动,因此,我们对于蛇的每一个节点都要存储其坐标,这样保证了我们之后处理移动,撞墙,吃掉自身等情况时基于坐标进行处理,同时,我们的蛇是一个整体,每一个节点都要连在一起,现实中我们可以用绳子串起来,在C语言中我们就可以利用单链表来将其连起来,故我们的每一个节点还要存储一个指向next指针。
故我们的节点结构体如下:

typedef struct SnakeNode
{
    
	//描述蛇身节点的坐标
	int x;
	int y;
	struct SnakeNode* next;
}SnakeNode,*pSnakeNode;//*pSnakeNode为这个结构体的指针重命名

单个节点写好后,我们还不够,我们还需要对蛇的整体可以进行掌控,故我们还需要建立一个蛇整体的结构体,来掌控蛇以及关于蛇的一切信息,在这里,让我们认真讨论一下蛇的结构体内部应该都有些什么:

typedef struct Snake
{
    
	pSnakeNode _pSnake;//指向贪吃蛇头节点的指针,我们在游戏过程中是操控蛇头进行游戏,利用蛇头来判定游戏状态,故控制蛇头很关键
	pSnakeNode _pFood;//指向食物的节点,本质上食物被蛇吃了后也算蛇的节点的一部分,故我们也用一个指针来管理
	int _Score;//累计的得分
	int _FoodWeight;//吃一个食物的分数
	int _SleepTime;//蛇的速度,本质上为一个延时函数,休眠的时间越长速度越慢,反之速度越快
	enum DIRECTION _Dir;//描述蛇的方向,由于蛇的方向固定,且每次只能按一个按键,故我们利用枚举体来处理,未来的Dir取值只可能是4个方向中的一种
	enum GAME_STATUS _Status;//游戏状态,判断蛇是撞墙还是自己吃到了自己还是按ESC键退出了
}Snake,*pSnake;

首先根据我们之前玩过的贪吃蛇的经历,我们知道我们控制的是蛇的第一个头节点,通过头节点的方向改变来控制蛇的移动,故我们的蛇结构体的第一个数据就要 是指向蛇头节点的指针_pSnake,同时,我们整个贪吃蛇游戏中和蛇交互的只有对应的食物,故我们完全可以将其放在蛇的结构体内部,从而实现蛇头位置和食物位置的判断_pFood,倘若单独写就要单独传参数,那样很麻烦。我们同时在蛇结构体里统计蛇的累计得分,每一个食物的分数(因为我们设计速度加快可以使吃一个食物所得的分数改变的机制),蛇的速度,本质上蛇的速度就是延时刷新率的改变,延时参数越大,速度越慢,反之速度越快,同时我们还需要记录游戏的状态(是撞墙还是正常游戏结束还是吃掉了自身)和蛇移动的方向,由于我们每次的移动方向只能有一个,但我们可以选择我们的4个移动方向,故我们利用一个枚举体来为存储我们每次的移动方向的选择,同理,我们的游戏状态每次也只能有一个,故我们也利用一个枚举体去存储可供我们选择的游戏状态,我们应该积累这个方法:**!!将为一个多种选择的变量存储在枚举体里,这样我们就可以实现一个选择的功能!!**由此,我们的游戏的基本对象属性配置就完成了,如下:

enum DIRECTION//蛇移动方向枚举体
{
    
	UP=1,
	DOWN,
	LEFT,
	RIGHT
};
enum GAME_STATUS//游戏状态枚举体
{
    
	OK,//游戏正常运行
	END_NORMAL,//正常退出
	KILL_BY_WALL,//撞墙
	KILL_BY_SELF//自己吃自己了
};
//贪吃蛇单个节点的结构体
typedef struct SnakeNode
{
    
	//描述蛇身节点的坐标
	int x;
	int y;
	struct SnakeNode* next;
}SnakeNode,*pSnakeNode;//*pSnakeNode为这个结构体的指针重命名

//贪吃蛇个体的结构体
//我们整个游戏要控制的是蛇
typedef struct Snake
{
    
	pSnakeNode _pSnake;//指向贪吃蛇头节点的指针,我们在游戏过程中是操控蛇头进行游戏,利用蛇头来判定游戏状态,故控制蛇头很关键
	pSnakeNode _pFood;//指向食物的节点,本质上食物被蛇吃了后也算蛇的节点的一部分,故我们也用一个指针来管理
	int _Score;//累计的得分
	int _FoodWeight;//吃一个食物的分数
	int _SleepTime;//蛇的速度,本质上为一个延时函数,休眠的时间越长速度越慢,反之速度越快
	enum DIRECTION _Dir;//描述蛇的方向,由于蛇的方向固定,且每次只能按一个按键,故我们利用枚举体来处理,未来的Dir取值只可能是4个方向中的一种
	enum GAME_STATUS _Status;//游戏状态,判断蛇是撞墙还是自己吃到了自己还是按ESC键退出了
}Snake,*pSnake;

3.游戏第一阶段———初始化阶段的具体实现:

在这个阶段:我们要实现的如下:

void GameStart(pSnake ps);//游戏第一阶段:游戏初始化阶段函数
void HideCursor();//隐藏光标
void Setpos(int x, int y);//光标位置调整
void WelcomeToGme();//打印欢迎界面
void CreateMap();//创建地图
void InitSnake(pSnake snake);//初始化蛇并在出生点打印出蛇
void CreateFood(pSnake snake);//设置第一个食物

我们统一将其放在GameStart函数里面:
那首先我们就需要学会掌控控制台:
何为控制台,即是我们或许运行过无数次跳出来的黑色框框,对于控制台,C语言使用system(“ ”)来调用windows指令来执行控制台的指令(其实Windows控制台和LINUX差不多,其基本的原理是相同的)在这里,我们需要掌握的几条指令如下:
1.system(“mode con cols=x lines=y”):对控制台的大小进行控制
2.system("title 对应名字“):用来改变控制台的名字
3…system(“pause”):负责暂停程序,并打印出按任意键继续,可控制输出坐标
4.system(“cls”):刷新界面,然后执行接下来的程序
注意:对于双引号里面的命令,其实就是对应着windows控制台的操作指令,只不过C语言用这种方式命令控制台执行!!
然后我们还要介绍一下何为API,这对于我们理解我们贪吃蛇的页面操作很关键:
Windows 这个多作业系统除了协调应⽤程序的执⾏、分配内存、管理资源之外, 它同时也是⼀个很⼤ 的服务中⼼,调⽤这个服务中⼼的各种服务(每⼀种服务就是⼀个函数),可以帮应⽤程式达到开启 视窗、描绘图形、使⽤周边设备等⽬的,由于这些函数服务的对象是应⽤程序(Application), 所以便称之为 Application Programming Interface,简称 API 函数。WIN32 API也就是Microsoft Windows32位平台的应⽤程序编程接⼝。

1.游戏欢迎界面以及游戏玩法介绍的函数:

我们大致要实现的效果如下:
在这里插入图片描述
在这里插入图片描述
你可以看到,这两张游戏界面中,我们首先修改了我们的控制台的名称,其次我们隐藏了我们的光标,然后我同时做到了在控制台上的任意位置打印输出我们的文字。
前面的改名字和暂停我已经说过,接下来让我们来说说如何改变控制台坐标以及如何隐藏光标。

1.改变控制台坐标

在windows API 中提供了一种结构体类型名为COORD,表示一个字符在控制台屏幕缓冲区上的坐标,坐标(0.0)的原点位于缓冲区顶部的左侧单元格。
结构体具体如下:

typedef struct _COORD {
    
 SHORT X;
 SHORT Y;
} COORD, *PCOORD;

故我们可以给坐标赋值COORD pos={x,y}.
但这样的处理控制台是不相应的,我们还需要另一个函数:SetConsoleCursorPosition
SetConsoleCursorPosition函数:
其函数的基本格式为:
BOOL WINAPI SetConsoleCursoPosition(HANDLE hconsoleOutput,COORD pos);
这个函数的作用是将我们设置的光标坐标配置到我们的屏幕设备缓冲区,我们的参数除了一个COORD的坐标值外,还需要一个获取到另一个参数,即我们的设备缓冲区的句柄,在这里我们就又需要一个函数:GetStdHandle
GetStdHandle函数:
其函数的基本格式为:
HANDLE GetStdHandle(DWORD nStdHandle);
这个函数的作用即是得到一个我们想要的设备的句柄,注意,在计算机世界中,任何设备的获得都需要得到其权限,你可以立即为这个函数是用来获取对应设备的权限来使我们可以继续使用设备的。,我们需要传入一个DWORD nstdHandle即一个标准的设备参数,在这里,我们选择STD_OUTPUT_HANDLE,即标准输出的DWORD参数作为设备参数即可。
故现在,我们将他们结合起来,就可以组成一个可以改变位置的函数–Setpos如下:

void Setpos(int x, int y)//光标位置调整
{
    
	HANDLE houtput = GetStdHandle(STD_OUTPUT_HANDLE);
	COORD pos = {
     x,y };
	SetConsoleCursorPosition(houtput, pos);
}

这样,我们只要出入对应的坐标,下一次我们就可以从对应的位置开始输出。

2.隐藏光标:

我们在贪吃蛇游戏中是不需要使用光标的,故我们直接将其隐藏即可,其方法如下:
同样,我们首先需要先获取控制台输出缓冲区的句柄,然后,我们还需要获取到控制台光标的大小和可见性信息,这就需要我们引入下一个函数:GetConsoleCursorInfo
GetConsoleCursorInfo函数:
函数的对应参数如下:
BOOL WINAPI GetConsoleCursorInfo( HANDLE hConsoleOutput, PCONSOLE_CURSOR_INFO* lpConsoleCursorInfo);
这个函数的作用即是获得对应的缓冲区的光标信息(但前提是,我们的这个句柄指向的应该是一个对应的输出缓冲区,这样才有光标的概念,指向音频设备等是没法使用这个函数的),而我们的第二个参数要注意,它是一个指向PCONSOLE_CURSOR_INFO类型变量的指针,而不是这个变量本身,故我们首先需要一个对应的变量,然后我们对其传地址。
光标信息的结构体PCONSOL_CURSOR_INFO结构体的的形式如下:

typedef struct _CONSOLE_CURSOR_INFO 
{
    
 DWORD dwSize;
 BOOL bVisible;
} CONSOLE_CURSOR_INFO, *PCONSOLE_CURSOR_INFO;

1.dwSize:光标填充的字符单元格的百分比,其值介于1-100之间,外观会随着百分比的变化而变化,其范围会从完全填充单元格到只剩下单元格底下的水平细线。
2.bvisible:控制光标的可见性:设置为TRUE(非0)即为可见(在不改变的情况下,默认bvisible都是TRUE),设置为false(0)即为不可见

故在这里,我们想要隐藏光标,就需要我们设置bvisible为false.
但与改变光标位置一样,我们同样也需要一个函数来配置我们的光标属性导入到屏幕缓冲区中—SetConsoleCursorInfo
SetConsoleCursorInfo函数:
其函数基本格式为:
BOOL WINAPI SetConsoleCursorInfo( HANDLE hConsoleOutput, const CONSOLE_CURSOR_INFO* lpConsoleCursorInfo);
这个函数的作用就是配置我们之前的关于光标的属性调整,将其导入到屏幕缓冲区中,我们的参数和前面的获取光标信息的参数一样,别忘了传指针即可。
由此,我们将上面的知识结合起来,即可组成一个隐藏光标的函数:

void HideCursor()//隐藏光标
{
    
	HANDLE houtput = GetStdHandle(STD_OUTPUT_HANDLE);//	获取输出设备句柄
	CONSOLE_CURSOR_INFO CursorInfo;
	GetConsoleCursorInfo(houtput, &CursorInfo);//获取光标属性信息
	CursorInfo.bVisible = false;//调整属性隐藏光标
	SetConsoleCursorInfo(houtput, &CursorInfo);//重置光标属性
}

3.打印欢迎界面和游戏提示界面:

前面的铺垫工作做好,我们就可以正式打印出来我们的界面了,其具体的函数实现如下:

void WelcomeToGame()//打印欢迎界面
{
    
	Setpos(40, 15);
	printf("欢迎来到贪吃蛇小游戏!");
	Setpos(40, 25);//让任意继续出现在别的位置,注意pause本质上也是要打印在屏幕上的,故也要调整位置
	system("pause");//按任意键继续
	system("cls");//刷新界面
	Setpos(25, 12);
	printf(" ↑ . ↓ . ← . → 分别控制蛇的移动, F3为加速,F4为减速\n");
	Setpos(25, 13);
	printf("加速将能得到更多的分数。\n");
	Setpos(40, 25);
	system("pause");
	system("cls");
}

注意,我们这里是反复利用cls和pause来进行停顿的页面切换以及页面的刷新,同时利用Setpos来改变我们的打印位置
先pause后cls,就可以做到按任意键切换页面的功能,这个在制作游戏的时候很常用,要积累下来

4.创建地图:

在这里插入图片描述
我们要打印的地图是由这样的正方形边框组成。
由于之前我们都学过打印空心正方体的方式,在这里,我们采取那样的方式就可以实现,但显然,在创建地图的时候我们有更重要的知识,我们应该如何理解我们的屏幕?
前面我们已经说过,我们的屏幕更像是一张二维坐标图,显然这没什么好说的,但本次我们的图形不再是基本的符号和数字了,你会从屏幕上看到,这些使用的符号都不是计算机默认的符号,我们将这样的符号统一称之为宽字符在游戏地图上,我们打印墙体使⽤宽字符:□,打印蛇使⽤宽字符●,打印⻝物使⽤宽字符★ 普通的字符是占⼀个字节的,这类宽字符是占⽤2个字节
C语言起初是不支持本地化符号的,后来为了C语言的全球推广化,针对不同的国家和地区,C语言单独封装了一个头文件库用来适配操作者所处的地区的带有本地色彩的符号,例如我们的汉字就是特殊符号。同样,在这里的三个特殊图形也是特殊符号,都以宽字节识别。我们在这里可以用setlocale函数来调整C语言本地化。
A.<locale.h>本地化
<locale.h>提供的函数⽤于控制C标准库中对于不同的地区会产⽣不⼀样⾏为的部分。 在标准可以中,依赖地区的部分有以下⼏项: 数字量的格式 ,货币量的格式 , 字符集 , ⽇期和时间的表⽰形式等。
B.类项
通过修改地区,程序可以改变它的⾏为来适应世界的不同区域。但地区的改变可能会影响库的许多部 分,其中⼀部分可能是我们不希望修改的。所以C语⾔⽀持针对不同的类项进⾏修改,下⾯的⼀个宏, 指定⼀个类项: • LC_COLLATE
• LC_CTYPE
• LC_MONETARY
• LC_NUMERIC
• LC_TIME
• LC_ALL - 针对所有类项修改
C.setlocale函数
其函数的基本格式如下:
char setlocale (int category, const char locale);**
setlocale 函数⽤于修改当前地区,可以针对⼀个类项修改,也可以针对所有类项。
setlocale 的第⼀个参数可以是前⾯说明的类项中的⼀个,那么每次只会影响⼀个类项,如果第⼀个参 数是LC_ALL,就会影响所有的类项。 C标准给第⼆个参数仅定义了2种可能取值:“C"和” “。其中的C代表标准模式,而“ ”则代表的本地化模式,而C语言程序默认都是以C开始,只有后续人为修改成’” “才可调整为本地化模式,在这之后的所有C语言程序皆为本地化编译方式。
当程序运⾏起来后想改变地区,就只能显⽰调⽤setlocale函数。⽤” "作为第2个参数,调⽤setlocale函数就可以切换到本地模式,这种模式下程序会适应本地环境。⽐如:切换到我们的本地模式后就⽀ 持宽字符(汉字)的输出等。这样,我们就可以宽字符的输出了。
D.宽字符的打印
宽字符类型为wchar_t,想要打印宽字符,必须加上前缀L,否则C语言会把字面量当成窄字符类型处理,前缀要加在单引号或双引号前面,无论是打印还是定义变量都要这么加前缀L,表示宽字符,其对应的打印函数也变成了wprintf,占位符不变,依旧是%c即可。若要打印字符串,则改成%s即可,其他的部分依旧遵守上面的规则。
例如:

int main()
{
    
    wchar_t a=L'a';
    wprintf(L"%c",a);
  return 0;
}

有了上面知识的铺垫。让我们回到创建地图上来:
首先,我们把小方块作为墙体,但每次打小方块太麻烦了,故我对其进行预处理,让其变为WALL,同时我们决定构建一个X=58 Y=27的整个地图空间,故我们同时预处理让其分别为COLS和LINES,如下:

#define WALL L'□'//打印宽字符,定义一个墙体,这样方便后序去写
#define COLS 58
#define LINES 27

由于从{0 0}开始,所以本质上屏幕的坐标和我们学过的二维数组很像,而前面我们得知,宽字符是占有X方向每次两格位置的,这意味着我们的边界墙体要从57坐标的前一格56开始打印,否则会出现打印一半的情况,而Y方向坐标则正常,同时也提醒我们的一点是,由于宽字符的特殊性,我们每次的横向坐标移动必须是偶数坐标移动,否则没法跟我们的墙体对齐,会出现BUG。
故地图的打印程序如下:

void CreateMap()//创建地图
{
    
	Setpos(0, 0);
	int i = 0;
	for (i = 0; i < COLS; i += 2)
	{
    
		int j = 0;
		for (j = 0; j < LINES; j++)
		{
    
			if (i==0||j==0||i==56||j==26)
			{
    
				Setpos(i, j);
				wprintf(L"%c", WALL);
			}
		}
	}
}

现在地图完成了,我们就可以开始打印我们的贪吃蛇了。

5.初始化贪吃蛇:

我们首先为我们的贪吃蛇设置一个出生点坐标:如下:

#define POS_X 22
#define POS_Y 6

然后,我们让蛇的初始长度为5,且蛇的每一个节点用实心圆点表示,并且吃食物会让其长度不断增加:

#define BODY L'●'//身体符号,BODY
#define SIZE 5

如同蛇的每一个节点都要紧挨着并且如同有一根线一样将其串联起来,我们看到的蛇或许是一个图形,但实际在管理的时候,它其实是数据,对于线性的数据,我们这里使用单链表来最合适不过了。想一想,如何能流畅的利用坐标打印出来图形呢?即利用单链表遍历然后每次打印图形,故我们首先需要创建一个单链表,并且为其配置上我们每一个节点的坐标,具体的实现如下:

void InitSnake(pSnake snake)//初始化贪吃蛇并在出生点打印贪吃蛇
{
    
	//初始化贪吃蛇的节点有5个,故我们要创建5个蛇的身体节点,并且将蛇打印出来,同时注意,我们要对蛇结构体的成员进行初始化
	pSnakeNode cur = NULL;//我们在创建和访问链表的时候一般都在前面设置一个空的指针,方便我们后序的使用
	int i = 0;
	for (i = 0; i < SIZE; i++)
	{
    
		cur = (pSnakeNode)malloc(sizeof(SnakeNode));
		if (cur == NULL)
		{
    
			perror("malloc failed");
			exit(-1);
		}
		cur->next = NULL;
		cur->x = POS_X + i * 2;
		cur->y = POS_Y;//处理节点的坐标
		//头插(实际上头插尾插都可以,主要是找一边为蛇头即可)
		if (snake->_pSnake == NULL)
		{
    
			snake->_pSnake = cur;
		}
		else
		{
    
			cur->next = snake->_pSnake;
			snake->_pSnake = cur;
		}
	}

	cur = snake->_pSnake;
	while (cur)
	{
    
		Setpos(cur->x, cur->y);
		wprintf(L"%c", BODY);
		cur = cur->next;
	}
	Setpos(50,27);//别忘了设置一下文字的位置,要不然蛇的身体会串行
	//对蛇结构体进行初始化
	snake->_Score = 0;//初始得分为0
	snake->_FoodWeight = 10;//每吃一个星星得10分
	snake->_Dir = RIGHT;//初始向右
	snake->_Status = OK;//状态设置为正常运行
	snake->_SleepTime = 200;//速度设置为0.2秒延时初速
}

注意这里有一个技巧,对于链表的创建和遍历而言,我们可以创建一个公共的指针cur,它不仅仅可以用来创建链表,还能用来遍历链表,故以后我们在处理这类问题的时候都可以先创建这么一个多面手方便使用。同时注意细节,我们的横坐标每次是POS_X+i*2,而不是仅仅+i,宽字符一次占两格这个问题一定要注意,很容易弄错。
构建完蛇并且打印完蛇之后,我们就要对蛇里面的数据进行一系列初始化:包括初始的分,每一个食物的初始分数,初始的方向,初始的游戏状态调整为OK,初始的速度,这样,我们的蛇就配置好了,如下:
在这里插入图片描述

6.创建食物:

在创建食物之前,我们首先要清楚的知道我们的食物生成点是不能跟蛇的任意位置的坐标重合的,故我们的食物要在蛇停顿的那一刻在蛇身体不在的地方生成,代码如下:

void CreateFood(pSnake snake)//设置第一个食物
{
    
	snake->_pFood = (pSnakeNode)malloc(sizeof(SnakeNode));
	if (snake->_pFood == NULL)
	{
    
		perror("malloc failed");
		exit(-1);
	}
 	int x = 25;
	int y = 13;
again:
	while(x%2!=0)
	{
    
		x = rand() % 53 + 2;//这样控制随机数使其到不了两边边界
		y = rand() % 25 + 1;
	}
	pSnakeNode cur = snake->_pSnake;
	while (cur)
	{
    
		if (cur->x == x && cur->y == y)
		{
    
			goto again;
	    }
		cur = cur->next;
	}
	snake->_pFood->x= x;
	snake->_pFood->y = y;
	Setpos(x, y);
	wprintf(L"%c", STAR);
	Setpos(50, 27);
}

首先我们要创建一个蛇节点作为来存储食物的坐标,然后利用我们熟悉的随机数创建的特点,将相应的坐标创建出来,然后遍历链表看是否有重合的点,倘若有,使用goto语句跳回上面的过程再生成随机数再向下判断,倘若没有就将i去赋给食物节点的x y成员,并且在对应的位置打印出来食物,这里最需要注意的点是:我们随机数的生成要恰好在墙体里面,也就是说,我们的食物要在X方向2到54之间 Y方向1到25之间,而且由于食物本身也是占两个,也要为其再预留出两格。
我们在这里使用星星符号来代表食物:

#define STAR L'★'//食物符号,STAR

由此,我们的初始化阶段便全部配置好了:

void GameStart(pSnake ps)//游戏第一阶段:游戏初始化阶段函数
{
    
	//控制台窗口的设置
	system("mode con cols=150 lines=40");//别忘了给一些文字留空间
	system("title 贪吃蛇");
	//隐藏光标
	 HideCursor();
	//打印欢迎界面
	 WelcomeToGame();
	//创建地图
	 CreateMap();
	//初始化贪吃蛇并在出生点打印贪吃蛇
	 InitSnake(ps);
	//设置第一个食物
	 CreateFood(ps);
}

4.第二阶段:游戏运行阶段,进行游戏的正常运行

在本章节的第二阶段,我们将对游戏运行进行书写,包括贪吃蛇是如何移动的,我们如何将键盘和贪吃蛇的交互链接起来,使我们能够真正使用键盘控制人物移动,同时解决蛇吃食物和正常移动的判定问题,以及如何处理碰墙和吃到自己的游戏结束方式判断。

1.游戏说明打印:

和上面我们的欢迎界面一样,在这里我不多赘述,直接上代码了:

void  PrintHelpInfp()//话语提示:帮助玩家如何控制方向移动,如何加速减速
{
    
	Setpos(64, 15);
	printf("不能穿墙,不能咬到自己\n");
	Setpos(64, 16);
	printf("↑.↓.←.→分别控制蛇的移动.\n");
	Setpos(64, 17);
	printf("F3 为加速,F4 为减速\n");
	Setpos(64, 17);
	printf("ESC:退出游戏 . space:暂停游戏\n");
	Setpos(64, 20);
	printf("xxxxxxxxxxxx制作\n");
	Setpos(64, 21);
	printf("详细制作过程及其制作原理,源代码皆由 XXXXXX 所有  XXXXXXXXX\n");
}

2.游戏按键交互:

这应当是每一位程序学习者最为激动人心的时刻,因为终于,你通过自己的双手在自己和电脑之间构建起了一个关系,像那些大型游戏一样,至少在这里,键盘操控着蛇的移动,就好比你操控着褪色者移动,操控CT T探员移动,操控英雄移动…是的,我们在这里向着游戏世界迈出了我们交互的第一步。
在windows中为我们提供了一个用来实现键盘和缓存交互方式–-虚拟键值,即它将键盘上的每一个按键都设置为一个对应的数字,然后让计算机去识别这些数字从而确定对应的是哪个按键,从而实现对应的功能,这就是我们按键操作的基础。然后,由于按键有按下和松开两种状态,计算机需要去识别状态,故我们接下来要介绍一个函数—GetAsyncKeyState
GetAsyncKeyState函数
函数基本格式如下:
SHORT GetAsyncKeyState( int vKey);
此函数的最大用处就是将键盘上的对应的虚拟键值作为参数传给函数,然后函数通过short形式返回来返回按键的状态,其具体的返回方式如下:
在这里插入图片描述
由此,让我们想想我们想怎样和键盘结合,我们按一次向上键位,倘若不改变,蛇就会一直向上走,只有当我们再按一次的时候蛇才会按照我们的意思改变方向或者我们仍然按上键位方向不变,故在这里,short的最低位就是我们最关注的,我们要判断short的最低位是否为1,从而判断蛇的方向或者暂停游戏,或者加速和减速,由此:我们可以利用按位与&1来获取这个最低位,而且,这种单一的判断方式,我们使用宏要比函数好很多,故我们这样写:

#define KEY_PRESS(VK)((GetAsyncKeyState(VK)&1)?1:0)//按键交互宏

这样,我们就得到了一个我们不按就保持原状,我们按键就会改变的键盘交互方式,故我们的第二阶段的主程序如下:

void GameRun(pSnake ps)//游戏第二阶段:游戏运行阶段函数
{
    
	//话语提示:帮助玩家如何控制方向移动,如何加速减速
	 PrintHelpInfp();
	//统计分数,以及蛇身体的移动问题(对蛇的状态每一次按键都要进行实时统计)
	 do
	 {
    
		 Setpos(64, 10);
		 printf("得分:%d    ", ps->_Score);
		 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_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))//暂停
		 {
    
			 system("pause");
		 }
		 else if (KEY_PRESS(VK_ESCAPE))//退出
		 {
    
			 ps->_Status = END_NORMAL;
		 }
		 else if (KEY_PRESS(VK_F3))//F3加速
		 {
    
			 if (ps->_SleepTime >= 50)
			 {
    
				 ps->_SleepTime -= 30;
				 ps->_FoodWeight += 2;
			 }
		 }
		 else if (KEY_PRESS(VK_F4))//F4减速
		 {
    
			 if (ps->_SleepTime < 350)
			 {
    
				 ps->_SleepTime += 30;
				 ps->_FoodWeight -= 2;
				 if (ps->_SleepTime == 350)
				 {
    
					 ps->_FoodWeight = 1;//即吃每一个节点最低的得分为1分
				 }
			 }
		 }
		 Sleep(ps->_SleepTime);//延时函数,将当前的程序停止多少秒重新进行
		 SnackMove(ps);//蛇移动函数,蛇吃到食物自身长度的增加,蛇碰墙,蛇碰到自己尾部,或者自身退出游戏结束运行阶段
	 }while (ps->_Status == OK);//根据游戏运行状态是否为OK正常状态来判断程序是否结束
}

其实,本质上,游戏画面根本不是静止的,而是不断的画面刷新使其给人感觉是静止,还有就是有些打印的位置数据存在,重复打印就会感觉画面不动,但我们的蛇由于一直在打印和尾部的打空格,故给人感觉就是蛇在移动,你可以从这组主程序中看到,我们每一次都输入一个键值,然后不断循环判断的是游戏状态,这个模板要记下来,游戏状态是一个很重要的东西,它是游戏进程的掌控者。延时函数的运用使得我们的看到的蛇的刷新率不断发生变化,从而影响了蛇的速度,这是一个很巧妙的方式。
在这里要强调一个事情,我们的蛇每次是不能移动和当前方向相反的方向的,比如向上的时候,你只能向左 上 右三个方向,而不能向后,所以我们的方向改变要加上这个一条判断!!!

3.蛇移动判断:

蛇的移动,本质上就是把下一个坐标先跟链表串联起来,然后打印出来,同时删除尾部节点,倘若吃到食物,就直接串联打印即可,这就是大致的思路,那我们怎样确定坐标呢?
在我们的主程序中,我们每次的按键都相应的改变了蛇结构体的移动方向的枚举体,这样,而我们规定蛇每次移动一步,故我们只要对相应方向位置+2格或者+1格即可,同时,创建一个下一个坐标的结构体节点,将坐标传给这个节点,然后再去判断这个节点是不是食物,倘若是就执行是的函数,反之执行正常走的指令,在进行这一步之后再去判断蛇是否撞墙或者是否吃到自己。
程序如下:

void SnackMove(pSnake snack)//蛇身移动函数
{
    
	//首先创建一个节点来存储下一步的坐标,为下面判断是否是食物,倘若是就吃食物,反之就移动不吃做铺垫
	pSnakeNode pNextNode = (pSnakeNode)malloc(sizeof(SnakeNode));
	if (pNextNode == NULL)
	{
    
		perror("malloc failed");
		exit(-1);
	}
	switch(snack->_Dir)//我们向哪里走是已经按过键盘了,在我们的运行函数里面已经写明了我们会根据按键改变方向,故我们只需要根据方向就知道应该向哪里走
	{
    
	    case UP:
	   {
    
			pNextNode->x = snack->_pSnake->x;
			pNextNode->y = snack->_pSnake->y-1;
	   }
	   break;
	    case DOWN:
	   {
    
			pNextNode->x = snack->_pSnake->x;
			pNextNode->y = snack->_pSnake->y + 1;
	   }
	   break;
		case RIGHT:
	   {
    
			pNextNode->x = snack->_pSnake->x+2;//注意,别忘了,对x操作是加2,而不是加1,因为宽字符一次占横坐标的两位
			pNextNode->y = snack->_pSnake->y;
	   }
	   break;
		case LEFT:
	   {
    
			pNextNode->x = snack->_pSnake->x-2;//注意,别忘了,对x操作是加2,而不是加1,因为宽字符一次占横坐标的两位
			pNextNode->y = snack->_pSnake->y;
	   }
	   break;
	}
	//对下一个要走的节点是否为食物节点进行判断
	if(NextIsFood(pNextNode,snack))//是食物
	{
    
		EatFood(pNextNode, snack);
	}
	else//不是食物
	{
    
		NoFood(pNextNode, snack);
	}
	//食物吃完后,最后再进行墙体触碰或者蛇本题是否触碰本题的函数判断:
	KillByWall(snack);//是否碰到墙体判断
	KillBySelf(snack);//是否碰到自身判断
}

!!!我前面说过,坐标是最为关键的一点在贪吃蛇中,因为碰墙,吃自己,吃食物,这些都与坐标有关。!!!!
然后判断我们开创的下一个节点是否为食物节点:

bool NextIsFood(pSnakeNode pnext,pSnake ps)//判断下一个节点是否为食物节点
{
    
	return (pnext->x == ps->_pFood->x) && (pnext->y == ps->_pFood->y);
}

倘若是,就进入吃食物函数:

void EatFood(pSnakeNode pnext, pSnake ps)//吃掉食物的函数
{
    
     //有食物就吃掉,利用头插的方法:
	//头插:
	pnext->next = ps->_pSnake;
	ps->_pSnake = pnext;
	//然后打印出新的贪吃蛇
	pSnakeNode cur = ps->_pSnake;
	while (cur)
	{
    
		Setpos(cur->x, cur->y);
		wprintf(L"%c", BODY);
		cur = cur->next;
	}
	//加上得分:
	ps->_Score += ps->_FoodWeight;
    //释放原食物节点,创造新的食物节点
	free(ps->_pFood);
	CreateFood(ps);
}

注意,别忘了释放之前存在的食物节点,然后使用前面的函数再创建一个食物节点
倘若不是:就进入不吃食物节点:

void NoFood(pSnakeNode pnext, pSnake ps)//不吃食物的函数
{
    
	//先头插
	pnext->next = ps->_pSnake;
	ps->_pSnake = pnext;
	//然后打印出新的贪吃蛇
	//直接放弃掉最后一个节点,反正我们的操作都是针对头插,对尾部无要求
	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;
}

我们的操作就是加一个节点给蛇,同时将最后一个节点的位置打印空格并且要释放掉,这里要强调很关键的一件事,我们的空格必须是空两格的,因为横向移动的时候一次走两个而不是一格,一旦这里处理错误就会出现蛇半个点移动的bug,这里是一定要注意的!!!
然后对蛇是否碰到墙体判断:

void 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;
	}
}

对蛇是否吃到自身判断:

void KillBySelf(pSnake ps)//是否碰到自身判断
{
    
	pSnakeNode cur = ps->_pSnake->next;//注意,这里要从头节点的第二位开始,否则头节点和头节点必定坐标相同,这样一开始就判定蛇吃自己了,就出现bug了,必须是蛇吃到了除去它头节点之外的其他节点判定为蛇吃了自身
	while (cur->next)
	{
    
		if (ps->_pSnake->x == cur->x && ps->_pSnake->y == cur->y)
		{
    
			ps->_Status = KILL_BY_SELF;
		}
		cur = cur->next;
	}
}

这里最要强调的一点:要从头节点的第二位开始,否则头节点和头节点必定坐标相同,这样一开始就判定蛇吃自己了,就出现bug了,必须是蛇吃到了除去它头节点之外的其他节点判定为蛇吃了自身,这个很关键,我在这里思考了很长时间,由于我们操控的是头节点,故我们不可能碰到自己,故我们要从头节点的下一个节点开始!!!

5.第三阶段:游戏结束判断——内存资源清理

这里就很简单了,承接着上一步第二阶段主函数结束的原因,我们分别针对其游戏结束状态返回对应的游戏结束结果即可,由于我们动态开辟了蛇,故我们不要忘了在最后要将堆区的内存资源清理释放掉,养成好习惯,也是为了放置内存泄漏的出现!!!
代码如下:

void GameEnd(pSnake ps)//游戏第三阶段:游戏结束方式的判定以及游戏结束的内存释放和资源清理
{
    
	//对结束条件进行判定
	switch(ps->_Status)
	{
    
	   case END_NORMAL:
	   {
    
		   Setpos(20, 27);
		   printf("您正常退出游戏,期待您的下一次游戏!\n");
		   break;
	   }
	   case KILL_BY_WALL:
	   {
    
		   Setpos(20, 27);
		   printf("您撞墙了,多加练习!\n");
		   break;
	   }
	   case KILL_BY_SELF:
	   {
    
		   Setpos(20, 27);
		   printf("您自己吃掉了您自己,下次可别犯这样的错误了!\n");
		   break;
	   }
	}
	//内存清理释放
	pSnakeNode cur = ps->_pSnake;
	while (cur)
	{
    
		pSnakeNode prev = cur;
		cur = cur->next;
		free(prev);
	}
	cur = NULL;
}

总结:

以上就是贪吃蛇的全部实现过程了,按理来说,它应该是我学习以来的第一款实现了交互的游戏,但我收获很多,如何和按键交互,如何控制移动,游戏状态的重要性,API的意义,我想,不管是多么复杂的大型游戏,都应当是按照这种大致的思路进行的,后面有时间的话,我会尝试制作坦克大战,俄罗斯方块,以及如何实现双人游戏(非网络版本,单纯键盘两人操控)。
我曾经操控着无数个他人创造的人物,但现在,我知道,终有一天,我会操控我自己的人物走在我自己的地图上,书写的我自己的故事!!!!!
在这里插入图片描述
源代码在下面,想要的可以自取:
sanke.h文件:

#pragma once
#include<locale.h>
#include<stdio.h>
#include<stdlib.h>
#include<stdbool.h>
#include<assert.h>
#include<windows.h>
#include<time.h>
#define WALL L'□'//打印宽字符,定义一个墙体,这样方便后序去写
#define BODY L'●'//身体符号,BODY
#define STAR L'★'//食物符号,STAR
#define COLS 58
#define LINES 27
#define SIZE 5
#define POS_X 22
#define POS_Y 6
#define KEY_PRESS(VK)((GetAsyncKeyState(VK)&1)?1:0)//按键交互宏
enum DIRECTION//蛇移动方向枚举体
{
    
	UP=1,
	DOWN,
	LEFT,
	RIGHT
};
enum GAME_STATUS//游戏状态枚举体
{
    
	OK,//游戏正常运行
	END_NORMAL,//正常退出
	KILL_BY_WALL,//撞墙
	KILL_BY_SELF//自己吃自己了
};
//贪吃蛇单个节点的结构体
typedef struct SnakeNode
{
    
	//描述蛇身节点的坐标
	int x;
	int y;
	struct SnakeNode* next;
}SnakeNode,*pSnakeNode;//*pSnakeNode为这个结构体的指针重命名

//贪吃蛇个体的结构体
//我们整个游戏要控制的是蛇
typedef struct Snake
{
    
	pSnakeNode _pSnake;//指向贪吃蛇头节点的指针,我们在游戏过程中是操控蛇头进行游戏,利用蛇头来判定游戏状态,故控制蛇头很关键
	pSnakeNode _pFood;//指向食物的节点,本质上食物被蛇吃了后也算蛇的节点的一部分,故我们也用一个指针来管理
	int _Score;//累计的得分
	int _FoodWeight;//吃一个食物的分数
	int _SleepTime;//蛇的速度,本质上为一个延时函数,休眠的时间越长速度越慢,反之速度越快
	enum DIRECTION _Dir;//描述蛇的方向,由于蛇的方向固定,且每次只能按一个按键,故我们利用枚举体来处理,未来的Dir取值只可能是4个方向中的一种
	enum GAME_STATUS _Status;//游戏状态,判断蛇是撞墙还是自己吃到了自己还是按ESC键退出了
}Snake,*pSnake;
//---------------------------------------------------------------
void GameStart(pSnake ps);//游戏第一阶段:游戏初始化阶段函数
void HideCursor();//隐藏光标
void Setpos(int x, int y);//光标位置调整
void WelcomeToGme();//打印欢迎界面
void CreateMap();//创建地图
void InitSnake(pSnake snake);//初始化蛇并在出生点打印出蛇
void CreateFood(pSnake snake);//设置第一个食物
//---------------------------------------------------------------
void GameRun(pSnake ps);//游戏第二阶段:游戏运行阶段函数
void  PrintHelpInfp();//话语提示:帮助玩家如何控制方向移动,如何加速减速
void SnackMove(pSnake snack);//蛇身移动函数
bool NextIsFood(pSnakeNode pnext, pSnake ps);//判断下一个节点是否为食物节点
void EatFood(pSnakeNode pnext, pSnake ps);//吃掉食物的函数
void NoFood(pSnakeNode pnext, pSnake ps);//不吃食物的函数
void KillByWall(pSnake ps);//是否碰到墙体判断
void KillBySelf(pSnake ps);//是否碰到自身判断
//---------------------------------------------------------------
void GameEnd(pSnake ps);//游戏第三阶段:游戏结束的内存释放和资源清理以及游戏结束方式的判定

snack.c文件

#define _CRT_SECURE_NO_WARNINGS 1
#include"snack.h"
//阶段一:游戏初始化阶段
//-------------------------------------------------------------------------------
void HideCursor()//隐藏光标
{
    
	HANDLE houtput = GetStdHandle(STD_OUTPUT_HANDLE);//	获取输出设备句柄
	CONSOLE_CURSOR_INFO CursorInfo;
	GetConsoleCursorInfo(houtput, &CursorInfo);//获取光标属性信息
	CursorInfo.bVisible = false;//调整属性隐藏光标
	SetConsoleCursorInfo(houtput, &CursorInfo);//重置光标属性
}
void Setpos(int x, int y)//光标位置调整
{
    
	HANDLE houtput = GetStdHandle(STD_OUTPUT_HANDLE);
	COORD pos = {
     x,y };
	SetConsoleCursorPosition(houtput, pos);
}
void WelcomeToGame()//打印欢迎界面
{
    
	Setpos(40, 15);
	printf("欢迎来到贪吃蛇小游戏!");
	Setpos(40, 25);//让任意继续出现在别的位置,注意pause本质上也是要打印在屏幕上的,故也要调整位置
	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()//创建地图
{
    
	Setpos(0, 0);
	int i = 0;
	for (i = 0; i < COLS; i += 2)
	{
    
		int j = 0;
		for (j = 0; j < LINES; j++)
		{
    
			if (i==0||j==0||i==56||j==26)
			{
    
				Setpos(i, j);
				wprintf(L"%c", WALL);
			}
		}
	}
}
void InitSnake(pSnake snake)//初始化贪吃蛇并在出生点打印贪吃蛇
{
    
	//初始化贪吃蛇的节点有5个,故我们要创建5个蛇的身体节点,并且将蛇打印出来,同时注意,我们要对蛇结构体的成员进行初始化
	pSnakeNode cur = NULL;//我们在创建和访问链表的时候一般都在前面设置一个空的指针,方便我们后序的使用
	int i = 0;
	for (i = 0; i < SIZE; i++)
	{
    
		cur = (pSnakeNode)malloc(sizeof(SnakeNode));
		if (cur == NULL)
		{
    
			perror("malloc failed");
			exit(-1);
		}
		cur->next = NULL;
		cur->x = POS_X + i * 2;
		cur->y = POS_Y;//处理节点的坐标
		//头插(实际上头插尾插都可以,主要是找一边为蛇头即可)
		if (snake->_pSnake == NULL)
		{
    
			snake->_pSnake = cur;
		}
		else
		{
    
			cur->next = snake->_pSnake;
			snake->_pSnake = cur;
		}
	}

	cur = snake->_pSnake;
	while (cur)
	{
    
		Setpos(cur->x, cur->y);
		wprintf(L"%c", BODY);
		cur = cur->next;
	}
	Setpos(50,27);//别忘了设置一下文字的位置,要不然蛇的身体会串行
	//对蛇结构体进行初始化
	snake->_Score = 0;//初始得分为0
	snake->_FoodWeight = 10;//每吃一个星星得10分
	snake->_Dir = RIGHT;//初始向右
	snake->_Status = OK;//状态设置为正常运行
	snake->_SleepTime = 200;//速度设置为0.2秒延时初速
}
void CreateFood(pSnake snake)//设置第一个食物
{
    
	snake->_pFood = (pSnakeNode)malloc(sizeof(SnakeNode));
	if (snake->_pFood == NULL)
	{
    
		perror("malloc failed");
		exit(-1);
	}
 	int x = 25;
	int y = 13;
again:
	while(x%2!=0)
	{
    
		x = rand() % 53 + 2;//这样控制随机数使其到不了两边边界
		y = rand() % 25 + 1;
	}
	pSnakeNode cur = snake->_pSnake;
	while (cur)
	{
    
		if (cur->x == x && cur->y == y)
		{
    
			goto again;
	    }
		cur = cur->next;
	}
	snake->_pFood->x= x;
	snake->_pFood->y = y;
	Setpos(x, y);
	wprintf(L"%c", STAR);
	Setpos(50, 27);
}
void GameStart(pSnake ps)//游戏第一阶段:游戏初始化阶段函数
{
    
	//控制台窗口的设置
	system("mode con cols=150 lines=40");//别忘了给一些文字留空间
	system("title 贪吃蛇");
	//隐藏光标
	 HideCursor();
	//打印欢迎界面
	 WelcomeToGame();
	//创建地图
	 CreateMap();
	//初始化贪吃蛇并在出生点打印贪吃蛇
	 InitSnake(ps);
	//设置第一个食物
	 CreateFood(ps);
}
//---------------------------------------------------------------------------------------
//阶段二:游戏运行阶段
void  PrintHelpInfp()//话语提示:帮助玩家如何控制方向移动,如何加速减速
{
    
	Setpos(64, 15);
	printf("不能穿墙,不能咬到自己\n");
	Setpos(64, 16);
	printf("↑.↓.←.→分别控制蛇的移动.\n");
	Setpos(64, 17);
	printf("F3 为加速,F4 为减速\n");
	Setpos(64, 17);
	printf("ESC:退出游戏 . space:暂停游戏\n");
	Setpos(64, 20);
	printf("XXXXXXXXXXX制作\n");
	Setpos(64, 21);
	printf("详细制作过程及其制作原理,源代码皆由 XXXXXXX 所有  XXXXXXXXX\n");
}
bool NextIsFood(pSnakeNode pnext,pSnake ps)//判断下一个节点是否为食物节点
{
    
	return (pnext->x == ps->_pFood->x) && (pnext->y == ps->_pFood->y);
}
void EatFood(pSnakeNode pnext, pSnake ps)//吃掉食物的函数
{
    
     //有食物就吃掉,利用头插的方法:
	//头插:
	pnext->next = ps->_pSnake;
	ps->_pSnake = pnext;
	//然后打印出新的贪吃蛇
	pSnakeNode cur = ps->_pSnake;
	while (cur)
	{
    
		Setpos(cur->x, cur->y);
		wprintf(L"%c", BODY);
		cur = cur->next;
	}
	//加上得分:
	ps->_Score += ps->_FoodWeight;
    //释放原食物节点,创造新的食物节点
	free(ps->_pFood);
	CreateFood(ps);
}
void NoFood(pSnakeNode pnext, pSnake ps)//不吃食物的函数
{
    
	//先头插
	pnext->next = ps->_pSnake;
	ps->_pSnake = pnext;
	//然后打印出新的贪吃蛇
	//直接放弃掉最后一个节点,反正我们的操作都是针对头插,对尾部无要求
	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;
}
void 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;
	}
}
void KillBySelf(pSnake ps)//是否碰到自身判断
{
    
	pSnakeNode cur = ps->_pSnake->next;//注意,这里要从头节点的第二位开始,否则头节点和头节点必定坐标相同,这样一开始就判定蛇吃自己了,就出现bug了,必须是蛇吃到了除去它头节点之外的其他节点判定为蛇吃了自身
	while (cur->next)
	{
    
		if (ps->_pSnake->x == cur->x && ps->_pSnake->y == cur->y)
		{
    
			ps->_Status = KILL_BY_SELF;
		}
		cur = cur->next;
	}
}
void SnackMove(pSnake snack)//蛇身移动函数
{
    
	//首先创建一个节点来存储下一步的坐标,为下面判断是否是食物,倘若是就吃食物,反之就移动不吃做铺垫
	pSnakeNode pNextNode = (pSnakeNode)malloc(sizeof(SnakeNode));
	if (pNextNode == NULL)
	{
    
		perror("malloc failed");
		exit(-1);
	}
	switch(snack->_Dir)//我们向哪里走是已经按过键盘了,在我们的运行函数里面已经写明了我们会根据按键改变方向,故我们只需要根据方向就知道应该向哪里走
	{
    
	    case UP:
	   {
    
			pNextNode->x = snack->_pSnake->x;
			pNextNode->y = snack->_pSnake->y-1;
	   }
	   break;
	    case DOWN:
	   {
    
			pNextNode->x = snack->_pSnake->x;
			pNextNode->y = snack->_pSnake->y + 1;
	   }
	   break;
		case RIGHT:
	   {
    
			pNextNode->x = snack->_pSnake->x+2;//注意,别忘了,对x操作是加2,而不是加1,因为宽字符一次占横坐标的两位
			pNextNode->y = snack->_pSnake->y;
	   }
	   break;
		case LEFT:
	   {
    
			pNextNode->x = snack->_pSnake->x-2;//注意,别忘了,对x操作是加2,而不是加1,因为宽字符一次占横坐标的两位
			pNextNode->y = snack->_pSnake->y;
	   }
	   break;
	}
	//对下一个要走的节点是否为食物节点进行判断
	if(NextIsFood(pNextNode,snack))//是食物
	{
    
		EatFood(pNextNode, snack);
	}
	else//不是食物
	{
    
		NoFood(pNextNode, snack);
	}
	//食物吃完后,最后再进行墙体触碰或者蛇本题是否触碰本题的函数判断:
	KillByWall(snack);//是否碰到墙体判断
	KillBySelf(snack);//是否碰到自身判断
}
void GameRun(pSnake ps)//游戏第二阶段:游戏运行阶段函数
{
    
	//话语提示:帮助玩家如何控制方向移动,如何加速减速
	 PrintHelpInfp();
	//统计分数,以及蛇身体的移动问题(对蛇的状态每一次按键都要进行实时统计)
	 do
	 {
    
		 Setpos(64, 10);
		 printf("得分:%d    ", ps->_Score);
		 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_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))//暂停
		 {
    
			 system("pause");
		 }
		 else if (KEY_PRESS(VK_ESCAPE))//退出
		 {
    
			 ps->_Status = END_NORMAL;
		 }
		 else if (KEY_PRESS(VK_F3))//F3加速
		 {
    
			 if (ps->_SleepTime >= 50)
			 {
    
				 ps->_SleepTime -= 30;
				 ps->_FoodWeight += 2;
			 }
		 }
		 else if (KEY_PRESS(VK_F4))//F4减速
		 {
    
			 if (ps->_SleepTime < 350)
			 {
    
				 ps->_SleepTime += 30;
				 ps->_FoodWeight -= 2;
				 if (ps->_SleepTime == 350)
				 {
    
					 ps->_FoodWeight = 1;//即吃每一个节点最低的得分为1分
				 }
			 }
		 }
		 Sleep(ps->_SleepTime);//延时函数,将当前的程序停止多少秒重新进行
		 SnackMove(ps);//蛇移动函数,蛇吃到食物自身长度的增加,蛇碰墙,蛇碰到自己尾部,或者自身退出游戏结束运行阶段
	 }while (ps->_Status == OK);//根据游戏运行状态是否为OK正常状态来判断程序是否结束
}
//---------------------------------------------------------------------------------------
void GameEnd(pSnake ps)//游戏第三阶段:游戏结束方式的判定以及游戏结束的内存释放和资源清理
{
    
	//对结束条件进行判定
	switch(ps->_Status)
	{
    
	   case END_NORMAL:
	   {
    
		   Setpos(20, 27);
		   printf("您正常退出游戏,期待您的下一次游戏!\n");
		   break;
	   }
	   case KILL_BY_WALL:
	   {
    
		   Setpos(20, 27);
		   printf("您撞墙了,多加练习!\n");
		   break;
	   }
	   case KILL_BY_SELF:
	   {
    
		   Setpos(20, 27);
		   printf("您自己吃掉了您自己,下次可别犯这样的错误了!\n");
		   break;
	   }
	}
	//内存清理释放
	pSnakeNode cur = ps->_pSnake;
	while (cur)
	{
    
		pSnakeNode prev = cur;
		cur = cur->next;
		free(prev);
	}
	cur = NULL;
}

test.c文件:

#define _CRT_SECURE_NO_WARNINGS 1
#include"snack.h"
void game()
{
    
	 Snake snake = {
     0 };//首先创建一条蛇
	//贪吃蛇游戏大概分为三个部分进行,故我们分为三个大函数来进行
	//1.游戏开始--初始化游戏的过程,将蛇自身结构体里面的数据进行初始化,地图的构造,蛇的出生点设置以及蛇的打印,游戏界面的生成等
	 GameStart(&snake);//传地址直接改变实参
	//2.游戏运行--游戏的正常运行过程
	 GameRun(&snake);
	//3.游戏结束--对游戏中的一些占用内存的资源进行释放回收和清理
	 GameEnd(&snake);
}
int main()
{
    
	int a = 0;
	do
	{
    
		srand(time(NULL));
		setlocale(LC_ALL, "");//设置为本地环境,注意配置本地化环境第二个参数中间不空格
		game();
		printf("您是否继续游戏?,倘若继续输入1,不继续就输入0:>");
		scanf("%d", &a);
	} while (a);
	return 0;
}

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/hbw040115/article/details/134048968

智能推荐

深入HQL学习以及HQL和SQL的区别_hql sql-程序员宅基地

文章浏览阅读5.8w次,点赞38次,收藏218次。HQL(Hibernate Query Language) 是面向对象的查询语言, 它和 SQL 查询语言有些相似. 在 Hibernate 提供的各种检索方式中, HQL 是使用最广的一种检索方式. 它有如下功能:在查询语句中设定各种查询条件;支持投影查询, 即仅检索出对象的部分属性;支持分页查询;支持连接查询;支持分组查询, 允许使用 HAVING 和 GROUP BY 关键字;提供内_hql sql

MFC-ListCtrl 可编辑重写 _mfc listctrl 编辑-程序员宅基地

文章浏览阅读6k次。MFC下,提供了List Control控件,当选择Report模式时,可以方便的做数据报表之类的应用。类似下图: 但是有个不大不小的问题是,当List Control选择可编辑模式时,只有每一行的第一列的单元格才能编辑,而且在默认情况下,当选中的时候,也只有被选中的这一行的第一个单元格才会反色显示~~这未免太BT了~在网上找了一些相关的帖子,解决整行选中的问题可以采用为List _mfc listctrl 编辑

tm影像辐射定标_Landsat-TM-辐射定标和大气校正步骤-程序员宅基地

文章浏览阅读2.3k次。Landsat-TM-辐射定标和大气校正步骤 Landsat TM 辐射定标和大气校正步骤 一、数据准备 从USGS网站或者马里兰大学下载TM原始数据, USGS网站下载的数据是原始数据,在ENVI软件File–Open External File–Landsat – Geotiff with meta中只需打开***********_MTL.txt即可打开所有波段数据(除band6); usgs..._tm影像辐射定标的流程

linux下make menuconfig在什么目录,Linux kernel的 Makefile和Kconfig以及Make menuconfig的关系...-程序员宅基地

文章浏览阅读407次。熟悉内核的Makefile对开发设备驱动、理解内核代码结构都是非常重要的linux2.6内核Makefile的许多特性和2.4内核差别很大,在内核目录的documention/kbuild/makefiles.txt中有详细的说明。===1、内核Makefile概述Linux内核的Makefile分为5个部分: Makefile最顶层Makefile.config内核当前配置文件,编译时成为顶层M..._make menucofnig 要多久能安装完

CSS 游戏动画案例_css 动画 游戏-程序员宅基地

文章浏览阅读318次。CSS 游戏动画图下载地址:http://www.aigei.com/s?q=%E4%BD%90&type=2d效果点击放技能<!DOCTYPE html><html> <head> <meta charset="UTF-8"> <title></title> <style type="text/css"> #d2{ width: 300px; height:_css 动画 游戏

uos 序列号_统信UOS桌面操作系统 v20.1021 专业版镜像-程序员宅基地

文章浏览阅读9k次。uos来头不小,统一多方平台,整合各种资源做出来的国产系统,实际上是基于国产深度系统deepin二改的,实际上deepin也非常不错,经过这么一搞,用户量会更上一个台阶。和深度对大的区别就是,UOS系统的兼容性更好。官方介绍到,在近半年的时间里,UOS 开发团队与龙芯中科的系统软件研发团队强强联合、紧密合作,采用联合技术攻关模式,针对 Linux 内核、BIOS 固件、编译器、浏览器、图形驱动等多..._统信激活序列号

随便推点

数据结构-线性表的两种实现方式:顺序表和链表_画出线性表两种不同实现方式的示意图-程序员宅基地

文章浏览阅读1.1k次。顺序表就是在内存中按顺序连续开辟一段空间来存储数据的结构,在java中就是数组,如a所示链表就是在内存中随机开辟内存一段段存储数据的结构,如图b所示线性表的接口使用一个接口表明基本操作的需求:public interface Ilist { //清空线性表 public void clear(); //判断线性表是否为空 public boolean isEmpty(); //获取线性表的长度 public int length(); /._画出线性表两种不同实现方式的示意图

ubuntu:Bro 网络分析框架_网卡bro端口-程序员宅基地

文章浏览阅读1.4k次。参考文章:https://mp.weixin.qq.com/s?__biz=MjM5NjQ4MjYwMQ==&amp;mid=2664609207&amp;idx=3&amp;sn=4a0331832b280f2f58644030a8771abe&amp;chksm=bdce8ef18ab907e7c8b2cc6687ec69521cb196de9008..._网卡bro端口

C语言注释规范_c语言 版本号注释-程序员宅基地

文章浏览阅读5.4k次,点赞5次,收藏8次。2-1:一般情况下,源程序有效注释量必须在20%以上。说明:注释的原则是有助于对程序的阅读理解,在该加的地方都加了,注释不宜太多也不能太少,注释语言必须准确、易懂、简洁。2-2:文件头部应进行注释,注释必须列出:版权说明、版本号、生成日期、作者、内容、功能、修改日志等。示例:下面这段头文件的头注释比较标准,当然,并不局限于此格式,但上述..._c语言 版本号注释

LINUX系统加固_2.linux系统加固中下图的命名执行后会输出一条执行结果-程序员宅基地

文章浏览阅读792次。LINUX系统加固目录一、关于服务器的安全级别 2二、系统加固 2A、漏洞修补 21、内核漏洞 42、应用漏洞 4B、系统防护提升 51、系统配置的安全性 52、应用软件配置的安全性 83、用户权限配置的安全性 12一、关于服务器的安全_2.linux系统加固中下图的命名执行后会输出一条执行结果

Python批量发送QQ邮件_python qq邮件批量发送-程序员宅基地

文章浏览阅读887次。哇奥,fantastic baby…今天 老Amy 开始薅头发~还有啥宝贝没给大家亮出来…就开始看到繁忙的 hr ,我设身处地的想,如果行政部门需要批量的给不同人员发送不同信息的邮件~是怎么来做的呢?emmm…或许excel、word和邮箱都有快捷的功能[原谅我布吉岛],可是万一用python更便捷呢?所以 老Amy 就开动了!需求如下如下图,邮件.xlsx 文件中含有一些基本信息,而我们需要给不同的收件人邮箱发送对应的正文内容。打BOSS第一版最开始,我们不要把事情想的太复杂,而是先用 Py_python qq邮件批量发送

python——列表_1、给定两个列表:ls1=[2,1,2,4,3,2]和ls2=[‘c’,’a’,’b’],按顺序执行-程序员宅基地

文章浏览阅读1.9k次。Python学习第五章序列,列表1.序列类型在python中,序列是基本的数据结构,主要利用序列进行的操作有更新,索引,切片,加,乘,删除等等。我们要熟练掌握牢记这些方法,运用于列表中。2.列表的类型及操作如何判断是一个列表呢,列表的特征是用方括号 [] 中的元素组成,内部是利用逗号隔开的元素,在查找删除等操作的时候可以指定某一个元素,其他元素依次输出。列表的内置函数也要掌握清楚,例如..._1、给定两个列表:ls1=[2,1,2,4,3,2]和ls2=[‘c’,’a’,’b’],按顺序执行以下语