Bootstrap

贪吃蛇项目实战——学习详解

1 准备知识

1.1 获取句柄

首先, 我们需要了解一下句柄:句柄就是把手, 手柄的意思。而我们可以通过**GetStdHandle()**函数获得标准输出的句柄, 通过这个句柄, 我们就可以使用一些函数接口来对标准输出进行一系列相关的操作。获得句柄的操作如下:

#include<Windows.h>//使用句柄操作需要包含Windows.h头文件
 
 
int main() 
{
	HANDLE h_out_put = GetStdHandle(STD_OUTPUT_HANDLE);
 
 
	return 0;
}

由这串代码我们可以知道:句柄的类型是HANDLE, 然后h_out_put就是一个句柄。GetStdHandle()是获取句柄的函数, 它里面的参数是从哪里获取句柄。而STD_OUTPUT_HANDLE就是标准输出的句柄。 我将它传给函数, 意思就是要获取标准输出的句柄。 所以, 上面这一行代码运行后, 我们就获取到了标准输出的句柄了。 也就是h_out_put。不要忘记包含头文件!

1.2 设置控制台界面

首先我们要确保我们的运行界面是控制台而不是终端。 就是这个东西:

img

img

如果是终端的话, 是这样的

img

img

我们要在控制台上面运行贪吃蛇, 如果是终端的话就不行了。 所以, 如果运行时弹出来的是终端的话,就要将终端改成控制台。 修改方法如下:

img

鼠标右击红框框处,然后弹出选项:

img

选择设置, 进入这个界面:

img

点击默认终端应用程序, 找到控制台, 修改即可。

img

不要忘记保存。

img

这样之后, 你再运行一次程序, 打开的就是控制台了。

img

然后, 设置好了控制台之后这, 我们还可以修改控制台的名称, 控制台的名称默认是它:

img

但是我们可以通过函数, 将这个控制台的名称修改成我们想要的。这个函数是system, 使用方法如下:

#include<Windows.h>//使用system函数需要包含windows.h头文件
 
int main() 
{
	//HANDLE h_out_put = GetStdHandle(STD_OUTPUT_HANDLE);
 
	system("title 贪吃蛇");
 
	return 0;
}

但是, 这样还不行, 当我们这样运行的时候, 运行出来的结果还可能和上面的一样,因为这个时候我们的程序运行速度太快了。就像如图:

img

这就是因为程序运行太快, 我们还没有看到控制台的名称改变程序就已经结束了。 所以我们可以在最后加一下暂停程序, 方便我们观察:

代码如下:

#include<Windows.h>
int main() 
{
	//HANDLE h_out_put = GetStdHandle(STD_OUTPUT_HANDLE);
 
	system("title 贪吃蛇");
 
 
	system("pause");//暂停程序
	return 0;
}

system(“pause”)就是暂停我们的程序。方便我们观察, 如下图就是我们在修改控制台名称后的程序界面:

img

修改完名称之后,现在我们来修改界面窗口的大小。修改界面窗口大小的函数如下:

#include<Windows.h>
int main() 
{
	//HANDLE h_out_put = GetStdHandle(STD_OUTPUT_HANDLE);
	//system("title 贪吃蛇");
	//system("pause");
 
    system("mode con cols=100 lines=30");
 
	return 0;
}

修改界面窗口的函数也是system函数, 只是里面的语句格式变成了: "mode con cols=(数字) lines=(数字));这是运行出来后的界面大小:

img

这是原本的界面大小:

img

以上是设置控制台界面的内容, 现在, 我们来看光标操作

1.3 操作光标

1.3.1 光标的显示

我们上文介绍了如何获取句柄。 那么句柄有什么用呢? 其实, 操作光标就是句柄的一个应用。 我们获取到标准输出的句柄之后, 就可以利用这个句柄来获取标准输出的光标, 然后我们就可以修改光标的信息了。

现在来具体操作一下:首先, 我们要创建一个光标的变量, 而光标类型的结构体是: CONSOLE_CURSOR_INFO,创建完成后是这样:

#include<stdio.h>
#include<Windows.h>
int main() 
{
 
	system("title 贪吃蛇");
	system("pause");
	system("mode con cols=100 lines=30");
 
	HANDLE h_out_put = GetStdHandle(STD_OUTPUT_HANDLE);
	CONSOLE_CURSOR_INFO cscsif;
 
 
	return 0;
}

这里面cscsif 就是一个光标变量。 这里面存储的是光标的信息, 但是因为现在只是刚刚定义出来, 还没有储存标准输出的光标信息。 现在我们通过标准输出的句柄来获取光标的信息。

这里又要用到一个函数: GetConsoleCursorInfo()。 这个函数有两个参数, 第一个参数用来传送句柄, 第二个参数用来传送光标变量的地址。 然后就可以将句柄所指的标准设备里面的光标信息拷贝到光标变量之中。

代码如下:

#include<Windows.h>
#include<stdio.h>
 
int main() 
{
 
	system("title 贪吃蛇");
	system("pause");
	system("mode con cols=100 lines=30");
 
	HANDLE h_out_put = GetStdHandle(STD_OUTPUT_HANDLE);
	CONSOLE_CURSOR_INFO cscsif;
	GetConsoleCursorInfo(h_out_put, &cscsif);//获取标准输出的光标信息
 
	return 0;
}

获取到光标的信息之后, 我们就可以对光标的信息进行修改。

在修改光标信息之前, 我们要知道光标的信息有哪些: 其实, 这里光标的信息, 也就是光标变量里面的成员变量只有两个, 一个是DWORD类型的 dwSize, 一个是BOOL类型的bVisible。

其中dwSize是控制光标的填充范围。这个数据是1 ~ 100,当dwSize == 100时, 光标是填满一个单位, 当dwSize == 1 时, 光标为一条横线。

具体的调整光标的填充范围的操作过程是这样的: 先获取光标信息。 然后将光标信息修改, 也就是将dwSize修改或者bVisible修改。 修改后再利用SetConsoleCursorInfo函数设置光标。 这样才能完成光标的设置, 如果没有最后的SetConsoleCursorInfo函数, 那么光标就完不成改变。

如图为本人写的光标设置的流程;调试到了光标为100的时候:

img

如图为调试到了光表为1的时候:

img

现在来看一下bVisible的用法,bVisible是控制光标的隐藏或者显示, 这个数据是false或者true。

bVisible的默认值是true, 也就是光标是显示的。 如果我们想要将光标设置为不显示, 那么就要让bVisible的值变成false。

具体的操作过程是这样的:先将bVisible的值置为false, 然后再使用SetConsoleCursorInfo()函数将光标信息设置。 代码如下:

#include<Windows.h>
#include<stdio.h>
#include<stdbool.h>
int main() 
{
 
	system("title 贪吃蛇");
	system("mode con cols=100 lines=30");
 
	HANDLE h_out_put = GetStdHandle(STD_OUTPUT_HANDLE);
	CONSOLE_CURSOR_INFO cscsif;
	GetConsoleCursorInfo(h_out_put, &cscsif);
 
	//将光标信息设置成false:
	cscsif.bVisible = false;
	SetConsoleCursorInfo(h_out_put, &cscsif);
 
 
	system("pause");
	return 0;
}

注意包含头文件: stdbool.h;下面为代码运行图:

img

像如图, 并没有光标闪动, 说明我们设置成功。

1.3.2 光标定位

光标定位也是我们实现贪吃蛇过程中需要使用到的一个重要操作。 接下来讲解什么是光标定位, 如果光标定位。

(1)什么是光标定位

如图, 我们的光标在这里:

img

假如我们想要让光标移动到中间, 那么我现在来操作一下, 我现在写一串代码, 让这个光标移动到中间。 如图:

img

这个过程, 就是光标定位的过程。 那么我是如何实现的呢, 现在我们就来解析, 如何进行光标定位。

(2)如何进行光标定位

在进行光标定位之前, 我们要引入一个新的概念。 就是控制台的坐标轴, 控制台的坐标轴。控制台是由坐标轴的, 就是如图:

img

这个坐标轴是以左上角为中心原点。然后向右为x轴的正方向, 向下为y轴的正方向。 并且我们使用鼠标左击控制台看到的黑格子就是一个单位面积:

img

**这个黑色长方形的长就是y轴上面的一个单位长度。 这个黑色长方形的宽就是x轴上面的一个单位长度。**然后,知道了这些概念之后, 我们就可以知道一个新的结构体了: COORD类型。

这个类型的对象可以用来给光标进行定位, 它里面右两个成员变量, 一个是short x, 一个是short y。 所以我们对这个类进行定义并且初始化的时候要这样写:

	COORD pos = { 40, 15 };//这里举一个例子

然后,我们对光标进行定位的时候需要用到一个新的函数SetConsoleCursorInfo(),这个函数有两个参数: 第一个参数是句柄类型, 传过去的是我们的句柄;第二个参数是坐标类对象, 传过去的是我们定义的坐标。 我们先看代码 具体操作代码如图:

#include<Windows.h>
#include<stdio.h>
#include<stdbool.h>
int main() 
{
 
	system("title 贪吃蛇");
	system("mode con cols=100 lines=30");
 
	HANDLE h_out_put = GetStdHandle(STD_OUTPUT_HANDLE);
 
 
	COORD pos = { 40, 15 };//坐标
	SetConsoleCursorPosition(h_out_put, pos);//设置坐标的函数
 
 
	system("pause");
	return 0;
}

这一串代码功能是这样的: 先创建了一个句柄变量, 然后获取到了标准输出的句柄。

然后又定义了坐标变量, 将坐标初始化为了(40, 15), 然后使用SetConsoleCursorPosition()函数将坐标设置。最后的system()就是普通的暂停, 为了将程序暂停一下方便我们进行观察。现在看一下运行结果:

img

如图, 我们已经成功的实现的坐标的定位。

现在为了下面实现贪吃蛇是更加方便, 我们在这里先封装一个光标定位的函数:

//光标定位的函数, 只需要传x, y就可以
void SetPos(int x, int y)
{
	HANDLE h_out_put = GetStdHandle(STD_OUTPUT_HANDLE);
	//获取拒柄
	COORD pos = { x, y };
	//设立坐标.
	SetConsoleCursorPosition(h_out_put, pos);//定位光标.
}

注意, 这一个函数很重要, 后面会反复用到。

1.4 按键情况

还有一个准备知识点就是如何获取键盘上面的案件情况。 这里需要用到一个函数:GetAsyncKeyState( int vkey ); 这个函数可以用来区分按键得状态。

这个函数的返回值是一个short类型的数据:如果返回值的最高位是1, 那么说明案件的状态是按下, 如果最高位是0, 那么说明按键的状态是抬起; 如果最低位是1, 说明这个按键被按过, 如果最低位是0, 说明这个按键没有被按过。显然, 他有一个参数, 这个参数虽然是整形类型。 但是它代表着键值。

这里有一个规定, 键盘上的每一个建都有一个虚拟键值。这个键值可以用来区分哪一个建被按过, 哪一个健没有被按。 现在我们来设计一个小程序来应用一下GetAsyncKeyState( int vkey ) 函数。

如下是程序的代码。 这个程序的意思就是:如果没有按0, 那么函数的返回值为0, 不会打破死循环, 一直打印1。 如果按0, 那么函数的返回值是1, 就会进入if判断中, 那么就会打破循环, 结束程序。

#include<Windows.h>
#include<stdio.h>
#include<stdbool.h>
int main() 
{
 
	while (1) 
	{
		printf("1\n");
		if (GetAsyncKeyState(0x30))
			break;//这里的0x30是数字0的键值。
	}
 
 
	system("pause");
	return 0;
}

下图为运行图: img

如图还没有按0的时候, 这个时候程序死循环的打印1.

img

当我按下0后, 程序就停下来了。这就是键值函数的功能。这就是因为如果我没按0,那么循环就一直跑,那个键值函数就一直在返回0, if判断就一直进不去。 但是我一旦按下0, 那么键值函数就会返回1, 就会进入if判断中。 然后break跳出循环,结束程序。

ok以上就是实现贪吃蛇需要学习的所有知识点。 下面开始贪吃蛇的学习。(先附上一个板块)

附:在本节要实现的贪吃蛇中要用到的键值如下(如果有兴趣了解更多的话,可以自行百度了解):

作用键值
VK_UP
VK_DOWN
VK_LEFT
VK_RIGHT
空格VK_SPACE
ESCVK_ESCAPE
F3VK_F3
F4VK_F4

因为后续会频繁判断是否按下某个健, 我们这里将它封装成一个宏。

//判断是否按下某个健的宏
#define KEY_PRESS(KV) ((GetAsyncKeyState(KV) & 0x1) ? 1 : 0)

注意,这个宏和SetPos一样, 很重要, 后面会频繁调用。

1.5 宽字符打印

最后一个需要知道的知识点就是宽字符的打印。 宽字符的打印就是打印汉字这种占用两个单位面积的符号。 在ASCII中, 一个符号是占用一个字节, 一个符号。 但是ASCII只有128个, 不能用来标识汉字以及其他一些国家的文字, 所以, 后来为了能标识汉字以及一些文字多的国家, 就引入了宽字符的类型。 宽字符一般占用两个字节(如果是ASCII上面的那些字符, 变成宽字符后也会占用两个字节), 打印的时候一般也会占用两个单位面积(这个和前面不同, 如果是ASCII上面的那些字符, 变成宽字符后打印的时候也是按照原本的方式进行打印)。

