贪吃蛇项目(小白保姆级教程)

游戏介绍

游戏背景:

贪吃蛇游戏是经典的游戏项目之一,也是很简单的小游戏

实现背景:

这里我们是基于32位的Win32_API进行实现的

需要的知识点:

C语言函数、枚举、结构体、动态内存管理、预处理指令、链表、Win32_API等

适合人群:有数据结构链表的知识,一定C语言代码能力人群

我这里使用到的搜索网站(用来搜索库函数头文件以及windows的库函数):

windows官方网站

SetConsoleCursorInfo 函数 - Windows Console | Microsoft Learn

C语言官方网站

C 关键词 - cppreference.com

C语言搜索网站

cplusplus.com/reference/cstdio/

需要注意

这里的贪吃蛇的实现,我们分装了很多函数,适合小白观看学习,因为函数分装过多,我甚至把本地化一段话都分装成函数。原因:这是一个知识点,可以更加详细的解释和增加注释。

游戏的思维导图

游戏的思维导图可以对照进行实现代码

创建文件

这里分别创建头文件,实现文件,测试文件

设置控制台信息

Win_API

Win_API是Windows应用程序接口(Windows Application Programming Interface)的缩写,它是一组函数、系统服务和程序接口,允许开发者在微软Windows操作系统上创建应用程序。Win32 API 是Windows API的一个主要部分,它为Windows应用程序提供了一系列的函数调用,这些函数可以用来执行各种任务,比如创建和管理窗口、处理用户输入、绘制图形、访问文件系统、网络通信等。


Win32 API 是Windows操作系统的一个核心组件,它为C和C++等编程语言提供了丰富的接口,使得开发者能够利用这些接口来控制和管理Windows操作系统的各个方面。通过调用这些API函数,开发者可以轻松地实现与操作系统相关的复杂功能,而无需关心底层操作系统的具体实现细节。


Win32 API 包括了数千个函数,涵盖了从基本的数据类型和宏定义到复杂的窗口管理和用户界面设计的各个方面。这些API函数被组织成不同的库,如Kernel32.dll、User32.dll、Gdi32.dll等,每个库都包含了与特定功能相关的函数。开发者可以在应用程序中动态地加载这些库,并调用相应的API函数来实现所需的功能。


Win32 API 是Windows平台上软件开发的基础,几乎所有的Windows应用程序都会直接或间接地使用Win32 API。因此,对于Windows平台上的软件开发者来说,了解和掌握Win32 API 是非常重要的。

Windows这个多作业系统除了协调应用程序的执行、分配内存、管理资源之外,它同时也是一个很大
的服务中心,调用这个服务中心的各种服务(每一种服务就是一个函数),可以帮应用程序达到开启
视窗、描绘图形、使用周边设备等目的,由于这些函数服务的对象是应用程序(Application),所以便
称之为ApplicationProgrammingInterface,简称API函数。WIN32_API也就是Microsoft Windows
32位平台的应用程序编程接口

编译器界面窗口的设置

这里使用的是vs编译器

首先我们需要改变属性

这里说明一下,不管你是让windows决定,还是windows控制价台主机,这里都是可以的,

只要不是windows终端就可以

因为不同电脑有些让windows决定产生的效果和windows终端的效果是一样的,所以我们可以使用windows控制台主机,我这里选择的就是windows控制台主机

这里和电脑有点关系,不用担心

设置控制台的大小

设置控制台的程序的大小,我们可以使用windows命令,来控制编译器,只需要进行调用就可以

mode con cols=100 lines=30

`mode con cols=100 lines=30` 是一个在DOS和Windows命令提示符(cmd.exe)中使用的命令,用于设置命令行窗口的大小。这个命令的作用是将命令行窗口的宽度和高度分别设置为100列和30行。
- `mode` 是命令行工具,用于配置系统设备的工作模式。
- `con` 指的是控制台(console),也就是命令行窗口。
- `cols=100` 指定命令行窗口的宽度为100个字符。
- `lines=30` 指定命令行窗口的高度为30行。
在Windows操作系统中,你可以通过打开命令提示符(cmd)并输入这个命令来改变窗口的大小。这个命令对于需要更多空间来查看文本输出或者进行文本操作的场合非常有用。

这里我们打开cmd演示一下

可以看到原本的大小

设置之后的大小,为了对比明显,我特地搞得小一点

system 

那么在编译器里面如果调用?并且设置控制台大小
我们只需要使用一个函数

语法格式

在C语言中,system 函数是一个库函数,用于执行宿主系统上的命令。这个函数声明在 stdlib.h 头文件中。它的原型如下:

int system(const char *command);

当你调用 system 函数时,它会将传递给它的字符串作为命令传给命令处理器(在Unix-like系统上是shell,在Windows上是cmd.exe),然后执行这个命令。函数的返回值依赖于具体的实现,但通常情况下,如果命令执行成功,它会返回0;如果执行失败,则会返回一个非零值。

此时我们也就是调用成功并且成功设置控制台大小了

如图

设置控制台的名字

设置控制台的名字也很简单,也一个cmd的使用

title + 名字

在编译器里面进行调用

封装一个控制台的函数

//设置控制台的相关属性
void set_Information()
{
	system("mode con cols=120 lines=35");
	system("title 贪吃蛇");
}

隐藏光标的属性 

目的

在编写贪吃蛇这样的文本模式游戏时,隐藏光标是一个常用的技巧,目的是为了提高用户体验和游戏的视觉表现。隐藏光标的属性目的主要包括以下几点:
1. **改善视觉效果**:

光标在屏幕上闪烁可能会分散玩家的注意力,尤其是在移动快速的贪吃蛇游戏中。隐藏光标可以让玩家更专注于游戏本身,而不是被光标干扰。


2. **避免混淆**:

在文本模式下,光标可能会与游戏中的字符混淆,尤其是当光标与游戏中的蛇或食物的字符颜色相同时。隐藏光标可以避免这种混淆,确保游戏的清晰度。


3. **保持游戏界面的整洁**:

贪吃蛇游戏通常有一个简洁的游戏界面,光标的存在可能会破坏这种简洁性。隐藏光标有助于保持游戏界面的整洁和统一。


4. **技术限制**:

在某些情况下,显示光标可能会影响游戏的刷新速度或响应时间。隐藏光标可以减少这种技术限制的影响,使得游戏运行更加流畅。


在C语言或其他编程语言中,通常会使用特定的系统调用来隐藏光标。例如,在Windows操作系统的命令行界面中,可以使用 `CONSOLE_CURSOR_INFO` 结构和 `GetConsoleCursorInfo` 和 `SetConsoleCursorInfo` 函数来隐藏或显示光标。在Linux或Unix系统中,可以使用终端控制序列来隐藏光标,例如在C语言中可以使用 `printf("\e[?25l")` 来隐藏光标,使用 `printf("\e[?25h")` 来显示光标。
隐藏光标是一个简单的步骤,但它可以提高贪吃蛇这类文本模式游戏的总体质量和玩家的游戏体验。

GetStdHandle 函数(句柄函数)

函数的使用

1,获得句柄

炒菜需要菜->锅->锅柄

我们这个时候需要拿到锅柄

简单的说就是,这里有三个参数分别对照的是输入流,屏幕显示,控制台

我们需要的是屏幕的光标消失

所以我们在GetStdHandle 函数里面输入STD_OUTPUT_HANDLE参数

然后命名一个变量进行接收

//举例
HANDLE hOutput = NULL;
//获取标准输出的句柄(用来标识不同设备的数值)
hOutput = GetStdHandle(STD_OUTPUT_HANDLE);

GetConsoleCursorlnfo(获得光标信息)

此时我们拿到锅柄了,那么我们此时需要拿到锅,有锅才能炒菜

可以看到,我们的函数语法一个是指向句柄,一个是指向参数的指针

所以这里我们还需要讲解一下

CONSOLE_CURSOR_INFO 结构

因为GetConsoleCursorlnfo(获得光标信息)函数事情比较多,里面的参数一个是句柄,一个在这个结构体

我们可以看见这个结构体一个是游标占比一个是游标的可见性,我们这里是设置的游标的可见性,那么参数里面说的很清楚,设置游标的可见性,可见可以直接设置为TRUE

那么不可见就可以设置为FALSE

此时我们回到GetConsoleCursorlnfo(获得光标信息)函数,我们需要了解的是,GetConsoleCursorlnfo(获得光标信息)接收的参数一个是句柄信息,一个是存储游标信息的结构体,但是我们不能直接进行设置

因为GetConsoleCursorlnfo(获得光标信息)本身就是获取光标信息的函数,我们需要先获取光标信息,再设置

我们先上一部分代码

	//句柄
    //句柄 目的就像锅的把柄一样  可以这样 HANDLE houtpu = GetStdHandle(STD_OUTPUT_HANDLE); HANDLE 数值就是指针
	HANDLE houtpu = NULL;
	houtpu = GetStdHandle(STD_OUTPUT_HANDLE);

	//获得光标信息
	//拿到句柄之后,需要获得光标的信息,就像我们有了锅的把柄,然后得有锅才能炒菜,改变菜的属性,生->熟
	//GetConsoleCursorInfo控制台光标信息 第一个参数的光标信息,第二个参数的指针,指向CONSOLE CURSOR INFO的指针
	//创建一个CONSOLE CURSOR INFO的结构体,这个结构体是描述创建控制台光标信息的,创建并且初始化
	CONSOLE_CURSOR_INFO cureor_info = { 0 };
	GetConsoleCursorInfo(houtpu, &cureor_info);

	//隐藏控制台光标,把创建的结构体指针设置为不可见
	//dwSize,光标填充的字符单元格的百分比。 此值介于1到100之间。 光标外观会变化,范围从完全填充单元格到单元底部的水平线条。
	//则此成员为 TRUE。CursorInfo.bVisible=false;//隐藏控制台光标	
	//bVisible,游标的可见性。 如果光标可见,
	cureor_info.bVisible = false;

提醒一下,这里的结构体的设置是需要进行初始化的,并且设置可见性,是在获取光标信息下的,因为设置需要先获取才能设置,但是此时的设置还没有设置到控制台里面

也就是此时虽然我们已经设置了光标的属性是不可见,但是此时还是可见的

因为设置光标信息还有一个函数

SetConsoleCursorInfo(设置光标信息)

我们看一下语法信息

我们在windows官网可以查询到,设置光标信息函数的参数一个是句柄函数,一个是结构体的参数

