【C语言-期末项目/项目实践】-贪吃蛇(两万五千字详解,手把手教程博文,期末答辩神助手)

news2024/11/15 17:34:55

重要:游戏逻辑设计图 

 

目录

1.游戏背景

2.项目效果展示 

3.项目实现要求

4.技能要求

5.技术要点

6.Win32 API介绍

6.1Win32API

6.2控制台程序

6.3控制台屏幕上的坐标COORD

6.4GetStdHandle

6.5GetConsoleCursorInfo

6.5.1 CONSOLE_CURSOR_ INFO

6.6 SetConsoleCursorInfo设置指定控制台屏幕缓冲区的光标的大小和可见性。 

6.7 SetConsoleCursorPosition

6.8GetAsyncKeyState

7.游戏地图和游戏界面的设计

7.1地图

7.1.1 本地化

7.1.2类项

7.1.3 setlocale函数

7.1.4宽字符的打印

7.1.5 地图坐标

7.2 蛇⾝和⻝物

7.3数据结构设计

7.4(重要核心)游戏的流程设计

8.核心逻辑实现分析

8.1游戏主逻辑

8.2当游戏开始

8.2.1 欢迎界面设计实现

8.2.2 创建地图

8.2.3 初始化蛇⾝

8.2.4 创建第⼀个⻝物

8.3 游戏运⾏过程

8.3.1 上面的KEY_PRESS

8.3.2 打印侧边提示信息和分数变化

8.3.3 蛇⾝的移动

8.3.3.1 NextIsFood

8.3.3.2 EatFood

8.3.3.3 NoFood

8.3.3.4 KillByWall 判断蛇头是否撞墙

8.3.3.5 KillBySelf 判断蛇头的坐标是否和蛇⾝体的坐标冲突

8.4 游戏结束

9.参考源码

10.结语


 

1.游戏背景

1976年,Gremlin平台推出了一款经典街机游戏Blockade。游戏中,两名玩家分别控制一个角色在屏幕上移动,所经之处砌起围栏。角色只能向左、右方向90度转弯,游戏目标保证让对方先撞上屏幕或围栏。 听起来有点复杂,其实就是下面这个样子:

基本上就是两条每走一步都会长大的贪吃蛇比谁后完蛋,玩家要做的就是避免撞上障碍物和越来越长的身体。更多照片、视频可以看 GamesDBase 的介绍。

Blockade 很受欢迎,类似的游戏先后出现在 Atari 2600、TRS-80、苹果 2 等早期游戏机、计算机上。但真正让这种游戏形式红遍全球的还是21年后随诺基亚手机走向世界的贪吃蛇游戏——Snake。

2.项目效果展示 

贪吃蛇

3.项目实现要求

项目环境:Windows环境控制台(linux和Windows控制台不一样)

项目语言:C语言

项目要求:使用C语言在windows环境的控制台中模拟实现经典游戏贪吃蛇

功能要求:

1.贪吃蛇地图绘制

2.蛇吃食物的功能(上、下、左、右方向键控制蛇的动作)

3.蛇撞墙死亡

4.蛇撞自身死亡

5.计算所得分数并显示

6.蛇身的加速和减速

7.按键(比如空格)可以暂停游戏

4.技能要求

1.对C语言语法巩固

2.C语言学完并且具备一定代码能力

3.初步接触过数据结构中的链表

5.技术要点

C语言函数(功能封装)、枚举、结构体、动态内存管理、预处理指令、链表、Win32API(下面补充讲解)

6.Win32 API介绍

6.1Win32API

windows这个多作业(完成某一种任务)系统除了协调应用程序的执行、分配内存、管理资源之外,它同时也是一个很大的服务中心,调用这个服务中心的各种服务(每一种服务就是一个函数),可以帮助应用程序达到开启视窗,描绘图形、使用周边设备等目的,(当前的操作系统,除了完成当前任务,我们肉眼能看到的功能之外还提供了后面很大的服务中心,是通过函数提供给用户,合适调用,可以帮用户完成各种各样的操作,通俗的讲就是每一种操作系统在完成正常的操作系统该有的功能如文件管理、内存管理等等功能之外还提供了一些接口(函数),这些函数提供给程序员,让程序员自己调用能实现自己想要的功能,将这些接口叫做API,以Windows为列,这些接口通用叫做WIN32 API)由于这些函数服务的对象是应用程序(Application),所以便称之为Application Programming Interface,简称API函数,WIN32 API也就是Microsoft Windows 32位平台的应用程序编译接口。

6.2控制台程序

平常我们运行起来的黑框程序起始就是我们的控制台程序(cmd程序也常称为终端,linux直接称为终端)

当我们使用vstudio创建新项目的时候也是创建的控制台项目:

我们可以使用cmd指令来打开控制台或者设置控制台的长宽:

那么,当我们打开控制台,就可以通过mode命令来设置控制台的长宽或者设置控制台窗口的名字:

比如我们设置控制台窗口的大小为:20行(宽度),10列(长度),可以这样调用命令:

mode con cols=10 lines = 20

con是控制台英文单词consle的缩写

可以通过title来改变我们的控制台窗口名字:

那么当我们在运行代码的时候可以设置我们的控制台端口的这些属性呢?

第一个方式如下,设置属性:

如图右键窗口设置属性:

2.通过系统操作函数system来进行代码设置

system函数就可以执行系统命令

int main()
{
	system("mode con cols=30 lines=20");
	system("title snake");

	//printf("hehe\n");
	return 0;
}

补充,有很多伙伴可能设置大小不起作用或者只有显示的内存行列变短但是黑框框大小没有改变,这是Win11系统的原因,解决办法如下:

然后重新运行就解决了:

恢复方法:

6.3控制台屏幕上的坐标COORD

COORD是windows API中定义的一个结构体,表示一个字符在控制台屏幕缓冲区上的坐标,坐标系原点(0,0)位于缓冲区的顶部左侧单元格。当有了这个我们就可以设置我们蛇要吃的食物随机出现的坐标。头文件包含:《windows.h》

具体的定义为:
 

typedef struct _COORD
{

SHORT x;
SHORT y;
}COORD,*PCOORD;

想指定一个坐标就可以这样:

COORD pos = {3,5};

之所以不自己定义,是因为COORD是我们WIN32 API自己的类型,和别人的项目具有更好弟弟兼容性。

6.4GetStdHandle

GetStdHandle是一个Windows API函数,它用于一个特定的标准设备(标准输入、标准输出或标准错误)中获得一个句柄(用来标识不同设备的数值),使用这个句柄就可以操作设备。简单来说就是获控制权限。(我们要操控蛇,或者要隐藏我们的鼠标,就要控制控制台,控制台也是设备,控制设备前首先要获得设备就要用到这个函数)

函数:

HANDLE WINAPI GetStdHandle(_In_ DWORD nStdHandle);

nStdHandle参数:标准设备,此参数的取值可为下列值之一

STD-INPUT-HANDLE      表示获得的是标准输入设备就是键盘的操作句柄

STD-OUTPUT-HANDLE   表示获得的是标准输出设备就是屏幕的操作句柄

STD-OUTPUT-HANDLE     表示标准错误设备

返回值:如果函数成功,则返回值为指定设备的句柄,或为由先前对SetStdHandle的调用设置的重定向句柄,除非应用程序已使用SetStdHandle来设置具有较少访问权限的标准句柄,否则该句柄具有GENERIC_READ和GENERIC_WRITE访问权限

如果函数失败,则返回值为 INVALID_HANDLE_VALUE,如果应用程序没有关联的标准语句炳(例如在交互式桌面上运行的服务),并且尚未重定向这些句柄,则返回值为NULL.

   

具体使用范例:

HANDLE houput = NULL;
//获取标准输出的句柄
houput = GetStdHandle(STD_OUTPUT_HANDLE);

6.5GetConsoleCursorInfo

检索有关指定控制台屏幕缓冲区的光标大小和可见性的信息(比如做到隐藏光标,先获得光标,再操作光标)

BOOL WINAPI GetConsoleCursorInfo(
HANDLE
hConsoleoutput,
PCONSOLE_CURSOR_INFO lpConsoleCursorInfo)
PCONSOLE_CURSOR_INFO 是指向 CONSOLE_CURSOR_INFO(光标信息,光标的高宽等)结构的指针,该结构接收有关主机游标(光标)的信息