使用宽字符, 需要用到locale.h头文件。 这里我们要引入类项的概念。

类项:一个库中是有很多部分的, 有字符串部分, 时间部分, 打印部分等等。 这些不同的部分就是不同的类项。 然后我们可以通过改变类型的模式,修改它的使用环境。这里就要用到一个函数: setlocale(该函数使用需要包含头文件locale.h);

setlocale函数可以将一个类项从正常的模式改变成其他的模式, 第二个参数是模式

我们在贪吃蛇的实现中也会用到这个函数, 但是我们只是使用它将所有的类项改编为本地模式, 所以我们这里只提到所有类项:LC_ALL, 以及本地模式 :“”;(注意, 本地模式就是一个双引用, 中间什么都没有, 如果像了解其他类项或者模式可以在百度搜索)

了解完类项的概念之后, 我们就需要知道宽字符如何打印了, 首先想要打印宽字符必须设置成本地模式, 否则打印出来的就是一堆”?” , 所以在打印宽字符之前我们要先setlocale一下.

img

将模式设置为本地模式之后, 我们就可以打印宽字符, 这里要用到一个新的打印函数wprintf(); 这个函数的使用方法和printf()类似, 具体使用如下:

img

重点要看到这里的L, wprintf里面的参数前面要加个L,代表打印宽字符。然后宽字符类型怎么表示呢 ?

宽字符有自己的类型, 这个类型是wchar_t, 这个类型的字符大小为两个字节。如图:

img

好, 以上就是宽字符的全部内容, 现在开始贪吃蛇的实现。

注意:因为贪吃蛇的实现要比通讯录或者扫雷之类的难,并且许多地方如何处理并不容易想到, 博主在实现过程中有些地方是直接给出结果的。 不会带友友们深究 如何想到这些的。(ps: 我也想问, 怎么想到这些的)

2 贪吃蛇实现

在实现贪吃蛇之前, 我们看一下这段视频:

贪吃蛇项目实战——学习详解

看一下第一个画面:

img

这是不是一个贪吃蛇游戏的初始画面, 也可以叫做主界面。然后, 那么显然, 我们的贪吃蛇游戏中必须要实现这么一个主界面。 然后我们再往下看:

img

如图是我截取的一张画面。 这张图中有蛇, 有食物, 有墙, 有分数等等。 这就说明我们要实现的贪吃蛇当中必须有这些东西。

我将贪吃蛇的实现分成了三个部分:

第一个部分是游戏的初始化, 用来初始化蛇, 食物, 以及打印墙这些操作。

第二个部分是程序的运行, 也是程序的主干和难点,需要用到链表的知识。这部主要是实现蛇的移动, 键值的判断, 是否吃掉食物, 撞到墙壁等等。

第三个部分就是程序的结束, 需要收尾的工作。

2.1 先定义好蛇的结构体

打开解决方案管理, 右击头文件, 点击添加新建项。

img

创建一个snake.h头文件, 用来定义蛇的结构体以及函数声明。

img

然后再打开解决方案管理器, 右击源文件, 点击新建项。

img

创建一个snake.c文件用来实现贪吃蛇的主要功能。

img

这些准备工作做好之后是这样的:

img

现在就来实现贪吃蛇的结构体。

分析:我们可以使用链表来作为贪吃蛇的身体以及食物。 蛇每吃掉一个食物, 那么它的长度就会增加1。就代表着只要蛇吃掉食物, 我们只需要将食物节点链接到蛇身上就可以。所以, 蛇的身体和食物我们可以使用链表节点来表示。

除了考虑蛇的身体和食物需要用链表打印之外, 我们还要考虑节点之中除了next指针之外, 还有什么变量。 这里很难想到, 我在这里直接告诉友友, 我们的节点之中除了next指针之外, 还要有节点的坐标, 这里的坐标就是这个节点在控制台的坐标轴里的坐标。 通过这个坐标, 我们就可以定位光标, 然后在坐标位置打印节点, 从而打印蛇的身体或者食物。 那么我们就先来定义一下节点的结构体。

#pragma once
#pragma warning(disable : 4996)
#define _CRT_SECURE_NO_WARNINGS 1
#include<Windows.h>
#include<stdio.h>
#include<stdbool.h>
#include<stdlib.h>
//以上为需要用到的头文件
 
 
//蛇的节点的结构体
typedef struct gamenode 
{
	struct gamenode* _next;
	int _x;
	int _y;
}gnode, * pgnode;

定义好了之后, 我们继续分析。 现在我们有了节点的结构体, 那么我们还要有什么? 是不是要有蛇头的方向,游戏的状态。 而蛇头的方向分为: 上下左右。 游戏的状态有正常的状态, 有死亡的状态。这里我直接使用枚举进行定义这两个状态。

//蛇头方向
enum direction 
{
	_up,
	_down,
	_left,
	_right
};
 
//游戏状态
enum gamestate 
{
	_ok,//游戏正常的状态
	_kill_by_self,//撞到自己身体的状态
	_kill_by_wall//撞到墙的状态
};

ok, 继续分析。现在我们有了蛇的身体, 蛇的食物, 游戏状态, 蛇头方向。 是不是还要有墙, 但是墙是一个静止不动的, 他不和前面这几个一样, 是动态的。 像蛇的身体会移动, 食物被吃掉会移动, 游戏状态会改变, 蛇头方向会改变。

所以, 像墙这种不会改变的数值我们可以直接打印。 那么就先不考虑它。 那么想一下, 食物的分数和游戏的难度还有游戏的总分是不是会发生变化, 他们是不是动态的。 所以他们要考虑。 怎么考虑?

这里直接给出答案, 我们可以将他们这些变的量, 都封装在一个结构体中。 类似于面向对象, 封装成一个游戏的结构体。 现在我们来做一下:

//贪吃蛇游戏的结构体
typedef struct snake
{
	pgnode _snake_head;    //蛇
	pgnode _food;          //食物
 
	enum direction _dir;   //蛇头方向
	enum gamestate _state; //游戏状态
 
	int _sum_score;		   //总分
	int _food_score;	   //食物的分数
	int _speed;            //游戏的难度
};

以上就是整个.h文件的结构体准备部分。 现在我们来看一下头文件中要有哪些内容:

img

2.2 游戏的初始化

现在, 我们来着手实现游戏的初始化部分。先将我们的SetPos和判断键值的宏放在.c文件中, 方便后续调用。

img

注意, 别忘了将SetPos放在头文件, 而KEY_PRESS我们直接放到头文件,这样可以做到main.c和snake.c都可以使用这两个操作。

img

img

