贪吃蛇——C语言(VS2022含源代码,及源代码zip文件)

news2024/11/25 18:38:11

一.游戏背景

贪吃蛇是一款在世界上盛名已久的小游戏,贪食蛇游戏操作简单,可玩性比较高。这个游戏难度最大的不是蛇长得很长的时候,而是开始。那个时候蛇身很短,看上去难度不大,却最容易死掉,因为把玩一条小短蛇让人容易走神,失去耐心。由于难度小,你会不知不觉加快调整方向的速度,在游走自如的时候蛇身逐渐加长了,而玩家却没有意识到危险,在最得意洋洋的一刻突然死亡。

接下来,我们利用编程来实现这个小游戏,既可以巩固我们的C语言的学习,也可以提高我们的学习兴趣。

二.Win32API介绍

在编写该小游戏之前我们需要了解一下win32api中某些函数的使用方法,在后续的编写中会用到。在使用之前我们要将我们的控制台程序(平常运行程序的黑框)改成windows控制台主机。

如果运行起来不是这种的而是下面这种黑款的话,就需要改成上面的windows控制台主机,否则无法实现贪吃蛇小程序。

1.mode命令

我们使用mode命令就可以调整我们的控制台窗口的大小。

system("mode con cols=100 lines=30");

cols控制的是列数,lines控制的行数,使用system函数必须包含头文件#include<stdlib.h>。

 由上图可知,在控制台窗口行和列不是1:1的关系,所以在后续坐标的计算上就会有些不同。

2.title命令 

如果我们运行程序之后,在运行窗口就会显示名称。

我们可以利用title命令来修改控制台的名称。 

system("title 贪吃蛇");

但是我们执行该语句,名字却并没有发生变化,这是为什么呢?这是因为该程序已经结束了,title命令的作用只在程序还在运行的时候生效。所以我们让程序暂停,观察名称。 

pause命令可以使程序暂停。 

3.控制台上的坐标COORD

在控制台上,坐标的表示和我们的直角坐标系有所不同

 而在win32api上定义了一个COORD的结构体,用来描述控制台窗口的坐标。COORD 结构 - Windows Console | Microsoft Learn 

该结构有两个成员分别是x和y,用来描述x坐标和y坐标。 

使用win32api函数要包含头文件#include<windows.h>。

COORD pos = { 14,32 };

我们可以这样来定义一个坐标信息。

4.GetStdHandle函数

该函数的作用是检索指定标准设备的句柄(标准输入、标准输出或标准错误)。GetStdHandle 函数 - Windows Console | Microsoft Learn

HANDLE WINAPI GetStdHandle(
  _In_ DWORD nStdHandle
);

该函数的参数已经给出了三种选择。

那么,什么是句柄呢?在我的理解来看,该函数就是得到你传的参数的操作权。例如你要获取屏幕(标准输出设备)的控制权,你只需要将第二个参数传给GetStdHandle函数,然后用一个handle类型的句柄接收即可,接下来就可以用该句柄来操纵屏幕。

HANDLE houtput = GetStdHandle(STD_OUTPUT_HANDLE);

接下来我们就可以利用houtput来操作屏幕。

5.GetConsoleCursorInfo 函数

该函数的功能是检索有关指定控制台屏幕缓冲区的游标大小和可见性的信息。GetConsoleCursorInfo 函数 - Windows Console | Microsoft Learn

BOOL WINAPI GetConsoleCursorInfo(
  _In_  HANDLE               hConsoleOutput,
  _Out_ PCONSOLE_CURSOR_INFO lpConsoleCursorInfo
);

我们可以看到该函数接收两个参数,第一个参数就是我们上面介绍的句柄,第二个参数是指向 CONSOLE_CURSOR_INFO 结构的指针,该结构接收有关控制台游标的信息

我们接下来看一下CONSOLE_CURSOR_INFO 结构 - Windows Console | Microsoft Learn该结构体:

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

该结构体有两个成员,分别是dwSize和bVisible。

dwSize是游标大小占的百分比, 该值介于 1 到 100 之间。 游标外观各不相同,范围从完全填充单元到显示为单元底部的横线。

bVisible是游标的可见性。如果游标可见则该成员为true,否则为false。

6.SetConsoleCursorInfo 函数

该函数的作用是为指定的控制台屏幕缓冲区设置光标的大小和可见性。SetConsoleCursorInfo 函数 - Windows Console | Microsoft Learn

BOOL WINAPI SetConsoleCursorInfo(
  _In_       HANDLE              hConsoleOutput,
  _In_ const CONSOLE_CURSOR_INFO *lpConsoleCursorInfo
);

我们可以看到该函数的参数与上面的参数相同。第一个参数是指定控制台的句柄,第二个参数是游标的结构体。 

该函数要和上面的函数同时使用,先获取指定控制台的操作权,然后获取光标信息,进行修改后然后在设置光标信息。 

我们现在可以利用上面介绍的几个函数来设置光标的大小:

int main()
{
	//获取屏幕的句柄(操作权)
	HANDLE houtput = GetStdHandle(STD_OUTPUT_HANDLE);

	//定义一个光标信息的结构体
	CONSOLE_CURSOR_INFO cursor_info = { 0 };

	//获取屏幕光标的操作权
	GetConsoleCursorInfo(houtput, &cursor_info);

	//修改光标大小
	cursor_info.dwSize = 100;

	//设置光标信息
	SetConsoleCursorInfo(houtput, &cursor_info);
    getchar();
	return 0;
}

 我们运行该代码看看如何,

我们看到,当我们修改光标大小为100后,的确在屏幕上可以观察出来。右图为默认的光标大小。 

我们还可以修改光标的可见性:(其余代码不变)

//修改光标的可见性
cursor_info.bVisible = false;

运行起来后,光标确实被隐藏了。要使用ture和false这类布尔类型需要包含头文件#include <stdbool.h> 。

7.SetConsoleCursorPosition 函数

该函数的功能是设置指定控制台屏幕缓冲区中的光标位置。SetConsoleCursorPosition 函数 - Windows Console | Microsoft Learn

BOOL WINAPI SetConsoleCursorPosition(
  _In_ HANDLE hConsoleOutput,
  _In_ COORD  dwCursorPosition
);

第一个参数就是指定控制台的句柄也就是操纵权,第二个是一个COORD结构体类的数据,表示新的光标位置信息。

我们可以利用这个函数定位光标的位置,让打印信息出现在我们期望的位置上。

int main()
{
    //设置控制台大小
    system("mode con cols=100 lines=30");
	//获取屏幕句柄
	HANDLE houtput = GetStdHandle(STD_OUTPUT_HANDLE);

	//定位新坐标
	COORD pos = { 50,15 };

	//设置光标位置
	SetConsoleCursorPosition(houtput, pos);

	printf("hello world\n");
	system("pause");
}

我们看到,我们利用该函数,定位光标位置,打印的信息机会从我们定义的位置开始打印。

7.1SetPos函数 

在接下来的贪吃蛇小游戏中,我们会多次用到定位光标这一操作,而对于定位光标来说,每一次的操作都是相同的,区别就在于传的x,y不同,所以我们就可以将定位光标这一操作封装成一个函数,这样就可以方便我们后续定位光标。

void SetPos(int x, int y)
{
	//设置控制台大小
	system("mode con cols=100 lines=30");

	//获取屏幕句柄
	HANDLE houtput = GetStdHandle(STD_OUTPUT_HANDLE);

	//定位新坐标
	COORD pos = { x,y };

	//设置光标位置
	SetConsoleCursorPosition(houtput, pos);
}

8.GetAsyncKeyState函数

该函数的作用是检测指定键是否被按下。getAsyncKeyState 函数 (winuser.h) - Win32 apps | Microsoft Learn

SHORT GetAsyncKeyState(
  [in] int vKey
);

