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

发布于:2024-07-06 ⋅ 阅读:(15) ⋅ 点赞:(0)

一.游戏背景

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

接下来,我们利用编程来实现这个小游戏,既可以巩固我们的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;
}

七八九均是文件名 


网站公告

今日签到

点亮在社区的每一天
去签到