首先,游戏的初始化我们必须把蛇和食物, 墙之类的都打印出来。 这其实就是一个绘图的过程。但是在打印这些东西之前,我们还要打印一下欢迎界面。

2.2.1 欢迎界面

我们先利用我们上面学习的知识。 将光标隐藏。

#include"snake.h"
 
 
//判断是否按下某个健的宏
#define KEY_PRESS(KV) ((GetAsyncKeyState(KV) & 0x1) ? 1 : 0)
 
//光标定位的函数, 只需要传x, y就可以
void SetPos(int x, int y)
{
	HANDLE h_out_put = GetStdHandle(STD_OUTPUT_HANDLE);
	//获取拒柄
	COORD pos = { x, y };
	//设立坐标.
	SetConsoleCursorPosition(h_out_put, pos);//定位光标.
}
 
void game_init() 
{
	//隐藏光标
		//欢迎界面的打印:
	//先隐藏光标
	HANDLE h_out_put = GetStdHandle(STD_OUTPUT_HANDLE);//获取句柄
	CONSOLE_CURSOR_INFO ConsoleCursor;//创建光标变量
	GetConsoleCursorInfo(h_out_put, &ConsoleCursor);//获取标准输出光标信息
	ConsoleCursor.bVisible = false;//将标准输出中的光标显示置为false
	SetConsoleCursorInfo(h_out_put, &ConsoleCursor);//将光标信息设置。成功隐藏光标
 
 
}

打印欢迎界面我们重新写一个函数, 我这里将这个函数写成welcome()实现过程如下:

 
//欢迎界面的打印
void welcomegame()
{
	//先设定好控制台窗口
	system("mode con cols=100 lines=30");
	system("title 贪吃蛇");
 
 
	//打印第一个欢迎窗口
	SetPos(40, 15);//定位光标到40, 15的位置。 
	printf("欢迎来到贪吃蛇\n");//打印欢迎来到贪吃蛇
	SetPos(40, 20);//定位光标到40, 20的位置。
	system("pause");
 
 
}

现在是打印的第一个界面,第一个界面只有一个“欢迎来到贪吃蛇” 这里几行代码如果运行的话打印是这样的:

img

这里我们可以使用一个界面刷新的操作, 将界面清理掉。 造成一种切换界面的视觉效果。 而界面刷新的函数是这个 : system(“cls”);如果我们不加刷新界面, 结束时这样的:

img

如果我们加上刷新界面, 那么结束时是这样的:

img

就相当于我们的界面清空了, 然后再结束的程序。

所以, 连接两个界面之间的操作,我们可以使用界面清空函数来完成。下面是实现好第二个界面打印的欢迎界面函数:

 
 
//欢迎界面的打印
void welcomegame()
{
	//先设定好控制台窗口
	system("mode con cols=100 lines=30");
	system("title 贪吃蛇");
 
 
	//打印第一个欢迎窗口
	SetPos(40, 15);//定位光标到40, 15的位置。 
	printf("欢迎来到贪吃蛇\n");//打印欢迎来到贪吃蛇
	SetPos(40, 20);//定位光标到40, 20的位置。
	system("pause");
 
 
	//刷新屏幕
	system("cls");
 
 
	//定位光标
	SetPos(40, 15);
	printf("贪吃蛇游戏:\n");
	//定位光标
	SetPos(10, 16);
	printf("上 : ↑, 下↓, 左←, 右→ 控制蛇的移动, 空格暂停游戏, Esc退出游戏\n");
	//定位光标
	SetPos(40, 20);
	system("pause");
	//末尾再刷新一下屏幕, 准备进入游戏界面
	system("cls");
}

现在来看一下运行效果:

img

第一张图是第一个界面, 第二张图是第二个界面。 这就是欢迎界面的打印。现在来看一下墙体的打印

2.2.2 墙体的打印

游戏界面因为游戏的界面我设置的是100列, 30行。 所以我们的墙体要打印在这个范围里,并且因为我们要给玩家一些提示性信息, 就要留出一些空位, 像这里:

img

这就是我预留出来的为了打印提示性信息的地方。 这里我将墙体打印成了一个59列, 26行的长方形。这里的59列, 其实真正有60列, 因为下标从0开始。 如果想打印其他大小的墙体友友们可以自行设置, 但要注意, *我们的墙体的列必须是一个奇数*,我们的蛇的身体一个节点要打印成宽字符就要占用两个x坐标。而下标是从0开始的。 如果墙体的列是奇数, 那么就可以做到我们墙里面的蛇的移动空间在x轴上面是偶数。 就不会出现蛇头半个嵌入墙体的情况。

还有一个点就是因为我们要打印墙, 还要经常打引蛇的身体和食物。 所以我们可以将这三个宽字符#define一下, 如图:

img

我在这里将打引墙壁封装成为了一个函数:

//墙的打印
void wall_print() 
{
	//在第0行从x = 0开始向后打印宽字符wall, 60个单位就是打印到i为29的位置
	for (int i = 0; i <= 29; i++)
	{
		wprintf(L"%lc", WALL);
	}
 
	SetPos(0, 26);//将光标定位到行为26的位置, 从第26行的x为0的位置向后打印29个宽字符wall
	for (int i = 0; i <= 29; i++)
	{
		wprintf(L"%lc", WALL);
	}
 
	//这里就是打印y轴上面的墙体了。
	for (int i = 1; i < 26; i++)
	{
		SetPos(0, i);//在循环里面定位坐标的行, 每一次打印完之后向下移动一位
		wprintf(L"%lc", WALL);
	}
 
	for (int i = 1; i < 26; i++)
	{
		SetPos(58, i);//注意, 要控制这里的列, 因为我们是打印到下表为59的位置, 而一个宽字符在x轴上面占用两个单位, 所以要定位到58列处
		wprintf(L"%lc", WALL);
	}
}

现在我们来打引初始化蛇和打引蛇

2.2.3 蛇初始化和打引

在对蛇进行初始化之前, 我们要先利用贪吃蛇的结构体创建一个实例化对象, 这个实例化对象要在初始化之前进行,方便我们后需进行操作。 所以, 我们可以这么做,先将我们的贪吃蛇结构体typedef一下, 如图:

//贪吃蛇游戏结构体
typedef struct snake
{
	pgnode _snake_head;    //蛇
	pgnode _food;          //食物
 
	enum direction _dir;   //蛇头方向
	enum gamestate _state; //游戏状态
 
	int _sum_score;		   //总分
	int _food_score;	   //食物的分数
	int _speed;            //游戏的难度
}snake, * psnake;//重点,后面的结构体指针,反复要用

再封装一个game_init函数, 如图为函数声明:

void game_init(psnake snake);

