贪吃蛇游戏

文章目录

    • 前言
    • 一.Win32API
      • 1.1GetStdHandle
      • 1.2GetConsoleCursorInfo
      • 1.3SetConsoleCursorInfo
      • 1.4SetConsoleCursorPosition
      • 1.5GetAsyncKeyState
      • 1.6setlocale
      • 二.游戏设计
      • 三.GameStart
        • 3.1蛇的创建
        • 3.2分文件
        • 3.3控制台设置
        • 3.4WelComeToGame
        • 3.5CreateMap
        • 3.6InitSnack
        • 3.7CreateFood
      • 四.GameRun
        • 4.1GameHelpInfo
        • 4.2判断按键
        • 4.3SnackMove
      • 五.GameEnd

前言

贪吃蛇是一款人人皆知的经典游戏,本篇文章使用C语言在Windows控制台环境下简单模拟实现贪吃蛇小游戏。

这篇博文需要读者熟悉C语言的指针、结构体、枚举、动态内存管理、以及数据结构的单链表等。补充知识有Windows操作系统为应用程序提供的功能接口函数Win32 API。

一.Win32API

API对应的英文单词是(Application Programming Interface),是Windows操作系统提供的32位平台应用程序接口,就叫做Win32API,是各种接口的集合。

为了实现贪吃蛇游戏,我们需要学习一些应用程序接口,借这些接口的功能来实现游戏效果,比如光标的隐藏,光标的定位。

贪吃蛇游戏_第1张图片

在Windows中,上面是控制台应用程序,在Linux经常被叫做终端,我们写的贪吃蛇游戏打算在这个应用程序上运行。

贪吃蛇游戏_第2张图片

在创建空项目时,创建了一个控制台程序。(博主使用VS2019编译器)

在前面反复提控制台,就是为了告诉读者控制台是一个应用程序,而Win32API就是提供给应用程序使用的函数接口,可以达到开启视窗、描绘图形、使用周边设备等目的。

1.1GetStdHandle

GetStdHandle是一个Windows API函数,这个函数可以从标准设备(标准输入、标准输出、标准错误)中获取一个句柄,控制台是一个标准输出设备。

每个设备都有一个句柄,并且句柄值不同。使用相应设备的句柄就可以控制这个设备。

贪吃蛇游戏_第3张图片