使用示范:

1HANDLE hOutput = NULL;
2//获取标准输出的句柄(用来标识不同设备的数值)
3 houtput =GetStdHandle(STD_OUTPUT_HANDLE);
4 CONSOLE_CURSOR_INFO CursorInfo;
5 GetConsoleCursorInfo(hOutput,&CursorInfo);//获取控制台光标信息

6.5.1 CONSOLE_CURSOR_ INFO

这个结构体,包含有关控制台光标的信息


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

  dwSize,由光标填充的字符单元格的百分比,光标宽度。 此值介于1到100之间。 光标外观会变化,范围从完全填充单元格到单元底部的水平线条。
  bVisible,游标的可见性。 如果光标可见,则此成员为 TRUE。

CursorInfo.bVisible = false;//隐藏控制台光标

6.6 SetConsoleCursorInfo
设置指定控制台屏幕缓冲区的光标的大小和可见性。 

1 BOOL WINAPI SetConsoleCursorInfo(
2
HANDLE hConsoleOutput,
3
const CONSOLE_CURSOR_INFO *IpConsoleCursorInfo
4);


实例:

I HANDLE hOutput = GetStdHandle(STD_OUTPUT_HANDLE);
2
3 //影藏光标操作
4 CONSOLE_CURSOR_INFO CursorInfo;
5 GetConsoleCursorInfo(hOutput,&Cursoranfo);//获取控制台光标信息
6 CursorInfo.bVisible = false;//隐藏控制台光标
7 SetConsoleCursorInfo(hOutput,&CursorInfo);//设置控制台光标状态


 

6.7 SetConsoleCursorPosition

设置指定控制台屏幕缓冲区中的光标位置,我们将想要设置的坐标信息放在COORD类型的pos中,调用
SetConsoleCursorPosition函数将光标位置设置到指定的位置。

BOOL WINAPI SetConsoleCursorPosition(
HANDLE hConsoleOutput,COORD pos);

下面是使用实例和效果:

COORD pos = { 10,5};
HANDLE hOutput = NULL;
//获取标准输出的句柄(用来标识不同设备的数值)houtput = GetStdHandle(STD_OUTPUT_HANDLE);//设置标准输出上光标的位置为pos
SetConsoleCursorPosition(hOutput, pos);

为了方便操作,我们可以利用上面的知识设置封装一个设置光标位置的函数,设置如下:

void SetPos(int x, int y)
{
	COORD pos = { x,y };
	HANDLE houtput = NULL;//获取标准输出也就是屏幕的控制句柄
	houtput = GetStdHandle(STD_OUTPUT_HANDLE);
	//设置标准输出上光标的位置为pos
	SetConsoleCursorPosition(houtput, pos);
}

使用的时候这里直接在主函数中传入我们的横纵坐标就好了。

6.8GetAsyncKeyState

获取按键情况(比如使用上下左右键控制蛇的移动),GetAsyncKeyState的函数原型如下:

SHORT GetAsyncKeyState(

int vKey
 );

将键盘上每个键的虚拟键值传递给函数,函数通过返回值来分辨按键的状态。WIN32API给键盘上的每一个键都编号了。虚拟键码


GetAsyncKeystate的返回值是short类型,在上一次调用GetAsyncKeyState 函数后,如果返回的16位的short数据中,最高位是1,说明按键的状态是按下(还没有抬起),如果最高是0,说明按键的状态是抬起;如果最低位被置为1则说明,该按键被按过,否则为0。
如果我们要判断一个键是否被按过,可以检测GetAsyncKeyState返回值的最低值是否为1.

 #define KEY_PRESS(VK)((GetAsyncKeyState(VK)& 0x1)?1:0 )

采用按位与的方式,按过返回1,没按过返回0.

接下来我们设计一个实例:我们可以对别人的键盘进行监控,那么就可以做到窃取别人的密码,比如我们判断别人按下的按键后就将对应按键的内容放入一个文件夹,就可以达到目的(注意技术向善哈哈)下面我们实现一下按下对应按键就打印对应按键内容:

#define KEY_PRESS(vk) ((GetAsyncKeyState(vk)&0x1)? 1:0)

int main()
{
	while (1)
	{
		if (KEY_PRESS(0x30))
		{
			printf("0\n");

		}
		else if (KEY_PRESS(0x31))
		{
			printf("1\n");
		}
		else if (KEY_PRESS(0x32))
		{
			printf("2\n");
		}
		else if (KEY_PRESS(0x33))
		{
			printf("3\n");
		}
	}
	return 0;
}

7.游戏地图和游戏界面的设计

7.1地图

这⾥不得不讲⼀下控制台窗⼝的⼀些知识,如果想在控制台的窗⼝中指定位置输出信息,我们得知道 该位置的坐标,所以⾸先介绍⼀下控制台窗⼝的坐标知识。 控制台窗⼝的坐标如下所⽰,横向的是X轴,从左向右依次增⻓,纵向是Y轴,从上到下依次增⻓。

在游戏地图上,我们打印墙体使用宽字符:口,打印蛇使用宽字符•,打印食物使用宽字符★
普通的字符是占一个字节的,这类宽字符是占用2个字节。

这⾥再简单的讲⼀下C语⾔的国际化特性相关的知识,过去C语⾔并不适合⾮英语国家(地区)使⽤。 C语⾔最初假定字符都是但⾃⼰的。但是这些假定并不是在世界的任何地⽅都适⽤。

ACSII码值的取值范围0~127

char ----普通字符

wchar----------宽字符

    C语言字符默认是采用ASCII编码的,ASCII字符集采用的是单字节编码,且只使用了单字节中的低7位,最高位是没有使用的,可表示为0xxxxxxxx;可以看到,ASCII字符集共包含128个字符,在英语国家中,128个字符是基本够用的,但是,在其他国家语言中,比如,在法语中,字母上方有注音符号,它就无法用 ASCII码表示。于是,一些欧洲国家就决定,利用字节中闲置的最高位编入新的符号。比如,法语中的e的编码为130(二进制10000010)。这样一来,这些欧洲国家使用的编码体系,可以表示最多256个符号。但是,这里又出现了新的问题。不同的国家有不同的字母,因此,哪怕它们都使用256个符号的编码方式,代表的字母却不一样。比如,130在法语编码中代表了e,在希伯来语编码中却代表了字母Gimel(入),在俄语编码中又会代表另一个符号。但是不管怎样,所有这些编码方式中,0--127表示的符号是一样的,不一样的只是128--255的这一段。至于亚洲国家的文字,使用的符号就更多了汉字就多达10万左右。一个字节只能表示256种符号,肯定是不够的,就必须使用多个字节表达一个符号。比如,简体中文常见的编码方式是GB2312,使用两个字节表示一个汉字,所以理论上最多可以表示 256×256=65536个符号。

  后来为了使C语言适应国际化,C语言的标准中不断加入了国际化的支持。比如:加入了宽字符的类型 wchar_t 和宽字符的输入和输出函数,加入了<locale.h>头文件,其中提供了允许程序员针对特定地区(通常是国家或者说某种特定语言的地理区域)调整程序行为的函数。

7.1.1 <locale.h>本地化

<locale.h>提供的函数用于控制C标准库中对于不同的地区会产生不一样行为的部分.
在标准可以中,依赖地区的部分有以下几项:

  • 数字量的格式
  • 货币量的格式。(比如英文环境中表示钱币使用符号和我们中文使用表示钱币的符号是不一样的)
  • 字符集
  • 日期和时间的表示形式
     

7.1.2类项

通过修改地区,程序可以改变它的行为来适应世界的不同区域。但地区的改变可能会影响库的许多部分,其中一部分可能是我们不希望修改的。所以C语言支持针对不同的类项进行修改,下面的一个宏,指定一个类项:

• LC_COLLATE:影响字符串比较函数 strcol1()和 strxfrm()
·LC_CTYPE:影响字符处理函数的行为。

•LC_MONETARY:影响货币格式。

• LC_NUMERIC:影响 printf()的数字格式。
• LC_TIME:影响时间格式strftime()和wcsftime()。
•LC_ALL-针对所有类项修改,将以上所有类别设置为给定的语言环境。