这个函数里面的参数我们传的是贪吃蛇结构体的指针。通过这个指针, 就可以找到贪吃蛇游戏里面的任何一个变量, 方便我们对游戏进行修改。 那么我们就可以将我们上面的欢迎界面打印以及墙体的打印等放进这个函数去了。如图:

img

我们的主函数可以这样写:

img

ok, 做好上面的操作之后, 我们可以着手实现蛇的初始化操作了。 这里先将贪吃蛇结构体对象里面的成员变量进行初始化。

 
//先将贪吃蛇结构体对象里面的成员初始化。
void snake_init(psnake snake) 
{
 
	snake->_snake_head = NULL;
	snake->_food = NULL;
	snake->_speed = 300;//游戏的初始速度是300毫秒
	snake->_food_score = 6;//初始分数是6分
	snake->_dir = _right;//初始方向是右边
	snake->_state = _ok;//游戏的初始状态是ok的
	snake->_sum_score = 0;//游戏的初始总分为0;
}

然后再创建蛇的身体并且打印, 同样封装成一个函数, 便于维护。

void snake_body(psnake snake) 
{
 
	//先初始化蛇的身体。
	for (int i = 0; i < 5; i++) 
	{
		//申请节点。
		pgnode newnode = (pgnode)malloc(sizeof(gnode));
		if (newnode == NULL)
		{
			perror("申请节点失败\n");
			return -1;
		}
		//
		newnode->_next = NULL;
 
 
		//申请成功之后
		if (snake->_snake_head == NULL)//如果蛇头是空指针, 那么就将节点连接到蛇头上面。
		{
			snake->_snake_head = newnode;
			newnode->_x = 24;                  //这里是设置蛇头的x坐标,
			newnode->_y = 5;                   //设置蛇头的y坐标
			SetPos(newnode->_x, newnode->_y);  //然后光标定位到这个坐标。
			wprintf(L"%lc", BODY);             //光标定位到这个坐标之后, 打印蛇的身体。
		}
		else
		{
			newnode->_x = 24 + 2 * i;               //同上, 这里设置蛇的身体的x坐标
			newnode->_y = 5;                        //设置蛇的身体的y坐标
			newnode->_next = snake->_snake_head;    //利用头插法连接蛇的身体。
			snake->_snake_head = newnode;           //
			SetPos(newnode->_x, newnode->_y);       //将光标定位到这个坐标
			wprintf(L"%lc", BODY);                  //打印蛇的身体
 
		}
 
	}
	SetPos(25, 6);
 
}

2.2.4 打印食物

打印食物的时候, 要注意两个细节 :第一个细节就是食物的坐标不能和蛇的坐标重合。第二个细节就是食物的x坐标必须是偶数, 因为蛇头的x坐标是偶数, 如果食物的坐标是奇数的话就会出现蛇吃掉半个食物的情况

//食物的创建以及打印
void snake_food(psnake snake) 
{
	srand(time(0));
	int x = 0;
	int y = 0;
 
	while (1)
	{
		int flag = 1;
		x = 2 + (rand() % 54);
		y = 1 + (rand() % 24);
 
		pgnode cur = snake->_snake_head;
		while (cur)                          
		{
			if (x % 2 != 0)                       //这里就是看食物的x坐标是否是偶数。
			{
				flag = 0;
			}
			if (cur->_x == x && cur->_y == y)     //这里是对蛇的身体进行检查, 看是否有身体的节点坐标等于食物的坐标
			{
				flag = 0;
			}
			if (flag == 0)                         //出现上面两种情况flag就变成0, 那么就会跳出循环。
			{
				break;
			}
			cur = cur->_next;
		}
		if (flag == 1)
		{
			break;
		}
 
	}
 
	snake->_food = (pgnode)malloc(sizeof(gnode));
	if (snake->_food == NULL) 
	{
		perror("申请节点失败");
		return -1;
	}
 
	//
	snake->_food->_next = NULL;
	snake->_food->_x = x;            
	snake->_food->_y = y;
	SetPos(x, y);                    //定位光标
	wprintf(L"%lc", FOOD);           //打印食物
}

其实到现在我们的界面就基本完成了, 只差一个帮助信息打印。现在友友们的.c文件game_init函数里面应该是这样的:

img

然后我们的主函数是这样的:

img

我们先来运行一下代码看一下效果:

img

img

img

现在来打引帮助信息。

2.2.5 帮助信息

帮助信息其实就是在我们墙体的右边部分打引上一些话,

img

如图为代码:

//打引帮助信息
void snake_help() 
{
	SetPos(63, 15);
 
	wprintf(L"贪吃蛇游戏:\n");
	SetPos(63, 16);
	wprintf(L"上:↑ 下:↓ 左:← 右:→ 控制蛇的移动\n");
	SetPos(63, 17);
	wprintf(L"空格暂停游戏,Esc退出游戏\n");
	SetPos(63, 18);
	wprintf(L"F3加大游戏难度, F4降低游戏难度");
	SetPos(63, 20);
	wprintf(L"作者:打鱼又晒网");
	SetPos(40, 29);
 
	return 0;
}

现在我们来看一下游戏界面:

img

以上, 就是整个游戏的初始化。 这里面有一些需要注意的点并没有说清楚, 但是也不好说清楚, 因为展开说篇幅太长。 就在简单提一句关于三个界面的切换问题。 相信友友们看到这可能很懵界面是怎么切换的。 其实上面也简单说过一次, 就是关于界面切换其实就是界面刷新。 营造出的一种界面切换的视觉效果。 看着一张图:

img

你看我在欢迎界面函数里面调用了两次界面刷新的函数, 为的就是对界面进行切换, 第一个界面刷新是为了跳到第二个欢迎界面, 第二个界面刷新是为了跳到游戏界面。 其他具体不做赘述, 接下来开始游戏的运行部分

2.3 游戏的运行

2.3.1 贪吃蛇的移动

接下来是贪吃蛇最难的一部分, 也是整个游戏的核心——贪吃蛇如何移动。我们先想一下蛇头的方向和贪吃的移动的关系:

对于贪吃蛇来说, 蛇头如果朝向右, 那么我们如果按下上和下或者右, 它都会将蛇头扭向我们按下的方向(原本朝向右, 按下右还是朝向右)。但是如果我们按下左, 它就不能将蛇头扭向左。

其他方向也是这样, 只要我们按下的不是对于这个方向来说相反的方向, 贪吃蛇就能将蛇头扭向那个方向。

ok, 那这里我们就处理完了第一个蛇头朝向的问题, 我们先封装一个game_run的函数, 在这个函数里面实现一下刚刚的操作。

 
//游戏的运行
void game_run(psnake snake)
{
 
	if (KEY_PRESS(VK_UP) && snake->_dir != _down)
	{
		snake->_dir = _up;
	}
	//下
	else if (KEY_PRESS(VK_DOWN) && snake->_dir != _up)
	{
		snake->_dir = _down;
	}
	//左
	else if (KEY_PRESS(VK_LEFT) && snake->_dir != _right)
	{
		snake->_dir = _left;
	}
	//右
	else if (KEY_PRESS(VK_RIGHT) && snake->_dir != _left)
	{
		snake->_dir = _right;
	}
 
}