以下这句代码,可以获取控制台的句柄(注意包含头文件

HANDLE hOutput = GetStdHandle(STD_OUTPUT_HANDLE);

1.2GetConsoleCursorInfo

贪吃蛇游戏_第4张图片

该函数的参数有两个(1.句柄,2.光标类型结构体变量的地址),作用是获取句柄设备上的光标信息放到结构体变量里去。

贪吃蛇游戏_第5张图片

CONSOLE_CURSOR_INFO是Windows操作系统提供的一个描述光标的结构体类型,成员变量dwSize表示光标的大小,bVisible表示光标的可见性,当bVisible被赋值成false时,光标可以实现隐藏

创建一个光标结构体类型变量,使用GetConsoleCursorInfo这个函数获取控制台上的光标信息,放到我们创建的结构体变量中。

HANDLE hOutput = GetStdHandle(STD_OUTPUT_HANDLE);//获取控制台句柄
CONSOLE_CURSOR_INFO CursorInfo;
GetConsoleCursorInfo(hOutput, &CursorInfo);//把控制台的光标信息存入光标结构体变量CursorInfo中

1.3SetConsoleCursorInfo

设置句柄设备上的光标。

贪吃蛇游戏_第6张图片

第一个参数是句柄,第二个参数是光标结构体类型变量的地址。意思是:将句柄设备上的光标设置成第二个参数传的光标。

于是通过1.21.3的组合,就可以设置控制台的光标了!代码如下:

HANDLE hOutput = GetStdHandle(STD_OUTPUT_HANDLE);
CONSOLE_CURSOR_INFO CursorInfo;
GetConsoleCursorInfo(hOutput, &CursorInfo);
//把可见性设为不可见,并设置进控制台
CursorInfo.bVisible = false;
SetConsoleCursorInfo(hOutput, &CursorInfo);

注意,我们必须获取控制台光标信息存到变量中,改变该变量的可见性,再设置进控制台。

不能单单创建一个光标变量,设置不可见性就设置进控制台。简而言之就是以下代码无法有效隐藏控制台光标:

HANDLE hOutput = GetStdHandle(STD_OUTPUT_HANDLE);
CONSOLE_CURSOR_INFO CursorInfo;
CursorInfo.bVisible = false;
SetConsoleCursorInfo(hOutput, &CursorInfo);//error

1.4SetConsoleCursorPosition

设置光标在控制台的位置。当我们输出一句“hello world”的时候,文字默认在左顶角(原点)开始输出,如何让文字在我们想要的位置输出呢?

为此,我们先来谈谈控制台上的坐标系吧。

贪吃蛇游戏_第7张图片

从原点出发,向右为x轴、向下为y轴、逐渐增大。在Windows中使用COORD这个结构体来表示坐标。

贪吃蛇游戏_第8张图片

创建这个坐标结构体类型变量,比如:

COORD pos = {3, 4};//这个坐标是3列4行的意思,x是竖,y是横。

接着使用SetConsoleCursorPosition这个函数可以做到设置光标位置,参数如下:

贪吃蛇游戏_第9张图片

​ 在句柄对应设备上,设置COORD类型变量为打印起始点。

//封装成这个函数,传参x和y,就可以指定控制台位置输出了。
void SetPos(int x, int y)
{
	COORD pos = { x, y };
	HANDLE hOutput = GetStdHandle(STD_OUTPUT_HANDLE);
	SetConsoleCursorPosition(hOutput, pos);
}
// 比如,SetPos(64, 15)把控制台光标设置在64列15行处,此时如果打印“hello world”,就不是在左顶角打印了。

1.5GetAsyncKeyState

贪吃蛇游戏_第10张图片

这是一个判断按键情况的函数,键盘上每一个键都有一个对应的虚拟键值,请看下图:

贪吃蛇游戏_第11张图片

贪吃蛇游戏_第12张图片

GetAsyncKeyState函数接收一个虚拟键值,然后返回一个short类型的数值,如果这个short类型(16个bit位)的最低位为1,则说明这个虚拟键值对应的键被按过。

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

GetAsyncKeyState(VK)返回值按位与1,如果结果为真,则返回1,说明该键被按过,否则为0。

1.6setlocale

计算机语言早期是美国以英语语种设计的语言,所以对其它国家语言兼容性不好,随着国际化越来越加深,计算机语言也支持了本地化(使用者所在地区的语言习惯)比如金钱符号本地化前是$,本地化后¥,以及日期的表示习惯。

对于本文而言,最重要的是宽字符的打印。

英文字母占控制台屏幕缓冲区一个字符位,而宽字符它占两个字符位。要使用宽字符,需要使用setlocale函数设置本地环境。

贪吃蛇游戏_第13张图片

category是setlocale的第一参数,是本地化的选项。LC_ALL将所有选项都本地化,还有其它单个选项,比如LC_TIME就只本地化时间。

locale是setlocale的第二个参数,只有两个选项,“C”或“ ”。双引号“ ”是执行本地化,而“C"是默认。

于是写上下面这句代码就可以进行本地化:

//设置本地环境
	setlocale(LC_ALL, "");

以上就是写贪吃蛇需要的Win32API接口函数的学习,下面进入游戏设计环节。

二.游戏设计

贪吃蛇游戏是一条蛇在围起来的地图里走动,通过↑、↓、←、→键控制蛇的行走方向,吃地图里的食物逐渐变长,分数累增的游戏。

这样,我们设计蛇头用宽字符○表示、蛇的身体用●、地图上的墙用□、食物使用★。

期望:一开始在一个大小合适的控制台窗口,在合适的位置打印信息。告诉玩家这是贪吃蛇游戏、规则介绍、打印地图、初始化蛇等。

接着是游戏运行界面,有帮助信息、总分数的变化、控制蛇在地图里走动,蛇吃食物等。

​最后游戏结束后给我们反馈游戏因为什么原因结束的,是撞墙了?还是吃到自己?

从这个期望中,我们分三部分:分别是游戏之前的准备(GameStart),游戏运行(GameRun)和游戏结束(GameEnd)。

三.GameStart

3.1蛇的创建

蛇是由一个个结点链接起来的整体,想要在控制台上准确打印出一条蛇,结点里存的信息应是用来表示坐标的,于是:

typedef struct SnackNode
{
	//x、y组合标识控制台上唯一的坐标
	int x;
	int y;
	struct SnackNode* next;
}SnackNode, *pSnackNode;

但是我们玩的是蛇,而不是一个一个结点,这时我们思考游戏在运行的时候,怎样能控制整条蛇、吃食物的总分数、蛇的速度、蛇的方向等等。

通过再封装一个结构体,完成对以上的整体控制:

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

enum GAME_STATUS
{
	OK,//蛇正常运行
	END_NORMAL,//家里有事,不玩游戏了,按Esc正常退出
	KILL_BY_WALL,//撞墙了
	KILL_BY_SELF//咬到自己了
};

typedef struct Snack
{
	pSnackNode _pSnack;//蛇头结点控制整条蛇
	pSnackNode _pFood;//维护食物的指针
	int _Score;
	int _FoodWeight;
	int _SleepTime;//控制蛇的速度
	enum DIRECTION _Dir;//表示蛇的行走方向
	enum GAME_STATUS _Status;//蛇的状态
}Snack, *pSnack;

这里的*pSnack的意思是:相当于typedef struct Snack *(指针)重定义为pSnack,上面蛇结点指针也是一样的。

可能不熟悉贪吃蛇的读者现在不太理解,为什么蛇结构里需要有维护食物的指针,往下看就明白了。

现在蛇结构蓝图设计好了,开始写代码!

3.2分文件

贪吃蛇游戏_第14张图片

Test.c文件用来测试,Snack.c文件用来写函数实现,Snack.h文件写头文件、写函数声明以及定义标识符、宏等。

现在进入GameStart的实现部分(Snack.c),按照预先的期望在大小合适的控制台上打印欢迎信息。

3.3控制台设置

为了方便讲解,博主在程序中会加入让运行逻辑停下来的getchar()函数。

贪吃蛇游戏_第15张图片

注意:如果读者的控制台大小无法设置,可能是控制台的设置问题,请按下面步骤进行解决。

贪吃蛇游戏_第16张图片

title让控制台标题变为贪吃蛇只在程序运行时生效,当程序结束后,标题便不再是我们设置的了。

3.4WelComeToGame

打印欢迎界面的时候,文字在屏幕中间显示是比较好的,而且最好不要有光标在控制台上一闪一闪的。

所以在这里需要前面的两步铺垫:如何隐藏光标、如何控制光标位置。

贪吃蛇游戏_第17张图片

void SetPos(int x, int y)
{
	COORD pos = { x, y };
	HANDLE hOutput = GetStdHandle(STD_OUTPUT_HANDLE);
	SetConsoleCursorPosition(hOutput, pos);
}

void WelComeToGame()
{
	//隐藏光标
	HANDLE hOutput = GetStdHandle(STD_OUTPUT_HANDLE);
	CONSOLE_CURSOR_INFO Cursor;
	GetConsoleCursorInfo(hOutput, &Cursor);
	Cursor.bVisible = false;
	SetConsoleCursorInfo(hOutput, &Cursor);
    
    SetPos(40, 14);
	printf("欢迎来到贪吃蛇小游戏");
	SetPos(80, 27);
	system("pause");
    
	//清屏
	system("cls");

	SetPos(36, 14);
	printf("使用↑、↓、←、→键控制蛇的移动");
	SetPos(80, 27);
	system("pause");
    //为下面打印地图做好准备
	system("cls");//清完屏后,光标会到原点
}

贪吃蛇游戏_第18张图片

system(“pause”)是让程序暂停下来,并且会自发在控制打印出“请按任意键继续. . .”,所以定位完“欢迎来到…”后,要给这句话再定位一下。

接着再介绍一下规则:

贪吃蛇游戏_第19张图片

到这里这个函数就完成了。下面到了游戏界面,我们要打印地图。

如果行列设置不合适,显示有问题,读者可以自行设置。

3.5CreateMap

创建地图,墙使用的宽字符是□,并且要执行本地化,支持宽字符的打印。

贪吃蛇游戏_第20张图片

宽字符占两个字符位(列),但是和ab一样都只占一行,使用wprintf打印宽字符,格式串前面加L,字符前面加L。

控制台屏幕是100列30行,那么我们就创建一个58列28行的地图吧!

贪吃蛇游戏_第21张图片

void CreateMap()
{
	setlocale(LC_ALL, "");
	int i = 0;
	//上
	for (i = 0; i <= 56; i += 2)
	{
		//#define WALL L'□'
		wprintf(L"%lc", WALL);
	}
	//下
	SetPos(0, 27);
	for (i = 0; i <= 56; i += 2)
	{
		wprintf(L"%lc", WALL);
	}
	//左
	for (i = 1; i <= 26; i++)
	{
		SetPos(0, i);
		wprintf(L"%lc", WALL);
	}
	//右
	for (i = 1; i <= 26; i++)
	{
		SetPos(56, i);
		wprintf(L"%lc", WALL);
	}
    //上图中的getchar()是为了不让程序结束
    //结束时打印的信息会覆盖下面的墙
}
3.6InitSnack

地图创建好后,初始化出一条蛇并打印在地图上。

贪吃蛇游戏_第22张图片

pSnackNode BuySnackNode(int x, int y)
{
	pSnackNode tmp = (pSnackNode)malloc(sizeof(SnackNode));
	if (tmp == NULL)
	{
		perror("BuySnackNode malloc fail");
		exit(-1);
	}
	tmp->x = x;
	tmp->y = y;
	tmp->next = NULL;
	return tmp;
}

void InitSnack(pSnack ps)
{
	assert(ps);
	//创建蛇结点并连起来成“蛇”
	int i = 0;
	pSnackNode cur = NULL;
	for (i = 0; i < 5; i++)
	{
        //#define POS_X 24 蛇尾的起点坐标POS_X和POX_Y
		//#define POS_Y 5
		cur = BuySnackNode(POS_X + i * 2, POS_Y);
		//单链表头插法
		if (ps->_pSnack == NULL)
		{
			ps->_pSnack = cur;
		}
		else
		{
			cur->next = ps->_pSnack;
			ps->_pSnack = cur;
		}
	}
	//打印蛇身
	cur = ps->_pSnack;
	int count = 0;
	while(cur)
	{
		SetPos(cur->x, cur->y);
        //第一次进来打印蛇头符号
		if (!(count++))
		{	//#define HEAD L'○'
			wprintf(L"%lc", HEAD);
		}
		else
		{	//#define BODY L'●'
			wprintf(L"%lc", BODY);
		}
		cur = cur->next;
	}
	//其它设置
	ps->_pFood = NULL;//食物还没创建,先赋空指针
	ps->_Score = 0;
	ps->_FoodWeight = 10;
	ps->_SleepTime = 150;//程序暂停时间,控制蛇的速度
	ps->_Dir = RIGHT;//初始蛇的方向为右
	ps->_Status = OK;//蛇运行状态是正常的
}
3.7CreateFood

创建食物,本质上也是创建蛇结点,蛇吃食物身体变长相当于把食物这个结点链接到蛇上。

食物的创建是有约束的:食物的x坐标应该和蛇一样,都是2的倍数。x列0-57总共58个,每个宽字符占2个字符位,食物也是宽字符,除去0-1和56-57是墙,2到54是食物的x坐标

食物行坐标在1-26之间,宽字符行与普通字符都是占一行,0行和27行被墙占了,食物不能与墙有重叠的。

还有一点是,食物不要与蛇的身体重合并且任意生成,以上这些约束要留意,下面开始写代码:

贪吃蛇游戏_第23张图片

void CreateFood(pSnack ps)
{
	assert(ps);
	int x = 0;
	int y = 0;
again:
	do 
    {	//随机数的使用,在主函数里设置一次生成起点
		x = rand() % 53 + 2;//取模53得到0~52的余数+2->2~54
		y = rand() % 26 + 1;//产生1~26之间的随机数
	} while (x % 2 != 0);//x是奇数重新生成坐标
	
	pSnackNode cur = ps->_pSnack;
	while (cur)
	{
		//判断蛇结点与食物结点是否重合
		if (cur->x == x && cur->y == y)
		{
			goto again;
		}
		cur = cur->next;
	}
	//到这里,食物结点坐标合法
	pSnackNode FoodNode = BuySnackNode(x, y);
	//打印食物在地图上的位置
	SetPos(x, y);
	wprintf(L"%lc", FOOD);//#define FOOD L'★'

	//出了创建食物的作用域,食物结点将丢失,于是使用蛇结构食物指针维护
	ps->_pFood = FoodNode;
}

到了这一步,游戏的所有准备工作都做好了,进入游戏阶段。

四.GameRun

游戏玩耍阶段,控制台左边是蛇在地图里走动,右边设置分数变化和按键说明的信息,比如:按空格键暂停、按Esc键正常退出等等。

贪吃蛇游戏_第24张图片

4.1GameHelpInfo

贪吃蛇游戏_第25张图片

void GameHelpInfo()
{
	SetPos(64, 14);
	printf("1.使用↑.↓.←.→键控制蛇的移动");
	SetPos(64, 15);
	printf("2.按F2键加速、F3减速");
	SetPos(64, 16);
	printf("3.蛇不能撞墙,不能咬到自己");
	SetPos(64, 17);
	printf("4.Space——暂停、Esc——正常退出");
	SetPos(84, 24);
	printf("@啊苏");
}
4.2判断按键

因为分数是时刻变化的,所以获得分数不在帮助信息里打印,细想什么是跟随的游戏一直在变的呢?没错就是玩家的按键逻辑,将分数的打印放在按键里。

贪吃蛇游戏_第26张图片

判断按键的情况在下面的代码中进行讲解:

void Pause()
{
	while (1)
	{
		Sleep(100);
		if (PRESS_KEY(VK_SPACE))
		{
			break;
		}
	}
}

void GameRun(pSnack ps)
{
	assert(ps);
	//打印帮助信息
	GameHelpInfo();
	//判断按键情况
	do
	{
		SetPos(64, 10);
		printf("获得的分数:%-4d", ps->_Score);
		SetPos(64, 11);
		printf("每个食物的分数:%-2d", ps->_FoodWeight);
        
        //键盘上的↑键对应的虚拟值是VK_UP这个常数
		if (PRESS_KEY(VK_UP) && ps->_Dir != DOWN)
		{	//#define PRESS_KEY(VK) ((GetAsyncKeyState(VK)&0x1) ? 1 : 0)
			ps->_Dir = UP;
		}
		else if (PRESS_KEY(VK_DOWN) && ps->_Dir != UP)
		{	//想让蛇的方向朝下,前提是蛇原型方向不能是朝上走的
			ps->_Dir = DOWN;
		}
		else if (PRESS_KEY(VK_LEFT) && ps->_Dir != RIGHT)
		{
			ps->_Dir = LEFT;
		}
		else if (PRESS_KEY(VK_RIGHT) && ps->_Dir != LEFT)
		{
			ps->_Dir = RIGHT;
		}
        else if (PRESS_KEY(VK_F2))
		{
			//最多加速三次(从设定的速度开始算)
			if (ps->_SleepTime > 30)
			{
                //_SleepTime控制蛇的速度,越短蛇走的越快
				ps->_SleepTime -= 40;
				ps->_FoodWeight += 5;
			}
		}
		else if (PRESS_KEY(VK_F3))
		{
			//最多减速一次(从设定的速度开始算)
			if (ps->_SleepTime < 190)
			{
				ps->_SleepTime += 40;
				ps->_FoodWeight -= 5;
			}
		}
		else if (PRESS_KEY(VK_ESCAPE))
		{
			ps->_Status = END_NORMAL;
			break;
		}
		else if (PRESS_KEY(VK_SPACE))
		{
			Pause();//暂停函数
		}
	} while (ps->_Status == OK);//只有蛇正常运行时,才能正常让玩家按键控制方向
}

读者可以在搜索引擎上找GetAsyncKeyState这个函数,然后里面有关于虚拟键值的超链接,点击跳转即可查看。

这么多按键是代码看起来很复杂,但其逻辑并不难理解,但cpu处理速度很快,不用担心处理不过来。

然后睡眠时间现在就起作用了。

贪吃蛇游戏_第27张图片

在判断按键后,用于Sleep函数让程序停下来一段时间,_SleepTime = 150,150是毫秒。

在短暂的停止后,蛇开始移动!

4.3SnackMove
//判断蛇下一位置是否为结点
int NextIsFood(pSnack ps, pSnackNode pnext)
{	//食物指针的用处体现了,在游戏期间,食物指针能很好的维护着食物,防止内存泄漏
	if (pnext->x == ps->_pFood->x && pnext->y == ps->_pFood->y)
	{
		//如果蛇结点里的食物坐标与蛇下一步走的位置相同 返回1确认是食物
		return 1;
	}
	else
	{
		//返回0说明蛇的下一位置不是食物
		return 0;
	}
}


//注意食物结点的坐标与预创建的结点坐标一致,但不是同一个结点!
void EatFood(pSnack ps, pSnackNode pnext)
{
	assert(ps);
    //预创建的结点成为蛇头,
	pnext->next = ps->_pSnack;
	ps->_pSnack = pnext;
	//然后打印蛇
	pSnackNode cur = ps->_pSnack;
	int count = 0;
	while (cur)
	{
		SetPos(cur->x, cur->y);
		if (!(count++))
		{
			wprintf(L"%lc", HEAD);
		}
		else
		{
			wprintf(L"%lc", BODY);
		}
		cur = cur->next;
	}
    //获得分数
	ps->_Score += ps->_FoodWeight;
	//释放食物结点,食物结点
	free(ps->_pFood);
    ps->_pFood = NULL;
	//重新生成食物,这个创建食物的函数会打印食物在地图上
	CreateFood(ps);
}

void NotFood(pSnack ps, pSnackNode pnext)
{
	assert(ps);
	//不是食物也要把预创建的结点链接进来成为蛇头
	pnext->next = ps->_pSnack;
	ps->_pSnack = pnext;
	//把蛇尾给去掉保持长度不变,使用前后指针,打印蛇身
	pSnackNode cur = ps->_pSnack;
	pSnackNode curPrev = ps->_pSnack;
    int count = 0;
	while (cur->next)
	{
        SetPos(cur->x, cur->y);
        if (!(count++))
		{
			wprintf(L"%lc", HEAD);
		}
		else
		{
			wprintf(L"%lc", BODY);
		}
		curPrev = cur;
		cur = cur->next;
	}
	//到这里cur是旧蛇尾,要把这里的宽字符打印成空格,打印空格覆盖掉地图上的宽字符
	SetPos(cur->x, cur->y);
	printf("  ");
    //释放旧蛇尾
	free(cur);
	//把新的蛇尾next指针赋空
	curPrev->next = NULL;
}

void SnackMove(pSnack ps)
{
	assert(ps);
	//蛇的移动选择预开辟下一位置结点方法
	pSnackNode pNext = NULL;
	pSnackNode pHead = ps->_pSnack;//旧蛇头
    //根据蛇的方向,选择下一结点的位置
	switch (ps->_Dir)
	{	//pNext是预创建的结点!
    case UP:
		pNext = BuySnackNode(pHead->x, pHead->y - 1);//上的话,列不变行减1
		break;
	case DOWN:
		pNext = BuySnackNode(pHead->x, pHead->y + 1);
		break;
	case LEFT:
		pNext = BuySnackNode(pHead->x - 2, pHead->y);//左是行不变,列减2,因为宽字符占两列
		break;
	case RIGHT:
		pNext = BuySnackNode(pHead->x + 2, pHead->y);
		break;
	}
	//蛇方向下一个结点有两种情况:是食物和不是食物
	if (NextIsFood(ps, pNext))
	{
		//是食物就把食物吃了,把pNext结点链接进蛇并成为蛇头
		EatFood(ps, pNext);
	}
	else
	{
		NotFood(ps, pNext);
	}

}

到这里,玩家就能正常在使用上下左右等键控制蛇的移动,加减速、暂停等。接下来要处理蛇在运行时,状态从OK变为不Ok的逻辑。

贪吃蛇游戏_第28张图片

void KillByWall(pSnack ps)
{
	assert(ps);
	if (ps->_pSnack->x == 0 ||
		ps->_pSnack->x == 56 ||
		ps->_pSnack->y == 0 ||
		ps->_pSnack->y == 27)
		ps->_Status = KILL_BY_WALL;
}

void KillBySelf(pSnack ps)
{
	assert(ps);
	pSnackNode cur = ps->_pSnack->next;
    //蛇头以后的结点与蛇头相比是否有坐标一致的
	while (cur)
	{
		if (cur->x == ps->_pSnack->x && cur->y == ps->_pSnack->y)
		{
			ps->_Status = KILL_BY_SELF;
			break;
		}
        cur = cur->next;
	}
}

到这里,蛇的逻辑就完整了,但是游戏结束后,还没能得到很好的善后处理,比如链表的释放、告诉玩家是怎么游戏结束的等等。

下面进入到游戏结束环节:

五.GameEnd

游戏结束后,在合适的位置打印信息给玩家:

void GameEnd(pSnack ps)
{
	assert(ps);
	SetPos(15, 10);
	switch (ps->_Status)
	{
	case END_NORMAL:
		printf("主动退出,游戏结束");
        break;
	case KILL_BY_WALL:
		printf("撞到墙了,游戏结束");
        break;
	case KILL_BY_SELF:
		printf("咬到自己,游戏结束");
        break;
	}
    //释放蛇
    pSnackNode cur = ps->_pSnack;
	while (cur)
	{
		pSnackNode curnext = cur->next;
		free(cur);
		cur = curnext;
	}
}

贪吃蛇游戏_第29张图片

程序在结束时有这句话,我们可以在程序结束前定好光标位置,让这句话不要影响游戏效果。

除了这个之外,游戏最重要的一个再来一局逻辑还没有设计,我们把最后的坑补上。

void Test()
{
	int input = 0;
	do 
	{
		Snack s = { 0 };
		//游戏开始
		GameStart(&s);
		//游戏运行
		GameRun(&s);
		//游戏结束
		GameEnd(&s);
		SetPos(15, 12);
		printf("是否想要再来一局(Y/N):");
		input = getchar();
		getchar();//清空输入缓冲区
	}while(input == 'Y' || input == 'y');
	SetPos(0, 28);
}

到这里,贪吃蛇小游戏实现完毕!

希望这篇博文对想实现贪吃蛇小游戏的读者有帮助。

你可能感兴趣的:(C语言,游戏,c语言,贪吃蛇)