贪吃蛇小游戏C语言全实现

介绍:

这张图展示的想必是我们再熟悉不过的一个小游戏,想来我们一定听说过贪吃蛇,吃掉食物,增加自身长度,这款游戏一直以来也有着不小的热度;

而我们在玩这款游戏的时候,是否有思考过,我们所操控的这个蛇初始是怎样生成的?又是如何运动、如何增加自身长度的?又是以一种怎样的方式宣告它的结束?

那今天这篇文章,就来像大家展示一个,用C语言实现一个简易版贪吃蛇的全过程,来向大家展示,贪吃蛇最初的长度是如何生成的,它如何移动以及如何吃掉食物等一系列问题,来帮大家更好的了解这个游戏的本质,相信大家在看完这篇文章后,会对贪吃蛇的代码实现有一个基础的认知,

下面先来向大家展示一下我们最终完工后这一简易贪吃蛇的成果:

那接下来我们就正式开始实现了。

1.部分API函数的简单介绍:

要实现贪吃蛇这个小游戏,那就不得不先提到API函数,对于刚刚接触计算机语言的朋友来说,这个函数可能略微有些陌生,那我们就先对其来个简单的介绍:

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

1.1.控制台程序:

平时我们每一次运行代码,代码的结果都会显示在运行结束后弹出来的一个框框里,那这个框框,也就是我们的控制台,我们这次要执行的贪吃蛇项目,也是在我们的控制台上执行

因此,在执行贪吃蛇之前,我们可以将控制台的大小和标题都改至符合项目的情况:

我们可以使用cmd指令来操控控制台的大小:

mode con cols=100 lines=30

cols表示列,lines表示行,在控制台上,每一行的大小是两倍的列的大小,其具体位置和行列的大小关系对应情况如下:

当我们使用这个命令后,控制台的大小就变为了30行、100列;

Mode命令详细可参考https://learn.microsoft.com/zh-cn/windows-server/administration/windows-commands/modeicon-default.png?t=N7T8https://learn.microsoft.com/zh-cn/windows-server/administration/windows-commands/modetitle命令可修改控制台的名称

title 贪吃蛇

title命令详细可参考

https://learn.microsoft.com/zh-cn/windows-server/administration/windows-commands/titleicon-default.png?t=N7T8https://learn.microsoft.com/zh-cn/windows-server/administration/windows-commands/title

1.2.控制台屏幕上的坐标COORD:

COORD是Windows API中定义的⼀个结构体,表示一个字符在控制台屏幕幕缓冲区上的坐标,坐标系(0,0)的原点位于缓冲区的顶部左侧单元格。

1.2.1.COORD类型的声明:

typedef struct _COORD {
SHORT X;
SHORT Y;
} COORD, *PCOORD;

给坐标赋值:

COORD pos = { 30, 15 };

1.3.GetStdHandle

HANDLE GetStdHandle(DWORD nStdHandle);

该API函数,它用于从⼀个特定的标准设备(标准输入、标准输出或标准错误)中取得⼀个句柄(⽤来标识不同设备的数值),使用这个句柄可以操作设备。

当我们需要对控制台进行任何操作时,意味着我们要对控制台的输出信息进行调整,因此我们要获得控制台的输出句柄,该句柄就像我们日常生活中使用的遥控器,控制台就相当于一个电视机,有了输出句柄我们才能对控制台上的输出信息进行操作:

那如何获得输出句柄呢,下面是实例操作:

//获得输出句柄
HANDLE Houtput = GetStdHandle(STD_OUTPUT_HANDLE);

1.4.GetConsoleCursorInfo

检索有关指定控制台屏幕缓冲区的光标大小和可见性的信息:

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

1.4.1.CONSOLE_CURSOR_INFO

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

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

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

隐藏光标:

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

1.5.SetConsoleCursorInfo

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

前面我们所使用的GetConsoleCursorInfo是为了获得我们正在使用的控制台的光标的信息,而当我们修改了这个信息之后,我们还需要把他重新运用到我们的控制台上,那这个时候就需要使用到SetConsoleCursorInfo这个API函数:

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

实例操作:

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

1.6.SetConsoleCursorPosition

设置光标的位置,该API函数的调用,可以直接使光标的位置移动到指定位置:

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

hConsoleOutput [in]
控制台屏幕缓冲区的句柄。 该句柄必须具有 GENERIC_READ 访问权限。 有关详细信息,请参阅控制台缓冲区安全性和访问权限icon-default.png?t=N7T8https://learn.microsoft.com/zh-cn/windows/console/console-buffer-security-and-access-rightsdwCursorPosition [in]
指定新光标位置(以字符为单位)的 COORD 结构。 坐标是屏幕缓冲区字符单元的列和行。 坐标必须位于控制台屏幕缓冲区的边界以内。

1.7.GetAsyncKeyState

当我们操作贪吃蛇的时候,一定是通过按键盘上的某些按键来操控贪吃蛇,那这个时候就需要使用到API函数GetAsyncKeyState,这个函数会判断我们是否按到过这个按键,我们再来通过这个返回值来进行对应功能的实现,该函数的原型如下:

SHORT GetAsyncKeyState(
int vKey
);

2.贪吃蛇游戏的实现:

在介绍完一部分我们会使用到的API函数后,我们就可以正式开始实现代码了,事实上API函数很多很多,有着成千上万种不同的功能,我们所了解的只是冰山一角,如果大家有兴趣的话当然可以可以自行了解;

贪吃蛇游戏的实现总共分为三个部分:游戏开始(GameStart)、游戏运行(GameRun)、游戏结束(GameOver),我们来一一进行实现:

2.1.GameStart

在该功能中,我们主要要实现的有以下几点:

1.控制台大小和名称的调整;

2.光标的隐藏;

3.欢迎界面的打印;

4.贪吃蛇游戏地图的创建;

5.贪吃蛇的创建和初始化;

6.食物的创建和初始化;

在实现这些功能之前,我们需要先实现两个非常简单的操作,虽然很简单,但是他们非常重要,这其中的一个操作是实现一个函数,这个函数贯穿我们整个贪吃蛇游戏,我们无时无刻不在使用它,他就是我们控制台光标位置的指定函数SetPos();

2.1.01.光标位置的指定SetPos的实现:

为了实现这个函数,我们当然是要结合到我们前面所了解到的API函数GetStdHandle、和SetConsoleCursorPosition,将光标设置到我们指定的x、y位置,实现如下:

void SetPos(short x, short y)
{
	//获得输出句柄
	HANDLE Houtput = GetStdHandle(STD_OUTPUT_HANDLE);
	COORD pos = { x,y }; //设置我们需要位置的坐标
	SetConsoleCursorPosition(Houtput, pos);  
}

在实现完这个函数后,我们就要开始实现第二个操作:

2.1.02.本地化:

这第二个非常简单的操作,当然就是本地化,我们在实现贪吃蛇这一游戏的时候,一定会使用到宽字符,为防止有些朋友对宽字符可能并无了解,这里简单介绍一下:

我们平常使用printf输出在控制台或者是终端上的字符,都是标准字符,站一个字节位,而我们的宽字符,会占据两个字节位,这里给出一张图便于大家直接感受:

a就是我们平常使用的标准字符,而 ● 就是我们在贪吃蛇游戏中要使用到的宽字符,我们可以明显发现,宽字符的站位大小是标准字符的站位大小的两倍,我们也可以发现,我们的中文汉字也是站两个字节位大小的;

但我们编译器本身提供的环境(也就是C环境)是不支持宽字符的(如果对宽字符不曾了解,可关注我后序的文章,会详细讲解,这里只需初步了解),也没有函数能将其输出在控制台上,但我们的当地的环境,也就是中国地区的环境下,是能支持宽字符的打印的,这个时候我们就要将环境调整为本地环境,这个时候,我们就要使用到locale.h头文件中的setlocale函数(其具体使用会和宽字符一同讲解,这里只需初步了解),将环境改为本地环境:

setlocale(LC_ALL, "");

在实现完这两个简单的操作后,我们就可以上手实现GameStart函数中的各种功能了;

2.1.1.控制台大小和名称的调整:

使用前面介绍过的mode和title命令,直接实现对控制台的调整:

void AdjustConsole()
{
	//调整控制台的大小
	system("mode con cols=100 lines=30");
	//修改控制台的名称
	system("title 贪吃蛇");
}

2.1.2.光标的隐藏

先创建出CONSOLE_CURSOR_INFO类型的结构体,利用句柄使其成为我们要使用的这个控制台的光标信息,将其中的是否可见变量设置为false(要包含头文件<stdbool.h>),再使用SetConsoleCursorInfo完成修改:

void HideCursorInfo()
{
	//获得输出句柄
	HANDLE Houtput = GetStdHandle(STD_OUTPUT_HANDLE);
	CONSOLE_CURSOR_INFO CursorInfo = { 0 };
	GetConsoleCursorInfo(Houtput, &CursorInfo);
	CursorInfo.bVisible = false;
	SetConsoleCursorInfo(Houtput, &CursorInfo);
}

2.1.3.欢迎界面的打印

void Welcome()

将光标调整到合适位置,打印欢迎界面的信息;

别忘了使用pause命令停顿,使我们贪吃蛇游戏的流程更加清晰明了:

void Welcome()
{
	SetPos(38, 13);
	wprintf(L"欢迎来到贪吃蛇小游戏");
	SetPos(38, 20);
	system("pause");
}

效果展示:

在实现完这个界面后,我们最好给出一个游戏说明的界面,能让玩家在游戏开始前提前了解到游戏的规则,如下:

void PrintHelp()
{
	SetPos(30, 10);
	wprintf(L"↑、↓、←、→分别控制蛇的上下左右移动");
	SetPos(30, 11);
	wprintf(L"空格(space)表示暂停,Esc表示正常退出游戏");
	SetPos(30, 13);
	wprintf(L"争取获得更高的分数");
	SetPos(30, 22);
	system("pause");
}

效果展示:

2.1.4.贪吃蛇游戏地图的创建

void CreateMap(pSnack ps)

我们整个控制台的大小是100列,30行,在创建地图之前,我们要确定好地图的大小,这里我们以27*56的地图大小来做示范,大家也可根据自身习惯,来决定地图的大小;

地图的样貌(27 * 58)如下图所示:

我们需要用到宽字符 □ 来作为地图的边框,来分别打印出四条地图的边框:

以打印上图来做示范,以此类推,打印出左右下的地图边框:

27*56地图的上边框共有28个 □ ,所以共要循环28次,如下:

SetPos(0, 0);
for (int i = 0; i < 56; i += 2)
{
	wprintf(L"□");
}

以次方法再打印出左右下的边框:

//下
SetPos(0, 26);
for (int i = 0; i < 56; i += 2)
{
	wprintf(L"□");
}
//左
for (int i = 1; i < 26; i++)
{
	SetPos(0, i);
	wprintf(L"□");
}
//右
for (int i = 1; i < 26; i++)
{
	SetPos(54, i);
	wprintf(L"□");
}