该函数的参数是键盘上每个键的虚拟键代码。虚拟键码 (Winuser.h) - Win32 apps | Microsoft Learn

那我们如何检测某个键是否被按下呢?

getAsyncKeyState的返回值是short类型,如果我们在调用该函数的时候,该返回值的最高位是1,则表示该键的状态是按下;如果是0,则表示该键的状态是抬起。如果最低位是1,则表示该键被按过,0则表示该键没有被按过。

而在我们的贪吃蛇小游戏中,我们并不需要检测键的状态,我们只需要知道该键有没有被按过即可。方法就是,我们让该函数的返回值按位与上0x1就行了。如果结果是1,则表示该键被按过,如果是0,则表示该键没有被按过。

我们可以看到,这样就可以巧妙地将检查最后一位是1或0转化成检查按位与后的结果是1还是0了。 

我们借助下面的测试代码来验证:我们将数字键盘0~9的虚拟代码传给该函数,并将返回值按位与1,检测返回值,如果为1,则在屏幕上打印该数,否则不打印。

#include <stdio.h>
#include <windows.h>

int main()
{
	//设置控制台窗口大小
	system("mode con cols=100 lines=30");

	while (1)
	{
		if ((GetAsyncKeyState(VK_NUMPAD0) & 0x1))
		{
			printf("0\n");
		}		
		else if (GetAsyncKeyState(VK_NUMPAD1) & 0x1)
		{
			printf("1\n");
		}
		else if (GetAsyncKeyState(VK_NUMPAD2) & 0x1)
		{
			printf("2\n");
		}
		else if (GetAsyncKeyState(VK_NUMPAD3) & 0x1)
		{
			printf("3\n");
		}
		else if (GetAsyncKeyState(VK_NUMPAD4) & 0x1)
		{
			printf("4\n");
		}
		else if (GetAsyncKeyState(VK_NUMPAD5) & 0x1)
		{
			printf("5\n");
		}
		else if (GetAsyncKeyState(VK_NUMPAD6) & 0x1)
		{
			printf("6\n");
		}
		else if (GetAsyncKeyState(VK_NUMPAD7) & 0x1)
		{
			printf("7\n");
		}
		else if (GetAsyncKeyState(VK_NUMPAD8) & 0x1)
		{
			printf("8\n");
		}
		else if (GetAsyncKeyState(VK_NUMPAD9) & 0x1)
		{
			printf("9\n");
		}
	}
	return 0;
}

我们可以看到,当我们按下哪个键,就会打印出对应的数字。但是我们看到,这个检测键是否被按过的代码都非常相似,那么我们可以将该代码写成宏,来使代码更清晰些: 

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

我们以后要检测某个键是否被按过的时候直接调用这个宏即可。

三.贪吃蛇游戏的设计与分析

我们依旧采用函数声明与函数实现分离的方法来实现贪吃蛇。我们需要用到三个文件:
Snake.h:函数声明以及头文件等内容;

Snake.c:函数的实现;

test.c:游戏的运行逻辑。

1.地图

我们在上面已经讲到了控制台窗口的坐标轴分布,那么我们怎么在控制台窗口上打印我们的地图呢?这里我们采用宽字符‘□’表示墙体,宽字符‘●’表示蛇的身体,宽字符‘★’表示食物。普通字符一个占一个字节,而宽字符一个占两个字节。我们的汉字就是宽字符

我们看到,汉字在打印的时候就占了两个字节,而字母只占一个字节。

C语言是英国人发明的,所以起初C语言只适用于英语国家地区,然而随着C语言的发展,国际C语言组织为了C语言能够适配其他非英语地区,引入了<locale.h>头文件,方便程序员对某些函数进行区域化的调整。 

1.1<locale.h>头文件

<locale.h>头文件是C标准库中的一个头文件,用于支持程序的国际化和本地化。它提供了一组函数和宏来设置或查询程序的本地化信息,例如日期、时间、货币、数字格式等。

1.2库宏

<locale.h>头文件中定义了一些宏,供头文件中的函数使用。

1.3setlocale函数 

setlocale()函数是<locale.h>中的一个库函数,用于设置或查询程序的本地化信息。它允许程序员指定用于字符分类、字符转换、货币格式、日期和时间格式、数字格式等的区域设置。

#include <locale.h>

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

C 库函数 – setlocale() | 菜鸟教程 (runoob.com)

该函数的第一个参数就是上面的库宏,第二个参数有两种选择:

NULL:查询当前的本地化信息;

"":设置为用户环境变量中的默认设置。

#include <stdio.h>
#include <locale.h>
int main()
{
	//第二个参数为NULL,此时函数的功能是查询当前的本地化信息
	char* ret = setlocale(LC_ALL, NULL);
	printf("%s\n", ret);

	//当第二个参数是""时,此时函数的功能是适配本地化
	ret = setlocale(LC_ALL, "");
	printf("%s\n", ret);
	return 0;
}

C代表的就是标准模式,Chinese就代表了已经将默认设置修改为本地模式。 

1.4宽字符的打印 

宽字符的打印需要用到wprintf,且格式控制前面需要加上L,占位符用%lc或者%ls。

#include <stdio.h>
#include <wchar.h>

int main() {
    wchar_t* wideStr = L"宽字符打印";
    wprintf(L"%ls\n", wideStr);
    return 0;
}
 

2.地图的打印 

我们为了主函数的清晰,所以将游戏的运行逻辑封装成函数,我们在函数中完成游戏的实现。

int main()
{
	//程序一开始就先设置本地化
	setlocale(LC_ALL, "");

	//游戏运行逻辑
	Game();

	return 0;
}
void Game()
{
	//游戏的初始化
	GameInit();

	//游戏运行
	GameRun();

	//游戏结束
	GameOver();
}

我们地图的打印、控制台窗口的设置以及欢迎界面和帮助信息的打印都在游戏初始化中完成。下面是游戏初始化函数的逻辑。

//游戏的初始化
void GameInit()
{
	//初始化窗口
	InitWindow();

	//欢迎界面的打印
	welcome();

	//地图的打印
	CreatMap();
}

2.1窗口设置

//初始化窗口
void InitWindow()
{
	//设置窗口大小
	system("mode con cols=100 lines=30");

	//设置窗口名称
	system("title 贪吃蛇");

	//隐藏光标
	//获取屏幕的句柄(操作权)
	HANDLE houtput = GetStdHandle(STD_OUTPUT_HANDLE);

	//定义一个光标信息的结构体
	CONSOLE_CURSOR_INFO cursor_info = { 0 };

	//获取屏幕光标的操作权
	GetConsoleCursorInfo(houtput, &cursor_info);

	//修改光标的可见性
	cursor_info.bVisible = false;

	//设置光标信息
	SetConsoleCursorInfo(houtput, &cursor_info);
}

2.2欢迎界面的打印

 我们地图的打印以及欢迎界面的打印都要在游戏的初始化中完成。

//欢迎界面的打印
void welcome()
{
	//定位光标
	SetPos(40, 15);
	printf("欢迎来到贪吃蛇\n");
	SetPos(35, 25);
	system("pause");
	system("cls");
	
	//打印帮助信息
	SetPos(25, 15);
	wprintf(L"用 ↑. ↓ . ← . → 来控制蛇的移动,按shift加速,ctrl减速\n");
	SetPos(35, 25);
	system("pause");
	system("cls");
}

上面就是欢迎界面以及帮助信息的打印,SetPos函数是我们之前写过的定位光标函数。

2.2地图的打印

我们给出地图的样例,我们要做成的就是像这样子的地图,58列x27行的大小。

而我们在前面已经知道一个宽字符占两个字节,所以我们第一行只需要打印29个方格,然后再让光标定位到第27行,再打印29个方格。然后在打印左边的墙和右边的墙。我们只需要固定x坐标,让y坐标由1增加到26即可,然后再固定x为56,y坐标由1增加到26即可。