这里要判断是否按键, 所以要用到我们定义的宏: KEY_PRESS(KV)。如果我们按下了右, 并且蛇头不朝向左边, 那么蛇头就扭向右;如果我们按下了左, 并且蛇头不朝向右边, 那么蛇头就扭向左; 如果我们按下了上, 并且蛇头不朝向下, 那么蛇头就扭向上; 如果我们按下了下,并且蛇头不朝向上, 蛇头就扭向下。

修改完方向之后, 我们既可以让蛇向前走一步。 走一步的本质其实就是创建一个节点连接到蛇头。 然后根据节点的坐标是否与食物相等判断是否要释放蛇尾节点。 如果新节点坐标和食物节点坐标相同,代表蛇吃到食物,长度加一,那么就不去释放蛇尾节点;如果新节点坐标和食物节点坐标不同, 代表蛇没有吃到食,蛇的长度应该不变, 但是我们现在新链接了一个头结点,那么就要去释放蛇尾节点。

我们先来封装一个根据蛇头方向在蛇头处连接一个头结点的函数。

 
//根据蛇头方向, 在蛇头处连接一个新节点
void snake_step(psnake snake, int x, int y) 
{
	pgnode newnode = (pgnode)malloc(sizeof(gnode));
	if (newnode == NULL) 
	{
		perror("内存不足\n");
		return -1;
	}
	//
	newnode->_next = NULL;
	newnode->_x = x;
	newnode->_y = y;
	newnode->_next = snake->_snake_head;
	snake->_snake_head = newnode;
 
}

下图是蛇走一步的代码

 
//这里的snake_step函数就是根据蛇头方向, 在蛇头处连接头结点的函数。
void step_move(psnake snake) 
{
	switch (snake->_dir) 
	{
	case _up:
		snake_step(snake, snake->_snake_head->_x, snake->_snake_head->_y - 1);
 
		break;
	case _down:
		snake_step(snake, snake->_snake_head->_x, snake->_snake_head->_y + 1);
 
		break;
	case _left:
		snake_step(snake, snake->_snake_head->_x - 2, snake->_snake_head->_y);
 
		break;
	case _right:
		snake_step(snake, snake->_snake_head->_x + 2, snake->_snake_head->_y);
 
		break;
	}
 
	if (snake->_snake_head->_x == snake->_food->_x && snake->_snake_head->_y == snake->_food->_y) 
	{
		Eatfood(snake);
	}
	else if (snake->_snake_head->_x <= 1 || snake->_snake_head->_x >= 58 || snake->_snake_head->_y == 0 || snake->_snake_head->_y == 26) 
	{
		//撞墙了。游戏结束
		snake->_state = _kill_by_wall;
	}
	else if (judge_self(snake)) 
	{
		snake->_state = _kill_by_self;
	}
	else 
	{
		Step(snake);
	}
}

这一串代码很重要, 我会着重讲解:

img

如图, 其实蛇走一步之后是有很多种情况的, 我在图中就将他们分成了****四种情况*。 我们先不谈这四种情况, 我们先来谈一下蛇走一步怎么走, 也就是我在图中红框框的部分*:如果蛇头的朝向是_up, 并且蛇头的坐标是(x, y),那么蛇的下一步蛇头的位置就应该是(x, y - 1); 如果蛇头的朝向是_down, 并且蛇头的坐标是(x, y), 那么蛇的下一步蛇头的位置就应该是(x, y +1); 如果蛇头的朝向是_left, 并且蛇头的坐标是(x, y) , 那么蛇的下一步蛇头的位置就应该是( x - 2, y); 如果蛇头的朝向是_right, 并且蛇头的坐标是(x, y), 那么蛇头的坐标就是(x + 2, y)。

而snake_step函数就是根据传进去的坐标创建节点连接到蛇头上面。 这样就完成了蛇走一步。

然后就是判断蛇走一步之后的情况:首先看一下绿框框, 绿框框就是判断是否走一步之后蛇头的位置等于食物的位置,而判断条件就是蛇头坐标是否等于食物坐标

其次再看一下蓝色框框, 蓝色框框就是判断蛇头坐标是否到了或者超出了墙。判断条件就是蛇头的x坐标是否小于等于1, 或者大于等于58;以及y坐标是否小于等于0, 大于等于26。

接下来看一下紫色框框, 紫色框框就是判断蛇是否撞到了自己。 因为判断过程比较复杂, 这里我封装成一个函数进行判断。

最后就是我画的红色横线, 这是什么情况都没有出现, 就是正常走一步。

知道这些之后, 我们再逐个对图中的函数进行实现, 先实现吃掉食物的函数。如下:

//吃掉食物
void Eatfood(psnake snake) 
{
    //定位光标到食物的位置, 将食物的位置打印成贪吃蛇的身体。
	SetPos(snake->_food->_x, snake->_food->_y);
    wprintf(L"%lc", BODY);
	free(snake->_food);//食物被吃掉, 那么食物节点接没有作用了。将其释放
 
    //打印完成之后再重新创建一个食物的节点
	int x = 0;
	int y = 0;
 
	while (1)
	{
		int flag = 1;
		x = 2 + (rand() % 54);
		y = 1 + (rand() % 24);
 
		pgnode cur = snake->_snake_head;
		while (cur)
		{
			if (x % 2 != 0) 
			{
				flag = 0;
			}
			if (cur->_x == x && cur->_y == y)
			{
				flag = 0;
			}
			cur = cur->_next;
		}
		if (flag == 1) 
		{
			break;
		}
 
	}
 
	pgnode newnode = (pgnode)malloc(sizeof(gnode));
	if (newnode == NULL) 
	{
		perror("申请节点失败\n");
		return -1;
	}
	//
	newnode->_next = NULL;
	newnode->_x = x;
	newnode->_y = y;
	snake->_food = newnode;
	SetPos(x, y);
	wprintf(L"%lc", FOOD);//打印新的食物节点
    snake->_sum_score += snake->_food_score;//吃掉食物要让总分增加
 
    
}

当撞到墙的时候说明游戏可以结束了, 那么就将游戏的状态置为kill_by_wall就可以。当撞到自己的身体的时候如何进行判断呢, 只需要让一个指针指向身体的第二个节点, 然后向后遍历, 只要发现该指针所指向的节点与蛇头节点的坐标相同,就说明撞到了贪吃蛇撞到了自己。 然后将游戏的状态置为kill_by_self就可以。下面是判断的过程:

//正常走一步的状态
void Step(psnake snake) 
{
    //让一个指针指向蛇头的下一个节点
	pgnode cur = snake->_snake_head->_next;
    //然后前一个指针指向蛇头
	pgnode prev = snake->_snake_head;
    //遍历, 让cur最终指向最后一个节点
	while (cur->_next != NULL) 
	{
		prev = cur;
		cur = cur->_next;
	}
	//定位光标到最后一个节点的坐标处, 将这个坐标打印成空
	SetPos(cur->_x, cur->_y);
	printf("%c%c", ' ', ' ');
	free(cur);//释放最后一个节点
    
    //这个时候因为prev指向的是cur前一个节点, 所以释放cur指向节点后,可以让prev指向的节点
    //的next指针指向空。
	prev->_next = NULL;
    //定位一下光标到蛇头处
	SetPos(snake->_snake_head->_x, snake->_snake_head->_y);
	//定位完成后打印蛇的身体
    wprintf(L"%lc", BODY);
}

最后就是正常走一步的情况,正常走一步的情况需要将蛇的尾节点删除, 还要将蛇的尾节点的坐标处打印上两个空格, 否则就会出现蛇身拉长的情况。如下是正常走一步的代码:

//正常走一步的状态
void Step(psnake snake) 
{
    //让一个指针指向蛇头的下一个节点
	pgnode cur = snake->_snake_head->_next;
    //然后前一个指针指向蛇头
	pgnode prev = snake->_snake_head;
    //遍历, 让cur最终指向最后一个节点
	while (cur->_next != NULL) 
	{
		prev = cur;
		cur = cur->_next;
	}
	//定位光标到最后一个节点的坐标处, 将这个坐标打印成空
	SetPos(cur->_x, cur->_y);
	printf("%c%c", ' ', ' ');
	free(cur);//释放最后一个节点
    
    //这个时候因为prev指向的是cur前一个节点, 所以释放cur指向节点后,可以让prev指向的节点
    //的next指针指向空。
	prev->_next = NULL;
    //定位一下光标到蛇头处
	SetPos(snake->_snake_head->_x, snake->_snake_head->_y);
	//定位完成后打印蛇的身体
    wprintf(L"%lc", BODY);
}

以上就是贪吃蛇移动一步的过程。 我们写完蛇走一步的过程之后, 就要处理蛇走多步的过程, 要知道, 贪吃蛇是不可能只走一步的, 所以我们接下来就要完成蛇的整个行走流程, 这里需要用到循环。如下 :

 
//游戏的运行
void game_run(psnake snake)
{
 
 
	do
	{
 
		Sleep(snake->_speed);//每走一步就挺speed秒, 这个speed也是用来控制游戏的难度的。
 
		//上
		if (KEY_PRESS(VK_UP) && snake->_dir != _down)
		{
			snake->_dir = _up;
		}
		//下
		else if (KEY_PRESS(VK_DOWN) && snake->_dir != _up)
		{
			snake->_dir = _down;
		}
		//左
		else if (KEY_PRESS(VK_LEFT) && snake->_dir != _right)
		{
			snake->_dir = _left;
		}
		//右
		else if (KEY_PRESS(VK_RIGHT) && snake->_dir != _left)
		{
			snake->_dir = _right;
		}
 
		//走一步
		step_move(snake);
 
	} while (snake->_state == _ok);//如果游戏状态是_ok那么就继续游戏, 否则退出游戏
 
}

2.3.2 辅助功能

实现了贪吃蛇的移动之后, 接下来就是贪吃蛇的加速, 咱等等一些辅助的功能了。现在来实现一下, 现将游戏分数, 食物分数和游戏难度进行打印:

 
void game_run(psnake snake)
{
 
	//打印游戏总分, 难度和食物的分数。
	SetPos(65, 5);
	wprintf(L"游戏总分:");
	SetPos(65, 7);
	wprintf(L"游戏难度:");
	SetPos(65, 9);
	wprintf(L"食物分数:");
 
 
	do
	{
		SetPos(74, 5);                              //每走一步总分, 难度和食物分数都可能被我们改变, 所以他们要跟着循环一起打印。
		wprintf(L"%d", snake->_sum_score);
		SetPos(74, 7);
		wprintf(L"%4d", 500 - snake->_speed);
		SetPos(74, 9);
		wprintf(L"%2d", snake->_food_score);
 
 
		Sleep(snake->_speed);//每走一步就挺speed秒, 这个speed也是用来控制游戏的难度的。
 
		//上
		if (KEY_PRESS(VK_UP) && snake->_dir != _down)
		{
			snake->_dir = _up;
		}
		//下
		else if (KEY_PRESS(VK_DOWN) && snake->_dir != _up)
		{
			snake->_dir = _down;
		}
		//左
		else if (KEY_PRESS(VK_LEFT) && snake->_dir != _right)
		{
			snake->_dir = _left;
		}
		//右
		else if (KEY_PRESS(VK_RIGHT) && snake->_dir != _left)
		{
			snake->_dir = _right;
		}
 
		//走一步
		step_move(snake);
 
	} while (snake->_state == _ok);//如果游戏状态是_ok那么就继续游戏, 否则退出游戏
 
}

然后再来实现加速和减速, 也就是加大游戏的难度和减少游戏的难度:

void game_run(psnake snake)
{
 
	//打印游戏总分, 难度和食物的分数。
	SetPos(65, 5);
	wprintf(L"游戏总分:");
	SetPos(65, 7);
	wprintf(L"游戏难度:");
	SetPos(65, 9);
	wprintf(L"食物分数:");
 
 
	do
	{
		SetPos(74, 5);                              //每走一步总分, 难度和食物分数都可能被我们改变, 所以他们要跟着循环一起打印。
		wprintf(L"%d", snake->_sum_score);
		SetPos(74, 7);
		wprintf(L"%4d", 500 - snake->_speed);
		SetPos(74, 9);
		wprintf(L"%2d", snake->_food_score);
 
 
		Sleep(snake->_speed);//每走一步就挺speed秒, 这个speed也是用来控制游戏的难度的。
 
		//上
		if (KEY_PRESS(VK_UP) && snake->_dir != _down)
		{
			snake->_dir = _up;
		}
		//下
		else if (KEY_PRESS(VK_DOWN) && snake->_dir != _up)
		{
			snake->_dir = _down;
		}
		//左
		else if (KEY_PRESS(VK_LEFT) && snake->_dir != _right)
		{
			snake->_dir = _left;
		}
		//右
		else if (KEY_PRESS(VK_RIGHT) && snake->_dir != _left)
		{
			snake->_dir = _right;
		}
		else if (KEY_PRESS(VK_F3))
		{
			//加速
			if (snake->_speed > 100)
			{
				snake->_speed -= 50;
				snake->_food_score += 2;
 
			}
		}
		else if (KEY_PRESS(VK_F4))
		{
			//减速
			if (snake->_speed < 600)
			{
				snake->_speed += 50;
				snake->_food_score -= 2;
			}
		}
 
		//走一步
		step_move(snake);
 
	} while (snake->_state == _ok);//如果游戏状态是_ok那么就继续游戏, 否则退出游戏
 
}