所以我们的完整代码可以写为

	//句柄
    //句柄 目的就像锅的把柄一样  可以这样 HANDLE houtpu = GetStdHandle(STD_OUTPUT_HANDLE); HANDLE 数值就是指针
	HANDLE houtpu = NULL;
	houtpu = GetStdHandle(STD_OUTPUT_HANDLE);

	//获得光标信息
	//拿到句柄之后,需要获得光标的信息,就像我们有了锅的把柄,然后得有锅才能炒菜,改变菜的属性,生->熟
	//GetConsoleCursorInfo控制台光标信息 第一个参数的光标信息,第二个参数的指针,指向CONSOLE CURSOR INFO的指针
	//创建一个CONSOLE CURSOR INFO的结构体,这个结构体是描述创建控制台光标信息的,创建并且初始化
	CONSOLE_CURSOR_INFO cureor_info = { 0 };
	GetConsoleCursorInfo(houtpu, &cureor_info);

	//隐藏控制台光标,把创建的结构体指针设置为不可见
	//dwSize,光标填充的字符单元格的百分比。 此值介于1到100之间。 光标外观会变化,范围从完全填充单元格到单元底部的水平线条。
	//则此成员为 TRUE。CursorInfo.bVisible=false;//隐藏控制台光标	
	//bVisible,游标的可见性。 如果光标可见,
	cureor_info.bVisible = false;

	//设置光标信息
	//SetConsoleCursorInfo设置光标信息
	//参数
	//BOOL WINAPI SetConsoleCursorInfo(
	//	_In_       HANDLE              hConsoleOutput,
	//	_In_ const CONSOLE_CURSOR_INFO * lpConsoleCursorInfo
	//);
	SetConsoleCursorInfo(houtpu, &cureor_info);

封装一个隐藏光标属性的函数

//隐藏光标的信息
void set_hide()
{
	//句柄
    //句柄 目的就像锅的把柄一样  可以这样 HANDLE houtpu = GetStdHandle(STD_OUTPUT_HANDLE); HANDLE 数值就是指针
	HANDLE houtpu = NULL;
	houtpu = GetStdHandle(STD_OUTPUT_HANDLE);

	//获得光标信息
	//拿到句柄之后,需要获得光标的信息,就像我们有了锅的把柄,然后得有锅才能炒菜,改变菜的属性,生->熟
	//GetConsoleCursorInfo控制台光标信息 第一个参数的光标信息,第二个参数的指针,指向CONSOLE CURSOR INFO的指针
	//创建一个CONSOLE CURSOR INFO的结构体,这个结构体是描述创建控制台光标信息的,创建并且初始化
	CONSOLE_CURSOR_INFO cureor_info = { 0 };
	GetConsoleCursorInfo(houtpu, &cureor_info);

	//隐藏控制台光标,把创建的结构体指针设置为不可见
	//dwSize,光标填充的字符单元格的百分比。 此值介于1到100之间。 光标外观会变化,范围从完全填充单元格到单元底部的水平线条。
	//则此成员为 TRUE。CursorInfo.bVisible=false;//隐藏控制台光标	
	//bVisible,游标的可见性。 如果光标可见,
	cureor_info.bVisible = false;

	//设置光标信息
	//SetConsoleCursorInfo设置光标信息
	//参数
	//BOOL WINAPI SetConsoleCursorInfo(
	//	_In_       HANDLE              hConsoleOutput,
	//	_In_ const CONSOLE_CURSOR_INFO * lpConsoleCursorInfo
	//);
	SetConsoleCursorInfo(houtpu, &cureor_info);
}

此时可以发现,界面没有光标显示了

设置定位坐标

目的

在编程和游戏开发中,设置定位坐标的目的是为了确定对象在屏幕或游戏世界中的具体位置。坐标通常由一对数值表示,例如 (x, y),其中 x 表示水平位置,y 表示垂直位置。设置定位坐标的目的包括:


1. **精确控制对象位置**:

通过坐标,开发者可以精确地控制游戏对象(如角色、道具、障碍物等)在屏幕上的位置,从而创建精确的游戏场景和布局。


2. **运动和动画**:

在游戏或动画中,对象的移动是通过不断更新其坐标来实现的。通过改变坐标,可以模拟对象在屏幕上的移动、跳跃、飞翔等动作。


3. **用户交互**:

在用户界面设计中,坐标用于确定按钮、文本框、图标等元素的位置,以便用户能够与之交互。正确的坐标设置可以提升用户体验。


4. **碰撞检测**:

在游戏中,坐标用于检测和响应对象之间的碰撞。通过比较对象的坐标和大小,可以确定是否发生了碰撞,并据此触发相应的游戏逻辑。


5. **图形渲染**:

在图形编程中,坐标用于确定图形元素(如点、线、形状、图像等)的绘制位置。正确的坐标设置可以确保图形按照预期显示。


6. **空间关系和布局**:

坐标用于建立对象之间的空间关系和布局,这对于游戏设计和用户界面设计至关重要。


7. **寻路和导航**:

在需要寻路或导航的游戏中,坐标用于确定角色或物体的移动路径,以及它们如何在不同地点之间移动。


8. **模拟和仿真**:

在模拟和仿真程序中,坐标用于表示现实世界对象的位置,这对于模拟真实世界中的物理现象或过程非常重要。


总之,设置定位坐标是编程和游戏开发中的一个基本概念,它允许开发者创建动态、交互式和有组织的视觉体验。
 

坐标的概念

这里我们可以看见,控制台也是按照坐标系进行计算然后设置围墙和定位的

这里我想表达的是,我们可以看见,一个字符占据的是1x,1y

但是,y比x长

也就是2x=y

所以在之后打印里面,一个宽字符占据的是两个字符的大小

也就是占据2x,1y的大小

COORD 结构

这个结构就是一个很简单的定位位置的结构体,

	//定位光标的位置
	COORD pos1 = { x,y };

这里注意括号{ },不要写成【】,因为这里是结构体

SetConsoleCursorPosition (定位光标位置)

我们可以看到定位光标的位置还是比较简单的,只需要句柄参数和COOR参数,什么意思呢也就是,我们主需要获得句柄,定义一个结构体位置信息,就可以定位光标的位置

	//设置光标的位置,目的是可以让最后的提示按照想出现的位置进行出现
	//BOOL WINAPI SetConsoleCursorPosition(
	//	In HANDLE hConsoleOutput,//句柄
	//	In COORD  dwCursorPosition//位置
	//);

	//获得句柄
	HANDLE houtpu = NULL;
	houtpu = GetStdHandle(STD_OUTPUT_HANDLE);

	//定位光标的位置
	COORD pos1 = { x,y };
	SetConsoleCursorPosition(houtpu, pos1);

封装一个设置定位坐标

//定义光标的位置/定义位置
void set_pos(short x, short y)
{
	//设置光标的位置,目的是可以让最后的提示按照想出现的位置进行出现
	//BOOL WINAPI SetConsoleCursorPosition(
	//	In HANDLE hConsoleOutput,//句柄
	//	In COORD  dwCursorPosition//位置
	//);

	//获得句柄
	HANDLE houtpu = NULL;
	houtpu = GetStdHandle(STD_OUTPUT_HANDLE);

	//定位光标的位置
	COORD pos1 = { x,y };
	SetConsoleCursorPosition(houtpu, pos1);
}

这里我们可以看到我们可以把欢迎来到贪吃蛇小游戏的界面文字定义到控制台中间

请按任意键继续

这里就是 一个很简单的cmd命令

	system("pause");

只要在欢迎来到贪吃蛇小游戏,下面定位一个位置,并且使用这个cmd命令进行调用就可以

	//1.打印环境界面
	set_pos(40, 14);
	wprintf(L"欢迎来到贪吃蛇小游戏\n");
	//按任意键继续
	set_pos(40, 15);
	system("pause");

游戏的界面设置

wprintf

`wprintf` 是一个函数,用于在Windows操作系统中以宽字符(wchar_t)形式输出格式化的字符串到标准输出(通常是控制台或终端)。它是 `printf` 函数的宽字符版本,用于处理多字节字符集,特别是在需要支持Unicode字符的场合。
`wprintf` 函数原型定义在 `stdio.h` 头文件中,如下所示:
 

int wprintf(const wchar_t *format, ...);

参数 `format` 是一个宽字符字符串,它指定了输出的格式和内容。格式字符串可以包含普通字符和格式规范符,格式规范符用于指定后续参数的输出格式。`wprintf` 函数后面的参数列表 `...` 表示函数可以接受多个参数,这些参数将替换格式字符串中的格式规范符。
与 `printf` 函数相比,`wprintf` 使用宽字符 `wchar_t` 作为字符串和字符的存储单位,而 `printf` 使用单字节的 `char`。在UTF-16编码的Windows操作系统中,宽字符通常用于表示Unicode字符。
下面是一个使用 `wprintf` 函数的例子:


#include <stdio.h>
int main() {
    wchar_t *text = L"Hello, World!";
    int number = 42;
    // 使用wprintf输出宽字符字符串和整数
    wprintf(L"%ls: %d\n", text, number);
    return 0;
}

在这个例子中,`%ls` 是一个格式规范符,用于输出宽字符字符串(`wchar_t*`),`%d` 用于输出整数。注意,字符串前缀 `L` 表示这是一个宽字符字符串。
在使用 `wprintf` 时,需要确保编译器和运行环境支持宽字符处理。在Windows平台上,这通常不是问题,因为Windows API原生支持宽字符。在Linux和其他Unix-like系统上,你可能需要使用 iconv 或其他库来处理宽字符编码。

本地化 

本地化

提供的函数⽤于控制C标准库中对于不同的地区会产⽣不⼀样⾏为的部分。

在标准中,依赖地区的部分有以下⼏项:

• 数字量的格式

• 货币量的格式

• 字符集

• ⽇期和时间的表⽰形式

类项

通过修改地区,程序可以改变它的⾏为来适应世界的不同区域。但地区的改变可能会影响库的许多部

分,其中⼀部分可能是我们不希望修改的。所以C语⾔⽀持针对不同的类项进⾏修改,下⾯的⼀个宏,

指定⼀个类项:

• LC_COLLATE:影响字符串⽐较函数 strcoll() 和 strxfrm() 。

• LC_CTYPE:影响字符处理函数的⾏为。

• LC_MONETARY:影响货币格式。

• LC_NUMERIC:影响 printf() 的数字格式。

• LC_TIME:影响时间格式 strftime() 和 wcsftime() 。

• LC_ALL - 针对所有类项修改,将以上所有类别设置为给定的语⾔环境。

封装本地化函数,在主函数里面进行调用

//设置当前地区属性
void snake_setlocale()
{
	//设置当前所在地区的模 char* setlocale (int category, const char* locale);式
	//• LC_COLLATE:影响字符串⽐较函数 strcoll() 和 strxfrm() 。
	//	• LC_CTYPE:影响字符处理函数的⾏为。
	//	• LC_MONETARY:影响货币格式。
	//	• LC_NUMERIC:影响 printf() 的数字格式。
	//	• LC_TIME:影响时间格式 strftime() 和 wcsftime() 。
	//	• LC_ALL - 针对所有类项修改,将以上所有类别设置为给定的语⾔环境。
	//不设置贪吃蛇本地属性,wprintf就不能打印出来

	setlocale(LC_ALL, "");
}

打印游戏的运行界面

//打印环境界面
void Welcome_To_Gmae()
{
	//1.打印环境界面
	set_pos(40, 14);
	wprintf(L"欢迎来到贪吃蛇小游戏\n");
	//按任意键继续
	set_pos(40, 15);
	system("pause");
	//清屏
	system("cls");


	set_pos(40, 14);
	wprintf(L"不能穿墙,可以咬到自己\n");
	set_pos(40, 15);
	wprintf(L"用 ↑. ↓. ←. →. 来控制蛇的移动\n");
	set_pos(40, 16);
	wprintf(L"按F3加速,F4减速\n");

	set_pos(40, 17);
	wprintf(L"加速可以获得更高的分数\n");

	set_pos(40, 18);
	wprintf(L"ESC :退出游戏.space(空格):暂停游戏.\n");

	set_pos(40, 19);
	wprintf(L"每个食物得分:20分\n");
	
	set_pos(90, 28);
	wprintf(L"精心制作\n");
	set_pos(90, 29);
	system("pause");
	system("cls");
}