每个类项的详细声明,可以参考

7.1.3 setlocale函数

1 char* setlocale (int category, const char* locale)

setlocale 函数用于修改当前地区,可以针对一个类项修改,也可以针对所有类项。
setlocale 的第一个参数可以是前面说明的类项中的一个,那么每次只会影响一个类项,如果第一个参数是LC_ALL,就会影响所有的类项。

C标准给第二个参数仅定义了2种可能取值:“”C”(正常模式)和””(本地模式)。

在任意程序执行开始,都会隐藏式执行调用:

1 setlocale(LC ALL,"C");

当地区设置为“C"时,库函数按正常方式执行,小数点是一个点。
当程序运行起来后想改变地区,就只能显示调用setlocale函数。用”“作为第2个参数,调用setlocale函数就可以切换到本地模式,这种模式下程序会适应本地环境。
比如:切换到我们的本地模式后就支持宽字符(汉字)的输出等。

setlocale(LC_ALL," ");//切换到本地环境

当我们支持本地化之后就支持我们宽字符的打印了。

7.1.4宽字符的打印

那如果想在屏幕上打印宽字符,怎么打印呢?
宽字符的字面量必须加上前缀”",否则C语言会把字面量当作窄字符类型处理。前缀”"在单引号前面,表示宽字符,对应wprintf()的占位符为%lc;在双引号前面,表示宽字符串,对应wprintf()的占位符为%ls。

#include<locale.h>

int main()

{
	setlocale(LC_ALL, "");
	wchar_t ch1 = L'比';
	
	printf("%c%c\n", 'b','c');
	wprintf(L"%lc\n", ch1);
	return 0;
}

从输出的结果来看,我们发现一个普通字符占一个字符的位置,但是打印一个汉字字符,占用两个字符的位置,那么如果我们要在贪吃蛇中使用宽字符,就得处理好地图上坐标的计算。

7.1.5 地图坐标

我们假设实现⼀个棋盘27⾏,58列的棋盘(⾏和列可以根据⾃⼰的情况修改),再围绕地图画出墙, 如下:

7.2 蛇⾝和⻝物

初始化状态,假设蛇的⻓度是5,蛇⾝的每个节点是●,在固定的⼀个坐标处,⽐如(24, 5)处开始出现 蛇,连续5个节点。注意:蛇的每个节点的x坐标必须是2个倍数,否则可能会出现蛇的⼀个节点有⼀半 ⼉出现在墙体中,另外⼀般在墙外的现象,坐标不好对⻬。 关于⻝物,就是在墙体内随机⽣成⼀个坐标(x坐标必须是2的倍数),坐标不能和蛇的⾝体重合,然 后打印★。

7.3数据结构设计

在游戏运行过程中,蛇每次吃一个食物,蛇的身体就会变长一节,如果我能使用链表来存储蛇的信息,那么蛇的每一节其实也就是链表的每个节点,每个节点只要记录好蛇身体在地图上的坐标就好,所以可以设计蛇节点结构体如下:

typedef struct SnakeNode
{
	//描述坐标的信息
	int x;
	int y;
	struct SnakeNode* next;
}SnakeNode,*pSnakeNOde;

顺带创建一个指针。整个游戏都在维护蛇也就是多个节点构成的这个链表,那要管理整条蛇,我们就可以再封装一个Snake的结构来维护整条蛇:

//贪吃蛇蛇的描述
typedef struct snake
{
	//需要指向这个蛇第一个节点的指针,方便找到整条蛇
	pSnakeNode _psnake;
	pSnakeNode _pFood;//这个指针指向的是食物节点,因为食物也可以看做是一个节点类型
	int _Score;//当前累计的分数,可以理解为当前有多少节点
	int _FoodWeight;//一个食物的分数是多少
	int _SleepTime;//定义每走一步休息的时间,我们定义的休息的时间越短,速度就越快,时间越长,速度就越慢
	enum DIRECTION _Dir;//描述蛇的方向
	enum GAME_STATUS _Status;//描述游戏的状态:正常退出,撞墙,吃到自己
}Snake,*pSnake;

蛇前进的方向不过上下左右,可以使用枚举类型来列举,也方便我们后面书写不同方向移动的情况代码:

enum DIRECTION
{
	UP = 1,
	DOWN,
	LEFT,
	RIGHT
};

在蛇内定义游戏状态就是为了判断蛇的移到会导致的游戏结束会有的情况比如用户自己退出,或者蛇撞到墙或者蛇吃到自身,就这几种情况,同样可以使用枚举类型。

enum GAME_STATUS
{
	OK,//正常运行
	END_NORMAL,//按ESC正常退出
	KILL_BY_WALL,
	KILL_BY_SELF
};

7.4(重要核心)游戏的流程设计

8.核心逻辑实现分析

8.1游戏主逻辑

void test()
{
	int ch = 0;
	do
	{
		Snake snake = { 0 };//首先创建一条贪吃蛇
		//游戏开始,游戏初始化:地图的打印和蛇 食物的打印
		GameStart(&snake);

		//2.游戏运行-游戏正常运行
		GameRun(&snake);

		//3.游戏结束,游戏善后,链表要释放资源
		GameEnd(&snake);
		SetPos(20, 18);
		printf("要再来一局吗?(Y/N):");
		ch = getchar();
		getchar();//清理回车

	} while (ch=='Y'||ch=='y');
	SetPos(0, 27);
}





int main()
{
	//设置程序适应本地环境,后面打印各种宽字符,蛇身体和食物墙等
	setlocale(LC_ALL ,"");
	srand((unsigned int)time(NULL));//随机生成食物坐标

	test();
	return 0;
}

8.2当游戏开始

游戏游戏开始,根据上面的流程图,游戏开始涉及操作有界面的设计,包括欢迎界面和地图,还要将我们的蛇的身体创建出来,打印我们的食物,所以我们封装为一个大的函数

void GameStart(pSnake ps)
{
	//控制台窗口大小的设置
	system("mode con cols=110 lines=30");
	system("title SNAKE");
	//将光标隐藏
	 //获取标准输出的句柄(⽤来标识不同设备的数值)
	HANDLE hOutput = GetStdHandle(STD_OUTPUT_HANDLE);
	//影藏光标操作
	CONSOLE_CURSOR_INFO CursorInfo;
	GetConsoleCursorInfo(hOutput, &CursorInfo);//获取控制台光标信息
	CursorInfo.bVisible = false; //隐藏控制台光标
	SetConsoleCursorInfo(hOutput, &CursorInfo);//设置控制台光标状态
	//打印欢迎界面
	WelComeToGame();
	//创建地图
	CreateMap();
	//初始化贪吃蛇
	InitSnake(ps);
	//创建食物
	CreatFood(ps);

}

8.2.1 欢迎界面设计实现

在进入游戏的时候我们需要设计一下欢迎的界面来美化我们的程序或者提示我们的玩家一些基本的操作流程

为了让我们的文字等等居中显示,我们使用了win32的一些知识,COORD封装为了函数来实现我们随时设置光标的位置然后打印的功能:

void SetPos(int x, int y)
{
	COORD pos = { x,y };
	HANDLE houtput = NULL;//获取标准输出也就是屏幕的控制句柄
	houtput = GetStdHandle(STD_OUTPUT_HANDLE);
	//设置标准输出上光标的位置为pos
	SetConsoleCursorPosition(houtput, pos);
}

然后打印我们的欢迎初始界面:

void 	WelComeToGame()
{
	//定位光标
	SetPos(40, 14);
	printf("欢迎来到贪吃蛇小游戏");
	SetPos(40, 25);//再次定位坐标
	system("pause");//暂停命令,执行到此处暂停)

   //来到下一个界面,我们把当前界面内容全部清理重新打印实现换屏效果
	system("cls");//清理屏幕
	SetPos(20, 14);
	printf("使用↑↓← →,分别控制蛇的上下左右移动,使用F3加速,F4减速");
	SetPos(40, 25);//再次定位坐标
	system("pause");//暂停命令,执行到此处暂停)
	system("cls");//清理屏幕

}

看一下实现效果:

8.2.2 创建地图

创建地图就是将墙打印出来,因为是宽字符打印,所有使⽤wprintf函数,打印格式串前使⽤L 打印地图的关键是要算好坐标,才能在想要的位置打印墙体。 墙体打印的宽字符:

为了使用方便我们直接将墙体,食物,蛇身等宽字符使用宏定义转换为我们键盘常打的字符这样方便我们写代码:

 

#define WALL L'□'
#define BODY L'●'
#define FOOD L'★'

墙体的打印就是将我们的光标首先定位在纵坐标为0的地方然后利用横坐标增加的办法就打印了我们的上面的墙,然后定位光标纵坐标到我们的下面的墙的坐标同样的方式打印我们下面的墙就好,左右的墙就保持横坐标不变,纵坐标增加就可以实现

void CreateMap()
{
	//上
	SetPos(0, 0);//重新定位光标位置
	int i = 0;
	for (i = 0; i <= 56; i += 2)
	{
		wprintf(L"%lc", WALL);
	}
	//下
	SetPos(0, 26);//重新定位光标位置
	for (i = 0; i <= 56; i += 2)
	{
		wprintf(L"%lc", WALL);
	}
	//左
	for (i = 1; i <= 25; i++)
	{
		SetPos(0, i);
		wprintf(L"%lc", WALL);
	}
	//右
	for (i = 1; i <= 25; i++)
	{
		SetPos(56, i);
		wprintf(L"%lc", WALL);
	}
	//getchar();//测试最后一行的打印

}

8.2.3 初始化蛇⾝

如果我们最开始设置蛇最开始⻓度为5节,每节对应链表的⼀个节点,蛇⾝的每⼀个节点都有⾃⼰的坐标。 创建5个节点,然后将每个节点存放在链表中进⾏管理。创建完蛇⾝后,将蛇的每⼀节打印在屏幕上。再设置当前游戏的状态,蛇移动的速度,默认的⽅向,初始成绩,蛇的状态,每个⻝物的分数。 蛇⾝打印的宽字符,就完成了蛇的初始化。

void InitSnake(pSnake ps)
{
	pSnakeNode cur = NULL;//为了实现复用就定义在外边
	int i = 0;
	for (i = 0; i < 5; i++)
	{
		 cur = (pSnakeNode)malloc(sizeof(SnakeNode));
		if (cur == NULL)
		{
			perror("malloc");
			return;
		}
		cur->x = POS_X+2*i;
		cur->y = POS_Y;
		cur->next = NULL;
		//头插法
		if (ps->_psnake == NULL)
		{
			ps->_psnake = cur;//第一个位置就给了蛇的第一块
		}
		else
		{
			cur->next = ps->_psnake;
			ps->_psnake = cur;

		}
	}
	//打印蛇,就是遍历链表
	cur = ps->_psnake;
	while (cur)
	{
		SetPos(cur->x, cur->y);//得到每个节点的横纵坐标,然后定位光标打印身体
		wprintf(L"%lc", BODY);

		cur = cur->next;

	}
	ps->_Status = OK;
	ps->_Score = 0;
	ps->_pFood = NULL;
	ps->_SleepTime = 200;
	ps->_FoodWeight = 10;
	ps->_Dir = RIGHT;

	//getchar();//测试打印结果因为会被最后一句话打乱


}

头插法图解:

8.2.4 创建第⼀个⻝物

• 先随机⽣成⻝物的坐标

        ◦ x坐标必须是2的倍数

        ◦ ⻝物的坐标不能和蛇⾝每个节点的坐标重复

• 创建⻝物节点,打印⻝物 

void CreatFood(pSnake ps)
{
	//分析:1.坐标应该是随机生成的,但是应该有下面的约束
	// 2.食物的坐标不能够在墙外
	// 3.食物的坐标最好是2的倍数,这样方便和我们的蛇身挂钩不出墙
	// 4.食物的坐标不能和蛇的身体冲突1
	// x>=2,x<=54
	// y>=1y<=25
	// 
	//
	int x = 0;
	int y = 0;
again:
	do
	{
		x = rand() % 53 + 2;//余数0~52+2
		y = rand() % 25 + 1;
	} while (x%2 != 0);//x的坐标必须是2的倍数
	//坐标不能和蛇身冲突
	pSnakeNode cur = ps->_psnake;
	while (cur)//五个坐标蛇身的所有坐标不可与食物重复
	{
		//比较坐标
		if (cur->x == x && cur->y == y)
		{
			goto again;
		}
		cur = cur->next;
	}
	pSnakeNode pFood = (pSnakeNode)malloc(sizeof(SnakeNode));
	if (pFood == NULL)
	{
		perror("CreateFood():malloc");
		return;
	}
	//节点创建成功
	pFood->x = x;
	pFood->y = y;
	ps->_pFood = pFood;
	//打印
	SetPos(x, y);
	wprintf(L"%lc", FOOD);
	//getchar();//测试打印结果因为会被最后一句话打乱
}

8.3 游戏运⾏过程

戏运⾏期间,右侧打印帮助信息,提⽰玩家 根据游戏状态检查游戏是否继续,如果是状态是OK,游戏继续,否则游戏结束。 如果游戏继续,就是检测按键情况,确定蛇下⼀步的⽅向,或者是否加速减速,是否暂停或者退出游 戏。 确定了蛇的⽅向和速度,蛇就可以移动了。

(判断方向的依据是蛇头朝向,比如当我们蛇头正在朝右移动,那么我们蛇头后面就有身体,此时我们不能向左移动,原地掉头就矛盾,只能右上下三键,那么换个思想,就是当我们按下左键的时候,此时我们的蛇头朝向不能是向右,那么就将我们蛇头朝向改为左,就可以实现)当我们的游戏状态为OK,是不是就要一直判断玩家按键,那么我们就可以使用我们都 while循环来实现。

void GameRun(pSnake ps)
{
	PrintHelpInfo();
	do
	{
	
		SetPos(64, 9);
		printf("得分:%5d", ps->_Score);
		SetPos(64, 10);
		printf("每个食物分数为:%2d", ps->_FoodWeight);
		if( KEY_PRESS(VK_UP) && ps->_Dir != DOWN)
		{
			//如果我们按下向上键,此时我们的蛇头的方向不向上,就调整为向上移动
			ps->_Dir = UP;
		}
		else if (KEY_PRESS(VK_DOWN) && ps->_Dir != UP)
		{
			ps->_Dir = DOWN; 
		}
		else if (KEY_PRESS(VK_LEFT) && ps->_Dir != RIGHT)
		{
			ps->_Dir = LEFT;
		}
		else if (KEY_PRESS(VK_RIGHT) && ps->_Dir != LEFT)
		{
			ps->_Dir = RIGHT;
		}
		else if (KEY_PRESS(VK_ESCAPE))
		{
			ps->_Status = END_NORMAL;
			break;
		}
		else if (KEY_PRESS(VK_SPACE))
		{
			//由于暂停程序并不结束,并且还要可以继续唤醒程序,我们封装一个函数
			Pause();//死循环休眠检测休眠
		}
		//加速
		else if (KEY_PRESS(VK_F3))
		{
			if (ps->_SleepTime >= 80)//设置加速门款
			{
				ps->_SleepTime -= 30;
				//速度越快,越容易树,风浪越大鱼越贵,我们把分数调高一些
				ps->_FoodWeight += 5;
			}
		}
		//减速
		else if (KEY_PRESS(VK_ESCAPE))
		{
			if (ps->_SleepTime <=320)//设置减速最低门坎
			{
				ps->_SleepTime += 30;
				//速度越快,越容易树,风浪越大鱼越贵,我们把分数调高一些
				ps->_FoodWeight -= 2;
			}
		}
		Sleep(ps->_SleepTime);
		//蛇动起来
		SnakeMove(ps);

	} while (ps->_Status==OK);

}

8.3.1 上面的KEY_PRESS

是上面补充知识中win32api知识中检查我们按键状态讲过,我们封装一个宏来进行检查

#define KEY_PRESS(VK) ((GetAsyncKeyState(VK)&0x1)? 1:0)//宏用于判断虚拟键有没有被用过

8.3.2 打印侧边提示信息和分数变化

由于分数要变化:比如我们吃掉一个食物或者加速减速吃掉一个食物,所以要一直检测我们蛇目前的状态,我们就将分数显示写入上面的循环,对于侧边信息的提示是固定的,我们直接打印就好:

void PrintHelpInfo()
{
	SetPos(64, 15);
	printf("1.不能穿墙,不可以咬到自己");
	SetPos(64, 16);
	printf("2.使用↑↓← →,分别控制蛇的上下左右移动");
	SetPos(64, 17);
	printf("3.使用F3加速蛇的移动,F4减速");
	SetPos(64, 18);
	printf("4.ESC退出,点击空格暂停游戏");

	SetPos(64, 20);
	printf("CSDN-Nicn版权");
	//getchar();

}

8.3.3 蛇⾝的移动

我们实现的思想是,无论蛇身怎么移动,蛇头都会1去到下一个位置,那么我们就先创建下⼀个节点,根据移动⽅向和蛇头的坐标,比如当前状态蛇头向上,那么下一个点的坐标是不是x不变,y坐标-1,蛇移动到下⼀个位置的坐标。 确定了下⼀个位置后,看下⼀个位置是否是⻝物(NextIsFood),是⻝物就做吃⻝物处理 (EatFood),如果不是⻝物则做前进⼀步的处理(NoFood)。 蛇⾝移动后,判断此次移动是否会造成撞墙(KillByWall)或者撞上⾃⼰蛇⾝(KillBySelf),从⽽影 响游戏的状态

整体实现代码为:

 

void SnakeMove(pSnake ps)//蛇移动
{
	//移动即是通过蛇头的方向判断下一个位置的坐标,然后我们就根据按键创建这个方向上的节点,然后再连接起来
	//那我们就先为未来可能走的这个节点创建一下
	pSnakeNode pNext = (pSnakeNode)malloc(sizeof(SnakeNode));
	//判断一下是否申请成功
	if (pNext == NULL)
	{
		perror("SnakeMove():malloc()");
		return;
	}
	pNext->next = NULL;//迅速将这个节点的指向下一个节点的指针置空,否则有些许凌乱,后续使用再修改

	
	
	switch (ps->_Dir)
	{
	case UP:
		//x不变,y-1
		pNext->x = ps->_psnake->x;
		pNext->y = ps->_psnake->y - 1;
		break;
	case DOWN:
		pNext->x = ps->_psnake->x;
		pNext->y = ps->_psnake->y + 1;
		break;
	case LEFT://x-1,y不变
		pNext->x = ps->_psnake->x-1;
		pNext->y = ps->_psnake->y ;
		break;
	case RIGHT:
		pNext->x = ps->_psnake->x+1;
		pNext->y = ps->_psnake->y;
		break;
	}
	//判断下一个位置是否是食物,是食物我们蛇身长度就要变,不是食物我们目前蛇的尾部就不应该显示
	//写一个判断函数
	if (NextIsFood(ps, pNext))
	{
		//吃掉食物,封装函数
		EatFood(ps,pNext);
	}
	else
	{
		//
		NotFood(ps, pNext);
	}
	//检测蛇是否撞墙(判断坐标)
	KillByWall( ps);
	//检测蛇是否吃到自身(判断坐标)
	KillBySelf(ps);
}
8.3.3.1 NextIsFood

当根据蛇头的方向我们确定了下一个点的坐标的时候,由于游戏开始我们就生成了一个食物,我们就要判断一下蛇头方向的下一个坐标是不是食物也就是坐标是否与我们的食物坐标相同:
 

int NextIsFood(pSnake ps, pSnakeNode pnext)
{
	if (ps->_pFood->x == pnext->x && ps->_pFood->y == pnext->y)
	{
		return 1;//是食物就返回1
	}
	else
	{
		return 0;//不是食物返回0
	}
}
8.3.3.2 EatFood

如果蛇头的下一个坐标是食物,那我们就要将食物吃掉

如何吃掉食物,我们将这个节点的next指针指向我们的蛇,让后再用蛇指针来维护我们原先的蛇和这个食物这个新的空间,就是头插法。那么我们最开始为我们的食物申请了一个空间,现在我又为我蛇头的下一个节点又申请了空间,两块空间现在重叠使用一份,就将原来申请的空间释放掉。吃掉一个食物为了游戏能够继续应该还要生成一个随机食物,所以再次调用我们的食物生成函数。

//下一个节点是食物
void EatFood(pSnake ps, pSnakeNode pnext)
{
	//头插法,注意和前面蛇身连接联系起来
	pnext->next = ps->_psnake;
	ps->_psnake = pnext;//节点变成蛇头我们就将这个节点的指针作为我们蛇的指针,方便找到蛇
	pSnakeNode cur = ps->_psnake;
	while (cur)
	{
		SetPos(cur->x, cur->y);
		wprintf(L"%lc", BODY);
		cur = cur->next;
		
	}
	//原先创建食物的时候我们为食物节点申请过一次空间,那么这里就释放掉那一次申请的空间
	free(ps->_pFood);
	//得分增加
	ps->_Score += ps->_FoodWeight;
	//我们还得生成食物
	CreatFood(ps);

}

8.3.3.3 NoFood

我们蛇头朝向的下一个坐标不是食物,此时我们使用头插法将将下⼀个节点头插⼊蛇的⾝体,但是要维持我们蛇身体长度不变,我们就不打印我们的蛇尾巴,就是让cur只移动到我们的蛇尾巴后面一节,并将之前蛇⾝最后⼀个节点打印为空格,放弃掉蛇⾝的最后⼀个节点

//下一个节点不是食物
void NotFood(pSnake ps, pSnakeNode pnext)
{
	//头插法,注意和前面蛇身连接联系起来
	pnext->next = ps->_psnake;
	ps->_psnake = pnext;//节点变成蛇头我们就将这个节点的指针作为我们蛇的指针,方便找到蛇
	//打印应该打印蛇身的长度,但是现在我们蛇挂上了下一个节点,我们就可以
	//将最后一个位置节点释放,并把到时第二个的next指针置空
	pSnakeNode cur = ps->_psnake;
	while (cur->next->next)
	{
		SetPos(cur->x, cur->y);
		wprintf(L"%lc", BODY);
		cur = cur->next;
	}
	SetPos(cur->next->x, cur->next->y);
	printf("  ");//将最后一个蛇节点打印空格
	//释放掉最后一个节点
	free(cur->next);
	cur->next = NULL;
	
}
8.3.3.4 KillByWall 判断蛇头是否撞墙

这个很简单就是通过蛇头的横纵坐标不能和墙的行列重合进行判断就可以了

void KillByWall(pSnake ps)
{
	//横坐标不能是0和56,左右墙,蛇头坐标
	if (ps->_psnake->x == 0 || ps->_psnake->x == 56
		||ps->_psnake->y==0||ps->_psnake->y==26)
	{
		ps->_Status = KILL_BY_WALL;

	}


}

8.3.3.5 KillBySelf 判断蛇头的坐标是否和蛇⾝体的坐标冲突

我们的蛇不能吃到自己,也就是说蛇头坐标不能和蛇第二个节点以后的坐标有重合,我们获得指向蛇第二个节点的坐标遍历蛇身体然后和蛇头坐标比对就可以实现:

void KillBySelf(pSnake ps)
{
	//遍历蛇头下一个坐标节点不能是自身
	//首先找到我们蛇的第二个节点,蛇头不能触碰第二个节点以后的坐标
	pSnakeNode cur = ps->_psnake->next;
	while (cur)
	{
		if (ps->_psnake->x == cur->x&&ps->_psnake->y==cur->y)
		{
			ps->_Status = KILL_BY_SELF;
		}
		cur = cur->next;
	}

}

8.4 游戏结束

游戏状态不再是OK(游戏继续)的时候,要告知游戏技术的原因,并且释放蛇⾝节点。 

void GameEnd(pSnake ps)
{
	SetPos(20, 12);
	switch (ps->_Status)
	{
	case END_NORMAL:
		printf("玩家自己退出游戏\n");
		break;
	case KILL_BY_SELF:
		printf("不好意思,您咬到了自己,中毒身亡了\n");
		break;
	case KILL_BY_WALL:
		printf("不好意思,您撞到墙了\n");
		break;
	}
	
	//释放蛇身的节点
	pSnakeNode cur = ps->_psnake;
	while (cur)
	{
		pSnakeNode det = cur;
		cur = cur->next;
		free(det);
	}
	ps->_psnake = NULL;
}

9.参考源码

