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

news2024/12/22 19:31:04

        前言:贪吃蛇是一个经典的游戏, 本节将使用c语言实现一个简易的的贪吃蛇小游戏。 本节内容适合已经学完c语言还有数据结构链表的友友们。

        我们要实现的贪吃蛇是在控制台进行游戏的。 它运行起来是这样的:

贪吃蛇

        那么, 为了实现这个小游戏。 我们需要学习一些关于控制台操作函数的准备知识。下面, 开始对这些知识进行讲解: 

先看一下我们的目录

目录

准备知识

        获取句柄

        设置控制台界面

        操作光标

        光标的显示

        光标定位

        什么是光标定位

        如何进行光标定位

        按键情况

        宽字符打印

        

贪吃蛇实现

先定义好蛇的结构体

游戏的初始化

欢迎界面

墙体的打印

蛇初始化和打引

打印食物 

帮助信息

游戏的运行

贪吃蛇的移动

贪吃蛇的加速, 暂停等辅助功能 

游戏结束, 收尾工作


ps:最后还有代码资源自取

准备知识

        获取句柄

        首先, 我们需要了解一下句柄:句柄就是把手, 手柄的意思。

        而我们可以通过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。

        不要忘记包含头文件!

        设置控制台界面

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

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

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

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

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

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

        

         不要忘记保存

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

    

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

        但是我们可以通过函数, 将这个控制台的名称修改成我们想要的。

        这个函数是system, 使用方法如下:

#include<Windows.h>//使用system函数需要包含windows.h头文件

int main() 
{
	//HANDLE h_out_put = GetStdHandle(STD_OUTPUT_HANDLE);

	system("title 贪吃蛇");

	return 0;
}

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

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

        代码如下:

#include<Windows.h>
int main() 
{
	//HANDLE h_out_put = GetStdHandle(STD_OUTPUT_HANDLE);

	system("title 贪吃蛇");


	system("pause");//暂停程序
	return 0;
}

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

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

修改界面窗口大小的函数如下:

#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=(数字));

        这是运行出来后的界面大小:

这是原本的界面大小:

         

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

        操作光标

                光标的显示

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

        现在来具体操作一下:

        首先, 我们要创建一个光标的变量, 而光标类型的结构体是: 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的时候:

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

        现在来看一下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

        下面为代码运行图:


        

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

        光标定位

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

        什么是光标定位

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

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

        这个过程, 就是光标定位的过程。 

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

        如何进行光标定位

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

        控制台是由坐标轴的, 就是如图:

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

        这个黑色长方形的长就是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()就是普通的暂停, 为了将程序暂停一下方便我们进行观察。

        现在看一下运行结果:

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

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

//光标定位的函数, 只需要传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);//定位光标.
}

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

按键情况

         还有一个准备知识点就是如何获取键盘上面的案件情况。 这里需要用到一个函数: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;
}

         下图为运行图: 

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

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

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

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

上:VK_UP

下:VK_DOWN

左:VK_LEFT

右:VK_RIGHT

空格:VK_SPACE

ESC:VK_ESCAPE

F3:VK_F3

F4:VK_F4

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

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

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

宽字符打印

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

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

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

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

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

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

         

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

        重点要看到这里的L, wprintf里面的参数前面要加个L,代表打印宽字符。

        然后宽字符类型怎么表示呢 ?

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

        如图:

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

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

        

贪吃蛇实现

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

贪吃蛇

看一下第一个画面:

        这是不是一个贪吃蛇游戏的初始画面, 也可以叫做主界面。

        然后, 那么显然, 我们的贪吃蛇游戏中必须要实现这么一个主界面。 然后我们再往下看:

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

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

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

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

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

          

        先定义好蛇的结构体

         

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

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

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

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

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

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

        分析: 我们可以使用链表来作为贪吃蛇的身体以及食物。 蛇每吃掉一个食物, 那么它的长度就会增加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文件的结构体准备部分。 现在我们来看一下头文件中要有哪些内容:

游戏的初始化

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

        

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

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

        欢迎界面

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

#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");


}

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

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

        如果我们不加刷新界面, 结束时这样的:

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

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

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



//欢迎界面的打印
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");
}

现在来看一下运行效果:

        第一张图是第一个界面, 第二张图是第二个界面。 这就是欢迎界面的打印。

现在来看一下墙体的打印

墙体的打印

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

        这就是我预留出来的为了打印提示性信息的地方。 

         

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

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

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

//墙的打印
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);
	}
}

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

        蛇初始化和打引

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

        先将我们的贪吃蛇结构体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);

 这个函数里面的参数我们传的是贪吃蛇结构体的指针。通过这个指针, 就可以找到贪吃蛇游戏里面的任何一个变量, 方便我们对游戏进行修改。 

        那么我们就可以将我们上面的欢迎界面打印以及墙体的打印等放进这个函数去了。如图:

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

        

        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);

}

打印食物 

        打印食物的时候, 要注意两个细节 :第一个细节就是食物的坐标不能和蛇的坐标重合。

        第二个细节就是食物的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函数里面应该是这样的:

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

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