这里就是采取定位,定位之后进行打印

看一下效果图

绘制地图

//这里是我们子啊头文件定义的围墙,方便我们随时更换围墙
#define WALL L'□'

//实现文件 3.绘制地图
void SnakeMap()
{
	//上
	for (int i = 0; i < 29; i++)
	{
		wprintf(L"%lc", WALL);
	}

	//下
	set_pos(0, 26);
	for (int i = 0; i < 29; i++)
	{
		wprintf(L"%lc", WALL);
	}
	//左
	for (int i = 0; i < 26; i++)
	{
		set_pos(0, i);
		wprintf(L"%lc", WALL);
	}
	//右
	for (int i = 0; i < 26; i++)
	{
		set_pos(56, i);
		wprintf(L"%lc", WALL);
	}

}

这里还是不难的,简单的说就是进行定位,然后进行打印地图,当然此时是蛇还是没有的


绘制地图的逻辑解释:
1. **绘制上边**:
   - 第一个 `for` 循环从 `i = 0` 开始,到 `i < 29` 结束,循环次数为29次。打印了29个围墙
   - 在每次循环中,使用 `wprintf` 函数打印一个宽字符 `WALL`,这代表地图的上边。
   - 因为循环体的作用是打印字符,所以循环结束后,会在当前行打印一排墙。
2. **绘制下边**:
   - `set_pos(0, 26);` 调用了一个假设的 `set_pos` 函数(这个函数没有在代码中定义),目的是将光标移动到第0列第26行的位置,这样下一个打印的字符就会出现在地图的底部。
   - 接下来的 `for` 循环与绘制上边类似,也是循环29次,打印一排墙,作为地图的下边。
3. **绘制左边**:
   - 第三个 `for` 循环从 `i = 0` 开始,到 `i < 26` 结束,循环次数为26次。
   - 在每次循环中,首先使用 `set_pos(0, i);` 将光标移动到第0列的第 `i` 行。
   - 然后打印一个宽字符 `WALL`,这代表地图的左边。
4. **绘制右边**
   - 第四个 `for` 循环与绘制左边类似,也是循环26次。
   - 在每次循环中,使用 `set_pos(56, i);` 将光标移动到第56列的第 `i` 行。
   - 然后打印一个宽字符 `WALL`,这代表地图的右边。
综合以上步骤,`SnakeMap` 函数会绘制一个由墙壁包围的地图,地图的宽度为58个字符(左右各一个边界,加上中间的56个空位),高度为28个字符(上下各一个边界,加上中间的26个空位)。这个地图是贪吃蛇游戏的基本框架,贪吃蛇在这个区域内移动,并且不能穿过墙壁。

效果图
 

补充说明

最终围墙的大小是偶数位的

图的尺寸和位置设置是基于偶数设计的。这是因为在某些文本模式下,字符单元格的宽度和高度可能不相等,特别是在使用宽字符(例如UTF-16编码的字符)时。在这种情况下,宽字符可能占据两个标准字符单元格的宽度,这就可能导致显示问题时字符单元格不成对出现。

以下是一些可能的原因:

  1. 宽字符的显示

    • 在宽字符模式下,每个宽字符可能需要两个字符单元格来显示。因此,如果地图的尺寸不是偶数,那么在绘制地图边界时可能会出现字符错位的问题。
  2. 光标定位的准确性

    • 在文本模式下,光标定位通常是基于字符单元格的。如果地图的尺寸是奇数,那么在尝试将光标定位到地图的角落或边缘时,可能会出现光标定位不准确的情况。
  3. 对称性

    • 使用偶数尺寸的地图可以确保地图在水平和垂直方向上都具有对称性,这有助于简化绘制逻辑和游戏逻辑。
  4. 边界条件

    • 在游戏逻辑中,处理边界条件(例如,蛇撞墙)可能会更加简单,如果地图的尺寸是偶数,那么判断蛇是否撞墙的算法可能会更直接。
  5. 蛇和食物:

    蛇的每个节点的x坐标必须是2个倍数,否则可能会出现蛇的⼀个节点有⼀半⼉出现在墙体中,另外⼀般在墙外的现象,坐标不好对⻬

总之,使用偶数尺寸的地图可能是为了确保显示的一致性和游戏逻辑的简化。然而,这并不是一个硬性规定,只是根据具体情况做出的设计选择。也可以使用奇数尺寸。

接下来我们马上步入正题,蛇的初始化

蛇的初始化 

创建蛇的节点(头文件实现)

//创建节点
typedef struct Snake_Snakenode
{
	int x;
	int y;
	struct Snake_Snakenode* next;
}Snake_Snakenode, * pSnake_Snakenode;

蛇的本质就是链表

创建蛇的状态(头文件)

//创建蛇的状态
//方向,头节点,状态,食物,一个食物的分数,时间,得分情况
typedef struct Snake_State
{
	pSnake_Snakenode _SNAKE_HEAD;//蛇的头节点
	pSnake_Snakenode _SNAKE_FOOD;//蛇的食物
	enum _STATE _SNAKE_STATE;//蛇的状态
	enum _DIR _SNAKE_DIR;//蛇的方向
	int _SNAKE_SCORE;//总成绩
	int _SNAKE_SLEEP_TIME;//蛇中间运动的时候的休息时间
	int _SNAKE_ONE_FOOD;//一个食物的分数
}Snake_State, * pSnake_State;

这里蛇需要有几个状态

方向,头节点,状态,食物,一个食物的分数,时间,得分情况

创建蛇绘制在地图(实现文件)

//创建蛇
void Snake_Create(pSnake_State ps)
{
	//初始蛇是五个节点
	pSnake_Snakenode cur = NULL;
	for (int i = 0; i < 5; i++)
	{
		//创建节点
		cur = (pSnake_Snakenode)malloc(sizeof(Snake_Snakenode));
		if (cur == NULL)
		{
			perror("Snake_Create:pSnake_Snakenode newnode:error:");
			return;
		}
		//进行定位
		cur->next = NULL;
		cur->x= POS_X + i * 2;
		cur->y = POS_Y;

		//进行头插
		if (ps->_SNAKE_HEAD == NULL)
		{
			ps->_SNAKE_HEAD = cur;
		}
		else
		{
			cur->next = ps->_SNAKE_HEAD;
			ps->_SNAKE_HEAD = cur;
		}
	}

	//进行打印
	cur = ps->_SNAKE_HEAD;
	while (cur)
	{
		Snake_Cursor_Position(cur->x, cur->y);
		wprintf(L"%lc", BOOY);
		cur = cur->next;
	}

	//初始化蛇的状态
	ps->_SNAKE_STATE = OK;//状态
	ps->_SNAKE_SLEEP_TIME = 200;//休息时间
	ps->_SNAKE_SCORE = 0;//总成绩
	ps->_SNAKE_ONE_FOOD = 20;//一个食物的分数
	ps->_SNAKE_DIR = RIGHT;//蛇的方向
}

这段代码是一个创建蛇的函数,用于初始化一个蛇形游戏中的蛇的状态和位置。在这个函数中,蛇被表示为一个链表,每个节点包含蛇的一段的位置信息。
下面是代码的解释:


1. `Snake_Create` 函数接受一个指向 `Snake_State` 结构的指针 `ps`,这个结构可能包含了蛇的状态信息,如头节点、状态、休息时间、得分等。


2. 在 `for` 循环中,代码创建了五个节点,每个节点代表蛇的一段。每个节点的 `x` 坐标从 `POS_X` 开始,每次增加 2,`y` 坐标保持为 `POS_Y`。


3. `malloc` 用于动态分配内存,创建一个新的蛇节点。如果分配失败,`malloc` 会返回 `NULL`,此时使用 `perror` 打印错误信息并返回。


4. 每个新创建的节点被插入到链表的头部,形成一个新的蛇。这是通过头插法实现的,新节点成为新的头节点,其 `next` 指针指向原来的头节点。


5. 在蛇的节点创建完成后,代码使用一个循环遍历蛇的每个节点,并使用 `Snake_Cursor_Position` 函数将光标定位到蛇的每个节点的位置,并打印出蛇的图形(可能是字符 `BOOY`)。


6. 最后,代码初始化蛇的状态,如设置蛇的状态为 `OK`,设置蛇的休息时间为 200 毫秒,设置蛇的得分为 0,设置一个食物的分数为 20,设置蛇的初始方向为 `RIGHT`。


这个函数是蛇形游戏初始化阶段的一部分,用于设置蛇的初始状态。在实际的游戏中,蛇会根据用户的输入或其他游戏逻辑进行移动,可能会吃到食物并增长,也可能会遇到边界或自身而游戏结束。
 

创建食物(实现文件)

//创建食物
void Snake_food(pSnake_State ps)
{
	int x = 0; 
	int y = 0;

again:
	do
	{
		x = rand() % 74 + 2;
		y = rand() % 28 + 1;
	} while (x % 2 != 0);
	
	//判断是不是和身体重合
	pSnake_Snakenode headcur = ps->_SNAKE_HEAD;
	while (headcur)
	{
		if (x == headcur->x && y == headcur->y)
		{
			goto again;
		}
		headcur = headcur->next;
	}

	//创建食物
	//pSnake_Snakenode curfood = (pSnake_Snakenode)malloc(sizeof(Snake_Snakenode));
	pSnake_Snakenode curfood = NULL;//必须初始化
	curfood = (pSnake_Snakenode)malloc(sizeof(Snake_Snakenode));
	if (curfood == NULL)
	{
		perror("pSnake_Snakenode curfood:error:");
		return;
	}
	curfood->next = NULL;
	curfood->x = x;
	curfood->y = y;

	Snake_Cursor_Position(x, y);
	wprintf(L"%lc", FOOD);
	
	//创建的食物放到蛇的状态里面去
	ps->_SNAKE_FOOD = curfood;
}

这段代码定义了一个函数 `Snake_food`,其目的是在蛇形游戏中创建食物。食物在游戏中的作用通常是让蛇去吃,从而增长蛇的长度,增加游戏的得分。
下面是代码的解释:


1. `Snake_food` 函数接受一个指向 `Snake_State` 结构的指针 `ps`,这个结构可能包含了蛇的状态信息,如头节点、状态、休息时间、得分等。


2. 使用 `rand()` 函数生成随机坐标 `x` 和 `y` 来放置食物。食物的位置需要在游戏界面的范围内,并且 `x` 坐标是偶数,这可能是为了与蛇的节点的坐标对齐。


3. 使用一个 `do-while` 循环来确保 `x` 坐标是偶数。如果生成的 `x` 是奇数,循环会再次执行,直到生成一个偶数。


4. 使用一个 `while` 循环遍历蛇的每个节点,检查新生成的食物坐标是否与蛇的身体重合。如果重合,使用 `goto` 语句跳转到 `again` 标签,重新生成食物的坐标。