//地图的打印
void CreatMap()
{
	int i = 0;
	//上墙
	for (i = 0; i < 29; i++)
	{
		wprintf(L"%lc ", L'□');
	}
	//下墙
	SetPos(0, 26);
	for (i = 0; i < 29; i++)
	{
		wprintf(L"%lc ", L'□');
	}
	//左墙
	for (i = 1; i < 27; i++)
	{
		SetPos(0, i);
		wprintf(L"%lc", L'□');
	}
	//右墙
	for (i = 1; i < 27; i++)
	{
		SetPos(56,i);
		wprintf(L"%lc", L'□');
	}
}

大家注意,不知道为什么我的电脑显示宽字符的时候依旧是占一个字节,所以我在打印上下墙的时候每次都多打印了一个空格。 

3.数据结构的分析与设计

贪吃蛇的身体其实就是一个一个的节点连接起来的,而在我们链表中,节点依次相连就构成了我们链表。所以我们可以利用链表这一数据结构来描述贪吃蛇。

//蛇身的节点
typedef struct SnakeNode
{
	int x;
	int y;
	struct SnakeNode* Next;
}SnakeNode,* pSnakeNode;

而对于一个贪吃蛇来说,并不是一个简简单单的节点就搞定了,还有很多其他的属性:蛇的方向、蛇的状态、食物、蛇头、单个食物的分值、总分数、每走一步的休眠时间。所以我们可以将上述内容再定义成一个结构体类型Snake,用该结构体来维护贪吃蛇的所有属性。

//蛇的方向
enum SnakeDirection
{
	UP,//向上
	DOWN,//向下
	LEFT,//向左
	RIGHT//向右
};

//蛇的状态
enum SnakeStatue
{
	OK,//正常
	KILL_BY_SELF,//撞到自己
	KILL_BY_WALL,//撞到墙
	END_NOEMAL//正常退出
};

typedef struct Snake
{
	pSnakeNode _pSnake;//维护蛇头的指针
	pSnakeNode _pFood;//维护食物的指针
	int _Food_Weight;//食物权重
	int ——score;//总分数
	enum SnskeDirection _dir;//蛇的方向
	enum SnakeStatue _Statue;//蛇的状态
	int _Sleep_Time;//休眠时间
}Snake,*pSnake;

蛇的方向以及蛇的状态都是可以一一列举出来的,所以我们可以直接用枚举类型给出蛇的状态和方向。在以后游戏的运行过程中,我们就直接用Snake来维护贪吃蛇。

3.1蛇身的初始化

蛇身其实就是一个一个的节点,所以我们默认蛇身有两个节点,然后给出默认的出现坐标。这样就将节点创建好了。然后我们需要将每一个节点连接起来,这里我们用到了头插法,不了解的可以看这篇文章C——单链表-CSDN博客。

将链表连接起来之后,蛇的身体也就连接起来了。然后我们通过打印宽字符的方式打印蛇的身体。注意每一次打印的时候要先定位光标。

打印完成后,我们就要初始化贪吃蛇的其他属性了。这些都是在游戏开始之前就要设置好的。大家可以根据自己的需求自己更改初始化的各种数据,这里只是给出一个例子。

//蛇身的初始化
void InitSnake(pSnake ps)
{
	//假设初始蛇有两个节点
	int i = 0;
	pSnakeNode newnode = NULL;
	for (i = 0; i < 2; i++)
	{
		//创建蛇的节点
		newnode = (pSnakeNode)malloc(sizeof(SnakeNode));
		if (newnode == NULL)
		{
			perror("InitSnake()::malloc()");
			return;
		}
		newnode->Next = NULL;

		//设置坐标
		//默认蛇节点从(12,4)开始
		newnode->x = 12 + 2 * i;
		newnode->y = 4;

		//利用头插法将新节点连接到链表中
		if (ps->_pSnake == NULL)
		{
			//空链表
			ps->_pSnake = newnode;
		}
		else
		{
			//非空链表
			newnode->Next = ps->_pSnake;
			ps->_pSnake = newnode;
		}
	}

	//打印蛇身体
	pSnakeNode cur = ps->_pSnake;
	while (cur)
	{
		//定位坐标
		SetPos(cur->x, cur->y);
		wprintf(L"%lc", L'※');
		cur = cur->Next;
	}

	//设置蛇的属性
	ps->_dir = RIGHT;//默认方向向右
	ps->_Statue = OK;//默认状态OK
	ps->_Food_Weight = 10;//默认一个食物10分
	ps->_score = 0;//默认起始总分数为0分
	ps->_Sleep_Time = 200;//默认休眠时间,单位毫秒
}

我们这样就初始化好了蛇身以及墙体。但是万一我们以后 想要改变墙体或者蛇的身体的时候就非常麻烦,要在多个地方进行修改。我们可以将墙体和蛇身定义成宏,以后想要修改的时候直接在头文件中修改即可。

#define WALL L'□'
#define BODY L'※'

后面遇到需要用墙体和蛇身的时候直接用WALL和BODY替代。

当然了,默认的初始位置也是可以改变的。所以我们将初始位置也定义成宏放在头文件中。

#define POX_X 12
#define POX_Y 4

3.2食物的初始化

食物是在地图上随机生成的,所以我们要用到rand来产生随机数,而调用rand要使用srand。

srand((unsigned int)time(NULL));

我们知道不管是墙还是蛇身还是食物都是宽字符,占两个字节,所以他们的x坐标应该都是2的倍数。所以这就是为什么我之前再初始化蛇身的坐标是会乘上2。

而对于食物来说,不仅是要x坐标是2的倍数,而且还得再墙体呢。我们墙x坐标的范围是0~58,又因为一个墙占两个字节,所以食物的x范围应该在2~54之间。 墙的y坐标的范围是0~26,所以食物的y范围应该在1~25之间。

而对于rand来说只能产生0~100的随机数。那怎么办呢?

2~54可以看作(0~52)+2,1~25可以看作(0~24)+1。

所以当我们要产生x的随机数时让其模上53加上2,就可以产生2~54之间的随机数;产生y的随机数时让其模上25加上1,就可以产生1~25之间的随机数。

产生随机数后,我们还得判断x坐标是否为2的倍数,然后在判断是否与蛇身的节点重合。满足这些要求之后,才可以作为食物的坐标。对于食物来说也一样,为了修改方便,我们将其定义成宏放在头文件中。

#define FOOD L'★'
//食物的初始化
void InitFood(pSnake ps)
{
	int x = 0;
	int y = 0;
again:
	do
	{
		//食物的坐标要随机生成
		x = rand() % 53 + 2;//产生2~54的随机数
		y = rand() % 25 + 1;//产生1~26的随机数
	} while (x % 2 != 0);

	//食物不能和蛇的身体重叠
	pSnakeNode cur = ps->_pSnake;
	while (cur)
	{
		if (cur->x == x && cur->y == y)
		{
			goto again;
		}
		cur = cur->Next;
	}

	//创建食物节点
	pSnakeNode food = (pSnakeNode)malloc(sizeof(SnakeNode));
	if (food == NULL)
	{
		perror("InitFood()::malloc()");
		return;
	}
	food->Next = NULL;
	food->x = x;
	food->y = y;

	//打印食物
	SetPos(food->x, food->y);
	wprintf(L"%lc", FOOD);

	//将食物让pSnake贪吃蛇维护
	ps->_pFood = food;
}

从上图可以很明显的看出,食物的节点是随机出现的,也并没有与蛇的节点重合。 

四.游戏的运行

经过上面的步骤,我们已经完成了游戏的初始化部分,接下来就是游戏的运行逻辑了。

1.打印帮助信息