分三个文件

test.c

#include"snake.h"





void test()
{
	int ch = 0;
	do
	{
		Snake snake = { 0 };//首先创建一条贪吃蛇
		//游戏开始,游戏初始化:地图的打印和蛇 食物的打印
		GameStart(&snake);

		//2.游戏运行-游戏正常运行
		GameRun(&snake);

		//3.游戏结束,游戏善后,链表要释放资源
		GameEnd(&snake);
		SetPos(20, 18);
		printf("要再来一局吗?(Y/N):");
		ch = getchar();
		getchar();//清理回车

	} while (ch=='Y'||ch=='y');
	SetPos(0, 27);
}





int main()
{
	//设置程序适应本地环境,后面打印各种宽字符,蛇身体和食物墙等
	setlocale(LC_ALL ,"");
	srand((unsigned int)time(NULL));//随机生成食物坐标

	test();
	return 0;
}

snake.h

#pragma once
#include<stdio.h>
#include<locale.h>
#include<stdlib.h>  //使用system函数
#include<windows.h>//使用Win32函数
#include<stdbool.h>//folse
#include<time.h>

#define WALL L'□'
#define BODY L'●'
#define FOOD L'★'

#define POS_X  24
#define POS_Y   5

#define KEY_PRESS(VK) ((GetAsyncKeyState(VK)&0x1)? 1:0)//宏用于判断虚拟键有没有被用过

enum DIRECTION
{
	UP = 1,
	DOWN,
	LEFT,
	RIGHT
};
enum GAME_STATUS
{
	OK,//正常运行
	END_NORMAL,//按ESC正常退出
	KILL_BY_WALL,
	KILL_BY_SELF
};
//贪吃蛇节点描述
typedef struct SnakeNode
{
	//描述坐标的信息
	int x;
	int y;
	struct SnakeNode* next;
}SnakeNode,*pSnakeNode;

//贪吃蛇蛇的描述
typedef struct snake
{
	//需要指向这个蛇第一个节点的指针,方便找到整条蛇
	pSnakeNode _psnake;
	pSnakeNode _pFood;//这个指针指向的是食物节点,因为食物也可以看做是一个节点类型
	int _Score;//当前累计的分数,可以理解为当前有多少节点
	int _FoodWeight;//一个食物的分数是多少
	int _SleepTime;//定义每走一步休息的时间,我们定义的休息的时间越短,速度就越快,时间越长,速度就越慢
	enum DIRECTION _Dir;//描述蛇的方向
	enum GAME_STATUS _Status;//描述游戏的状态:正常退出,撞墙,吃到自己
}Snake,*pSnake;



void GameStart(pSnake ps);
void SetPos(int x, int y);//定位坐标
void InitSnake(pSnake ps);//初始化蛇
//创建食物
void CreatFood(pSnake ps);


void GameRun(pSnake ps);//游戏的运行过程
void PrintHelpInfo();//打印帮助信息

void Pause();//死循环休眠检测

void SnakeMove(pSnake ps);//蛇移动起来

int NextIsFood(pSnake ps, pSnakeNode pnext);//判断下一个节点是否是食物

//当下一个节点是食物,我们执行操作
void EatFood(pSnake ps, pSnakeNode pnext);
void NotFood(pSnake ps, pSnakeNode pnext);


void KillByWall(pSnake ps);
void KillBySelf(pSnake ps);

void GameEnd(pSnake ps);//游戏结束后的一些处理

snake.c

#include "snake.h"

void SetPos(int x, int y)
{
	COORD pos = { x,y };
	HANDLE houtput = NULL;//获取标准输出也就是屏幕的控制句柄
	houtput = GetStdHandle(STD_OUTPUT_HANDLE);
	//设置标准输出上光标的位置为pos
	SetConsoleCursorPosition(houtput, pos);
}
void 	WelComeToGame()
{
	//定位光标
	SetPos(40, 14);
	printf("欢迎来到贪吃蛇小游戏");
	SetPos(40, 25);//再次定位坐标
	system("pause");//暂停命令,执行到此处暂停)

   //来到下一个界面,我们把当前界面内容全部清理重新打印实现换屏效果
	system("cls");//清理屏幕
	SetPos(20, 14);
	printf("使用↑↓← →,分别控制蛇的上下左右移动,使用F3加速,F4减速");
	SetPos(40, 25);//再次定位坐标
	system("pause");//暂停命令,执行到此处暂停)
	system("cls");//清理屏幕

}

void CreateMap()
{
	//上
	SetPos(0, 0);//重新定位光标位置
	int i = 0;
	for (i = 0; i <= 56; i += 2)
	{
		wprintf(L"%lc", WALL);
	}
	//下
	SetPos(0, 26);//重新定位光标位置
	for (i = 0; i <= 56; i += 2)
	{
		wprintf(L"%lc", WALL);
	}
	//左
	for (i = 1; i <= 25; i++)
	{
		SetPos(0, i);
		wprintf(L"%lc", WALL);
	}
	//右
	for (i = 1; i <= 25; i++)
	{
		SetPos(56, i);
		wprintf(L"%lc", WALL);
	}
	//getchar();//测试最后一行的打印

}

void InitSnake(pSnake ps)
{
	pSnakeNode cur = NULL;//为了实现复用就定义在外边
	int i = 0;
	for (i = 0; i < 5; i++)
	{
		 cur = (pSnakeNode)malloc(sizeof(SnakeNode));
		if (cur == NULL)
		{
			perror("malloc");
			return;
		}
		cur->x = POS_X+2*i;
		cur->y = POS_Y;
		cur->next = NULL;
		//头插法
		if (ps->_psnake == NULL)
		{
			ps->_psnake = cur;//第一个位置就给了蛇的第一块
		}
		else
		{
			cur->next = ps->_psnake;
			ps->_psnake = cur;

		}
	}
	//打印蛇,就是遍历链表
	cur = ps->_psnake;
	while (cur)
	{
		SetPos(cur->x, cur->y);//得到每个节点的横纵坐标,然后定位光标打印身体
		wprintf(L"%lc", BODY);

		cur = cur->next;

	}
	ps->_Status = OK;
	ps->_Score = 0;
	ps->_pFood = NULL;
	ps->_SleepTime = 200;
	ps->_FoodWeight = 10;
	ps->_Dir = RIGHT;

	//getchar();//测试打印结果因为会被最后一句话打乱


}

void CreatFood(pSnake ps)
{
	//分析:1.坐标应该是随机生成的,但是应该有下面的约束
	// 2.食物的坐标不能够在墙外
	// 3.食物的坐标最好是2的倍数,这样方便和我们的蛇身挂钩不出墙
	// 4.食物的坐标不能和蛇的身体冲突1
	// x>=2,x<=54
	// y>=1y<=25
	// 
	//
	int x = 0;
	int y = 0;
again:
	do
	{
		x = rand() % 53 + 2;//余数0~52+2
		y = rand() % 25 + 1;
	} while (x%2 != 0);//x的坐标必须是2的倍数
	//坐标不能和蛇身冲突
	pSnakeNode cur = ps->_psnake;
	while (cur)//五个坐标蛇身的所有坐标不可与食物重复
	{
		//比较坐标
		if (cur->x == x && cur->y == y)
		{
			goto again;
		}
		cur = cur->next;
	}
	pSnakeNode pFood = (pSnakeNode)malloc(sizeof(SnakeNode));
	if (pFood == NULL)
	{
		perror("CreateFood():malloc");
		return;
	}
	//节点创建成功
	pFood->x = x;
	pFood->y = y;
	ps->_pFood = pFood;
	//打印
	SetPos(x, y);
	wprintf(L"%lc", FOOD);
	//getchar();//测试打印结果因为会被最后一句话打乱
}


void GameStart(pSnake ps)
{
	//控制台窗口大小的设置
	system("mode con cols=110 lines=30");
	system("title SNAKE");
	//将光标隐藏
	 //获取标准输出的句柄(⽤来标识不同设备的数值)
	HANDLE hOutput = GetStdHandle(STD_OUTPUT_HANDLE);
	//影藏光标操作
	CONSOLE_CURSOR_INFO CursorInfo;
	GetConsoleCursorInfo(hOutput, &CursorInfo);//获取控制台光标信息
	CursorInfo.bVisible = false; //隐藏控制台光标
	SetConsoleCursorInfo(hOutput, &CursorInfo);//设置控制台光标状态
	//打印欢迎界面
	WelComeToGame();
	//创建地图
	CreateMap();
	//初始化贪吃蛇
	InitSnake(ps);
	//创建食物
	CreatFood(ps);

}