5. 分配内存创建一个食物节点 `curfood`。如果内存分配失败,使用 `perror` 打印错误信息并返回。


6. 将食物的坐标设置为生成的 `x` 和 `y`,并将其 `next` 指针设置为 `NULL`。


7. 使用 `Snake_Cursor_Position` 函数将光标定位到食物的位置,并打印出食物的图形(可能是字符 `FOOD`)。


8. 将创建的食物节点存储在 `Snake_State` 结构的 `_SNAKE_FOOD` 成员中,这样蛇就可以在游戏中吃掉这个食物了。


这个函数是蛇形游戏的一部分,用于在游戏界面中随机位置生成食物。在实际的游戏中,这个函数可能会被周期性地调用,以在蛇吃掉食物后生成新的食物。
 

游戏的运行逻辑

随机生成食物

随机生成食物其实本质上还是坐标是生成,生成坐标就需要创建节点

但是需要知道的一点是我们创建的蛇是偶数位置的,所以我们吃的食物也得是偶数位置的,但是,

1,我们这里是食物创建是随机数值,我们不知道实物创建是偶数还是奇数,所以我们需要给一个循环

2,我们创建的食物还不能和身体进行重合(否则就进行重新创建)

3,创建的区间应该是围墙里面,不能是围墙外面(否则就进行重新创建)

4,这些条件都满足之后,我们把做坐标放到节点里面

5,最后进行定位和打印

//创建食物
void Snake_food(pSnake_State ps)
{
	int x = 0; 
	int y = 0;

again:
	do
	{
		x = rand() % 74 + 2;
		y = rand() % 28 + 1;
	} while (x % 2 != 0);
	
	//判断是不是和身体重合
	pSnake_Snakenode headcur = ps->_SNAKE_HEAD;
	while (headcur)
	{
		if (x == headcur->x && y == headcur->y)
		{
			goto again;
		}
		headcur = headcur->next;
	}

	//创建食物
	//pSnake_Snakenode curfood = (pSnake_Snakenode)malloc(sizeof(Snake_Snakenode));
	pSnake_Snakenode curfood = NULL;//必须初始化
	curfood = (pSnake_Snakenode)malloc(sizeof(Snake_Snakenode));
	if (curfood == NULL)
	{
		perror("pSnake_Snakenode curfood:error:");
		return;
	}
	curfood->next = NULL;
	curfood->x = x;
	curfood->y = y;

	Snake_Cursor_Position(x, y);
	wprintf(L"%lc", FOOD);
	
	//创建的食物放到蛇的状态里面去
	ps->_SNAKE_FOOD = curfood;
}

这段代码是用于在一个蛇游戏中创建食物的功能。以下是代码的详细解释:


1. `void Snake_food(pSnake_State ps)`:这是一个函数声明,意味着这个函数接受一个指向 `Snake_State` 结构体的指针作为参数,但没有返回值。


2. `int x = 0; int y = 0;`:定义了两个整数变量 `x` 和 `y`,用来存储食物的位置。


3. `again:`:这是一个标签,用于 `goto` 语句,如果食物位置不合理,会跳转到这个标签重新生成食物位置。


4. `do { ... } while (x % 2 != 0);`:这是一个 `do-while` 循环,首先执行循环体一次,然后检查条件 `x % 2 != 0`。如果条件为真,继续执行循环体;如果为假,退出循环。循环的目的是确保 `x` 是一个偶数,因为代码中提到“while (x % 2 != 0)”,但实际上蛇的食物位置应该在网格的交点上,即 x 和 y 坐标都应该是一个偶数(如果是从 0 开始的话)


5. `pSnake_Snakenode headcur = ps->_SNAKE_HEAD;`:定义了一个指向蛇头节点的指针 `headcur`,用于在蛇的身体中遍历,以检查新产生的食物位置是否与蛇的身体重合。


6. `while (headcur)`:这是一个循环,只要 `headcur` 不是 `NULL`,就会继续执行。

7. `if (x == headcur->x && y == headcur->y)`:检查当前食物位置 `(x, y)` 是否与蛇的头部位置相同。

8. `goto again;`:如果食物位置与蛇头位置相同,跳转到 `again` 标签,重新生成食物位置。

9. `curfood = (pSnake_Snakenode)malloc(sizeof(Snake_Snakenode));`:动态分配一个 `Snake_Snakenode` 类型大小的内存块,并将地址强制转换为 `pSnake_Snakenode` 类型,然后将其赋值给 `curfood` 指针。

10. `if (curfood == NULL)`:检查 `malloc` 是否分配成功。如果失败,打印错误信息并返回。

11. `curfood->next = NULL;`:初始化新分配的节点,使其 `next` 指针为 `NULL`。

12. `curfood->x = x;`:将食物节点的 `x` 坐标设置为新产生的食物位置 `x`。

13. `curfood->y = y;`:将食物节点的 `y` 坐标设置为新产生的食物位置 `y`。

14. `Snake_Cursor_Position(x, y);`:这个函数可能是用来将光标移动到食物位置的函数,但在这段代码中没有给出定义。

15. `wprintf(L"%lc", FOOD);`:在控制台上输出食物字符。`wprintf` 是用于宽字符的输出函数,`%lc` 用于输出一个宽字符。`FOOD` 应该是一个定义了食物字符的常量。

16. `ps->_SNAKE_FOOD = curfood;`:将新产生的食物节点赋值给 `Snake_State` 结构体中的 `_SNAKE_FOOD` 字段,这样食物就保存在蛇的状态中,可以在游戏中使用。

虚拟键代码

虚拟键码 (Winuser.h) - Win32 apps | Microsoft Learn

在Windows操作系统中,虚拟键代码(Virtual-Key Codes)是一组用来表示键盘上按键的数值。这些代码通常用于Windows API函数,以便程序能够识别和处理键盘输入。
虚拟键代码是按键的物理位置定义的,而不是按字符本身定义的。这意味着,无论用户的键盘布局如何,同一物理按键的虚拟键代码都是相同的。例如,无论用户使用的是美式键盘、德式键盘还是日式键盘,字母 'A' 键的虚拟键代码都是 `VK_A`。
虚拟键代码通常以 `VK_` 开头,后面跟着一个或多个大写字母,表示特定的按键。例如:
- `VK_A` 表示 'A' 键。
- `VK_B` 表示 'B' 键。
- `VK_SPACE` 表示空格键。
- `VK_RETURN` 表示回车键。
- `VK_ESCAPE` 表示 ESC 键。
- `VK_UP` 表示向上箭头键。
- `VK_DOWN` 表示向下箭头键。
- `VK_LEFT` 表示向左箭头键。
- `VK_RIGHT` 表示向右箭头键。
- `VK_F1` 到 `VK_F12` 表示功能键 F1 到 F12。
在程序中,可以通过 `GetAsyncKeyState`、`GetKeyState` 或 `TranslateMessage` 等Windows API函数来检测虚拟键代码,从而判断用户是否按下了某个键。
 

if (KEY_PRESS(VK_UP) && ps->_SNAKE_DIR != DOWN)
{
    ps->_SNAKE_DIR = UP;
}

这里的 `KEY_PRESS(VK_UP)` 是一个宏或函数,用来检查用户是否按下了向上箭头键(`VK_UP`)。如果按下了,并且蛇当前的方向不是向下(`DOWN`),则将蛇的方向设置为向上(`UP`)。
这些虚拟键代码使得开发者能够以一种与键盘布局无关的方式处理键盘输入,从而编写出更加通用和易于移植的软件。
简单的说虚拟键代码的目的就是,进行识别是否按键

封装一个宏

我们封装一个宏,进行虚拟键代码的判断

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

宏 `KEY_PRESS(vk)` 是一个用于检测指定虚拟键 `vk` 是否被按下的辅助宏。它使用了 `GetAsyncKeyState` 函数,这是Windows API中的一个函数,用于检查异步键盘输入的状态。


下面是宏 `KEY_PRESS(vk)` 的详细解释:
- `GetAsyncKeyState(vk)`:这个函数接受一个虚拟键代码 `vk` 作为参数,并返回该键的异步键状态。返回值是一个16位的整数,其中第15位(从最低位开始的最高位)表示该键是否被按下。如果该位为1,则键被按下;如果为0,则键没有按下。


- `&1`:这是一个位运算符,用于对 `GetAsyncKeyState(vk)` 的返回值进行按位与操作。这里 `1` 表示二进制的最低位为1,其他位为0。因此,`&1` 操作实际上是在检查

`GetAsyncKeyState(vk)` 返回值的第15位是否为1。


- `? :`:这是C语言中的三元条件运算符。如果 `GetAsyncKeyState(vk)&1` 的结果为真(非零),则表达式的值为 `1`,表示键被按下;如果为假(零),则表达式的值为 `0`,表示键没有被按下。


综上所述,`KEY_PRESS(vk)` 宏的目的是简化对 `GetAsyncKeyState` 函数的调用,并返回一个布尔值(0或1),指示指定的虚拟键 `vk` 是否被按下。在您的代码中,这个宏被用于检测用户的键盘输入,并根据这些输入来更新蛇的方向或游戏状态。
 

按键信息(实现文件)