当我们进入游戏界面后,我们最好在地图旁边的空白部分加上一些帮助信息,帮助游戏人更好的进行游戏。我们可以告诉他们游戏的使用方法,游戏的一些功能以及游戏失败的分类。

void PrintHelpInfo(pSnake ps)
{
	//定位光标
	SetPos(65, 18);
	wprintf(L"用 ↑. ↓ . ← . → 来控制蛇的移动");
	SetPos(65, 19);
	printf("按Shift加速,Ctrl减速");
	SetPos(65, 20);
	printf("加速可以得到更高的分数");
	SetPos(65, 21);
	printf("小心不要撞到自己和墙");
	SetPos(65, 22);
	printf("ESC退出游戏,SPACE暂定游戏");
}

大家可以先不管上面的score以及foodweight。

2.蛇移动的准备工作

蛇的移动其实就是每次走一步的过程,之所以游戏里面看起来好像一直在走就是因为休眠的时间很短。而蛇走的前提就是蛇的状态得是正常的。所以我们将蛇的移动逻辑写成do while循环,来保证蛇可以一直移动,判断条件就是蛇的状态是正常的就可以移动,如果不是则推出循环,游戏结束。

2.1蛇移动的方向

我们之前规定了蛇默认是向右移动的,但是在其运动过程中,方向是可以更改的。所以在移动之前我们先设置蛇移动的方向。如果没有设置,那就默认向右。

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

这里就需要用到我们之前判断的是否按过某个键。如果我们按了一次上键,那么我们就将蛇的方向设为向上,但是前提是蛇不能向下移动。如果贪吃蛇正在向下移动,你却按了上键,此时是没法反应的。所以其他方向也同理。

2.2加速减速

在移动之前除了要设置方向外,还要设置速度。速度反应的其实就是休眠的时间长短。我们前面规定了shift为加速,ctrl为减速。当我们按了shift或者ctrl之后,就要对贪吃蛇的休眠时间属性进行修改。当然了,加速之后,食物的权重就可以增加,反之减小。

//加速
ps->_Sleep_Time -= 20;
ps->_Food_Weight += 2;
//减速
ps->_Sleep_Time += 20;
ps->_Food_Weight -= 2;

但是,我们可以让他一直加速或者减速下去么?当然不行了。所以对于速度的上限和下限都要有限定。我们既可以用时间限定也可以用分值限定。

else if (KEY_PRESS(VK_SHIFT))
{
	//加速
	//限定速度最大值
	if(ps->_Sleep_Time > 100)
	{
		ps->_Sleep_Time -= 20;
		ps->_Food_Weight += 2;
	}
}
else if (KEY_PRESS(VK_CONTROL))
{
	//减速
	//限定最小分数,分数不能为0
	if(ps->_Food_Weight >= 2)
	{
		ps->_Sleep_Time += 20;
		ps->_Food_Weight -= 2;
	}
}

2.3暂停与退出

我们在运行过程中,可能按空格,使游戏暂停;也可能按ESC退出游戏。所以在移动之前也要判断我们是否按下了这些键。

退出游戏其实就是将贪吃蛇的状态设置成了END_NORAML,走完一一步后,do while循环检查贪吃蛇状态,发现不是OK,此时就会退出该循环,使游戏结束。

else if (KEY_PRESS(VK_ESCAPE))
{
	//退出
	ps->_Statue = END_NOEMAL;
}

使游戏暂停,其实就是增长了休眠时间,我们可以写一个休眠函数,让他死循环的进行休眠,当然在休眠的过程中也要判断是否按下了空格键,打破了暂停;或者按下了ESC退出了游戏。

//暂停
void Suspend_time_out(pSnake ps)
{
	do
	{
		Sleep(300);
		if (KEY_PRESS(VK_SPACE))
		{
			break;
		}
		else if (KEY_PRESS(VK_ESCAPE))
		{
			ps->_Statue = END_NOEMAL;
		}
	} while (1);
}
		
else if (KEY_PRESS(VK_SPACE))
{
	//暂停
    Suspend_time_out(ps);
}

到此,就完成了我们贪吃蛇走一步的准备工作,下面将整个准备工作合并在一块就行了。

//游戏运行
void GameRun(pSnake ps)
{
	//首先在游戏右边打印帮助信息
	PrintHelpInfo(ps);

	//检测按过哪个键
	do
	{
		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_SHIFT))
		{
			//加速
			//限定速度最大值
			if(ps->_Sleep_Time > 100)
			{
				ps->_Sleep_Time -= 20;
				ps->_Food_Weight += 2;
			}
		}
		else if (KEY_PRESS(VK_CONTROL))
		{
			//减速
			//限定最小分数,分数不能为0
			if(ps->_Food_Weight >= 2)
			{
				ps->_Sleep_Time += 20;
				ps->_Food_Weight -= 2;
			}
		}
		else if (KEY_PRESS(VK_SPACE))
		{
			//暂停
			Suspend_time_out(ps);
		}
		else if (KEY_PRESS(VK_ESCAPE))
		{
			//退出
			ps->_Statue = END_NOEMAL;
		}
	} while (ps->_Statue == OK);
}

3.蛇走一步

蛇肯定不会只走一步,所以蛇走一步这个过程也应该在do while()循环中。蛇走一步的逻辑其实可以这样来完成:我们已经知道了蛇移动的方向,所以我们可以先计算出蛇下一个位置的坐标,然后创建一个新节点将这个坐标信息保存起来。接下来分析这个节点是否使食物,如果是食物那就吃掉食物,如果不是食物,那就往前走一步。完成这一步之后,还得判断是否撞到了墙或者自己。

所以蛇走一步的逻辑大概是这样的:

void SnakeMove(pSnake ps)
{
	//创建新节点

	//计算下一个位置的坐标,并保存到新结点中

	//判断下一个位置是不是食物
	//吃掉食物
	//不是食物

	//判断是否撞到墙或者自己
}

3.1计算下一个位置的坐标

我们计算完坐标之后还要将其保存到一个新结点中。所以我们先创建一个新的节点

//先创建一个新节点
pSnakeNode node = (pSnakeNode)malloc(sizeof(SnakeNode));
if (node == NULL)
{
    perror("SnskeMove()::malloc()");
	return;
}

计算下一个位置的坐标:

计算的前提是知道蛇移动的方向。如果蛇向上移动,那么x坐标不变,y-1;如果蛇向下移动,那么x坐标不变,y+1;如果蛇向左移动,那么y坐标不变,x-2;如果蛇向右移动,那么y坐标不变,x+2。

//计算下一个位置的坐标
switch (ps->_dir)
{
case UP:
{
	node->x = ps->_pSnake->x;
	node->y = ps->_pSnake->y - 1;
	break;
}
case DOWN:
{
	node->x = ps->_pSnake->x;
	node->y = ps->_pSnake->y + 1;
	break;
}
case LEFT:
{
	node->x = ps->_pSnake->x - 2;
	node->y = ps->_pSnake->y;
	break;
}
case RIGHT:
{
	node->x = ps->_pSnake->x + 2;
	node->y = ps->_pSnake->y;
	break;
}
}

3.2判断下一个位置是不是食物

判断是不是食物,我们只需要将该节点的x,y坐标与贪吃蛇的属性中的食物的x,y坐标进行比较就行。

//下一个位置是不是食物
int NextPositionIsFood(pSnake ps,pSnakeNode pnode)
{
	return ((ps->_pFood->x == pnode->x) && (ps->_pFood->y == pnode->y));
}

3.3下一个位置是食物

如果下一个位置是食物,那么我们就吃掉食物,也就是将食物节点头插到链表中即可。而这里的节点和食物指向的是同一个位置,但是我们却动态开辟了两次,所以我们只需要将其中一个连接到链表中,然后将另一个销毁掉,防止内存泄漏。然后再打印蛇身,并创建一个新的食物。吃掉食物之后,我们的总分也应该增加,所以总分还要加上此时的分值。