效果展示:

为了便于视觉上的体验,我们还能在地图的附近选择合适的位置给出提示信息:

//打印出帮助信息
SetPos(60, 19);
wprintf(L"↑、↓、←、→分别控制蛇的上下左右移动");
SetPos(60, 21);
wprintf(L"空格(space)表示暂停");
SetPos(60, 23);
wprintf(L"Esc表示正常退出游戏");

2.1.5.贪吃蛇的创建和初始化

贪吃蛇是基于链表实现的,因此贪吃蛇的每一个节点都相当于是一个链表的结点,我们知道,链表中的每一个结点都是由两个部分组成的,一个数节点中储存的数据,另一个是指向下一个结点的指针,而贪吃蛇的一个结点中存储的数据,是该节点的坐标位置,我们可以通过这个坐标,来打印出贪吃蛇的这一个结点,如下所示,我们先定义出贪吃蛇的节点:

//定义的一个结点的坐标
typedef struct CoordinateOfSnackNode
{
	short x;
	short y;
}NodeCoord;

//定义一个蛇的结点————由链表实现
typedef struct SnackNode
{
	NodeCoord coord;
	struct SnakcNode* next;
}SnackNode, * pSnackNode;

在定义出蛇的节点后,我们不妨再把整个贪吃蛇要用到的所有数据同时定义出来,这便是很重要的面向对象的思维如下,整个贪吃蛇应包含如下信息:

//蛇的移动方向
enum DIRECTION
{
	UP,
	DOWN,
	LEFT,
	RIGHT
};

//蛇的状态
enum STATE
{
	OK,
	END_NORMAL,
	HIT_BYSELF,
	HIT_BYWALL
};

//定义一整条蛇————包含如下信息
typedef struct Snack
{
	//蛇头
	pSnackNode _SnackHead;
	//蛇的移动方向
	enum DIRECTION _dire;
	//每走一步的休眠时间
	int _sleeptime;
	//蛇的状态
	enum STATE _state;
	//食物
	pSnackNode _food;
	//一个食物的分数
	int _foodweight;
	//当前获得的总分数
	int _score;
}Snack, * pSnack;

下面来进行蛇的初始化:

我们默认开始的贪吃蛇本身有5个蛇结点,每个结点的申请构造方式如下

2.1.5.1.BuySnackNode
pSnackNode BuySnackNode(NodeCoord coord)

利用malloc申请空间,最后返回申请到的节点:

pSnackNode BuySnackNode(NodeCoord coord)
{
	pSnackNode newnode = (pSnackNode)malloc(sizeof(SnackNode));
	if (newnode == NULL)
	{
		perror("BuySnackNode()::malloc");
		exit(-1);
	}
	newnode->coord = coord;
	newnode->next = NULL;

	return newnode;
}
2.1.5.2.SnackInit

我们假定第一个蛇节点的位置为x=24,y=6(根据个人习惯),来循环创建结点,最后采用头插的方式并接到蛇身上:

void SnackInit(pSnack ps)
{
	//初始蛇有5个结点,假设蛇头的初位置为24,6;
	for (int i = 0; i < 5; i++)
	{
		NodeCoord coord = { POS_X + i * 2, POS_Y };
		if (ps->_SnackHead == NULL)
		{
			ps->_SnackHead = BuySnackNode(coord);
		}
		else
		{
			//头插数据
			pSnackNode newnode = BuySnackNode(coord);
			newnode->next = ps->_SnackHead;
			ps->_SnackHead = newnode;
		}
	}
}

既然创建好了蛇,我们当然是要将其显示出来,因此我们创建一个函数,这个函数的功能就是打印出整条蛇,蛇的打印,相当于链表的遍历,这里我们为了可视性,将蛇头用宽字符方块表示,蛇身用宽字符圆点表示:如下所示:

void ShowSnack(pSnack ps)
{
	pSnackNode cur = ps->_SnackHead;
	while (cur)
	{
		if (cur == ps->_SnackHead)
		{
			SetPos(cur->coord.x, cur->coord.y);
			wprintf(L"%lc", HEAD);
		}
		else
		{
			SetPos(cur->coord.x, cur->coord.y);
			wprintf(L"%lc", BODY);
		}
		cur = cur->next;
	}
}

2.1.6.食物的创建和初始化

食物的本身也是一个蛇结点,所以我们仍然可以使用BuySnackNode来创建食物,只不过我们食物的坐标需要随机生成,这个时候我们就要想起随机数的生成方法:

srand利用时间戳在每时每刻获得不同的unsigned int类型的值,然后用rand函数就可以生成随机数,这里不要忘了包含头文件time.h和stdlib.h,

srand的调用如下:

//调用srand函数,便于生成随机数
srand((unsigned int)time(NULL));

有了随机数的生成,我们就能随机生成食物的坐标,但这里有两个需要注意的地方:

1.食物坐标的x要是2的倍数,否则无法贪吃蛇的头结点无法与食物重合,也就无法吃到食物;

2.食物的坐标不能和蛇身重合,当生成的随机数与蛇身有重合,应使用goto语句再次生成,直到符合条件为止;

再创造完食物后可将食物给显示出来,食物我们用宽字符五角星来表示;

如下所示:

void CreateFood(pSnack ps)
{
	int x = 0, y = 0;
	again:
	//食物的坐标要在地图范围内,同时x要是2的倍数
	do
	{
		x = 2 + rand() % (52 - 2 + 1);
		y = 1 + rand() % (25 - 1 + 1);
	} while (x % 2 != 0);

	//坐标不能与蛇身重合
	pSnackNode cur = ps->_SnackHead;
	while (cur)
	{
		if (cur->coord.x == x && cur->coord.y == y)
			goto again;
		cur = cur->next;
	}

	NodeCoord FoodPos = { x,y };
	ps->_food = BuySnackNode(FoodPos);

	//显示出食物
	SetPos(ps->_food->coord.x, ps->_food->coord.y);
	wprintf(L"%lc", FOOD);
}

再将贪吃蛇中未初始化的几个变量初始化一下,那整个GameStart函数就算实现好了;

//初始化蛇自身的部分
ps->_state = OK;//状态默认OK
ps->_dire = RIGHT;//方向默认向右
ps->_sleeptime = 200;//每走一步中间的间隔时间默认为200ms;
ps->_SnackHead = NULL;
//初始化食物的其余数据
ps->_foodweight = 10;
ps->_score = 0;

GameStart整体效果展示:

2.2.GameRun

游戏运行,这个功能的实现就比较直接了,简单来说,这个函数要做的就是通过按键,来改变贪吃蛇中的各个变量,再根据各个变量的改变来进行对应功能的实现:

“↑、↓、←、→分别控制蛇的上下左右移动”

“空格(space)表示暂停”

“Esc表示正常退出游戏”

这是我们的玩法说明,怎么判断我们是否按了这几个按键,这就需要使用到我们前面提到的API函数GetAsyncKeyState,该函数中传入对应按键的虚拟按键值,当按下这个按键之后,该函数的二进制最低位会返回1,否则返回0,以此我们就可以来定义一个宏KET_PRESS(VK),来判断我们是否按下过这个按键,用GetAsyncKeyState的返回值按位与(&)上一个1之后,得到的就是他的二进制最低位的数:

//定义一个宏,用于确定按键是否被按,按后则返回1,否则返回0;
#define KEY_PRESS(VK) (GetAsyncKeyState(VK) & 0x1)

那GameRun函数的执行,就是在一个do......while循环中,通过判断某个按键来改变蛇的状态,每循环一次蛇就会往指定方向走一步,每次循环都有间隔,间隔时间为蛇每走一步的休眠时间,那么do...while循环的结束条件是什么呢?在前面我们定义蛇的时候就知道,蛇有四个状态,用一个枚举变量表示出来,如下所示:

//蛇的状态
enum STATE
{
	OK,
	END_NORMAL,
	HIT_BYSELF,
	HIT_BYWALL
};

正常、正常结束、撞到自己和撞到墙,当我们蛇的状态是正常的时候,它就能持续运动,否则就会退出循环,结束游戏:

当按到上下左右按键时,对应的运动方向发生改变,按到空格时,就需要暂停(暂停的方法是使其休眠时间变为无穷),按到Esc时,状态变为自动结束,F3或者F4时,休眠的时间变长或者变短,对应一个食物的分数增多或者减少;

因此该函数的大体框架就如下所示:

void GameRun(pSnack ps)
{

	do
	{
		//显示得分情况
		ShowScore(ps);

		if (KEY_PRESS(VK_UP) && ps->_dire != DOWN)
			ps->_dire = UP;
		else if (KEY_PRESS(VK_DOWN) && ps->_dire != UP)
			ps->_dire = DOWN;
		else if (KEY_PRESS(VK_LEFT) && ps->_dire != RIGHT)
			ps->_dire = LEFT;
		else if (KEY_PRESS(VK_RIGHT) && ps->_dire != LEFT)
			ps->_dire = RIGHT;
		else if (KEY_PRESS(VK_SPACE))
			SleepForever();
		else if (KEY_PRESS(VK_ESCAPE))
			ps->_state = END_NORMAL;
		else if (KEY_PRESS(VK_F3))
		{
			//加速,每走一步休眠时间变短,得分更高
			if (ps->_sleeptime > 50)
			{
				ps->_sleeptime -= 30;//不能一直加速,最多加速5次
				ps->_foodweight += 2;
			}
		}
		else if (KEY_PRESS(VK_F4))
		{
			//加速,每走一步休眠时间变短,得分更高
			if (ps->_sleeptime < 320)
			{
				ps->_sleeptime += 30;//不能一直加速,最多加速4次
				ps->_foodweight -= 2;
			}
		}

		SnackMove(ps);
		ShowSnack(ps);
		Sleep(ps->_sleeptime);

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

}

然后就通过按键所改变的状态,决定是暂停。退出、或者是向哪边移动;

2.2.1.暂停

暂停函数,SleepForever,就是使其休眠时间无线延长,我们可以用一个死循环来表示,当然,再按一次空格,我们就退出暂停状态,因此这个循环中还需要存在是否按到空格这么一个宏

void SleepForever()
{
	while (1)
	{
		Sleep(200);
		if (KEY_PRESS(VK_SPACE))
		{
			//再按一次,继续运动
			break;
		}
	}
}

2.2.2.移动

void SnackMove(pSnack ps)

该函数的实现需要考虑到很多因素,大概思路如下所示,通过蛇的移动方向找到下一个结点,然后通过这下一个节点判断蛇是吃到食物、撞到自己还是撞墙、当这三者都不发生,蛇就是正常移动,我们先来找到下一个节点:

2.2.2.1.NextNode

根据对应状态,调整坐标,根据调整后的坐标申请节点,采用头插法使这个下一个结点成为新的头结点:

pSnackNode NextNode(pSnack ps)
{
	pSnackNode nextnode = NULL;
	switch (ps->_dire)
	{
	case UP:
	{
		NodeCoord coord = { ps->_SnackHead->coord.x,ps->_SnackHead->coord.y - 1 };
		nextnode = BuySnackNode(coord);
		break;
	}
	case DOWN:
	{
		NodeCoord coord = { ps->_SnackHead->coord.x,ps->_SnackHead->coord.y + 1 };
		nextnode = BuySnackNode(coord);
		break;
	}
	case RIGHT:
	{
		NodeCoord coord = { ps->_SnackHead->coord.x + 2,ps->_SnackHead->coord.y };
		nextnode = BuySnackNode(coord);
		break;
	}
	case LEFT:
	{
		NodeCoord coord = { ps->_SnackHead->coord.x - 2,ps->_SnackHead->coord.y };
		nextnode = BuySnackNode(coord);
		break;
	}
	}

	return nextnode;
}
2.2.2.2.判断是否撞墙或者撞到自己

当撞墙或者撞到自己,就改变贪吃蛇的状态,本次循环走完后就会结束do...while循环,意味着本次游戏的结束,当下一个结点的坐标和墙或者自己重合的时候,意味着撞墙或者撞到了自己,撞墙,即x为0或者54,y为0或26,撞到自己需要以此遍历自身结点,观察下一个结点的坐标是否与自身的某个结点重合,如下所示:

bool IsBeHitedByWall(pSnackNode nextnode)
{
	return nextnode->coord.x == 0 || nextnode->coord.x == 54 ||
		nextnode->coord.y == 0 || nextnode->coord.y == 26;
}


bool IsBeHitedBySelf(pSnack ps, pSnackNode nextnode)
{
	pSnackNode cur = ps->_SnackHead;
	while (cur)
	{
		if (cur->coord.x == nextnode->coord.x && cur->coord.y == nextnode->coord.y)
			return true;
		cur = cur->next;
	}

	return false;
}
2.2.2.3.下一个节点是不是食物
bool IsFood(pSnack ps, pSnackNode nextnode)

这是整个贪吃蛇游戏的一个比较核心的环节,贪吃蛇只有吃掉食物,自身长度才能增长,才能获得更高的得分,那怎么样才能吃到食物呢,首先,我们需要判断下一个结点是不是食物,即下一个结点的坐标和食物的坐标是否相同,若相同,则返回true,否则返回false,如下:

bool IsFood(pSnack ps, pSnackNode nextnode)
{
	return ps->_food->coord.x == nextnode->coord.x &&
		   ps->_food->coord.y == nextnode->coord.y;
}
2.2.2.4.吃掉食物
void EatFood(pSnack ps)

如果判断好下一个结点就是食物(IsFood的返回值为true),那这个时候我们就应该吃掉食物,吃食物的方法很简单,我们只掉,食物本身也是贪吃蛇的一个节点,那这个时候,我们只需要让这个食物成为贪吃蛇这个链表的头结点,还是采用简单的头插数据就可以实现,但不要忘了,吃掉食物之后一定记得再生成一个新的食物,如下所示:

void EatFood(pSnack ps)
{
	//食物成为新一个头结点,蛇的结点增加
	ps->_food->next = ps->_SnackHead;
	ps->_SnackHead = ps->_food;

	//加分
	ps->_score += ps->_foodweight;

	//创造一个新食物
	CreateFood(ps);
}
2.2.2.5.正常移动Move

如果说贪吃蛇既没有吃到食物,也没有撞墙或者是撞到自己,那么它就需要正常移动,将下一个结点采用头插法插入到贪吃蛇的链表中,同时需要尾删贪吃蛇的最后一个蛇结点,因为贪吃蛇的正常移动,它的蛇身长度是不会改变的,只是它的位置改变了,删除节点我们需要使用到free函数

但不要忘了,再释放最后一个结点之前,应该将最后一个节点对应坐标处用两个空格覆盖掉,要不然控制台所展示出来的就不会是正常移动的图像,而是一条越拉越长的蛇,该函数的实现如下:

void Move(pSnack ps, pSnackNode nextnode)
{
	nextnode->next = ps->_SnackHead;
	ps->_SnackHead = nextnode;
	pSnackNode cur = ps->_SnackHead;
	pSnackNode prev = NULL;
	while (cur->next != NULL)
	{
		prev = cur;
		cur = cur->next;
	}
	SetPos(cur->coord.x, cur->coord.y);
	printf("  ");
	free(cur);
	prev->next = NULL;
}

在实现完以上功能后,我们SnackMove的整体框架和逻辑就出现了,如下:

void SnackMove(pSnack ps)
{
	//下一个头结点
	pSnackNode nextnode = NextNode(ps);

	//判断下一个头结点是否是食物
	if (IsFood(ps, nextnode))
	{
		//吃掉食物
		EatFood(ps);
	}
	//判断是否撞墙
	else if (IsBeHitedByWall(nextnode))
	{
		ps->_state = HIT_BYWALL;
	}
	//判断是否撞到自己
	else if (IsBeHitedBySelf(ps, nextnode))
	{
		ps->_state = HIT_BYSELF;
	}
	else
	{
		//正常移动
		Move(ps, nextnode);
	}
}

那当我们移动一步后,我们当然就需要把整条贪吃蛇再一次显示在控制台上,也就是再调用一次ShowSnack函数,这样来看,贪吃蛇移动一步就算是已经实现好了,我们只需要在每次移动的间隙中利用Sleep命令停顿上我们所设置好的休眠时间,就可以实现贪吃蛇游戏的运行:

2.3.GameOver

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

游戏结束这个函数功能中主要由两点要实现的功能:

1.游戏是如何结束的声明,是主动结束还是撞到墙,亦或是撞到自己;

2.贪吃蛇是基于链表实现的,而链表的每个结点,包括食物,都是利用动态内存开辟malloc在堆上开辟的空间,因此我们需要释放掉这部分空间,避免造成内存泄露;整体的实现也比较简单,只需遍历整个链表,释放当前结点前,储存好下一个结点的指针,再依次向后跑,如下所示:

void GameOver(pSnack ps)
{
	SetPos(40, 12);
	if (ps->_state == HIT_BYWALL)
		printf("您已撞墙\n");
	else if (ps->_state == HIT_BYSELF)
		printf("您撞到了自己\n");
	else if (ps->_state == END_NORMAL)
		printf("您主动结束了游戏\n");

	pSnackNode cur = ps->_SnackHead;
	while (cur)
	{
		pSnackNode next = cur->next;
		free(cur);
		cur = next;
	}

	free(ps->_food);
	ps->_food = NULL;

}

游戏到这那我们的贪吃蛇游戏基本就算完全实现好了,我们就可以在主函数中依次调用这三个函数,来进行贪吃蛇游戏,如果我们想要在一局游戏结束后,通过某一个条件来判断我们是否要重新再开一把游戏,那这个时候我们就可以发挥自己的想象,用一个do...while循环来包含这三个函数,通过某一条件来确定循环是否继续执行,这里也就不再过多介绍,参考代码会放在整篇文章的最后;

3.结语:

整体来说,贪吃蛇小游戏这个项目,虽然说并不是很复杂,但是它完美结合了很多C语言的知识,在我们实现贪吃蛇的过程中,也算是对我们以往知识的一个复习与巩固,我们所实现的只不过是一个很简单的贪吃蛇基层逻辑,这个游戏还有很多可以优化的地方,这可以锻炼我们的思维,在学习的同时,我们一定要着手写出代码,只有多写才会有进步,对代码才会更加熟练;

那这篇文章就介绍到这里,感谢大家观看!

4.参考代码:

4.1.snack.h

#pragma once

//贪吃蛇小游戏的完全实现
#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
#include <time.h>
#include <Windows.h>
#include <locale.h>


//定义的一个结点的坐标
typedef struct CoordinateOfSnackNode
{
	short x;
	short y;
}NodeCoord;

//定义一个蛇的结点————由链表实现
typedef struct SnackNode
{
	NodeCoord coord;
	struct SnakcNode* next;
}SnackNode, * pSnackNode;

//蛇的移动方向
enum DIRECTION
{
	UP,
	DOWN,
	LEFT,
	RIGHT
};

//蛇的状态
enum STATE
{
	OK,
	END_NORMAL,
	HIT_BYSELF,
	HIT_BYWALL
};

//定义一整条蛇————包含如下信息
typedef struct Snack
{
	//蛇头
	pSnackNode _SnackHead;
	//蛇的移动方向
	enum DIRECTION _dire;
	//每走一步的休眠时间
	int _sleeptime;
	//蛇的状态
	enum STATE _state;
	//食物
	pSnackNode _food;
	//一个食物的分数
	int _foodweight;
	//当前获得的总分数
	int _score;
}Snack, * pSnack;


//游戏开始
void GameStart(pSnack ps);

//设置光标位置
void SetPos(short x, short y);

//隐藏光标
void HideCursorInfo();

//调整好控制台的大小和名称
void AdjustConsole();

//打印欢迎界面
void Welcome();

//打印游戏说明界面
void PrintHelp();

//创造游戏地图
void CreateMap(pSnack ps);

//蛇头的初位置
#define POS_X 24
#define POS_Y 5

//初始化蛇身
void SnackInit(pSnack ps);

//创建蛇的一个结点
pSnackNode BuySnackNode(NodeCoord coord);

//打印蛇
//蛇头
#define HEAD L'■'
//蛇身
#define BODY L'●'
void ShowSnack(pSnack ps);

//创造一个食物
#define FOOD L'★'
void CreateFood(pSnack ps);

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

//定义一个宏,用于确定按键是否被按,按后则返回1,否则返回0;
#define KEY_PRESS(VK) (GetAsyncKeyState(VK) & 0x1)

//显示得分情况
void ShowScore(pSnack ps);

//空格space暂停
void SleepForever();

//移动一步
void SnackMove(pSnack ps);

//下一个头结点
pSnackNode NextNode(pSnack ps);

//判断下一个结点是否是食物
bool IsFood(pSnack ps, pSnackNode nextnode);

//吃掉食物
void EatFood(pSnack ps);

//判断是否撞墙
bool IsBeHitedByWall(pSnackNode nextnode);

//判断是否撞到自己
bool IsBeHitedBySelf(pSnack ps, pSnackNode nextnode);

//正常移动
void Move(pSnack ps, pSnackNode nextnode);

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

4.2.snack.c

#define  _CRT_SECURE_NO_WARNINGS 1

#include "snack.h"

//设置光标位置
void SetPos(short x, short y)
{
	//获得输出句柄
	HANDLE Houtput = GetStdHandle(STD_OUTPUT_HANDLE);
	COORD pos = { x,y };
	SetConsoleCursorPosition(Houtput, pos);
}


void HideCursorInfo()
{
	//获得输出句柄
	HANDLE Houtput = GetStdHandle(STD_OUTPUT_HANDLE);
	CONSOLE_CURSOR_INFO CursorInfo = { 0 };
	GetConsoleCursorInfo(Houtput, &CursorInfo);
	CursorInfo.bVisible = false;
	SetConsoleCursorInfo(Houtput, &CursorInfo);
}


void AdjustConsole()
{
	//调整控制台的大小
	system("mode con cols=100 lines=30");
	//修改控制台的名称
	system("title 贪吃蛇");
}


void Welcome()
{
	SetPos(38, 13);
	wprintf(L"欢迎来到贪吃蛇小游戏");
	SetPos(38, 20);
	system("pause");
}


void PrintHelp()
{
	SetPos(30, 10);
	wprintf(L"↑、↓、←、→分别控制蛇的上下左右移动");
	SetPos(30, 11);
	wprintf(L"空格(space)表示暂停,Esc表示正常退出游戏");
	SetPos(30, 13);
	wprintf(L"争取获得更高的分数");
	SetPos(30, 22);
	system("pause");
}


void CreateMap(pSnack ps)
{
	//以56*27的大小来创建地图
	//上
	SetPos(0, 0);
	for (int i = 0; i < 56; i += 2)
	{
		wprintf(L"□");
	}
	//下
	SetPos(0, 26);
	for (int i = 0; i < 56; i += 2)
	{
		wprintf(L"□");
	}
	//左
	for (int i = 1; i < 26; i++)
	{
		SetPos(0, i);
		wprintf(L"□");
	}
	//右
	for (int i = 1; i < 26; i++)
	{
		SetPos(54, i);
		wprintf(L"□");
	}
	//打印出帮助信息
	SetPos(60, 19);
	wprintf(L"↑、↓、←、→分别控制蛇的上下左右移动");
	SetPos(60, 21);
	wprintf(L"空格(space)表示暂停");
	SetPos(60, 23);
	wprintf(L"Esc表示正常退出游戏");
}


pSnackNode BuySnackNode(NodeCoord coord)
{
	pSnackNode newnode = (pSnackNode)malloc(sizeof(SnackNode));
	if (newnode == NULL)
	{
		perror("BuySnackNode()::malloc");
		exit(-1);
	}
	newnode->coord = coord;
	newnode->next = NULL;

	return newnode;
}


void SnackInit(pSnack ps)
{
	//初始蛇有5个结点,假设蛇头的初位置为24,6;
	for (int i = 0; i < 5; i++)
	{
		NodeCoord coord = { POS_X + i * 2, POS_Y };
		if (ps->_SnackHead == NULL)
		{
			ps->_SnackHead = BuySnackNode(coord);
		}
		else
		{
			//头插数据
			pSnackNode newnode = BuySnackNode(coord);
			newnode->next = ps->_SnackHead;
			ps->_SnackHead = newnode;
		}
	}
}


void ShowSnack(pSnack ps)
{
	pSnackNode cur = ps->_SnackHead;
	while (cur)
	{
		if (cur == ps->_SnackHead)
		{
			SetPos(cur->coord.x, cur->coord.y);
			wprintf(L"%lc", HEAD);
		}
		else
		{
			SetPos(cur->coord.x, cur->coord.y);
			wprintf(L"%lc", BODY);
		}
		cur = cur->next;
	}
}


void CreateFood(pSnack ps)
{
	int x = 0, y = 0;
	again:
	//食物的坐标要在地图范围内,同时x要是2的倍数
	do
	{
		x = 2 + rand() % (52 - 2 + 1);
		y = 1 + rand() % (25 - 1 + 1);
	} while (x % 2 != 0);

	//坐标不能与蛇身重合
	pSnackNode cur = ps->_SnackHead;
	while (cur)
	{
		if (cur->coord.x == x && cur->coord.y == y)
			goto again;
		cur = cur->next;
	}

	NodeCoord FoodPos = { x,y };
	ps->_food = BuySnackNode(FoodPos);

	//显示出食物
	SetPos(ps->_food->coord.x, ps->_food->coord.y);
	wprintf(L"%lc", FOOD);
}


void GameStart(pSnack ps)
{
	//隐藏光标
	HideCursorInfo();
	//调整好控制台的大小和名称
	AdjustConsole();

	//初始化蛇自身的部分
	ps->_state = OK;//状态默认OK
	ps->_dire = RIGHT;//方向默认向右
	ps->_sleeptime = 200;//每走一步中间的间隔时间默认为200ms;
	ps->_SnackHead = NULL;

	//打印欢迎界面
	Welcome();
	system("cls");
	//打印游戏说明界面
	PrintHelp();
	system("cls");
	//创造游戏地图
	CreateMap(ps);

	//初始化蛇身
	SnackInit(ps);
	//打印蛇
	ShowSnack(ps);

	//创造一个食物
	CreateFood(ps);

	//初始化食物的其余数据
	ps->_foodweight = 10;
	ps->_score = 0;
}


void ShowScore(pSnack ps)
{
	SetPos(60, 10);
	wprintf(L"一个食物的分数为%2d", ps->_foodweight);
	SetPos(60, 11);
	wprintf(L"目前总的分数为%2d", ps->_score);

}


void SleepForever()
{
	while (1)
	{
		Sleep(200);
		if (KEY_PRESS(VK_SPACE))
		{
			//再按一次,继续运动
			break;
		}
	}
}


pSnackNode NextNode(pSnack ps)
{
	pSnackNode nextnode = NULL;
	switch (ps->_dire)
	{
	case UP:
	{
		NodeCoord coord = { ps->_SnackHead->coord.x,ps->_SnackHead->coord.y - 1 };
		nextnode = BuySnackNode(coord);
		break;
	}
	case DOWN:
	{
		NodeCoord coord = { ps->_SnackHead->coord.x,ps->_SnackHead->coord.y + 1 };
		nextnode = BuySnackNode(coord);
		break;
	}
	case RIGHT:
	{
		NodeCoord coord = { ps->_SnackHead->coord.x + 2,ps->_SnackHead->coord.y };
		nextnode = BuySnackNode(coord);
		break;
	}
	case LEFT:
	{
		NodeCoord coord = { ps->_SnackHead->coord.x - 2,ps->_SnackHead->coord.y };
		nextnode = BuySnackNode(coord);
		break;
	}
	}

	return nextnode;
}


bool IsFood(pSnack ps, pSnackNode nextnode)
{
	return ps->_food->coord.x == nextnode->coord.x &&
		   ps->_food->coord.y == nextnode->coord.y;
}


void EatFood(pSnack ps)
{
	//食物成为新一个头结点,蛇的结点增加
	ps->_food->next = ps->_SnackHead;
	ps->_SnackHead = ps->_food;

	//加分
	ps->_score += ps->_foodweight;

	//创造一个新食物
	CreateFood(ps);
}


bool IsBeHitedByWall(pSnackNode nextnode)
{
	return nextnode->coord.x == 0 || nextnode->coord.x == 54 ||
		nextnode->coord.y == 0 || nextnode->coord.y == 26;
}


bool IsBeHitedBySelf(pSnack ps, pSnackNode nextnode)
{
	pSnackNode cur = ps->_SnackHead;
	while (cur)
	{
		if (cur->coord.x == nextnode->coord.x && cur->coord.y == nextnode->coord.y)
			return true;
		cur = cur->next;
	}

	return false;
}


void Move(pSnack ps, pSnackNode nextnode)
{
	nextnode->next = ps->_SnackHead;
	ps->_SnackHead = nextnode;
	pSnackNode cur = ps->_SnackHead;
	pSnackNode prev = NULL;
	while (cur->next != NULL)
	{
		prev = cur;
		cur = cur->next;
	}
	SetPos(cur->coord.x, cur->coord.y);
	printf("  ");
	free(cur);
	prev->next = NULL;
}


void SnackMove(pSnack ps)
{
	//下一个头结点
	pSnackNode nextnode = NextNode(ps);

	//判断下一个头结点是否是食物
	if (IsFood(ps, nextnode))
	{
		//吃掉食物
		EatFood(ps);
	}
	//判断是否撞墙
	else if (IsBeHitedByWall(nextnode))
	{
		ps->_state = HIT_BYWALL;
	}
	//判断是否撞到自己
	else if (IsBeHitedBySelf(ps, nextnode))
	{
		ps->_state = HIT_BYSELF;
	}
	else
	{
		//正常移动
		Move(ps, nextnode);
	}
}

void GameRun(pSnack ps)
{

	do
	{
		//显示得分情况
		ShowScore(ps);

		if (KEY_PRESS(VK_UP) && ps->_dire != DOWN)
			ps->_dire = UP;
		else if (KEY_PRESS(VK_DOWN) && ps->_dire != UP)
			ps->_dire = DOWN;
		else if (KEY_PRESS(VK_LEFT) && ps->_dire != RIGHT)
			ps->_dire = LEFT;
		else if (KEY_PRESS(VK_RIGHT) && ps->_dire != LEFT)
			ps->_dire = RIGHT;
		else if (KEY_PRESS(VK_SPACE))
			SleepForever();
		else if (KEY_PRESS(VK_ESCAPE))
			ps->_state = END_NORMAL;
		else if (KEY_PRESS(VK_F3))
		{
			//加速,每走一步休眠时间变短,得分更高
			if (ps->_sleeptime > 50)
			{
				ps->_sleeptime -= 30;//不能一直加速,最多加速5次
				ps->_foodweight += 2;
			}
		}
		else if (KEY_PRESS(VK_F4))
		{
			//加速,每走一步休眠时间变短,得分更高
			if (ps->_sleeptime < 320)
			{
				ps->_sleeptime += 30;//不能一直加速,最多加速4次
				ps->_foodweight -= 2;
			}
		}

		SnackMove(ps);
		ShowSnack(ps);
		Sleep(ps->_sleeptime);

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

}


void GameOver(pSnack ps)
{
	SetPos(40, 12);
	if (ps->_state == HIT_BYWALL)
		printf("您已撞墙\n");
	else if (ps->_state == HIT_BYSELF)
		printf("您撞到了自己\n");
	else if (ps->_state == END_NORMAL)
		printf("您主动结束了游戏\n");

	pSnackNode cur = ps->_SnackHead;
	while (cur)
	{
		pSnackNode next = cur->next;
		free(cur);
		cur = next;
	}

	free(ps->_food);
	ps->_food = NULL;

}

4.3.test.c

#define  _CRT_SECURE_NO_WARNINGS 1

#include "snack.h"

int main()
{
	//将环境改为本地环境,方便宽字符的使用
	setlocale(LC_ALL, "");
	//调用srand函数,便于生成随机数
	srand((unsigned int)time(NULL));
	int ch = 0;
	do
	{
		system("cls");
		//定义一条蛇
		Snack snack;

		//游戏开始——准备工作
		GameStart(&snack);

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

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

		SetPos(40, 13);
		wprintf(L"需要再来一局吗?(Y/N)");
		scanf("%c", &ch);
		while (getchar() != '\n')
			;
	} while (ch == 'Y' || ch == 'y');

	SetPos(0, 27);

	return 0;
}

  • 28
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值