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

        前言:贪吃蛇是一个经典的游戏, 本节将使用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文库

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

添加Redis缓存

1.缓存查询 在service层Impl文件中&#xff0c;进行查询时优先向Redis中查数据&#xff0c;查到就查到了&#xff0c;没有查到向mysql数据库中查&#xff0c;查到之后不先返回&#xff0c;而是先将数据存到数据库&#xff08;缓存&#xff09;,在再返回数据。 1.1 代码实现(缓…

鸿蒙端云一体化开发--调用云函数--适合小白体制

如何实现在端侧调用云函数&#xff1f; 观看前&#xff0c;友情提示&#xff1a; 不知道《如何一键创建端云一体化模板》的小白同学&#xff0c;请看&#xff1a; 鸿蒙端云一体化开发--开发云函数--适合小白体制-CSDN博客 实现方法&#xff1a; 第一步&#xff1a;添加依赖 …

98%的企业与被入侵的第三方有关联,如何有效的防止入侵

技术供应链漏洞使威胁参与者能够以最小的努力扩展其运营&#xff0c;在导致第三方入侵的外部B2B关系中&#xff0c;75%涉及软件或其他技术产品和服务&#xff0c;其余25%的第三方违规涉及非技术产品或服务。 入侵通常需要几个月或更长的时间才能公之于众&#xff0c;受害者可能…

【Leetcode】string类刷题

&#x1f525;个人主页&#xff1a;Quitecoder &#x1f525;专栏&#xff1a;Leetcode刷题 目录 1.仅反转字母2.字符串中第一个唯一字符3.验证回文串4.字符串相加5.反转字符串I I6.反转字符串中的单词III7.字符串相乘8.把字符串转换为整数 1.仅反转字母 题目链接&#xff1a;…

C++:模板详解

模板详解 1.函数模板1.概念2.语法3.原理4.实例化1.隐式实例化2.显示实例化 5.匹配原则 2.类模板1.格式2.实例化 3.非类型模板参数注意点 4.特化1.概念2.函数模板特化1.前提2.语法说明3.示例 3.类模板特化1.全特化2.偏特化/半特化3.选择顺序 4.按需实例化 5.模板的分离编译1.分离…

开发一个农场小游戏需要多少钱

开发一个农场小游戏的费用因多个因素而异&#xff0c;包括但不限于游戏的规模、复杂性、功能需求、设计复杂度、开发团队的规模和经验&#xff0c;以及项目的时间周期等。因此&#xff0c;无法给出确切的费用数字。 具体来说&#xff0c;游戏的复杂程度和包含的功能特性数量会直…

巧用断点设置查找bug【debug】

默认设置的断点&#xff0c;当代码运行到断点处MCU就会被挂起&#xff0c;从而停在断点处。 但在某些情况下&#xff0c;如调试FCCU时&#xff0c;如果设置断点&#xff0c;MCU停下后将会导致 FCCU 配置WDG超时。或在调试类似电机控制类的应用时&#xff0c;不适当的断点会导 致…

镜舟科技荣获金科创新社 2024 年度金融数据智能解决方案奖

近日&#xff0c; 镜舟科技凭借领先的金融实时数仓构建智能经营解决方案&#xff0c;在“金科创新社第六届金融数据智能优秀解决方案评选”活动中&#xff0c;成功入选“数据治理与数据平台创新优秀解决方案”榜单。 金科创新社主办的“鑫智奖”评选活动&#xff0c;旨在展示…

详解IIC通信协议以及FPGA实现

一、IIC简介 IIC也称为I2C&#xff08;Inter-Integrated Circuit&#xff09;由飞利浦公司&#xff08;现在的恩智浦半导体&#xff09;开发&#xff0c;是一种用于短距离数字通信的串行&#xff0c;同步&#xff0c;半双工通信接口协议&#xff1b;传输在标准模式下可以达到10…

python:元组,字符串,切片

一、元组# 列表可以修改内容&#xff0c;元组可以不被修改 # 在程序内封装数据&#xff0c;不希望数据被篡改&#xff0c;所以使用元组 # 语法&#xff1a; 不限制类型 # 定于元组的字面量&#xff1a; &#xff08;元素&#xff0c;元素&#xff0c;元素.....&#xff09; # 定…