//吃食物
void EatFood(pSnake ps,pSnakeNode pnode)
{
	//此时pnode和食物节点是同一个节点,我们可任选一个头插到链表中,然后经另一个给释放掉
	pnode->Next = ps->_pSnake;
	ps->_pSnake = pnode;

	//释放食物节点
	free(ps->_pFood);
	ps->_pFood = NULL;

	//创建一个新食物
	InitFood(ps);

	//吃了一个食物总分数要增加
	ps->_score += ps->_Food_Weight;

	//打印蛇身
	pSnakeNode cur = ps->_pSnake;
	while (cur)
	{
		//定位光标
		SetPos(cur->x, cur->y);
		wprintf(L"%lc", BODY);
		cur = cur->Next;
	}
}

3.4下一个位置不是食物

如果下一个位置不是食物的话,我们就要往前走一步。其实走一步的过程可以转化为将新节点作为贪吃蛇的头部,然后释放掉最后一个节点即可。所以我们依旧先将节点头插到链表中,然后打印蛇身,同时还要找到尾节点,此时就该释放尾节点了。释放了就完了么?

当然不是,尾节点这个位置已经打印了一次蛇身,为了清除掉这个蛇身,我们还要再这个位置上打印两个空白字符,将不用的身体给覆盖掉。

那么怎么找尾节点呢?

我们可以利用cur->next->next来作为判断条件来寻找尾节点。就如上图,cur首先指向头节点,此时cur->next->ntxt!=NULL,所以打印这个节点,然后cur走到下一个位置,在进行判断;此时cur->next->next == NULL,所以此时不进入while循环,这就找到了倒数第二个节点,也就相当于找到了尾节点。

//不是食物
void NoFood(pSnake ps, pSnakeNode pnode)
{
	//将pnode节点作为新的蛇头,然后释放掉贪吃蛇的最后一个节点,并在其位置上打印两个空白字符,覆盖掉原先的蛇身
	pnode->Next = ps->_pSnake;
	ps->_pSnake = pnode;

	pSnakeNode cur = ps->_pSnake;
	while (cur->Next->Next)
	{
		SetPos(cur->x, cur->y);
		wprintf(L"%lc", BODY);
		cur = cur->Next;
	}
	//此时cur为倒数第二个节点
	SetPos(cur->Next->x, cur->Next->y);

	//打印两个空格,覆盖原来的蛇身
	printf("  ");
	free(cur->Next);
	cur->Next = NULL;
}

有的人可能会有疑问了:蛇身明明有两个,而while循环只执行了一次,所以不是少打印了一个蛇身么?这代码不是有问题么?

其实是没有问题的,为什么呢?因为这个位置上本来已经打印了一次蛇身了。就算不打印也不会影响。所以只需要打印除了倒数第一和倒数第二的蛇身即可。然后在尾节点出打印两个空白格即可。

到这里我们的贪吃蛇已经可以走起来了:

QQ202475-183434

我们从视频中得出,虽然我们完成了贪吃蛇移动的过程,但是此时的贪吃蛇还没有完成撞到墙或者自己导致游戏结束的情况。我们接下来完成这两步。

4.撞墙

其实贪吃蛇是否撞墙的判断是非常简单的,我们只需要判断蛇头与x轴和y轴的关系即可。蛇头的x坐标不可以等于0、56否则就会撞到左右两边的墙,蛇头的y坐标不可以等于0、26否则就会撞到上下两边的墙。

如果撞到了,那就将蛇的状态设置为KILL_BY_WALL,然后返回,如果没有撞到,直接返回就行。

//撞墙
void KillByWall(pSnake ps)
{
	pSnakeNode cur = ps->_pSnake;
	if (cur->x == 0 || cur->x == 56)
	{
		ps->_Statue = KILL_BY_WALL;
		return;
	}
	else if (cur->y == 0 || cur->y == 26)
	{
		ps->_Statue = KILL_BY_WALL;
		return;
	}
	else
	{
		return;
	}
}

5.撞自己

撞到自己的判定条件和撞到墙相差不大,只不过是要将头节点与蛇的剩余节点进行比较,只要蛇头与某个蛇身的x,y坐标都相等,就说明撞上了,否则就没有。

//撞到自己
void KillBySelf(pSnake ps)
{
	pSnakeNode next = ps->_pSnake->Next;
	while (next)
	{
		if (ps->_pSnake->x == next->x && ps->_pSnake->y == next->y)
		{
			ps->_Statue = KILL_BY_SELF;
			return;
		}
		next = next->Next;
	}
}

6.总分数以及食物权重 

我们在视频中看到,在地图的右边显示了我们的得分,以及食物的权重,我们每走一步,就要打印这些信息,以免他们发生变化。所以他们也是包含在游戏运行的do while()循环中。

//打印总分数以及单个食物的分数
SetPos(69, 7);
printf("Score:%2d", ps->_score);
SetPos(69, 8);
printf("Food Weight:%2d", ps->_Food_Weight);

到这里,我们贪吃蛇的移动就完成了。接下来就是游戏结束后的善后工作。

五.游戏结束

我们游戏结束之后,可以提示一下玩家,他们是怎么输的,将信息打印到屏幕上。然后就是蛇身的销毁。蛇身都是一个一个的节点,是通过malloc动态申请的内存空间。当我们使用完后,最好将链表给销毁掉,防止内存泄漏。

//销毁链表
void ReleaseSnake(pSnake ps)
{
	//销毁链表
	pSnakeNode cur = ps->_pSnake;
	pSnakeNode next = NULL;
	while (cur)
	{
		next = cur->Next;
		free(cur);
		cur = next;
	}
	ps = NULL;
}

//游戏结束
//善后工作
void GameOver(pSnake ps)
{
	//判断游戏是怎样结束的
	if (ps->_Statue == END_NOEMAL)
	{
		SetPos(16, 7);
		printf("You voluntarily quit the game");
	}
	else if (ps->_Statue == KILL_BY_WALL)
	{
		SetPos(16, 7);
		printf("You've hit a wall");
	}
	else if (ps->_Statue == KILL_BY_SELF)
	{
		SetPos(16, 7);
		printf("You bumped into yourself");
	}
	//释放节点
	ReleaseSnake(ps);
	ps = NULL;
}

六.游戏完善

到这里,游戏已经写完了,我们在这里再进行完善。如果我们玩儿完了一局还想再开一局,此时就要重新运行程序,很麻烦,我们可不可以在游戏结束后给出选项,yes or no,选择yes就直接再来一局,选择no就退出游戏。

我们的逻辑就只有三个:

	//游戏的初始化
	GameInit(&s);

	//游戏运行
	GameRun(&s);

	//游戏结束
	GameOver(&s);

所有的功能都在这三个函数中执行。所以为了能够多次进行游戏,我们可以将这三个函数写进循环中。

void Game()
{
	Snake s = { 0 };
	int ch = 0;
	do
	{
		//游戏的初始化
		GameInit(&s);

		//游戏运行
		GameRun(&s);

		//游戏结束
		GameOver(&s);
		s._pSnake = NULL;

		Sleep(1000);
		system("cls");
		SetPos(35, 12);
		printf("Do you want another round?");
		SetPos(45, 13);
		printf("Y/N");
		ch = _getch();
	} while (ch == 'Y' || ch == 'y');
}

利用这个循环我们就可以实现多次游戏。当然,如果我们不想玩了此时就回到了主函数,我们在主函数也要打印退出游戏的信息来提示用户。

int main()
{
	//程序一开始就先设置本地化
	setlocale(LC_ALL, "");

	srand((unsigned int)time(NULL));

	//游戏运行逻辑
	Game();
	system("cls");
	SetPos(37, 12);
	printf("Exit the game");
	SetPos(0, 25);
	return 0;
}