再来实现空格暂停健, 同时我们要封装一个函数, 用来死循环暂停游戏, 当我们再次按到空格键的时候, 就跳出死循环, 这个函数是这样封装的:

//暂停
void Stop()
{
	while (1)
	{
		Sleep(100);
		if (KEY_PRESS(VK_SPACE))
		{
			break;
		}
	}
}
 
 
void game_run(psnake snake)
{
 
	//打印游戏总分, 难度和食物的分数。
	SetPos(65, 5);
	wprintf(L"游戏总分:");
	SetPos(65, 7);
	wprintf(L"游戏难度:");
	SetPos(65, 9);
	wprintf(L"食物分数:");
 
 
	do
	{
		SetPos(74, 5);                              //每走一步总分, 难度和食物分数都可能被我们改变, 所以他们要跟着循环一起打印。
		wprintf(L"%d", snake->_sum_score);
		SetPos(74, 7);
		wprintf(L"%4d", 500 - snake->_speed);
		SetPos(74, 9);
		wprintf(L"%2d", snake->_food_score);
 
 
		Sleep(snake->_speed);//每走一步就挺speed秒, 这个speed也是用来控制游戏的难度的。
 
		//上
		if (KEY_PRESS(VK_UP) && snake->_dir != _down)
		{
			snake->_dir = _up;
		}
		//下
		else if (KEY_PRESS(VK_DOWN) && snake->_dir != _up)
		{
			snake->_dir = _down;
		}
		//左
		else if (KEY_PRESS(VK_LEFT) && snake->_dir != _right)
		{
			snake->_dir = _left;
		}
		//右
		else if (KEY_PRESS(VK_RIGHT) && snake->_dir != _left)
		{
			snake->_dir = _right;
		}
		else if (KEY_PRESS(VK_F3))
		{
			//加速
			if (snake->_speed > 100)
			{
				snake->_speed -= 50;
				snake->_food_score += 2;
 
			}
		}
		else if (KEY_PRESS(VK_F4))
		{
			//减速
			if (snake->_speed < 600)
			{
				snake->_speed += 50;
				snake->_food_score -= 2;
			}
		}
		else if (KEY_PRESS(VK_SPACE))
		{
			//暂停
			Stop();
 
		}
 
		//走一步
		step_move(snake);
 
	} while (snake->_state == _ok);//如果游戏状态是_ok那么就继续游戏, 否则退出游戏
 
}

最后再来一个函数功能, 游戏运行就收工了。esc退出:

 
void game_run(psnake snake)
{
 
	//打印游戏总分, 难度和食物的分数。
	SetPos(65, 5);
	wprintf(L"游戏总分:");
	SetPos(65, 7);
	wprintf(L"游戏难度:");
	SetPos(65, 9);
	wprintf(L"食物分数:");
 
 
	do
	{
		SetPos(74, 5);                              //每走一步总分, 难度和食物分数都可能被我们改变, 所以他们要跟着循环一起打印。
		wprintf(L"%d", snake->_sum_score);
		SetPos(74, 7);
		wprintf(L"%4d", 500 - snake->_speed);
		SetPos(74, 9);
		wprintf(L"%2d", snake->_food_score);
 
 
		Sleep(snake->_speed);//每走一步就挺speed秒, 这个speed也是用来控制游戏的难度的。
 
		//上
		if (KEY_PRESS(VK_UP) && snake->_dir != _down)
		{
			snake->_dir = _up;
		}
		//下
		else if (KEY_PRESS(VK_DOWN) && snake->_dir != _up)
		{
			snake->_dir = _down;
		}
		//左
		else if (KEY_PRESS(VK_LEFT) && snake->_dir != _right)
		{
			snake->_dir = _left;
		}
		//右
		else if (KEY_PRESS(VK_RIGHT) && snake->_dir != _left)
		{
			snake->_dir = _right;
		}
		else if (KEY_PRESS(VK_F3))
		{
			//加速
			if (snake->_speed > 100)
			{
				snake->_speed -= 50;
				snake->_food_score += 2;
 
			}
		}
		else if (KEY_PRESS(VK_F4))
		{
			//减速
			if (snake->_speed < 600)
			{
				snake->_speed += 50;
				snake->_food_score -= 2;
			}
		}
		else if (KEY_PRESS(VK_SPACE))
		{
			//暂停
			Stop();
 
		}
		else if (KEY_PRESS(VK_ESCAPE))
		{
			//退出
			break;
		}
		//走一步
		step_move(snake);
 
	} while (snake->_state == _ok);//如果游戏状态是_ok那么就继续游戏, 否则退出游戏
 
}

这里游戏的运行部分就完成了。

2.4 游戏结束, 收尾工作

到这里我们的贪吃蛇其实就剩下一个收尾工作了。 我这里将其封装成了一个函数叫game_over, 这个函数的主要功能就是释放蛇的身体节点和食物节点

//游戏结束, 释放蛇的身体节点和食物节点。
void game_over(psnake snake) 
{
	free(snake->_food);
	pgnode cur = snake->_snake_head;
	pgnode next = cur->_next;
	while (cur != NULL) 
	{
		free(cur);
		cur = next;
		if (cur != NULL) 
		{
			next = cur->_next;
		}
	}
	
	SetPos(15, 15);
	printf("game_over!");
}

然后我们再将main.c里面的测试函数做一下包装, 就可以开始游戏了:

 
void test() 
{
	int Y;
	setlocale(LC_ALL, "");
	do 
	{
		system("cls");
 
		//游戏初始化
		snake snake;
 
		game_init(&snake);
 
		//游戏运行
 
		game_run(&snake);
 
		//游戏结束
		game_over(&snake);
 
		SetPos(30, 15);
 
		printf("是否再来一局(Y|N):>");
		Y = getchar();
		getchar();
 
	} while (Y == 'Y' || Y == 'y');
 
	system("pause");
}
 
int main() 
{
	test();
	return 0;
}

做好这些游戏基本上就能运行了。 关注公众号“指尖动听知识库”这里有我写的贪吃蛇整个代码。 自取:回复关键词<贪吃蛇>

文章已获作者授权转载,版权归原作者所有,如有侵权,与本账号无关,可联系删除。 原文作者:打鱼又晒网

原文链接:https://blog.csdn.net/strive_mianyang/article/details/137963046

在这里插入图片描述

;