现在来打引帮助信息。

帮助信息

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

        如图为代码: 

//打引帮助信息
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;
}

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

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

        看着一张图:

        你看我在欢迎界面函数里面调用了两次界面刷新的函数, 为的就是对界面进行切换, 第一个界面刷新是为了跳到第二个欢迎界面, 第二个界面刷新是为了跳到游戏界面。  

        其他具体不做赘述, 这里开始游戏的运行部分

游戏的运行

        贪吃蛇的移动

        接下来是贪吃蛇最难的一部分, 也是整个游戏的核心——贪吃蛇如何移动。

        我们先想一下蛇头的方向和贪吃的移动的关系:

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

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

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

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

        如图, 其实蛇走一步之后是有很多种情况的, 我在图中就将他们分成了四种情况。 我们先不谈这四种情况, 我们先来谈一下蛇走一步怎么走, 也就是我在图中红框框的部分:如果蛇头的朝向是_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就可以。下面是判断的过程:

//判断自己是否撞到了自己
bool judge_self(psnake snake) 
{
	pgnode cur = snake->_snake_head->_next;
	while (cur != NULL) 
	{
		if (cur->_x == snake->_snake_head->_x && cur->_y == snake->_snake_head->_y) 
		{
			return true;
		}
		cur = cur->_next;
	}
	
	return false;
}

  

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

//正常走一步的状态
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那么就继续游戏, 否则退出游戏

}


贪吃蛇的加速, 暂停等辅助功能 

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


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那么就继续游戏, 否则退出游戏

}

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

游戏结束, 收尾工作

        到这里我们的贪吃蛇其实就剩下一个收尾工作了。 我这里将其封装成了一个函数叫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;
}

做好这些游戏基本上就能运行了。 

这里有我写的贪吃蛇整个代码。 自取:

【免费】c语言贪吃蛇-项目实战资源-CSDN文库

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1617605.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

图解KMP算法——字符串搜索

原文&#xff1a;最初发布地址 一、问题描述 来源&#xff1a;Leetcode 难度&#xff1a;中等 给你两个字符串 haystack 和 needle &#xff0c;请你在 haystack 字符串中找出 needle 字符串的第一个匹配项的下标&#xff08;下标从 0 开始&#xff09;。如果 needle 不是 hays…

2024年最新版云开发cms开通步骤,开始开发微信小程序前的准备工作,认真看完奥!

小程序官方有改版了&#xff0c;搞得石头哥不得不紧急的再新出一版&#xff0c;教大家开通最新版的cms网页管理后台 一&#xff0c;技术选型和技术点 1&#xff0c;小程序前端 wxml css JavaScript MINA原生小程序框架 2&#xff0c;数据库 云开发 云数据库 云…

SOTAX溶出测试系统PC触摸屏维修三部曲

SOTAX溶出测试系统作为一款广泛应用于制药行业的知名品牌&#xff0c;具有高精度、操作简便、稳定性好等特点。它适用于各种类型的药品研发和生产环节&#xff0c;为科研人员提供可靠的数据支持。瑞士SOTAX溶出仪是实验室中常用的设备&#xff0c;其触摸屏是用户交互的重要界面…

程序员读的经典著作有哪些?

一、程序员读的经典著作有哪些&#xff1f; 在编程的世界里&#xff0c;阅读经典著作不仅能够帮助我们深入理解编程的本质&#xff0c;也能为我们提供解决问题的新思路和方法。以下是几本被广大程序员推崇的经典著作&#xff0c;每本书都有其独特的价值和深远的影响。 1. 《代…

【C++】双指针算法:快乐数

1.题目 题目中一定要理解快乐数的含义&#xff0c;否则题目难度直逼困难。 在示例1中n19&#xff0c;经过几步操作后结果变成1。 那么示例2中n2是什么情况呢&#xff1a; 2->4->16->37->58->89->145->42->20->4(与前面的4形成闭环) 在计算机中in…

苍穹外卖day11 Apache ECharts 数据统计-图形报表

文章目录 前言一、Apache ECharts二、营业额统计1. 业务规则2. 接口设计3. 代码实现 三、用户统计1. 业务规则2. 接口设计3. 代码实现 四、订单统计1. 业务规则2. 接口设计3. 代码实现 五、销量排名Top101. 业务规则2. 接口设计3. 代码实现 前言 作为后端开发人员使用Echarts&…

【C++】类和对象④(类的默认成员函数:取地址及const取地址重载 | 再谈构造函数:初始化列表,隐式类型转换,缺省值)

&#x1f525;个人主页&#xff1a;Forcible Bug Maker &#x1f525;专栏&#xff1a;C 目录 前言 取地址及const取地址操作符重载 再谈构造函数 初始化列表 隐式类型转换 explicit关键字 成员变量缺省值 结语 前言 本篇主要内容&#xff1a;类的六个默认成员函数中…

算法训练营day15