到这里,我们的贪吃蛇就已经写完了。大家可以根据自己的想法再添加一些其他的功能。下面给出源代码。

七.Snake.h

#pragma once
#include <stdio.h>
#include <stdlib.h>
#include <windows.h>
#include <stdbool.h>
#include <locale.h>
#include <time.h>
#include <conio.h>

#define WALL L'□'
#define BODY L'※'
#define POX_X 12
#define POX_Y 4
#define FOOD L'★'
#define KEY_PRESS(VK) ((GetAsyncKeyState(VK)&0x1) ? 1 : 0)

//蛇身的节点或者食物节点
typedef struct SnakeNode
{
	int x;
	int y;
	struct SnakeNode* Next;
}SnakeNode,* pSnakeNode;

//蛇的方向
enum SnakeDirection
{
	UP,//向上
	DOWN,//向下
	LEFT,//向左
	RIGHT//向右
};

//蛇的状态
enum SnakeStatue
{
	OK,//正常
	KILL_BY_SELF,//撞到自己
	KILL_BY_WALL,//撞到墙
	END_NOEMAL//正常退出
};

typedef struct Snake
{
	pSnakeNode _pSnake;//维护蛇头的指针
	pSnakeNode _pFood;//维护食物的指针
	int _Food_Weight;//食物权重
	int _score;//总分数
	enum SnskeDirection _dir;//蛇的方向
	enum SnakeStatue _Statue;//蛇的状态
	int _Sleep_Time;//休眠时间
}Snake,*pSnake;

//游戏运行逻辑
void Game();

//定位光标
void SetPos(int x, int y);

//游戏的初始化
void GameInit(pSnake ps);

//初始化窗口
void InitWindow();

//欢迎界面的打印
void welcome();

//地图的打印
void CreatMap();

//蛇身的初始化
void InitSnake(pSnake ps);

//食物的初始化
void InitFood(pSnake ps);

//游戏运行
void GameRun(pSnake ps);

//蛇走一步
void SnakeMove(pSnake ps);

//吃食物
void EatFood(pSnake ps, pSnakeNode pnode);

//不是食物
void NoFood(pSnake ps, pSnakeNode pnode);

//撞墙
void KillByWall(pSnake ps);

//撞到自己
void KillBySelf(pSnake ps);

//游戏结束
void GameOver(pSnake ps);

八.Snake.c

#define _CRT_SECURE_NO_WARNINGS 1
#include "Snake.h"

//定位光标
void SetPos(int x, int y)
{
	//获取屏幕句柄
	HANDLE houtput = GetStdHandle(STD_OUTPUT_HANDLE);

	//定位新坐标
	COORD pos = { x,y };

	//设置光标位置
	SetConsoleCursorPosition(houtput, pos);
}

//初始化窗口
void InitWindow()
{
	//设置窗口大小
	system("mode con cols=100 lines=30");

	//设置窗口名称
	system("title 贪吃蛇");

	//隐藏光标
	//获取屏幕的句柄(操作权)
	HANDLE houtput = GetStdHandle(STD_OUTPUT_HANDLE);

	//定义一个光标信息的结构体
	CONSOLE_CURSOR_INFO cursor_info = { 0 };

	//获取屏幕光标的操作权
	GetConsoleCursorInfo(houtput, &cursor_info);

	//修改光标的可见性
	cursor_info.bVisible = false;

	//设置光标信息
	SetConsoleCursorInfo(houtput, &cursor_info);
}

//欢迎界面的打印
void welcome()
{
	//定位光标
	SetPos(40, 15);
	printf("欢迎来到贪吃蛇\n");
	SetPos(35, 25);
	system("pause");
	system("cls");
	
	//打印帮助信息
	SetPos(25, 15);
	wprintf(L"用 ↑. ↓ . ← . → 来控制蛇的移动,按Shift加速,Ctrl减速\n");
	SetPos(35, 25);
	system("pause");
	system("cls");
}

//地图的打印
void CreatMap()
{
	int i = 0;
	//上墙
	for (i = 0; i < 29; i++)
	{
		wprintf(L"%lc ", WALL);
	}
	//下墙
	SetPos(0, 26);
	for (i = 0; i < 29; i++)
	{
		wprintf(L"%lc ", WALL);
	}
	//左墙
	for (i = 1; i < 27; i++)
	{
		SetPos(0, i);
		wprintf(L"%lc", WALL);
	}
	//右墙
	for (i = 1; i < 27; i++)
	{
		SetPos(56,i);
		wprintf(L"%lc", WALL);
	}
}

//蛇身的初始化
void InitSnake(pSnake ps)
{
	//假设初始蛇有两个节点
	int i = 0;
	pSnakeNode newnode = NULL;
	for (i = 0; i < 2; i++)
	{
		//创建蛇的节点
		newnode = (pSnakeNode)malloc(sizeof(SnakeNode));
		if (newnode == NULL)
		{
			perror("InitSnake()::malloc()");
			return;
		}
		newnode->Next = NULL;

		//设置坐标
		//默认蛇节点从(12,4)开始
		newnode->x = POX_X + 2 * i;
		newnode->y = POX_Y;

		//利用头插法将新节点连接到链表中
		if (ps->_pSnake == NULL)
		{
			//空链表
			ps->_pSnake = newnode;
		}
		else
		{
			//非空链表
			newnode->Next = ps->_pSnake;
			ps->_pSnake = newnode;
		}
	}

	//打印蛇身体
	pSnakeNode cur = ps->_pSnake;
	while (cur)
	{
		//定位坐标
		SetPos(cur->x, cur->y);
		wprintf(L"%lc", BODY);
		cur = cur->Next;
	}

	//设置蛇的属性
	ps->_dir = RIGHT;//默认方向向右
	ps->_Statue = OK;//默认状态OK
	ps->_Food_Weight = 10;//默认一个食物10分
	ps->_score = 0;//默认起始总分数为0分
	ps->_Sleep_Time = 200;//默认休眠时间,单位毫秒
}

//食物的初始化
void InitFood(pSnake ps)
{
	int x = 0;
	int y = 0;
again:
	do
	{
		//食物的坐标要随机生成
		x = rand() % 53 + 2;//产生2~54的随机数
		y = rand() % 25 + 1;//产生1~26的随机数
	} while (x % 2 != 0);

	//食物不能和蛇的身体重叠
	pSnakeNode cur = ps->_pSnake;
	while (cur)
	{
		if (cur->x == x && cur->y == y)
		{
			goto again;
		}
		cur = cur->Next;
	}

	//创建食物节点
	pSnakeNode food = (pSnakeNode)malloc(sizeof(SnakeNode));
	if (food == NULL)
	{
		perror("InitFood()::malloc()");
		return;
	}
	food->Next = NULL;
	food->x = x;
	food->y = y;

	//打印食物
	SetPos(food->x, food->y);
	wprintf(L"%lc", FOOD);

	//将食物让pSnake贪吃蛇维护
	ps->_pFood = food;
}

//游戏的初始化
void GameInit(pSnake ps)
{
	//初始化窗口
	InitWindow();

	//欢迎界面的打印
	welcome();

	//地图的打印
	CreatMap();

	//蛇身的初始化
	InitSnake(ps);

	//食物的初始化
	InitFood(ps);
}

void PrintHelpInfo(pSnake ps)
{
	//定位光标
	SetPos(65, 18);
	wprintf(L"用 ↑. ↓ . ← . → 来控制蛇的移动");
	SetPos(65, 19);
	printf("按Shift加速,Ctrl减速");
	SetPos(65, 20);
	printf("加速可以得到更高的分数");
	SetPos(65, 21);
	printf("小心不要撞到自己和墙");
	SetPos(65, 22);
	printf("ESC退出游戏,SPACE暂定游戏");
}