void PrintHelpInfo()
{
	SetPos(64, 15);
	printf("1.不能穿墙,不可以咬到自己");
	SetPos(64, 16);
	printf("2.使用↑↓← →,分别控制蛇的上下左右移动");
	SetPos(64, 17);
	printf("3.使用F3加速蛇的移动,F4减速");
	SetPos(64, 18);
	printf("4.ESC退出,点击空格暂停游戏");

	SetPos(64, 20);
	printf("CSDN-Nicn版权");
	//getchar();

}



void Pause()
{
	while (1)
	{
		Sleep(100);//一直睡眠
		if (KEY_PRESS(VK_SPACE))
		{
			break;//按空格跳出
		}
	}

}

int NextIsFood(pSnake ps, pSnakeNode pnext)
{
	if (ps->_pFood->x == pnext->x && ps->_pFood->y == pnext->y)
	{
		return 1;//是食物就返回1
	}
	else
	{
		return 0;//不是食物返回0
	}
}

//下一个节点是食物
void EatFood(pSnake ps, pSnakeNode pnext)
{
	//头插法,注意和前面蛇身连接联系起来
	pnext->next = ps->_psnake;
	ps->_psnake = pnext;//节点变成蛇头我们就将这个节点的指针作为我们蛇的指针,方便找到蛇
	pSnakeNode cur = ps->_psnake;
	while (cur)
	{
		SetPos(cur->x, cur->y);
		wprintf(L"%lc", BODY);
		cur = cur->next;
		
	}
	//原先创建食物的时候我们为食物节点申请过一次空间,那么这里就释放掉那一次申请的空间
	free(ps->_pFood);
	//得分增加
	ps->_Score += ps->_FoodWeight;
	//我们还得生成食物
	CreatFood(ps);

}
//下一个节点不是食物
void NotFood(pSnake ps, pSnakeNode pnext)
{
	//头插法,注意和前面蛇身连接联系起来
	pnext->next = ps->_psnake;
	ps->_psnake = pnext;//节点变成蛇头我们就将这个节点的指针作为我们蛇的指针,方便找到蛇
	//打印应该打印蛇身的长度,但是现在我们蛇挂上了下一个节点,我们就可以
	//将最后一个位置节点释放,并把到时第二个的next指针置空
	pSnakeNode cur = ps->_psnake;
	while (cur->next->next)
	{
		SetPos(cur->x, cur->y);
		wprintf(L"%lc", BODY);
		cur = cur->next;
	}
	SetPos(cur->next->x, cur->next->y);
	printf("  ");//将最后一个蛇节点打印空格
	//释放掉最后一个节点
	free(cur->next);
	cur->next = NULL;
	
}
void KillByWall(pSnake ps)
{
	//横坐标不能是0和56,左右墙,蛇头坐标
	if (ps->_psnake->x == 0 || ps->_psnake->x == 56
		||ps->_psnake->y==0||ps->_psnake->y==26)
	{
		ps->_Status = KILL_BY_WALL;

	}


}
void KillBySelf(pSnake ps)
{
	//遍历蛇头下一个坐标节点不能是自身
	//首先找到我们蛇的第二个节点,蛇头不能触碰第二个节点以后的坐标
	pSnakeNode cur = ps->_psnake->next;
	while (cur)
	{
		if (ps->_psnake->x == cur->x&&ps->_psnake->y==cur->y)
		{
			ps->_Status = KILL_BY_SELF;
		}
		cur = cur->next;
	}

}

void SnakeMove(pSnake ps)//蛇移动
{
	//移动即是通过蛇头的方向判断下一个位置的坐标,然后我们就根据按键创建这个方向上的节点,然后再连接起来
	//那我们就先为未来可能走的这个节点创建一下
	pSnakeNode pNext = (pSnakeNode)malloc(sizeof(SnakeNode));
	//判断一下是否申请成功
	if (pNext == NULL)
	{
		perror("SnakeMove():malloc()");
		return;
	}
	pNext->next = NULL;//迅速将这个节点的指向下一个节点的指针置空,否则有些许凌乱,后续使用再修改

	
	
	switch (ps->_Dir)
	{
	case UP:
		//x不变,y-1
		pNext->x = ps->_psnake->x;
		pNext->y = ps->_psnake->y - 1;
		break;
	case DOWN:
		pNext->x = ps->_psnake->x;
		pNext->y = ps->_psnake->y + 1;
		break;
	case LEFT://x-1,y不变
		pNext->x = ps->_psnake->x-1;
		pNext->y = ps->_psnake->y ;
		break;
	case RIGHT:
		pNext->x = ps->_psnake->x+1;
		pNext->y = ps->_psnake->y;
		break;
	}
	//判断下一个位置是否是食物,是食物我们蛇身长度就要变,不是食物我们目前蛇的尾部就不应该显示
	//写一个判断函数
	if (NextIsFood(ps, pNext))
	{
		//吃掉食物,封装函数
		EatFood(ps,pNext);
	}
	else
	{
		//
		NotFood(ps, pNext);
	}
	//检测蛇是否撞墙(判断坐标)
	KillByWall( ps);
	//检测蛇是否吃到自身(判断坐标)
	KillBySelf(ps);
}



void GameRun(pSnake ps)
{
	PrintHelpInfo();
	do
	{
	
		SetPos(64, 9);
		printf("得分:%5d", ps->_Score);
		SetPos(64, 10);
		printf("每个食物分数为:%2d", ps->_FoodWeight);
		if( KEY_PRESS(VK_UP) && ps->_Dir != DOWN)
		{
			//如果我们按下向上键,此时我们的蛇头的方向不向上,就调整为向上移动
			ps->_Dir = UP;
		}
		else if (KEY_PRESS(VK_DOWN) && ps->_Dir != UP)
		{
			ps->_Dir = DOWN; 
		}
		else if (KEY_PRESS(VK_LEFT) && ps->_Dir != RIGHT)
		{
			ps->_Dir = LEFT;
		}
		else if (KEY_PRESS(VK_RIGHT) && ps->_Dir != LEFT)
		{
			ps->_Dir = RIGHT;
		}
		else if (KEY_PRESS(VK_ESCAPE))
		{
			ps->_Status = END_NORMAL;
			break;
		}
		else if (KEY_PRESS(VK_SPACE))
		{
			//由于暂停程序并不结束,并且还要可以继续唤醒程序,我们封装一个函数
			Pause();//死循环休眠检测休眠
		}
		//加速
		else if (KEY_PRESS(VK_F3))
		{
			if (ps->_SleepTime >= 80)//设置加速门款
			{
				ps->_SleepTime -= 30;
				//速度越快,越容易树,风浪越大鱼越贵,我们把分数调高一些
				ps->_FoodWeight += 5;
			}
		}
		//减速
		else if (KEY_PRESS(VK_ESCAPE))
		{
			if (ps->_SleepTime <=320)//设置减速最低门坎
			{
				ps->_SleepTime += 30;
				//速度越快,越容易树,风浪越大鱼越贵,我们把分数调高一些
				ps->_FoodWeight -= 2;
			}
		}
		Sleep(ps->_SleepTime);
		//蛇动起来
		SnakeMove(ps);

	} while (ps->_Status==OK);

}



void GameEnd(pSnake ps)
{
	SetPos(20, 12);
	switch (ps->_Status)
	{
	case END_NORMAL:
		printf("玩家自己退出游戏\n");
		break;
	case KILL_BY_SELF:
		printf("不好意思,您咬到了自己,中毒身亡了\n");
		break;
	case KILL_BY_WALL:
		printf("不好意思,您撞到墙了\n");
		break;
	}
	
	//释放蛇身的节点
	pSnakeNode cur = ps->_psnake;
	while (cur)
	{
		pSnakeNode det = cur;
		cur = cur->next;
		free(det);
	}
	ps->_psnake = NULL;
}

10.结语