apipost、postman等工具上传图片测试flask、fastapi的文件api接口

参考&#xff1a;https://blog.csdn.net/qq_15821487/article/details/119354129 https://www.cnblogs.com/wyxjava/p/16076176.html 选择from-data&#xff0c;下拉选择file上传文件发送即可

线上真实案例之执行一段逻辑后报错Communications link failure

1.问题发现 在开发某个项目的一个定时任务计算经销商返利的功能时&#xff0c;有一个返利监测的调度&#xff0c;如果某一天返利计算调度失败了&#xff0c;需要重新计算&#xff0c;这个监测的调度就会重新计算某天的数据。 在UAT测试通过&#xff0c;发布生产后&#xff0c…

NVIDIA安装程序失败-Nsight Visual Studio Edition失败解决办法

博主是要升级cuda版本&#xff0c;那么在安装新版本之前需要卸载以前的版本。 博主一溜卸载下去&#xff0c;最后有这么个东西卸载不掉&#xff0c;Nsight Visual Studio Edition 不管是电脑系统卸载还是360卸载&#xff0c;都卸载不掉。 此时安装新的cuda也遇到了这个问题 由…

PLC存储器分类及西门子SIMATIC S7-1200存储器参数

存储器用来储存程序和数据&#xff0c;分为系统存储器和用户存储器。 系统存储器存放由PLC生产厂商编写好的系统程序&#xff0c;并固化在只读存储器&#xff08;ROM&#xff09;内&#xff0c;用户不能修改。用户存储器存放用户根据控制要求编写的应用程序。目前大多数PLC采用…

面试经典150题——从中序与后序遍历序列构造二叉树

1. 题目描述 2. 题目分析与解析 其实这个题目和昨天那个很相似&#xff0c;思考思路如下&#xff1a; 解决从中序&#xff08;inorder&#xff09;与后序&#xff08;postorder&#xff09;遍历序列构造二叉树的问题时&#xff0c;考虑到这两个遍历序列为我们提供了树结构中…

解决方案 SHUTDOWN_STATE xmlrpclib.py line: 794 ERROR: supervisor shutting down

Supervisor操作命令 重新加载 Supervisor 配置&#xff1a; sudo supervisorctl reread sudo supervisorctl update sudo supervisorctl restart all这将重新读取 Supervisor 的配置文件&#xff0c;更新进程组&#xff0c;然后重启所有进程。 查看 Supervisor 日志&#xff1…

尺取法知识点讲解

一、固定长度的情况&#xff1a; 最小和(sum) 输入N个数的数列&#xff0c;所有相邻的M个数的和共有N-M1个&#xff0c;求其中的最小值。 输入格式 第1行&#xff0c;2个整数N&#xff0c;M&#xff0c;范围在[3…100000]&#xff0c;N>M。 第2行&#xff0c;有N个正…

R语言入门:“Hellinger“转化和“normalize“转化(弦转化)的公式表示与R代码实现

1、写在前面 vegan包中的decostand()函数为群落生态学研究提供了一些流行的(和有效的)标准化方法。有关decostand()函数标准化的一些标准化方法可以看我的另一篇笔记&#xff1a;R语言入门&#xff1a;vegan包使用decostand()函数标准化方法 由于在网络上没有找到关于这两个转…

Redis-键值设计

Redis-键值设计 1.设置key的规范 遵循基本格式&#xff1a;【业务名称】&#xff1a;【数据名】&#xff1a;【id】 可读性强&#xff0c;在客户端的情况下使用:如果前缀相同会分目录层级长度不超过44字节 string数据结构的三种类型&#xff0c;在44字节之内是embstring 内存…

鸿蒙应用开发之Web组件3

前面学习了从网上加载网页的显示,本文将要学习加载本地的网页。比如很多显示的内容,可以制作网页的文件格式,然后直接使用它来显示,就可以减少界面的制作。另外,当手机没有网络的时候,如果想从网络上获取内容就会失败,这时候可以使用本地的网页内容来代替。这样不会导致…