//暂停
void Suspend_time_out(pSnake ps)
{
	do
	{
		Sleep(300);
		if (KEY_PRESS(VK_SPACE))
		{
			break;
		}
		else if (KEY_PRESS(VK_ESCAPE))
		{
			ps->_Statue = END_NOEMAL;
		}
	} while (1);
}

//下一个位置是不是食物
int NextPositionIsFood(pSnake ps,pSnakeNode pnode)
{
	return ((ps->_pFood->x == pnode->x) && (ps->_pFood->y == pnode->y));
}

//吃食物
void EatFood(pSnake ps,pSnakeNode pnode)
{
	//此时pnode和食物节点是同一个节点,我们可任选一个头插到链表中,然后经另一个给释放掉
	pnode->Next = ps->_pSnake;
	ps->_pSnake = pnode;

	//释放食物节点
	free(ps->_pFood);
	ps->_pFood = NULL;

	//创建一个新食物
	InitFood(ps);

	//吃了一个食物总分数要增加
	ps->_score += ps->_Food_Weight;

	//打印蛇身
	pSnakeNode cur = ps->_pSnake;
	while (cur)
	{
		//定位光标
		SetPos(cur->x, cur->y);
		wprintf(L"%lc", BODY);
		cur = cur->Next;
	}
}

//不是食物
void NoFood(pSnake ps, pSnakeNode pnode)
{
	//将pnode节点作为新的蛇头,然后释放掉贪吃蛇的最后一个节点,并在其位置上打印两个空白字符,覆盖掉原先的蛇身
	pnode->Next = ps->_pSnake;
	ps->_pSnake = pnode;

	pSnakeNode cur = ps->_pSnake;
	while (cur->Next->Next)
	{
		SetPos(cur->x, cur->y);
		wprintf(L"%lc", BODY);
		cur = cur->Next;
	}
	//此时cur为倒数第二个节点
	SetPos(cur->Next->x, cur->Next->y);

	//打印两个空格,覆盖原来的蛇身
	printf("  ");
	free(cur->Next);
	cur->Next = NULL;
}

//撞墙
void KillByWall(pSnake ps)
{
	pSnakeNode cur = ps->_pSnake;
	if (cur->x == 0 || cur->x == 56)
	{
		ps->_Statue = KILL_BY_WALL;
		return;
	}
	else if (cur->y == 0 || cur->y == 26)
	{
		ps->_Statue = KILL_BY_WALL;
		return;
	}
	else
	{
		return;
	}
}

//撞到自己
void KillBySelf(pSnake ps)
{
	pSnakeNode next = ps->_pSnake->Next;
	while (next)
	{
		if (ps->_pSnake->x == next->x && ps->_pSnake->y == next->y)
		{
			ps->_Statue = KILL_BY_SELF;
			return;
		}
		next = next->Next;
	}
}

//蛇走一步
void SnakeMove(pSnake ps)
{
	//先创建一个新节点
	pSnakeNode node = (pSnakeNode)malloc(sizeof(SnakeNode));
	if (node == NULL)
	{
		perror("SnskeMove()::malloc()");
		return;
	}

	//计算下一个位置的坐标
	switch (ps->_dir)
	{
	case UP:
	{
		node->x = ps->_pSnake->x;
		node->y = ps->_pSnake->y - 1;
		break;
	}
	case DOWN:
	{
		node->x = ps->_pSnake->x;
		node->y = ps->_pSnake->y + 1;
		break;
	}
	case LEFT:
	{
		node->x = ps->_pSnake->x - 2;
		node->y = ps->_pSnake->y;
		break;
	}
	case RIGHT:
	{
		node->x = ps->_pSnake->x + 2;
		node->y = ps->_pSnake->y;
		break;
	}
	}

	//判断下一个位置是不是食物
	//是食物
	if (NextPositionIsFood(ps,node))
	{
		//吃食物
		EatFood(ps,node);
	}
	//不是食物
	else
	{
		NoFood(ps,node);
	}

	//蛇在走一步的过程中可能会撞到墙或者自己
	//撞墙
	KillByWall(ps);

	//撞到自己
	KillBySelf(ps);
}

//游戏运行
void GameRun(pSnake ps)
{
	//首先在游戏右边打印帮助信息
	PrintHelpInfo(ps);

	//检测按过哪个键
	do
	{
		//打印总分数以及单个食物的分数
		SetPos(69, 7);
		printf("Score:%2d", ps->_score);
		SetPos(69, 8);
		printf("Food Weight:%2d", ps->_Food_Weight);

		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_SHIFT))
		{
			//加速
			//限定速度最大值
			if(ps->_Sleep_Time > 100)
			{
				ps->_Sleep_Time -= 20;
				ps->_Food_Weight += 2;
			}
		}
		else if (KEY_PRESS(VK_CONTROL))
		{
			//减速
			//限定最小分数,分数不能为0
			if(ps->_Food_Weight >= 2)
			{
				ps->_Sleep_Time += 20;
				ps->_Food_Weight -= 2;
			}
		}
		else if (KEY_PRESS(VK_SPACE))
		{
			//暂停
			Suspend_time_out(ps);
		}
		else if (KEY_PRESS(VK_ESCAPE))
		{
			//退出
			ps->_Statue = END_NOEMAL;
			break;
		}

		//蛇走一步
		SnakeMove(ps);

		Sleep(ps->_Sleep_Time);

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

//销毁链表
void ReleaseSnake(pSnake ps)
{
	//销毁链表
	pSnakeNode cur = ps->_pSnake;
	pSnakeNode next = NULL;
	while (cur)
	{
		next = cur->Next;
		free(cur);
		cur = next;
	}
	ps = NULL;
}

//游戏结束
//善后工作
void GameOver(pSnake ps)
{
	//判断游戏是怎样结束的
	if (ps->_Statue == END_NOEMAL)
	{
		SetPos(16, 7);
		printf("You voluntarily quit the game");
	}
	else if (ps->_Statue == KILL_BY_WALL)
	{
		SetPos(16, 7);
		printf("You've hit a wall");
	}
	else if (ps->_Statue == KILL_BY_SELF)
	{
		SetPos(16, 7);
		printf("You bumped into yourself");
	}
	//释放节点
	ReleaseSnake(ps);
	ps = NULL;
}

九.test.c

#define _CRT_SECURE_NO_WARNINGS 1
#include "Snake.h"

void Game()
{
	Snake s = { 0 };
	int ch = 0;
	do
	{
		//游戏的初始化
		GameInit(&s);

		//游戏运行
		GameRun(&s);

		//游戏结束
		GameOver(&s);
		s._pSnake = NULL;

		Sleep(1000);
		system("cls");
		SetPos(35, 12);
		printf("Do you want another round?");
		SetPos(45, 13);
		printf("Y/N");
		ch = _getch();
	} while (ch == 'Y' || ch == 'y');
}

int main()
{
	//程序一开始就先设置本地化
	setlocale(LC_ALL, "");

	srand((unsigned int)time(NULL));

	//游戏运行逻辑
	Game();
	system("cls");
	SetPos(37, 12);
	printf("Exit the game");
	SetPos(0, 25);
	return 0;
}

七八九均是文件名 

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

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

相关文章

生产力工具|viso常用常见科学素材包

一、科学插图素材网站 一图胜千言&#xff0c;想要使自己的论文或重要汇报更加引人入胜&#xff1f;不妨考虑利用各类示意图和科学插图来辅助研究工作。特别是对于新手或者繁忙的科研人员而言&#xff0c;利用免费的在线科学插图素材库&#xff0c;能够极大地节省时间和精力。 …

基于CNN卷积神经网络的步态识别matlab仿真,数据库采用CASIA库