一、层序遍历 参考链接7.2 二叉树遍历 - Hello 算法 (hello-algo.com) 层序遍历本质上属于广度优先遍历&#xff0c;也称广度优先搜索&#xff0c; BFS通常借助队列的先入先出的特性实现 参考链接102. 二叉树的层序遍历 - 力扣&#xff08;LeetCode&#xff09; 像这种较为…

Go语言并发控制

channel // cancelFn 数据通道关闭通知退出 func cancelFn(dataChan chan int) {for {select {case val, ok : <-dataChan:// 关闭data通道时&#xff0c;通知退出// 一个可选是判断data指定值时退出if !ok {fmt.Printf("Channel closed &#xff01;&#xff01;&…

前端页面助手 (vue)

快速开发页面&#xff08;图形化开发页面&#xff09; 自主编辑 然后自己也可以修改属性 最后导出页面即可 github地址 ;https://github.com/opentiny/tiny-engine

蚓链数字化营销系统与数字资产的关系

蚓链数字化营销系统是一种利用数字技术来实现营销目标的系统。它集成了多种数字营销工具和渠道&#xff0c;以收集、分析和利用客户数据&#xff0c;优化营销活动&#xff0c;并提高营销效果。 数字资产是一种新型的资产类别&#xff0c;它们以电子数据的形式存在&#xff0c;可…

前端开发攻略---实现发送手机验证码60s倒计时效果(手机号验证+按钮文字自定义显示+Vue2写法+Vue3写法)

1、演示 2、说明 1、为了便于演示&#xff0c;本示例将在3秒后就再次发送。您可以根据需要自定义此时间间隔。 2、采用最少的变量以满足需求&#xff0c;以减少内存占用。 3、不仅仅局限于按钮情况&#xff0c;也可应用于不禁用按钮的情况&#xff0c;以实现更多的扩展性。 4、…

02 VMware下载安装银河麒麟(Kylin)系统

02 VMware下载&安装银河麒麟&#xff08;Kylin&#xff09;系统 一、官网1、官网地址 二、下载1、官网下载&#xff08;1&#xff09;服务器操作系统&#xff08;2&#xff09;申请试用&#xff08;3&#xff09;产品试用申请&#xff08;4&#xff09;点击下载连接即可 2、…

Redis篇:缓存穿透以及解决方案

1.何为缓存穿透 缓存穿透 &#xff1a;缓存穿透是指客户端请求的数据在缓存中和数据库中都不存在&#xff0c;这样缓存永远不会生效&#xff0c;这些请求都会打到数据库。 比如查询一个id 0的数据&#xff0c;这是在redis和数据库中肯定不存在的&#xff0c;这样就属于缓存穿…

vue2+vxe-table实现表格增删改查+虚拟滚动

vue2vxe-table实现表格增删改查虚拟滚动 使用的vxe-table版本&#xff1a;v3.x (vue 2.6 长期维护版) 完整代码 <template><div><vxe-toolbar ref"xToolbar" export :refresh"{query: findList}"><template #buttons><vxe-b…

【网络安全】在网络中如何对报文和发送实体进行鉴别?

目录 1、报文鉴别 &#xff08;1&#xff09;使用数字签名进行鉴别 &#xff08;2&#xff09;密码散列函数 &#xff08;3&#xff09;报文鉴别码 2、实体鉴别 鉴别(authentication) 是网络安全中一个很重要的问题。 一是要鉴别发信者&#xff0c;即验证通信的对方的确是…

富唯智能:打造未来机器人教育新标杆

随着科技的飞速发展&#xff0c;机器人教育正逐渐成为培养未来人才的重要领域。富唯智能&#xff0c;作为业内领先的机器人技术提供商&#xff0c;近日推出了一款全新的机器人教育实践平台系统&#xff0c;旨在为学生提供更加丰富、更具挑战性的学习体验。 该平台系统以AUBO-i5…

A Neural Span-Based Continual Named Entity Recognition Model

《A Neural Span-Based Continual Named Entity Recognition Model》------------AAAI’23 论文链接&#xff1a;https://arxiv.org/abs/2302.12200 代码&#xff1a;https://github.com/Qznan/SpanKL 当前问题&#xff1a; 1.现有的NER模型在适应新的实体类型时往往表现不佳…

基于CAPL的S19文件解析

&#x1f345; 我是蚂蚁小兵&#xff0c;专注于车载诊断领域&#xff0c;尤其擅长于对CANoe工具的使用&#x1f345; 寻找组织 &#xff0c;答疑解惑&#xff0c;摸鱼聊天&#xff0c;博客源码&#xff0c;点击加入&#x1f449;【相亲相爱一家人】&#x1f345; 玩转CANoe&…

【Qt 学习笔记】Qt常用控件 | 按钮类控件 | Check Box的使用及说明

博客主页&#xff1a;Duck Bro 博客主页系列专栏&#xff1a;Qt 专栏关注博主&#xff0c;后期持续更新系列文章如果有错误感谢请大家批评指出&#xff0c;及时修改感谢大家点赞&#x1f44d;收藏⭐评论✍ Qt常用控件 | 按钮类控件 | Check Box的使用及说明 文章编号&#xff…