这个项目的实现逻辑相对于通讯录较为复杂,但是对于C语言学完的伙伴来说还是比较容易掌握,新知识只有WIN32API的知识补充,但是也不是很难,大家可以结合参考资料理解,特别是我们的流程图。以上就是本期的所有内容,知识含量蛮多,大家可以配合解释和原码运行理解。创作不易,大家如果觉得还可以的话,欢迎大家三连,有问题的地方欢迎大家指正,一起交流学习,一起成长,我是Nicn,正在c++方向前行的奋斗者,感谢大家的关注与喜欢。

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

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

相关文章

初始Nginx(基本概念)

目录 一、Nginx的概念 二、Nginx常用功能 1、HTTP(正向)代理&#xff0c;反向代理 1.1正向代理 1.2 反向代理 2、负载均衡 2.1 轮询法&#xff08;默认方法&#xff09; 2.2 weight权重模式&#xff08;加权轮询&#xff09; 2.3 ip_hash 3、web缓存 三、基础特性 四…

JAVA工程师面试专题-Mysql篇

一、基础 1、mysql可以使用多少列创建索引&#xff1f; 16 2、mysql常用的存储引擎有哪些 存储引擎Storage engine&#xff1a;MySQL中的数据、索引以及其他对象是如何存储的&#xff0c;是一套文件系统的实现。常用的存储引擎有以下&#xff1a; Innodb引擎&#xff1a;In…

Git拉取gitee代码至本地

首先需要有Git&#xff0c;才能拉取代码。 安装参考 Git安装与卸载_git卸载重装-CSDN博客 安装好后在自己想要拉取代码的位置&#xff0c;右键Git bash here gitee 点击 复制 填写到打开的窗口即可

GEE数据集——30 米全球年度烧毁面积地图 (GABAM)(更新)

30 米全球年度烧毁面积地图 (GABAM) 迄今为止&#xff0c;全球烧毁面积&#xff08;BA&#xff09;产品只有较高的空间分辨率&#xff0c;因为目前大多数全球烧毁面积产品都是在主动火灾探测或密集时间序列变化分析的帮助下生成的&#xff0c;这需要非常高的时间分辨率。不过&a…

GoLand 相关

goland 下载依赖 go mod tidy&#xff1a;保持依赖整洁 go mod tidy 命令的作用是清理未使用的依赖&#xff0c;并更新 go.mod 以及 go.sum 文件。 go mod tidy 和 go mod vendor 两个命令是维护项目依赖不可或缺的工具。go mod tidy 确保了项目的 go.mod 文件精简且准确&…

Nacos注册中心实战

目录 为什么需要注册中心&#xff1f; 注册中心选型 Nacos是什么&#xff1f; 微服务整合Nacos注册中心实战 Nacos Server环境搭建 微服务提供者整合Nacos 微服务调用者整合Nacos 整合RestTemplateSpring Cloud LoadBalancer实现微服务调用 为什么需要注册中心&#xf…

简单几步通过DD工具把云服务器系统Linux改为windows

简单几部通过DD安装其他系统&#xff0c;当服务器的web控制台没有我们要装的系统&#xff0c;就需要通过DD&#xff08;Linux磁盘&#xff09;工具来更改系统&#xff0c;&#xff08;已知支持KVM系统&#xff09; 本文如何简单的更换系统&#xff0c;不通过web控制台来更换&a…

掌握Docker:让你的应用轻松部署和管理

文章目录 一、引言&#xff08;为什么要学习docker&#xff1f;&#xff09;1.1 环境不一致1.2 隔离性1.3 弹性伸缩1.4 学习成本 二、Docker介绍2.1 Docker的由来2.2 什么是Docker2.3 为什么要用Docker2.3.1 虚拟机2.3.2 Linux容器 2.4 Docker与传统虚拟机的区别2.5 Docker的思…

利用采购软件有效管理采购支出流程

在资本驱动的商业世界中&#xff0c;企业花的每一分钱都必须通过可量化的回报来证明其合理性。 采购支出管理通过简化企业的采购流程来实现支出的无缝优化。适当的程序可通过加强与供应商和贸易伙伴的沟通&#xff0c;来帮助企业最大限度地节省成本。 采购支出管理的重要性 企…

代码随想录算法训练营第四十天 343. 整数拆分、 96.不同的二叉搜索树

代码随想录算法训练营第四十天 | 343. 整数拆分、 96.不同的二叉搜索树 343. 整数拆分 题目链接&#xff1a;343. 整数拆分 - 力扣&#xff08;LeetCode&#xff09; 例如 n 10, 可以拆分为 3 * dp[7] 。因为dp[7]之前已经计算过最大 3 * 4&#xff0c; 所以dp[10] 3 * 3 …

conda 进入python环境里pip install安装不到该环境或不生效

参考&#xff1a;https://blog.csdn.net/weixin_47834823/article/details/128951963 https://blog.51cto.com/u_15060549/4662570?loginfrom_csdn 1、直接进入python Scripts目录下安装 cmd打开运行窗口&#xff0c;cd切换路径至指定虚拟环境下的Scripts路径后再pip安装 擦…

二叉树中的第K大层和

1.题目 这道题是2024-2-23的签到题&#xff0c;题目难度为中等。 考察知识点为BFS算法&#xff08;树的层序遍历&#xff09; 大根堆&#xff08;优先队列&#xff09;。 题目链接&#xff1a;2583. 二叉树中的第 K 大层和 - 力扣&#xff08;LeetCode&#xff09; 给你一棵…

MySQL学习Day18——逻辑架构

一、逻辑架构剖析: 1.服务器处理客户端请求: 首先 MySQL 是典型的C/S架构&#xff0c;即client/Server架构&#xff0c;服务器端程序使用的mysqld。不论客户端进程和服务器进程是采用哪种方式进行通信&#xff0c;最后实现的效果都是:客户端进程向服务器进程发送段文本(SQL语…

QT中调用python

一.概述 1.Python功能强大&#xff0c;很多Qt或者c/c开发不方便的功能可以由Python编码开发&#xff0c;尤其是一些算法库的应用上&#xff0c;然后Qt调用Python。 2.在Qt调用Python的过程中&#xff0c;必须要安装python环境&#xff0c;并且Qt Creator中编译器与Python的版…

docker 可视化管理工具 ui-for-docker

1、查询 docker search ui-for-docker 2、拉取镜像 docker pull uifd/ui-for-docker 3、运行启动容器 docker run -it -d \ --name docker-web \ -p 9010:9000 \ --privilegedtrue \ -v /var/run/docker.sock:/var/run/docker.sock \ ui-for-docker 4、页面访问 ​http:/…

vex-table链接

vxe-table v4https://vxetable.cn/#/table/start/quick

测试开源C#人脸识别模块DlibDotNet

百度“C# 换脸”找到参考文献4&#xff0c;发现其中使用DlibDotNet检测并识别人脸&#xff08;之前主要用的是ViewFaceCore&#xff09;&#xff0c;DlibDotNet是Dlib的.net封装版本&#xff0c;后者为开源C工具包&#xff0c;支持机器学习算法、图像处理等算法以支撑各类高级应…

应用感知型网络性能管理

网络基础设施似乎日益复杂和先进&#xff0c;迫使网络管理员抛弃传统的管理方法。应用感知型网络性能管理是一种用于监控网络性能的新型整体方法&#xff0c;它为管理员提供了强大的 IT 资源管理功能。应用感知型网络性能管理为 IT 管理员带来了精细视图、动态资源分配、主动故…

如何使用NPM包管理器在Node.js项目中安装和管理依赖

随着现代开发技术的快速发展&#xff0c;前端开发工程师们面临着越来越多的挑战。其中一个重要的挑战之一就是管理项目中的依赖关系。NPM&#xff08;Node Package Manager&#xff09;是一个业界领先的包管理器&#xff0c;被广泛应用于Node.js项目中。本文将详细介绍如何使用…

【 buuctf-NTFS 数据流】

这里要用到NtfsStreamEditor Ntfs数据流处理工具2.0 NtfsStreamsEditor 2.0 预览 090410更新_原创工具区_安全区 卡饭论坛 - 互助分享 - 大气谦和!可以从这个网站下载&#xff0c;注意包含 ntfs 数据流的压缩包要用 winrar 解压缩&#xff0c;扫描 flag 文件&#xff0c;会出现…