目录 1.算法运行效果图预览 2.算法运行软件版本 3.部分核心程序 4.算法理论概述 4.1步态识别系统框架 4.2 CNN原理及数学表述 4.3 CASIA步态数据库 5.算法完整程序工程 1.算法运行效果图预览 (完整程序运行后无水印) 1.训练过程 2.样本库 3.提取的步态能量图 4.步态识…

【YOLOv5/v7改进系列】改进池化层为ASPP

一、导言 Atrous Spatial Pyramid Pooling (ASPP)模块是一种用于多尺度特征提取的创新技术&#xff0c;旨在提升深度学习模型在语义图像分割任务中的表现。ASPP模块通过在不同的采样率下应用空洞卷积&#xff0c;可以捕获不同大小的对象以及图像的上下文信息&#xff0c;从而增…

Activity、Window、DecorView的关系

目录 一、Activity、Window、DecorView的层级关系如下图所示&#xff1a; 1、Activity 2、Window 3、DecorView 二、DecorView初始化相关源码 三、DecorView显示时机 前言&#xff1a; 不同的Android版本有差异&#xff0c;以下基于Android 11进行讲解。 一、Activi…

昇思25天学习打卡营第13天|linchenfengxue

Diffusion扩散模型 关于扩散模型&#xff08;Diffusion Models&#xff09;有很多种理解&#xff0c;本文的介绍是基于denoising diffusion probabilistic model &#xff08;DDPM&#xff09;&#xff0c;DDPM已经在&#xff08;无&#xff09;条件图像/音频/视频生成领域取得…

【ARMv8/v9 GIC 系列 5.1 -- GIC GICD_CTRL Enable 1 of N Wakeup Function】

请阅读【ARM GICv3/v4 实战学习 】 文章目录 GIC Enable 1 of N Wakeup Function基本原理工作机制配置方式应用场景小结GIC Enable 1 of N Wakeup Function 在ARM GICv3(Generic Interrupt Controller第三代)规范中,引入了一个名为"Enable 1 of N Wakeup"的功能。…

2024年【湖北省安全员-C证】考试资料及湖北省安全员-C证考试试卷

题库来源&#xff1a;安全生产模拟考试一点通公众号小程序 湖北省安全员-C证考试资料是安全生产模拟考试一点通生成的&#xff0c;湖北省安全员-C证证模拟考试题库是根据湖北省安全员-C证最新版教材汇编出湖北省安全员-C证仿真模拟考试。2024年【湖北省安全员-C证】考试资料及…

7.1作业6

uart4.h #ifndef __UART4_H__ #define __UART4_H__ #include "stm32mp1xx_gpio.h" #include "stm32mp1xx_rcc.h" #include "stm32mp1xx_uart.h" //rcc/gpio/uart4初始化 void hal_uart4_init(); //发送一个字符 void hal_put_char(const char s…

探索Sui的面向对象模型和Move编程语言

Sui区块链作为一种新兴的一层协议&#xff08;L1&#xff09;&#xff0c;采用先进技术来解决常见的一层协议权衡问题。Cointelegraph Research详细剖析了这一区块链新秀。 Sui使用Move编程语言&#xff0c;该语言专注于资产表示和访问控制。本文探讨了Sui的对象中心数据存储模…

滑动窗口(C++)

文章目录 1、长度最小的子数组2、无重复字符的最长子串3、最大连续1的个数 Ⅲ4、将x减到0的最小操作数5、水果成篮6、找到字符串中所有字母异位词7、串联所有单词的子串8、最小覆盖子串 通常&#xff0c;算法的主体说明会放在第一道题中。但实际上&#xff0c;不通常。 算法在代…

Solr安装IK中文分词器

Solr安装IK中文分词器 如何安装Solr与导入数据&#xff1f;为什么要安装中文分词器下载与安装IK分词器1.1、下载IK分词器1.2、安装IK  第一步&#xff1a;非常简单&#xff0c;我们直接将在下的Ik分词器的jar包移动到以下文件夹中  第二步&#xff1a;修改Core文件夹名下\c…

代理设计模式和装饰器设计模式的区别

代理设计模式: 作用:为目标(原始对象)增加功能(额外功能,拓展功能) 三种经典应用场景: 1&#xff1a;给原始对象增加额外功能(spring添加事务,Mybatis通过代理实现缓存功能等等) 2&#xff1a;远程代理&#xff08;网络通信&#xff0c;输出传输&#xff08;RPC&#xff0c;D…

Motion Guidance: 扩散模型实现图像精确编辑的创新方法

在深度学习领域&#xff0c;扩散模型&#xff08;diffusion models&#xff09;因其能够根据文本描述生成高质量图像而备受关注。然而&#xff0c;这些模型在精确编辑图像中对象的布局、位置、姿态和形状方面仍存在挑战。本文提出了一种名为“运动引导”&#xff08;motion gui…

图书馆数据仓库

目录 1.数据仓库的数据来源为业务数据库&#xff08;mysql&#xff09; 初始化脚本 init_book_result.sql 2.通过sqoop将mysql中的业务数据导入到大数据平台&#xff08;hive&#xff09; 导入mysql数据到hive中 3.通过hive进行数据计算和数据分析 形成数据报表 4.再通过sq…

如何取消闪迪Micro SD卡的写保护?这个技巧很有效!

由于受写保护影响&#xff0c;无法格式化闪迪Micro SD卡&#xff1f;别担心&#xff01;通过本文你可以学习如何解除闪迪Micro SD卡的写保护。 我的闪迪SD卡有写保护怎么办&#xff1f; “我打算格式化我的闪迪SD卡。但当我进行格式化时&#xff0c;提示我磁盘被写保护。我想用…

Linux配置固定ip地址

虚拟机的Linux操作系统&#xff0c;其IP地址是通过DHCP服务获取的 DHCP&#xff1a;动态获取IP地址&#xff0c;即每次重启设备后都会获取一次&#xff0c;可能导致IP地址频繁变更。 一般系统默认的ip地址设置都是自动获取&#xff0c;故每次系统重启后ip地址都可能会不一样&a…

数字化产科管理平台全套源码,java产科电子病历系统源码

数字化产科管理平台全套成品源码&#xff0c;产科电子病历系统源码&#xff0c;多家大型妇幼专科医院应用案例。源码完全授权交付。 数字化产科管理平台&#xff08;智慧产科系统&#xff09;是为医院产科量身定制的信息管理系统。它管理了孕妇从怀孕开始到生产结束42天以内的一…

欢乐钓鱼大师攻略:西沙群岛攻略,内置自动辅助云手机!

《欢乐钓鱼大师》是一款以钓鱼为主题的休闲游戏&#xff0c;玩家可以在虚拟的钓鱼世界中体验真实的钓鱼乐趣&#xff0c;并通过捕捉各种珍稀鱼类来提升自己的钓鱼技能和成就。在这篇攻略中&#xff0c;我们将重点介绍如何在西沙群岛区域有效地捕捉各种典藏鱼类&#xff0c;并提…

数据结构之顺序表专题

在学习数据结构之前我们要先了解什么是数据结构&#xff1f; 1.数据结构相关概念 1.什么是数据结构&#xff1f; 数据结构是由“数据”和“结构”两词组合而来。 什么是数据?常见的数值1、2、3、4.、教务系统里保存的用户信息(姓名、性别、年龄、学历等等)、网页里肉眼可以…

Qt项目:基于Qt实现的网络聊天室---注册模块

文章目录 基本页面设计创建登录界面创建注册界面优化样式完善注册类界面 客户端逻辑完善客户端增加post逻辑客户端配置管理 邮箱注册服务认证服务读取配置邮箱验证服务联调设置验证码过期封装redis操作类封装redis连接池注册功能Server端接受注册请求封装mysql连接池封装DAO操作…