//蛇的运行逻辑
void Snake_RunGame(pSnake_State ps)
{
	do
	{
		//打印分数
		Snake_Cursor_Position(90, 20);
		wprintf(L"当前的得分是:%d", ps->_SNAKE_SCORE);

		Snake_Cursor_Position(90, 24);
		wprintf(L"一个食物的分数是:%d", ps->_SNAKE_ONE_FOOD);
		if (KEY_PRESS(VK_UP) && ps->_SNAKE_DIR != DOWN)
		{
			ps->_SNAKE_DIR = UP;
		}
		else if (KEY_PRESS(VK_DOWN) && ps->_SNAKE_DIR != UP)
		{
			ps->_SNAKE_DIR = DOWN;
		}
		else if (KEY_PRESS(VK_LEFT) && ps->_SNAKE_DIR != RIGHT)
		{
			ps->_SNAKE_DIR = LEFT;
		}
		else if (KEY_PRESS(VK_RIGHT) && ps->_SNAKE_DIR != LEFT)
		{
			ps->_SNAKE_DIR = RIGHT;
		}
		if (KEY_PRESS(VK_ESCAPE))//退出
		{
			ps->_SNAKE_STATE = KILL_ESE;
		}
		if (KEY_PRESS(VK_F4))//减速
		{
			if (ps->_SNAKE_ONE_FOOD > 10)
			{
				ps->_SNAKE_SLEEP_TIME += 30;
				ps->_SNAKE_ONE_FOOD = ps->_SNAKE_ONE_FOOD - 5;
			}
		}
		if (KEY_PRESS(VK_F3))//加速
		{
			if (ps->_SNAKE_ONE_FOOD < 45)
			{
				ps->_SNAKE_SLEEP_TIME -= 30;
				ps->_SNAKE_ONE_FOOD = ps->_SNAKE_ONE_FOOD + 5;
			}
		}
		if (KEY_PRESS(VK_SPACE))//暂停
		{
			SNAKEVK_SPACE();
		}
		//蛇的移动
		Snake_move(ps);
		//蛇的每次移动的休眠时间
		Sleep(ps->_SNAKE_SLEEP_TIME);

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

这段代码定义了一个函数 `Snake_RunGame`,它实现了蛇形游戏的主要运行逻辑。这个函数包含了蛇的移动、用户输入的检测、得分的更新以及游戏速度的控制。
下面是代码的解释:


1. `Snake_RunGame` 函数接受一个指向 `Snake_State` 结构的指针 `ps`,这个结构包含了蛇的状态信息,如头节点、状态、休息时间、得分等。


2. 函数使用一个 `do-while` 循环来持续运行游戏,直到蛇的状态不再是 `OK`(例如,蛇撞墙或吃到自己时)。


3. 在循环内部,首先使用 `Snake_Cursor_Position` 和 `wprintf` 函数打印当前得分和一个食物的分数。


4. 接下来,使用 `KEY_PRESS` 宏(或函数)来检测用户的键盘输入。根据用户按下的键(上、下、左、右箭头键),更新蛇的方向。如果蛇当前不是朝相反方向移动,蛇的方向会被更新。


5. 如果用户按下 `ESC` 键,游戏状态被设置为 `KILL_ESE`,表示游戏结束。


6. 如果用户按下 `F4` 键,游戏速度会减慢,蛇的休息时间增加,同时每个食物的分数减少。


7. 如果用户按下 `F3` 键,游戏速度会加快,蛇的休息时间减少,同时每个食物的分数增加。


8. 如果用户按下空格键,游戏会暂停。这可能是通过调用一个名为 `SNAKEVK_SPACE` 的函数实现的,但这个函数在提供的代码片段中没有定义。


9. `Snake_move` 函数被调用来处理蛇的移动逻辑。这个函数可能会更新蛇的位置,检查是否有食物被吃掉,以及蛇是否撞墙或吃到自己。


10. `Sleep` 函数用于控制游戏的更新速度,根据 `ps->_SNAKE_SLEEP_TIME` 的值来暂停一段时间。


11. `do-while` 循环的条件是蛇的状态为 `OK`,如果蛇的状态变为其他值(如 `KILL_ESE`),循环结束,游戏结束。


这个函数是蛇形游戏的核心部分,它负责处理游戏的主要逻辑和用户交互。在实际的游戏中,`Snake_move` 函数的实现细节对于游戏的行为至关重要。
 

这里代码有些函数是写完直接进行调用的,下面我会一一进行解释

蛇的运行逻辑(实现代码(主体逻辑))

//蛇的运行逻辑
void Snake_RunGame(pSnake_State ps)
{
	do
	{
		//打印分数
		Snake_Cursor_Position(90, 20);
		wprintf(L"当前的得分是:%d", ps->_SNAKE_SCORE);

		Snake_Cursor_Position(90, 24);
		wprintf(L"一个食物的分数是:%d", ps->_SNAKE_ONE_FOOD);
		if (KEY_PRESS(VK_UP) && ps->_SNAKE_DIR != DOWN)
		{
			ps->_SNAKE_DIR = UP;
		}
		else if (KEY_PRESS(VK_DOWN) && ps->_SNAKE_DIR != UP)
		{
			ps->_SNAKE_DIR = DOWN;
		}
		else if (KEY_PRESS(VK_LEFT) && ps->_SNAKE_DIR != RIGHT)
		{
			ps->_SNAKE_DIR = LEFT;
		}
		else if (KEY_PRESS(VK_RIGHT) && ps->_SNAKE_DIR != LEFT)
		{
			ps->_SNAKE_DIR = RIGHT;
		}
		if (KEY_PRESS(VK_ESCAPE))//退出
		{
			ps->_SNAKE_STATE = KILL_ESE;
		}
		if (KEY_PRESS(VK_F4))//减速
		{
			if (ps->_SNAKE_ONE_FOOD > 10)
			{
				ps->_SNAKE_SLEEP_TIME += 30;
				ps->_SNAKE_ONE_FOOD = ps->_SNAKE_ONE_FOOD - 5;
			}
		}
		if (KEY_PRESS(VK_F3))//加速
		{
			if (ps->_SNAKE_ONE_FOOD < 45)
			{
				ps->_SNAKE_SLEEP_TIME -= 30;
				ps->_SNAKE_ONE_FOOD = ps->_SNAKE_ONE_FOOD + 5;
			}
		}
		if (KEY_PRESS(VK_SPACE))//暂停
		{
			SNAKEVK_SPACE();
		}
		//蛇的移动
		Snake_move(ps);
		//蛇的每次移动的休眠时间
		Sleep(ps->_SNAKE_SLEEP_TIME);

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

 蛇的暂停函数(实现文件)

//暂停
void SNAKEVK_SPACE()
{
	while (1)
	{
		if (KEY_PRESS(VK_SPACE))
		{
			break;
		}
		Sleep(200);
	}
}

`SNAKEVK_SPACE` 函数是一个用于处理游戏暂停逻辑的函数。当游戏处于暂停状态时,这个函数会等待用户按下空格键(`VK_SPACE`)来恢复游戏。


下面是 `SNAKEVK_SPACE` 函数的详细解释:


1. 函数内部有一个 `while (1)` 无限循环,这意味着循环会一直执行,直到遇到 `break` 语句。


2. 在循环内部,使用 `KEY_PRESS` 宏来检查空格键是否被按下。如果 `KEY_PRESS(VK_SPACE)` 返回 `true`(即空格键被按下),则执行 `break` 语句,退出循环。


3. 如果空格键没有被按下,循环会继续执行,并且在每次迭代中使用 `Sleep(200)` 来暂停200毫秒。这样可以减少循环的执行频率,避免占用过多的CPU资源。


4. 当用户按下空格键时,`SNAKEVK_SPACE` 函数会退出循环,从而允许游戏逻辑继续执行,游戏恢复。


这个函数通常会在游戏的主循环中调用,当检测到用户按下了空格键时,主循环会调用 `SNAKEVK_SPACE` 函数,游戏进入暂停状态。当用户再次按下空格键时,游戏会从暂停状态恢复。

下面会实现

蛇的移动函数Snake_move​​​​​​​

蛇的移动(实现文件)

//蛇的移动
void Snake_move(pSnake_State ps)
{
	//创建的下一个节点
	pSnake_Snakenode newnode = (pSnake_Snakenode)malloc(sizeof(Snake_Snakenode));
	if (newnode == NULL)
	{
		perror("Snake_move:newnode:error:");
		return;
	}
	switch (ps->_SNAKE_DIR)
	{
	case UP:
		newnode->x = ps->_SNAKE_HEAD->x;
		newnode->y = ps->_SNAKE_HEAD->y - 1;
		break;
	case DOWN:
		newnode->x = ps->_SNAKE_HEAD->x;
		newnode->y = ps->_SNAKE_HEAD->y + 1;
		break;
	case RIGHT:
		newnode->x = ps->_SNAKE_HEAD->x + 2;
		newnode->y = ps->_SNAKE_HEAD->y;
		break;
	case LEFT:
		newnode->x = ps->_SNAKE_HEAD->x - 2;
		newnode->y = ps->_SNAKE_HEAD->y;
		break;
	}
	//判断下一个节点是不是食物
	if (IsFood(ps, newnode))//如果是食物
	{
		//吃掉食物,并且创建食物
		EatFood(ps, newnode);
		newnode = NULL;
	}
	else//如果不是食物
	{
		//没有遇见食物,正常移动
		NoFood(ps, newnode);
		newnode = NULL;
	}
	//不能咬到自己
	Kill_myself(ps);

	//不能撞墙
	Kill_wall(ps);
}

`Snake_move` 函数是蛇形游戏中的核心函数之一,负责处理蛇的移动逻辑。这个函数根据蛇的方向来计算蛇的新头部位置,并检查这个位置是否是食物、是否撞到自己或墙壁。
下面是 `Snake_move` 函数的详细解释:


1. 首先,函数分配一个新节点 `newnode`,这个节点将代表蛇移动后的新头部。


2. 使用一个 `switch` 语句根据蛇的方向 `ps->_SNAKE_DIR` 来计算新节点的 `x` 和 `y` 坐标。例如,如果蛇向上移动,则 `y` 坐标减1;如果蛇向右移动,则 `x` 坐标加2(假设蛇的每个节点占两个单位宽度)。


3. `IsFood` 函数被调用来检查新节点是否位于食物的位置。如果是食物,则调用 `EatFood` 函数来处理吃食物的逻辑,这通常包括增加蛇的长度和分数,以及重新生成食物。


4. 如果新节点不是食物,则调用 `NoFood` 函数来处理正常移动的逻辑,这通常包括在蛇的头部添加新节点,并删除蛇的尾部节点。


5. `Kill_myself` 函数被调用来检查蛇是否咬到了自己。如果蛇的头部与身体的任何部分重合,游戏通常会结束。


6. `Kill_wall` 函数被调用来检查蛇是否撞到了墙壁。如果蛇的头部超出了游戏界面的边界,游戏也通常会结束。


7. 在函数的最后,如果新节点 `newnode` 被分配了内存,它会被设置为 `NULL`,以便在函数结束时释放内存。


这个函数是蛇形游戏逻辑的关键部分,它确保了蛇的移动符合游戏规则,并且正确地处理了各种游戏状态的变化。
 

判断下一个节点是不是食物(吃掉或者移动)(实现文件)

这里只有不是食物的时候会进行移动,是食物的话进行头插,

头插的原因是食物在身体前面

当然这些总体在循环里面

这里判断的思路是

1,首先判断下一个节点是不是食物,我们需要去结构体里面,食物阶段去寻找,对比坐标是不是一样的

2,是食物的时候,我们需要进行头插,此时需要注意的一点是,食物是一个节点,传递过来的节点也就是蛇移动的下一个方块也是一个节点,所以我们需要进行头插食物,释放传递过来的节点。然后打印这个蛇

3,不是食物的时候我们需要进行移动,并且释放最后一个节点,并且在尾部打印空格,进行覆盖尾部,防止形成拖尾

//判断下一个节点是不是食物
int IsFood(pSnake_State ps, pSnake_Snakenode newnode)
{
	return (ps->_SNAKE_FOOD->x == newnode->x && ps->_SNAKE_FOOD->y == newnode->y);
}

//下一个节点是食物,吃掉食物
void EatFood(pSnake_State ps, pSnake_Snakenode newnode)
{
	//头插
	ps->_SNAKE_FOOD->next = ps->_SNAKE_HEAD;
	ps->_SNAKE_HEAD = ps->_SNAKE_FOOD;

	//释放节点
	free(newnode);
	newnode = NULL;

	//打印蛇
	pSnake_Snakenode cur = ps->_SNAKE_HEAD;
	while (cur)
	{
		Snake_Cursor_Position(cur->x, cur->y);
		wprintf(L"%lc", BOOY);
		cur = cur->next;
	}
	//得分
	ps->_SNAKE_SCORE += ps->_SNAKE_ONE_FOOD;

	//再次创建食物
	Snake_food(ps);

}

//没有遇见食物,正常移动
void NoFood(pSnake_State ps, pSnake_Snakenode newnode)
{
	//没有遇见食物,需要头插空节点,释放最后一个节点
	newnode->next = ps->_SNAKE_HEAD;
	ps->_SNAKE_HEAD = newnode;

	//打印蛇
	pSnake_Snakenode cur = ps->_SNAKE_HEAD;
	while (cur->next->next)
	{
		Snake_Cursor_Position(cur->x, cur->y);
		wprintf(L"%lc", BOOY);
		cur = cur->next;
	}

	//最后一个空格打印成空
	Snake_Cursor_Position(cur->next->x, cur->next->y);
	wprintf(L"  ");

	//释放最后一个节点(节点不释放还是产生拖尾效果)
	free(cur->next);
	cur->next = NULL;

}

这段代码是用于处理蛇类游戏中的食物逻辑,其中包含三个函数,分别是判断下一个节点是否为食物、吃掉食物时的处理以及没有遇见食物时的正常移动处理。下面是这些函数的中文解释:
1. `IsFood` 函数:这个函数用来判断蛇即将移动到的下一个节点是否是食物。它通过比较食物节点的坐标(`ps->_SNAKE_FOOD->x` 和 `ps->_SNAKE_FOOD->y`)与新节点的坐标(`newnode->x` 和 `newnode->y`)是否相等来判断。如果相等,说明下一个节点是食物,函数返回 `TRUE`;否则返回 `FALSE`。


2. `EatFood` 函数:当蛇吃到食物时,这个函数会被调用。它执行以下操作:
   - 将食物节点移动到蛇头的位置,即食物的 `next` 指针指向原来的蛇头,然后将 `_SNAKE_HEAD` 设置为食物节点。
   - 释放新节点的内存,因为新节点现在成为了蛇的一部分。
   - 打印更新后的蛇的位置到控制台。
   - 增加分数,分数的增加量由 `_SNAKE_ONE_FOOD` 决定。
   - 调用 `Snake_food` 函数在游戏中放置新的食物。


3. `NoFood` 函数:当蛇移动到一个不是食物的位置时,这个函数会被调用。它执行以下操作:
   - 将新节点添加到蛇头的位置,新节点的 `next` 指针指向原来的蛇头,然后将 `_SNAKE_HEAD` 设置为新节点。
   - 打印更新后的蛇的位置到控制台,除了最后一个节点,因为它后面没有节点了。
   - 释放最后一个节点,以避免在屏幕上留下尾巴效果。


在这段代码中,`ps` 是一个指向 `Snake_State` 结构体的指针,这个结构体可能包含了蛇的状态信息,如食物和蛇的位置等。`newnode` 是一个指向 `Snake_Snakenode` 结构体的指针,这个结构体可能代表了蛇的每一个节点。`_SNAKE_FOOD`、`_SNAKE_HEAD`、`_SNAKE_SCORE` 和 `_SNAKE_ONE_FOOD` 是 `ps` 结构体中的成员,分别表示食物节点、蛇头节点、得分和每吃一个食物增加的分数。
`Snake_Cursor_Position` 函数用来设置控制台光标的位置,`wprintf` 函数用来在控制台打印宽字符,`BOOY` 是一个宽字符常量,代表了蛇或食物在控制台上的表示。这个常量应该是在其他地方定义的。

 不能碰到自己+不能撞墙(实现文件)

最后判断能不能撞墙能不能咬到自己

//不能碰到自己
void Kill_myself(pSnake_State ps)
{
	//头结点的下一个节点,因为头结点不能咬到头结点
	pSnake_Snakenode cur = ps->_SNAKE_HEAD->next;
	while (cur)
	{
		if (cur->x == ps->_SNAKE_HEAD->x && cur->y == ps->_SNAKE_HEAD->y)
		{
			ps->_SNAKE_STATE = KILL_MYSELS;
			break;
		}
		cur = cur->next;
	}
}
//不能撞墙
void Kill_wall(pSnake_State ps)
{
	if (ps->_SNAKE_HEAD->x == 0 || ps->_SNAKE_HEAD->x == 76 || ps->_SNAKE_HEAD->y == 1 || ps->_SNAKE_HEAD->y == 30)
	{
		ps->_SNAKE_STATE = KILL_WALL;
	}
}

`Kill_myself` 和 `Kill_wall` 函数分别用于检查蛇是否咬到了自己或者撞到了墙壁,并在发生这些情况时更新游戏状态。
下面是这两个函数的详细解释:
1. `Kill_myself` 函数:
   - 这个函数检查蛇的头节点是否与身体的任何部分重合,这会在蛇移动时发生。
   - 函数遍历蛇的身体(从第二个节点开始,因为头节点不能咬到自己),使用一个 `while` 循环和一个指针 `cur` 来访问每个节点。
   - 如果发现头节点的 `x` 和 `y` 坐标与身体任何节点的坐标相同,这意味着蛇咬到了自己,函数将蛇的状态 `ps->_SNAKE_STATE` 设置为 `KILL_MYSELS`,这通常意味着游戏结束。
2. `Kill_wall` 函数:
   - 这个函数检查蛇的头节点是否超出了游戏界面的边界。
   - 函数检查头节点的 `x` 和 `y` 坐标是否等于界面的边缘坐标(例如,`x` 坐标为0或76,`y` 坐标为1或30)。
   - 如果头节点超出了这些边界,函数将蛇的状态 `ps->_SNAKE_STATE` 设置为 `KILL_WALL`,这也通常意味着游戏结束。
这两个函数在 `Snake_move` 函数中被调用,以确保蛇的移动符合游戏规则。如果蛇咬到了自己或撞到了墙壁,游戏状态会被更新,从而在游戏循环中触发游戏的结束。

蛇的结束逻辑

#include"Snake.h"
void Snake01()
{
	int input = 0;
	do
	{
		//这里需要进行初始化,否则创建蛇的时候,会导致死循环
		Snake_State ps = { 0 };
		//控制面板的操作
		Snake_Control_Panel();

		//隐藏光标信息
		Snake_Hide_Cursor();

		//初始化界面
		Snake_interface();

		//蛇的初始化
		Snake_Initialize(&ps);

		//蛇的运行逻辑
		Snake_RunGame(&ps);

		//是否再来一局
		Snake_Cursor_Position(24, 13);
		wprintf(L"是否再来一局(Y/N):\n");
		Snake_Cursor_Position(42, 13);
		scanf("%s", &input);


	} while (input == 'y' || input == 'Y');

}
int main()
{
	srand((unsigned int)time(NULL));
	//不设置贪吃蛇本地属性,wprintf就不能打印出来
	setlocale(LC_ALL, "");
	
	//贪吃蛇的主要逻辑
	Snake01();

	//贪吃蛇的结束标志的所在位置
	Snake_Cursor_Position(0, 40);
	
	return 0;
}

蛇的结束逻辑这里我们不单独写一下了,直接在主函数里面实现,简单是说就行循环

最后那个定位的坐标的意思是,把结束的标语定位到最后面。美观

解释一下代码的意思
1. `Snake01` 函数:这是游戏的主体部分,它包含了一个 `do-while` 循环,用于重复游戏过程直到用户选择不再玩。
   - `Snake_State ps = { 0 };`:这里初始化了一个 `Snake_State` 结构体,用于存储游戏的状态,包括蛇的位置、食物的位置、得分等。
   - `Snake_Control_Panel();`:这个函数可能会显示游戏控制面板,比如游戏设置或者游戏选项。
   - `Snake_Hide_Cursor();`:隐藏控制台的光标,以免在游戏过程中干扰玩家。
   - `Snake_interface();`:初始化游戏界面,可能包括清屏、设置文本颜色等。
   - `Snake_Initialize(&ps);`:初始化游戏,包括创建蛇和食物,以及初始化游戏状态。
   - `Snake_RunGame(&ps);`:运行游戏循环,处理蛇的移动、食物的生成、碰撞检测等。
   - `Snake_Cursor_Position(24, 13); wprintf(L"是否再来一局(Y/N):\n");`:在控制台定位光标并询问用户是否想要再次玩游戏。
   - `scanf("%s", &input);`:读取用户的输入,用户可以输入 'Y' 或 'N' 来选择是否再来一局。
   - `do { ... } while (input == 'y' || input == 'Y');`:这是一个循环,只有当用户输入 'Y' 或 'y' 时,游戏才会重新开始。


2. `main` 函数:这是程序的入口点,它设置了随机种子(用于生成随机数)和本地设置(为了让 `wprintf` 能够正确打印宽字符)。
   - `srand((unsigned int)time(NULL));`:使用当前时间来初始化随机数生成器的种子,这样每次运行游戏时产生的随机数序列都会不同。
   - `setlocale(LC_ALL, "");`:设置本地环境,以确保 `wprintf` 能够正确处理宽字符。
   - `Snake01();`:调用游戏主体函数,开始游戏。
   - `Snake_Cursor_Position(0, 40);`:在控制台定位光标,通常是为了在游戏结束时显示结束标志或者分数。
   - `return 0;`:程序结束,返回 0。
整个游戏逻辑的主要部分都在 `Snake01` 函数中,它包含了游戏的初始化、运行和结束逻辑。游戏运行时,它会不断循环,直到用户选择退出。

代码的总结

Snake.h

#define _CRT_SECURE_NO_WARNINGS 1
#define KEY_PRESS(vk) (GetAsyncKeyState(vk)&1?1:0)
#pragma once
#include<stdio.h>
#include<stdlib.h> 
#include<windows.h>
#include<stdbool.h>
#include<string.h>
#include<locale.h>
#include<time.h>
#define POS_X 24
#define POS_Y 5

#define BOOY L'●'
#define FOOD L'★'
#define WALL L'□'
//控制面板的操作
void Snake_Control_Panel();

//隐藏光标信息
void Snake_Hide_Cursor();

//指定光标的位置
void Snake_Cursor_Position(short x , short y);

//初始化界面
void Snake_interface();

//蛇的状态
enum _STATE
{
	OK,//正常
	KILL_ESE,//退出
	KILL_WALL,//撞墙
	KILL_MYSELS//咬到自己
};

//蛇的方向
enum _DIR
{
	UP,//上
	DOWN,//下
	LEFT,//左
	RIGHT//右
};
//创建节点
typedef struct Snake_Snakenode
{
	int x;
	int y;
	struct Snake_Snakenode* next;
}Snake_Snakenode, * pSnake_Snakenode;

//创建蛇的状态
//方向,头节点,状态,食物,一个食物的分数,时间,得分情况
typedef struct Snake_State
{
	pSnake_Snakenode _SNAKE_HEAD;//蛇的头节点
	pSnake_Snakenode _SNAKE_FOOD;//蛇的食物
	enum _STATE _SNAKE_STATE;//蛇的状态
	enum _DIR _SNAKE_DIR;//蛇的方向
	int _SNAKE_SCORE;//总成绩
	int _SNAKE_SLEEP_TIME;//蛇中间运动的时候的休息时间
	int _SNAKE_ONE_FOOD;//一个食物的分数
}Snake_State, * pSnake_State;

//蛇的初始化
void Snake_Initialize(pSnake_State ps);
绘制地图
void Snake_Map();
创建蛇
void Snake_Create(pSnake_State ps);
创建食物
void Snake_food(pSnake_State ps);


//蛇的运行逻辑
void Snake_RunGame(pSnake_State ps);
暂停
void SNAKEVK_SPACE();
蛇的移动
void Snake_move(pSnake_State ps);
判断下一个节点是不是食物
int IsFood(pSnake_State ps, pSnake_Snakenode newnode);
下一个节点是食物,吃掉食物
void EatFood(pSnake_State ps, pSnake_Snakenode newnode);

//蛇的结束

Snake.c

#include"Snake.h"
//控制面板的操作
void Snake_Control_Panel()
{
	system("mode con cols=130 lines=45 ");
	system("title 贪吃蛇");
}
//隐藏光标信息
void Snake_Hide_Cursor()
{
	//获得句柄
	HANDLE input = GetStdHandle(STD_OUTPUT_HANDLE);
	//创建光标结构体
	CONSOLE_CURSOR_INFO info;

	//先获得光标信息
	GetConsoleCursorInfo(input, &info);
	//再设置为不可见
	info.bVisible = false;
	
	//设置光标信息
	SetConsoleCursorInfo(input, &info);
}
//指定光标的位置
void Snake_Cursor_Position(short x, short y)
{
	//获得句柄
	HANDLE input = GetStdHandle(STD_OUTPUT_HANDLE);
	//定位坐标
	COORD pos = { x,y };
	
	//设置光标信息
	SetConsoleCursorPosition(input, pos);
}
//初始化界面
void Snake_interface()
{
	//1.打印环境界面
	Snake_Cursor_Position(47, 19);
	wprintf(L"欢迎来到贪吃蛇小游戏\n");
	//按任意键继续
	Snake_Cursor_Position(100, 35);
	system("pause");
	//清屏
	system("cls");

	Snake_Cursor_Position(47, 16);
	wprintf(L"不能穿墙,可以咬到自己\n");

	Snake_Cursor_Position(47, 17);
	wprintf(L"用 ↑. ↓. ←. →. 来控制蛇的移动\n");

	Snake_Cursor_Position(47, 18);
	wprintf(L"按F3加速,F4减速\n");

	Snake_Cursor_Position(47, 19);
	wprintf(L"加速可以获得更高的分数\n");

	Snake_Cursor_Position(47, 20);
	wprintf(L"ESC :退出游戏.space(空格):暂停游戏.\n");

	Snake_Cursor_Position(47, 21);
	wprintf(L"每个食物得分:20分\n");

	Snake_Cursor_Position(100, 35);
	wprintf(L"贾杰森精心制作\n");

	Snake_Cursor_Position(100, 36);
	system("pause");
	system("cls");



	//游戏界面的相关介绍
	Snake_Cursor_Position(90, 25);
	wprintf(L"不能穿墙,不能咬到自己\n");

	Snake_Cursor_Position(90, 26);
	wprintf(L"用 ↑. ↓. ←. →. 来控制蛇的移动\n");

	Snake_Cursor_Position(90, 27);
	wprintf(L"按F3加速,F4减速\n");

	Snake_Cursor_Position(90, 28);
	wprintf(L"加速可以获得更高的分数\n");

	Snake_Cursor_Position(90, 29);
	wprintf(L"ESC :退出游戏.space(空格):暂停游戏.\n");

	Snake_Cursor_Position(90, 30);
	wprintf(L"贾杰森精心制作\n");
}
//绘制地图
void Snake_Map()
{
	//建立围墙(偶数)绘制地图
	Snake_Cursor_Position(0, 0);
	for (int i = 0; i < 40; i++)
	{
		wprintf(L"%lc", WALL);
	}
	Snake_Cursor_Position(0, 31);
	for (int i = 0; i < 40; i++)
	{
		wprintf(L"%lc", WALL);
	}
	for (int i = 0; i < 31; i++)
	{
		Snake_Cursor_Position(0, i);
		wprintf(L"%lc", WALL);
	}
	for (int i = 0; i < 31; i++)
	{
		Snake_Cursor_Position(78, i);
		wprintf(L"%lc", WALL);
	}
}

//创建蛇
void Snake_Create(pSnake_State ps)
{
	//初始蛇是五个节点
	pSnake_Snakenode cur = NULL;
	for (int i = 0; i < 5; i++)
	{
		//创建节点
		cur = (pSnake_Snakenode)malloc(sizeof(Snake_Snakenode));
		if (cur == NULL)
		{
			perror("Snake_Create:pSnake_Snakenode newnode:error:");
			return;
		}
		//进行定位
		cur->next = NULL;
		cur->x= POS_X + i * 2;
		cur->y = POS_Y;

		//进行头插
		if (ps->_SNAKE_HEAD == NULL)
		{
			ps->_SNAKE_HEAD = cur;
		}
		else
		{
			cur->next = ps->_SNAKE_HEAD;
			ps->_SNAKE_HEAD = cur;
		}
	}

	//进行打印
	cur = ps->_SNAKE_HEAD;
	while (cur)
	{
		Snake_Cursor_Position(cur->x, cur->y);
		wprintf(L"%lc", BOOY);
		cur = cur->next;
	}

	//初始化蛇的状态
	ps->_SNAKE_STATE = OK;//状态
	ps->_SNAKE_SLEEP_TIME = 200;//休息时间
	ps->_SNAKE_SCORE = 0;//总成绩
	ps->_SNAKE_ONE_FOOD = 20;//一个食物的分数
	ps->_SNAKE_DIR = RIGHT;//蛇的方向
}
//暂停
void SNAKEVK_SPACE()
{
	while (1)
	{
		if (KEY_PRESS(VK_SPACE))
		{
			break;
		}
		Sleep(200);
	}
}
//创建食物
void Snake_food(pSnake_State ps)
{
	int x = 0; 
	int y = 0;

again:
	do
	{
		x = rand() % 74 + 2;
		y = rand() % 28 + 1;
	} while (x % 2 != 0);
	
	//判断是不是和身体重合
	pSnake_Snakenode headcur = ps->_SNAKE_HEAD;
	while (headcur)
	{
		if (x == headcur->x && y == headcur->y)
		{
			goto again;
		}
		headcur = headcur->next;
	}

	//创建食物
	//pSnake_Snakenode curfood = (pSnake_Snakenode)malloc(sizeof(Snake_Snakenode));
	pSnake_Snakenode curfood = NULL;//必须初始化
	curfood = (pSnake_Snakenode)malloc(sizeof(Snake_Snakenode));
	if (curfood == NULL)
	{
		perror("pSnake_Snakenode curfood:error:");
		return;
	}
	curfood->next = NULL;
	curfood->x = x;
	curfood->y = y;

	Snake_Cursor_Position(x, y);
	wprintf(L"%lc", FOOD);
	
	//创建的食物放到蛇的状态里面去
	ps->_SNAKE_FOOD = curfood;
}
//判断下一个节点是不是食物
int IsFood(pSnake_State ps, pSnake_Snakenode newnode)
{
	return (ps->_SNAKE_FOOD->x == newnode->x && ps->_SNAKE_FOOD->y == newnode->y);
}
//下一个节点是食物,吃掉食物
void EatFood(pSnake_State ps, pSnake_Snakenode newnode)
{
	//头插
	ps->_SNAKE_FOOD->next = ps->_SNAKE_HEAD;
	ps->_SNAKE_HEAD = ps->_SNAKE_FOOD;

	//释放节点
	free(newnode);
	newnode = NULL;

	//打印蛇
	pSnake_Snakenode cur = ps->_SNAKE_HEAD;
	while (cur)
	{
		Snake_Cursor_Position(cur->x, cur->y);
		wprintf(L"%lc", BOOY);
		cur = cur->next;
	}
	//得分
	ps->_SNAKE_SCORE += ps->_SNAKE_ONE_FOOD;

	//再次创建食物
	Snake_food(ps);

}
//没有遇见食物,正常移动
void NoFood(pSnake_State ps, pSnake_Snakenode newnode)
{
	//没有遇见食物,需要头插空节点,释放最后一个节点
	newnode->next = ps->_SNAKE_HEAD;
	ps->_SNAKE_HEAD = newnode;

	//打印蛇
	pSnake_Snakenode cur = ps->_SNAKE_HEAD;
	while (cur->next->next)
	{
		Snake_Cursor_Position(cur->x, cur->y);
		wprintf(L"%lc", BOOY);
		cur = cur->next;
	}

	//最后一个空格打印成空
	Snake_Cursor_Position(cur->next->x, cur->next->y);
	wprintf(L"  ");

	//释放最后一个节点(节点不释放还是产生拖尾效果)
	free(cur->next);
	cur->next = NULL;

}
//不能碰到自己
void Kill_myself(pSnake_State ps)
{
	//头结点的下一个节点,因为头结点不能咬到头结点
	pSnake_Snakenode cur = ps->_SNAKE_HEAD->next;
	while (cur)
	{
		if (cur->x == ps->_SNAKE_HEAD->x && cur->y == ps->_SNAKE_HEAD->y)
		{
			ps->_SNAKE_STATE = KILL_MYSELS;
			break;
		}
		cur = cur->next;
	}
}
//不能撞墙
void Kill_wall(pSnake_State ps)
{
	if (ps->_SNAKE_HEAD->x == 0 || ps->_SNAKE_HEAD->x == 76 || ps->_SNAKE_HEAD->y == 1 || ps->_SNAKE_HEAD->y == 30)
	{
		ps->_SNAKE_STATE = KILL_WALL;
	}
}
//蛇的移动
void Snake_move(pSnake_State ps)
{
	//创建的下一个节点
	pSnake_Snakenode newnode = (pSnake_Snakenode)malloc(sizeof(Snake_Snakenode));
	if (newnode == NULL)
	{
		perror("Snake_move:newnode:error:");
		return;
	}
	switch (ps->_SNAKE_DIR)
	{
	case UP:
		newnode->x = ps->_SNAKE_HEAD->x;
		newnode->y = ps->_SNAKE_HEAD->y - 1;
		break;
	case DOWN:
		newnode->x = ps->_SNAKE_HEAD->x;
		newnode->y = ps->_SNAKE_HEAD->y + 1;
		break;
	case RIGHT:
		newnode->x = ps->_SNAKE_HEAD->x + 2;
		newnode->y = ps->_SNAKE_HEAD->y;
		break;
	case LEFT:
		newnode->x = ps->_SNAKE_HEAD->x - 2;
		newnode->y = ps->_SNAKE_HEAD->y;
		break;
	}
	//判断下一个节点是不是食物
	if (IsFood(ps, newnode))//如果是食物
	{
		//吃掉食物,并且创建食物
		EatFood(ps, newnode);
		newnode = NULL;
	}
	else//如果不是食物
	{
		//没有遇见食物,正常移动
		NoFood(ps, newnode);
		newnode = NULL;
	}
	//不能咬到自己
	Kill_myself(ps);

	//不能撞墙
	Kill_wall(ps);
}
//蛇的运行逻辑
void Snake_RunGame(pSnake_State ps)
{
	do
	{
		//打印分数
		Snake_Cursor_Position(90, 20);
		wprintf(L"当前的得分是:%d", ps->_SNAKE_SCORE);

		Snake_Cursor_Position(90, 24);
		wprintf(L"一个食物的分数是:%d", ps->_SNAKE_ONE_FOOD);
		if (KEY_PRESS(VK_UP) && ps->_SNAKE_DIR != DOWN)
		{
			ps->_SNAKE_DIR = UP;
		}
		else if (KEY_PRESS(VK_DOWN) && ps->_SNAKE_DIR != UP)
		{
			ps->_SNAKE_DIR = DOWN;
		}
		else if (KEY_PRESS(VK_LEFT) && ps->_SNAKE_DIR != RIGHT)
		{
			ps->_SNAKE_DIR = LEFT;
		}
		else if (KEY_PRESS(VK_RIGHT) && ps->_SNAKE_DIR != LEFT)
		{
			ps->_SNAKE_DIR = RIGHT;
		}
		if (KEY_PRESS(VK_ESCAPE))//退出
		{
			ps->_SNAKE_STATE = KILL_ESE;
		}
		if (KEY_PRESS(VK_F4))//减速
		{
			if (ps->_SNAKE_ONE_FOOD > 10)
			{
				ps->_SNAKE_SLEEP_TIME += 30;
				ps->_SNAKE_ONE_FOOD = ps->_SNAKE_ONE_FOOD - 5;
			}
		}
		if (KEY_PRESS(VK_F3))//加速
		{
			if (ps->_SNAKE_ONE_FOOD < 45)
			{
				ps->_SNAKE_SLEEP_TIME -= 30;
				ps->_SNAKE_ONE_FOOD = ps->_SNAKE_ONE_FOOD + 5;
			}
		}
		if (KEY_PRESS(VK_SPACE))//暂停
		{
			SNAKEVK_SPACE();
		}
		//蛇的移动
		Snake_move(ps);
		//蛇的每次移动的休眠时间
		Sleep(ps->_SNAKE_SLEEP_TIME);

	} while (ps->_SNAKE_STATE == OK);
}
//蛇的初始化
void Snake_Initialize(pSnake_State ps)
{
	//绘制地图
	Snake_Map();

	//创建蛇
	Snake_Create(ps);

	//创建食物
	Snake_food(ps);

	//蛇的运行逻辑
	Snake_RunGame(ps);
	
	//蛇的结束

}

test.c

#include"Snake.h"
void Snake01()
{
	int input = 0;
	do
	{
		//这里需要进行初始化,否则创建蛇的时候,会导致死循环
		Snake_State ps = { 0 };
		//控制面板的操作
		Snake_Control_Panel();

		//隐藏光标信息
		Snake_Hide_Cursor();

		//初始化界面
		Snake_interface();

		//蛇的初始化
		Snake_Initialize(&ps);

		//蛇的运行逻辑
		Snake_RunGame(&ps);

		//是否再来一局
		Snake_Cursor_Position(24, 13);
		wprintf(L"是否再来一局(Y/N):\n");
		Snake_Cursor_Position(42, 13);
		scanf("%s", &input);


	} while (input == 'y' || input == 'Y');

}
int main()
{
	srand((unsigned int)time(NULL));
	//不设置贪吃蛇本地属性,wprintf就不能打印出来
	setlocale(LC_ALL, "");
	
	//贪吃蛇的主要逻辑
	Snake01();

	//贪吃蛇的结束标志的所在位置
	Snake_Cursor_Position(0, 40);
	
	return 0;
}

图片详解

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

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

相关文章

java中的字符串(String)常量池理解

下面创建String对象的方式一样吗&#xff1f; 上述程序创建对象类似&#xff0c;为什么s1和s2引用对象一样&#xff0c;但是s3和s4不一样呢&#xff1f; 在java程序中&#xff0c;许多基本类型的字面常量会经常用到&#xff0c;例如2,3.11&#xff0c;“hyy”等。为了提升程序…

C语言动态内存管理malloc、calloc、realloc、free函数、内存泄漏、动态内存开辟的位置等的介绍

文章目录 前言一、为什么存在动态内存管理二、动态内存函数的介绍1. malloc函数2. 内存泄漏3. 动态内存开辟位置4. free函数5. calloc 函数6. realloc 函数7. realloc 传空指针 总结 前言 C语言动态内存管理malloc、calloc、realloc、free函数、内存泄漏、动态内存开辟的位置等…

25.哀家要长脑子了---哈希表

1.525. 连续数组 - 力扣&#xff08;LeetCode&#xff09; 在我对通义千问的一番折磨下&#xff0c;终于弄清楚一点点了。哈希表存储前缀和数组值 用一个counter来记录nums中0、1数量差值的变化。 哈希表map存储某个特定的counter值首次出现的位置。counter的计算&#xff1a;…

【LeetCode 121】买卖股票的最佳时机

思路 思路&#xff1a; 所谓代码的复杂性来源于业务的复杂性&#xff0c;如果能够想清楚业务实现逻辑&#xff0c;就能够轻松写出代码&#xff1b; 假设当前是第i天&#xff0c;如何在第i天赚到最多的钱&#xff1f;需要在第i天之前以最低价买入股票&#xff1b; 所以需要求…

13 【PS作图】人物绘画理论-脸型

三庭五眼 三庭&#xff1a;脸的长度比例 &#xff08;1&#xff09;发际线到眉毛 &#xff08;2&#xff09;眉毛到鼻底 &#xff08;3&#xff09;鼻底到下巴 三个部分大致为三等分 五眼&#xff1a;脸的宽度比例 以眼睛长度为单位&#xff0c;把脸的宽度分成五等分&#x…

[极客大挑战 2019]PHP

1.通过目录扫描找到它的备份文件&#xff0c;这里的备份文件是它的源码。 2.源码当中涉及到的关键点就是魔术函数以及序列化与反序列化。 我们提交的select参数会被进行反序列化&#xff0c;我们要构造符合输出flag条件的序列化数据。 但是&#xff0c;这里要注意的就是我们提…

求解亲和数

【问题描述】 古希腊数学家毕达哥拉斯在自然数研究中发现&#xff0c;220的所有真约数&#xff08;即不是自身 的约数&#xff09;之和为&#xff1a; 1245101120224455110284。而284的所有真约数为1、2、4、71、142&#xff0c;加起来恰好为220。人 们对这样的数感到很惊奇&am…

五种主流数据库:窗口函数

SQL 窗口函数为在线分析系统&#xff08;OLAP&#xff09;和商业智能&#xff08;BI&#xff09;提供了复杂分析和报表统计的功能&#xff0c;例如产品的累计销量统计、分类排名、同比/环比分析等。这些功能通常很难通过聚合函数和分组操作来实现。 本文比较了五种主流数据库实…

嵌入式学习67-C++(多线程,自定义信号合槽,串口通信)

知识零碎&#xff1a; QmessageBox 报错提示框 GPS传感器获取到的 经纬度信息并不是真实的物理坐标&#xff0c;还需要转换 signals&#xff1a; …

【JAVA入门】Day03 - 数组

【JAVA入门】Day03 - 数组 文章目录 【JAVA入门】Day03 - 数组一、数组的概念二、数组的定义2.1 数组的静态初始化2.2 数组的地址值2.3 数组元素的访问2.4 数组遍历2.5 数组的动态初始化2.6 数组的常见操作2.7 数组的内存分配2.7.1 Java内存分配2.7.2 数组的内存图 一、数组的概…

234234235

c语言中的小小白-CSDN博客c语言中的小小白关注算法,c,c语言,贪心算法,链表,mysql,动态规划,后端,线性回归,数据结构,排序算法领域.https://blog.csdn.net/bhbcdxb123?spm1001.2014.3001.5343 给大家分享一句我很喜欢我话&#xff1a; 知不足而奋进&#xff0c;望远山而前行&am…

周刊是聪明人筛选优质知识的聪明手段!

这是一个信息过载的时代&#xff0c;也是一个信息匮乏的时代。 这种矛盾的现象在 Python 编程语言上的表现非常明显。 它是常年高居编程语言排行榜的最流行语言之一&#xff0c;在国外发展得如火如荼&#xff0c;开发者、项目、文章、播客、会议活动等相关信息如海如潮。 但…

对XYctf的一些总结

对XYctf的一些总结 WEB 1.http请求头字段 此次比赛中出现的&#xff1a; X-Forwarded-For/Client-ip&#xff1a;修改来源ip via&#xff1a;修改代理服务器 还有一些常见的字段&#xff1a; GET&#xff1a;此方法用于请求指定的资源。GET请求应该安全且幂等&#xff0c…

QTday1

1、QT思维导图 2、自由发挥应用场景&#xff0c;实现登录 #include "mywidget.h"MyWidget::MyWidget(QWidget *parent): QWidget(parent) {this->resize(642,493);this->setFixedSize(642,493);this->setWindowIcon(QIcon("D:/QTText/pictrue/qq.png…

Windows系统本地部署Net2FTP文件管理网站并实现远程连接上传下载

文章目录 1.前言2. Net2FTP网站搭建2.1. Net2FTP下载和安装2.2. Net2FTP网页测试 3. cpolar内网穿透3.1.Cpolar云端设置3.2.Cpolar本地设置 4.公网访问测试5.结语 1.前言 文件传输可以说是互联网最主要的应用之一&#xff0c;特别是智能设备的大面积使用&#xff0c;无论是个人…

Vue入门到关门之Vue高级用法

一、在vue项目中使用ref属性 ref 属性是 Vue.js 中用于获取对 DOM 元素或组件实例的引用的属性。通过在普通标签上或组件上添加 ref 属性&#xff0c;我们可以在 JavaScript 代码中使用 this.$refs.xxx 来访问对应的 DOM 元素或组件实例。 放在普通标签上&#xff0c;通过 th…

CRE-LLM:告别复杂特征工程,直接关系抽取

CRE-LLM&#xff1a;告别复杂特征工程&#xff0c;直接关系抽取 提出背景CRE-LLM 宏观分析CRE-LLM 微观分析1. 构建指令集&#xff08;Instruction Design&#xff09;2. 高效微调大型语言模型&#xff08;Efficient Fine-Tuning on LLMs&#xff09;3. 方法讨论&#xff08;Di…

数据结构——链表专题2

文章目录 一、返回倒数第k 个节点二、链表的回文结构三、相交链表 一、返回倒数第k 个节点 原题链接&#xff1a;返回倒数第k 个节点 利用快慢指针的方法&#xff1a;先让fast走k步&#xff0c;然后fast和slow一起走&#xff0c;直到fast为空&#xff0c;最后slow指向的结点就…

智慧工地)智慧工地标准化方案(107页)

2.2 设计思路 对于某某智慧工地管理系统的建设&#xff0c;绝不是对各个子系统进行简单堆砌&#xff0c;而是在满足各子系统功能的基础上&#xff0c;寻求内部各子系统之间、与外部其它智能化系统之间的完美结合。系统主要依托于智慧工地管理平台&#xff0c;来实现对众多子系统…

动态规划算法:路径问题

例题一 解法&#xff08;动态规划&#xff09;&#xff1a; 算法思路&#xff1a; 1. 状态表⽰&#xff1a; 对于这种「路径类」的问题&#xff0c;我们的状态表⽰⼀般有两种形式&#xff1a; i. 从 [i, j] 位置出发&#xff0c;巴拉巴拉&#xff1b; ii. 从起始位置出…
最新文章