C语言实现贪吃蛇小游戏

谈到贪吃蛇游戏,我们都不陌生,贪吃蛇游戏最早是由瑞典程序员伊维·达赫尔(Yevgeny Dvorzhak)在1979年开发的,它在1980年开始流行。今天我将使用Win11 VS2022社区版带领大家一步一步实现贪吃蛇,希望这能多多少少对大家有些帮助。

一、贪吃蛇游戏的预期实现结果

我们可以参考下面这个视频:

贪吃蛇实现效果

二、贪吃蛇实现的大致思路及Snake.h

结合平时玩贪吃蛇的经验,我们可以按照如下思路来实现贪吃蛇代码:

其中游戏开始部分是创建初始环境,欢迎信息和地图、蛇、食物信息。

游戏进行部分是核心,处理蛇的移动。

游戏结束部分是完成游戏的善后。

我们按照上述思路先写一份头文件,把思路先理一理:

//"Snake.h"
#pragma once
# include<stdio.h>
# include<stdlib.h>//动态内存开辟、时间函数
# include<stdbool.h>//布尔类型
# include<time.h>//时间函数
# include<windows.h>//需要用到Win32的方法
# include<locale.h>//设置本地环境,方便打印宽字符'□'等

# define KEY_STATE(VK) ((GetAsyncKeyState(VK) & 0x1) ? 1 : 0)//定义按键宏,获取按键信息

# define WALL L'□'
# define BODY L'●'
# define FOOD L'★'

# define POS_X 24//定义初始x坐标
# define POS_Y 5//定义初始y坐标

//定义蛇节点
typedef struct SnakeNode
{
	//坐标信息
	int x;
	int y;
	struct SnakeNode* next;//方便链接下一个蛇节点
}SnakeNode, * pSnakeNode;

//定义方向状态
enum DIRECTION
{
	UP,//向上
	DOWN,//向下
	LEFT,//向左
	RIGHT//向右
};

//定义游戏状态
enum GAMESTATE
{
	OK,//正常进行
	BREAK_NORMAL,//正常退出
	KILL_BY_WALL,//撞墙而死
	KILL_BY_SELF//撞到自身而死
};

//定义整个蛇整体变量
typedef struct Snake
{
	pSnakeNode _pSnake;//存放小蛇的头指针
	pSnakeNode _pFood;//存放食物的地址
	int _Count;//记录游戏总得分
	int _FoodWeight;//记录当前食物的得分(由于蛇速度影响)
	int _SleepTime;//存放蛇每走一个格子需要停顿的时间,停顿时间越短,蛇移动越快
	enum DIRECTION _Dir;//存放蛇的运动方向
	enum GAMESTATE _Game_State;//存放蛇的游戏状态
}Snake,*pSnake;

//设置坐标
void SetPos(int x, int y);
//隐藏光标
void ConsoleCursorHide();
//设置窗口大小以及标题

//游戏开始
void GameStart(pSnake ps);
//打印欢迎界面
void WelcomeToGame();
//打印地图
void CreateMap();
//初始化小蛇
void InitSnake(pSnake ps);
//创建食物
void CreateFood(pSnake ps);

//游戏运行
void GameRun(pSnake ps);
//打印提示信息
void PrintHelpInfo();
//暂停
void Pause();//空格为暂停
//蛇的移动
void MoveSnake(pSnake ps);//更新蛇头等
//判断下一个是不是食物
bool IsNextFood(pSnake ps, pSnakeNode pNext);//比较下一个位置和食物的位置
//吃掉食物
void EatFood(pSnake ps, pSnakeNode pNext);//覆盖、更新食物
//不吃食物
void NoFood(pSnake ps, pSnakeNode pNext);
//撞墙
void KillByWall(pSnake ps);
//自杀
void KillBySelf(pSnake ps);

//游戏结束
void GameEnd(pSnake ps);//销毁动态蛇节点内存

咱们为什么要定义比较复杂的结构体变量和枚举变量,这是为了减少因为使用太多的函数而增加实现难度,我们在下面就会感受到这样定义的好处。

三、基本知识

在打印欢迎页面之前,我们需要了解一个知识,就是我们使用VS2022运行的时候调用的命令提示框(黑框框)是有坐标的概念的,如图:

默认左上角的坐标是(0,0),而且x,y坐标的含义和数学的不太一样,具体如下:

在输出的时候,输出的英文字符占据1个x长度1个y长度,代表占据一个字节,而输出汉字占据两2个x单位1个y单位,代表占据两个字节。换算关系:2个x长度等于1个y长度。即随着数值的增大,坐标往y轴移动的速度是坐标往x轴移动的速度的2倍。

当谈到设置坐标以确保相关文字能在指定的位置输出,我们需要掌握一部分的Win32API知识,注意,这一部分涉及的函数仅仅在Windows操作系统下生效,在其他的如Linux、mac等不支持。

那么,WindowsAPI是干什么的呢?

Windows 这个多作业系统除了协调应⽤程序的执⾏、分配内存、管理资源之外, 它同时也是⼀个很⼤ 的服务中⼼,调⽤这个服务中⼼的各种服务(每⼀种服务就是⼀个函数),可以帮应⽤程式达到开启 视窗、描绘图形、使⽤周边设备等⽬的,由于这些函数服务的对象是应⽤程序(Application), 所以便 称之为 Application Programming Interface,简称 API 函数。WIN32 API也就是Microsoft Windows 32位平台的应⽤程序编程接口。
在这里,我们只需要知道它能够帮助我们设置光标属性就可以了。
我们先调用system函数,它是库函数,需要include<stdlib.h>,以设置当前窗口的大小和名称,便于后期展示贪吃蛇。
# include<stdlib.h>
int main()
{
	system("mode con cols=100 lines=30");
	system("title 贪吃蛇");
	return 0;
}

即可以将窗口设置成往x轴方向100个x单位,y轴方向30个y单位,标题变为“贪吃蛇”,如下图:

变化前:

变化后:

为了确保我们能够成功更改标题,我们需要做如下设置:

Win11:

打开cmd,点击这个小三角:

点击设置:

点击该处,选择“Windows控制台主机”

保存并关闭

就设置成功了。

Win10:

打开cmd,鼠标划到“命令提示符”页面上端白色部分,右键后,点击“属性”

点击终端->默认终端改为Windows控制台主机,点击确定,再关掉cmd,就设置好了。

为了实现能够更改光标位置,在特定位置打印文字,我们需要在include<windows.h>的前提下实现以下两个函数:

//设置坐标
void SetPos(int x, int y)
{
	COORD pos = { x,y };
	HANDLE hOutput = GetStdHandle(STD_OUTPUT_HANDLE);//获得输出句柄,方便设置位置
	SetConsoleCursorPosition(hOutput, pos);//传入句柄和位置,设置光标位置
}
//隐藏光标
void ConsoleCursorHide()
{
	HANDLE hOutput = GetStdHandle(STD_OUTPUT_HANDLE);
	CONSOLE_CURSOR_INFO console_cursor_info;//定义鼠标结构体,控制光标时是否隐藏
	GetConsoleCursorInfo(hOutput,&console_cursor_info);//获取输出句柄和鼠标信息
	console_cursor_info.bVisible = false;//设置光标不可见
	SetConsoleCursorInfo(hOutput, &console_cursor_info);//重新设置鼠标信息
}

我这里简单介绍一下句柄,句柄用来标识不同设备,获得句柄并将其传递个相关函数,方可更改光标属性(显示和不显示,显示的位置)。上述函数和数据类型都在windows.h中声明过了,我们只需要知道它们能够改变光标位置、设置光标不可见,并把上面的代码记一下会用就行了。如果想要进一步了解函数的具体使用方法,可以进一步查询微软官网。

光标隐藏可设置可不设置,不影响效果,但是设置光标位置是至关重要的。

以上是基本知识,我们下面具体实现。

为了方便调试、函数调用等,我们需要三个文件,分别是Snake.h(类型、定义、函数声明)、Snake.c(函数的实现)、test.c(测试函数)。

注意:若您想按照下面的函数块实现相同的效果,您不妨自己设置主函数,在您的Snake.c和test.c中包括我上面给到的头文件Snake.h,然后在主函数里面调用一下看看效果就行了。在以函数为单位产生效果的时候,可以不计较在您的Snake.c中的函数先后顺序,随后弄清楚每一个函数是在做什么,再在Snake.c中做进一步的封装就行了。

四、游戏开始GameStart()

按照逻辑,我们依次进行以下操作:

1.设置窗口大小和名称

在上文已经提及到,我们此处略过。

2.打印欢迎界面WelComeToGame()

为了实良好的人机交互页面,我们需要打印欢迎信息,具体代码如下:

//Snake.c
//打印欢迎界面
void WelcomeToGame()
{
	//第一张页面
	SetPos(40, 14);
	printf("欢迎来到贪吃蛇!");
	SetPos(40, 15);
	system("pause");//暂停且显示“按任意键继续”
	system("cls");//清除当前画面

	//第二张页面
	SetPos(20, 14);
	printf("请使用 ↑ ↓ ← → 键控制蛇的移动");
	SetPos(20, 15);
	printf("F3加速,F4减速,ESC退出,空格暂停");
	SetPos(40, 25);
	system("pause");
	system("cls");
}

由于我们设置的窗口大小是100*30,我们引入下面这张图您会更理解光标的位置:

效果对应视频里面的前两张页面:

3.打印地图CreateMap()

我们先把墙体打印好,我们定义的墙体的范围是x:0~57,y:0~27,示意图如下:

这里我们用宽字符‘□’来打印墙体,由于1个宽字符占2个字节,即2个x单位,1个y单位,因此墙体的位置在x轴方向上必须是偶数,否则会出现错位的情况。
涉及到宽字符的打印,我们需要在include<locale.h>的条件下,利用setlocale(LC_ALL, "")初始化本地环境,这样才能使用wprintf(L"%lc", WALL);打印宽字符,否则会产生乱码或者打印异常。
代码如下:
//Snake.c
# include"Snake.h"
//打印地图
void CreateMap()
{
	SetPos(0, 0);//坐标归位
	int i = 0;
	//先打印最上面的墙
	for (i = 0; i <= 56; i += 2)
	{
		wprintf(L"%lc",WALL);
	}
	//再打印左右两边的墙
	for (i = 1; i <= 25; i++)
	{
		SetPos(0, i);
		wprintf(L"%lc", WALL);
		SetPos(56, i);
		wprintf(L"%lc", WALL);
	}
	//最后打印最下面的墙
	SetPos(0, 26);
	for (i = 0; i <= 56; i += 2)
	{
		wprintf(L"%lc", WALL);
	}
}

为了观看效果,到目前为止,您的主函数可以按照如下设置:

//test.c
# include"Snake.h"
//# include<locale.h>
//# include<windows.h>//在Snake.h中已经有定义,此处可以不写,仅方便演示
int main()
{
	setlocale(LC_ALL, "");
	//srand((unsigned int)time(NULL));
	test();//传您的测试函数
	return 0;
}

打印的效果如下:

注意:为什么我在头文件中# define WALL L"□" ,是为了在需要更换墙体时方便一键替换,下文的BODY和FOOD同理,且方便记忆。

4.初始化小蛇InitSnake()

其核心是动态开辟若干块内存,并打印蛇。这里需要用上链表的相关知识。

代码如下:

//Snake.c
# include"Snake.c"
//初始化小蛇
void InitSnake(pSnake ps)
{
	pSnakeNode cur = NULL;
	int i = 0;
	for (i = 0; i < 5; i++)
	{
		//新建蛇节点
		cur = (pSnakeNode)malloc(sizeof(SnakeNode));
		if (cur == NULL)
		{
			perror("InitSnake()::malloc()");
			return;
		}
		cur->x = POS_X + 2 * i;//坐标必须是隔2,否则打印错位
		cur->y = POS_Y;//存储坐标
		cur->next = NULL;

		SetPos(cur->x, cur->y);
		wprintf(L"%lc", BODY);//记得要打印
        
		//典型的链表头插拼接
		cur->next = ps->_pSnake;
		ps->_pSnake = cur;
		
	}
	ps->_Count = 0;//设置初始得分为0
	ps->_FoodWeight = 10;//设置初始吃掉单个食物的分数为10
	ps->_SleepTime = 300;//设置初始休眠时间为300
	ps->_Dir = RIGHT;//设置蛇的初始运动方向为向右
	ps->_Game_State = OK;//设置游戏状态为正常
	ps->_pFood = NULL;//设置食物的地址为空
    //顺便初始化其他参数
}

注意:您在主函数测试该函数的时候,别忘了在主函数中初始化Snake ps = {0},将ps的地址传入该函数,或者在主函数中pSnake ps = (pSnake)malloc(sizeof(Snake)),将ps->的所有成员初始化为0后直接将ps传入该函数。我这里以第二种为例。

预期效果如下:

5.创建食物CreateFood()

创建食物的时候,我们要注意:

1.食物不能和墙重合,且食物不能出墙,不能和蛇本身重合。

2.食物的x坐标必须是2的倍数,否则打印错位。

3.食物的出现是随机的。

具体的实现代码如下:

//Snake.c
//创建食物
void CreateFood(pSnake ps)
{
	int x = 0, y = 0;
	pSnakeNode pFood = (pSnakeNode)malloc(sizeof(SnakeNode));
	pSnakeNode cur = ps->_pSnake;
	repeat:
	do
	{
		x = rand() % 53 + 2;
		y = rand() % 25 + 1;
	} while (x % 2 != 0);//x必须是偶数

	while (cur)//遍历蛇
	{
		if (cur->x == x && cur->y == y)
		{
			goto repeat;//如果坐标和蛇重合,那么必须重新生成随机数
		}
		cur = cur->next;
	}
	pFood->x = x;
	pFood->y = y;
	ps->_pFood = pFood;//获取食物正确坐标后,赋给ps->_pFood
	SetPos(x, y);
	wprintf(L"%lc", FOOD);
}

注意:我们用上了随机数,因此,我们需要包括stdlib.h,但这已经在Snake.h中导入了。您在主函数中应当包含srand((unsigned int)time(NULL)),以确保随机数的生成。

实现的效果如下:

食物是随机生成的。

到目前为止,小分块已经完成,我们将以上几个小部分放入GameStart()中,方便主函数调用他们:

//"Snake.c"
# include"Snake.h"
//游戏开始
void GameStart(pSnake ps)
{
	//设置窗口大小以及标题
	system("mode con cols=100 lines=30");
	system("title 贪吃蛇");

	//ConsoleCursorHide();//隐藏光标,可隐藏可不隐藏

	WelcomeToGame();//打印欢迎界面

	CreateMap();//打印地图

	InitSnake(ps);//初始化小蛇

	CreateFood(ps);//创建食物
}

五、游戏运行GameRun()

在保留上述函数的前提下,我们接着实现运行阶段。

为了便于理解,我们直接从GameRun()的框架入手。进入运行阶段,肯定要检测按键的情况。大体逻辑如下:

//Snake.c
# include"Snake.h"
void GameRun(pSnake ps)
{
	PrintHelpInfo();//再次打印页面提示信息,防止玩家忘记游戏规则
	do {
		SetPos(64, 10);
		printf("您的得分为:%05d", ps->_Count);
		SetPos(64, 11);
		printf("单个食物的分数为:%d", ps->_FoodWeight);//需要不断打印得分情况
		if (KEY_STATE(VK_LEFT) && ps->_Dir != RIGHT)//检测运动和按键状态
		{
			ps->_Dir = LEFT;
		}
		else if (KEY_STATE(VK_RIGHT) && ps->_Dir != LEFT)
		{
			ps->_Dir = RIGHT;
		}
		else if (KEY_STATE(VK_UP) && ps->_Dir != DOWN)
		{
			ps->_Dir = UP;
		}
		else if (KEY_STATE(VK_DOWN) && ps->_Dir != UP)
		{
			ps->_Dir = DOWN;
		}
		else if (KEY_STATE(VK_F3))
		{
			if (ps->_SleepTime >= 100)
			{
				ps->_SleepTime -= 30;
				ps->_FoodWeight += 2;
			}
		}
		else if (KEY_STATE(VK_F4))
		{
			if (ps->_SleepTime <= 300)
			{
				ps->_SleepTime += 30;
				ps->_FoodWeight -= 2;
			}
		}
		else if (KEY_STATE(VK_ESCAPE))
		{
			ps->_Game_State = BREAK_NORMAL;
		}
		else if (KEY_STATE(VK_SPACE))
		{
			Pause();
		}
		Sleep(ps->_SleepTime);
		MoveSnake(ps);
	} while (ps->_Game_State == OK);
	
	SetPos(20, 10);
	if (ps->_Game_State == KILL_BY_WALL)
	{
		printf("您撞墙而死,游戏结束!");
	}
	else if (ps->_Game_State == KILL_BY_SELF)
	{
		printf("您自杀而死,游戏结束!");
	}
}

注意:

1.在实际的贪吃蛇中,不支持蛇直走还能直接掉头的情况,故在上述代码中我们不支持这种逻辑。

2.打印提示信息的目的是防止玩家在游戏过程中忘记游戏规则。

3.检测按键需要用到GetAsyncKeyState()函数,为了回顾宏的知识、三元运算符、表达简便,我们使用KEY_STATE(VK)宏来封装GetAsyncKeyState()函数。

4.检测蛇的下一步移动是否合法(合法即没有挂,但判断有没有吃食物也比较复杂)(不合法即撞墙、自杀),逻辑较为复杂,需要重点考虑。

5.得分需要实时更新。

6.为了激励按F3加速的玩家和惩罚按F4减速的玩家,在休眠时间合理的情况下,对单个食物得分做出一定的调整。

下面我们逐个实现函数。

1.打印页面提示信息PrintHelpInfo()

其目的是打印游戏规则,防止玩家忘记,代码如下:

//Snake.c
#include"Snake.h"
//打印提示信息
void PrintHelpInfo()
{
	SetPos(64, 17);
	printf("1.按 ↑ ↓ ← → 控制蛇的移动");
	SetPos(64, 18);
	printf("2.不能撞墙,不能吃到自己");
	SetPos(64, 19);
	printf("3.按F3加速,F4减速");
	SetPos(64, 20);
	printf("4.按ESC退出,空格暂停");
}

效果如下:

2.检测按键情况GetAsyncKeyState()

定义的宏如下:

# define KEY_STATE(VK) ((GetAsyncKeyState(VK) & 0x1) ? 1 : 0)//定义按键宏,获取按键信息

这在Snake.h中已经定义过。

GetAsyncKeyState(VK)用于检测按键是否被用过,如果按过,其按位与0x1得到1,否则得到零,这使得其套用三目表达式时方便。

3.暂停Pause()

其目的是当玩家按下空格键时,停止但不结束游戏,其实现如下:

//"Snake.c"
# include"Snake.h"
void Pause()//空格为暂停
{
	while(1)
	{
		Sleep(200);
		if (KEY_STATE(VK_SPACE))
		{
			break;
		}
	}
}

解释:当玩家按下空格键时,函数从GameRun()进入Pause();当玩家再按下空格时,自动跳出暂停,游戏继续。

4.蛇移动MoveSnake()

为了便于描述蛇如何运动,我将IsNextFood()、EatFood()、NoFood()、KILLBYWALL()、KILLBYSELF()放入MoveSnake(),代码如下:

//蛇的移动
void MoveSnake(pSnake ps)//更新蛇头等
{
	//判断蛇的状态,有没有撞墙,有没有自杀,有没有吃到食物
	pSnakeNode pNext = (pSnakeNode)malloc(sizeof(SnakeNode));
	if (pNext == NULL)
	{
		perror("MoveSnake()::malloc()");
		return;
	}
	//考虑到蛇要走下一步,那么我们要先求出下一步是什么
	switch (ps->_Dir)
	{
	case LEFT:
		pNext->x = ps->_pSnake->x - 2;
		pNext->y = ps->_pSnake->y;
		break;
	case RIGHT:
		pNext->x = ps->_pSnake->x + 2;
		pNext->y = ps->_pSnake->y;
		break;
	case UP:
		pNext->x = ps->_pSnake->x;
		pNext->y = ps->_pSnake->y - 1;
		break;
	case DOWN:
		pNext->x = ps->_pSnake->x;
		pNext->y = ps->_pSnake->y + 1;
		break;
	}//这样就求得下一个点的信息了

	if (IsNextFood(ps, pNext))//如果下一步是食物
	{
		EatFood(ps, pNext);
	}
	else//没有吃到食物
	{
		NoFood(ps, pNext);
	}

	KillByWall(ps);//检查是否撞墙

	KillBySelf(ps);//检查是否自杀了
}

解释:

1.由于蛇要走向下一步,因此我们需要开辟额外的空间给下一个节点。

2.在创建好空间的同时,获取相应的坐标,并移动光标打印蛇身,再进行下一步判断。

5.下一个点是不是食物IsNextFood()

如果下一个点是食物,返回true,否则返回false,当然这是布尔类型,需要include<stdbool.h>才能正常使用。

代码如下:

//Snake.c
# include"Snake.h"
bool IsNextFood(pSnake ps, pSnakeNode pNext)//比较下一个位置和食物的位置
{
	if (ps->_pFood->x == pNext->x && ps->_pFood->y == pNext->y)
	{
		return true;
	}
	return false;
}

6.吃食物EatFood()

当下一个点是食物的时候,蛇要吃掉食物,同时蛇的长度会增加1,因此需要将被吃掉的食物的节点转换为蛇的头结点,并存储、打印相关坐标。同时,蛇吃掉了食物,食物需要重新生成,因此需要再次调用上文已经写好的CreateFood().

代码如下:

//Snake.c
# include"Snake.h"
//吃掉食物
void EatFood(pSnake ps, pSnakeNode pNext)//覆盖、更新食物
{
	ps->_Count += ps->_FoodWeight;//加上食物的分数

	pSnakeNode cur = NULL;

	pNext->next = ps->_pSnake;
	ps->_pSnake = pNext;//头插,将被吃掉的食物的节点转换为蛇的头结点

	cur = ps->_pSnake;
	while (cur)//遍历,打印蛇身
	{
		SetPos(cur->x, cur->y);
		wprintf(L"%lc", BODY);
		cur = cur->next;
	}
	//创建新食物
	CreateFood(ps);
}

通过坐标的不断调用,我们不难理解为什么要定义pSnakeNode或SnakeNode,原因就是既能存储相关坐标,在内存上又连在一起,方便连续访问内存。

我们来看一下效果:

吃掉食物前:

吃掉食物后:

不难发现得分也增加了。

7.不吃食物NoFood()

如果下一个点不是食物,那下一个点成为蛇的心头结点,并打印蛇身,且蛇的尾将要去掉1,因为蛇的长度不能改变。因此,在蛇尾处,我们需要打印两个英文空格将蛇尾抹掉,并且释放掉蛇尾内存。涉及到链表的头插和尾删操作,代码如下:

//不吃食物
void NoFood(pSnake ps, pSnakeNode pNext)
{
	//头插
	pNext->next = ps->_pSnake;
	ps->_pSnake = pNext;

	pSnakeNode cur = ps->_pSnake;
	while (cur->next->next)
	{
		SetPos(cur->x, cur->y);//光标需要不断移动
		wprintf(L"%lc", BODY);
		cur = cur->next;
	}
	//留下cur->next为尾结点,释放
	SetPos(cur->next->x, cur->next->y);
	free(cur->next);
	cur->next = NULL;
	printf("  ");//打印两个空格补充
}

注意:为什么判断条件是while(cur->next->next)呢,因为我要释放最后一个节点,此时退出循环后cur指向倒数第二个节点,让cur->next释放并置为空就行了,这是常见的链表尾删操作。

8.判断是否撞到墙KILLBYWALL()

怎么判断有没有撞到墙呢?

将墙的图拿过来,不难发现(蛇头坐标)当x == 0 或 x == 56 或 y == 0 或 y == 26时,蛇撞墙了,游戏状态也要变为KILL_BY_WALL使得游戏终止了,如图所示:

代码如下:

//Snake.c
# include"Snake.h"
void KillByWall(pSnake ps)
{
	if (ps->_pSnake->x == 0 || ps->_pSnake->x == 56 || ps->_pSnake->y == 0 || ps->_pSnake->y == 26)
	{
		ps->_Game_State = KILL_BY_WALL;
	}
}

9.判断是否撞到自己KILLBYSELF()

判断蛇是否撞到自己时,由于我们定义的蛇节点中存储了内存信息和坐标信息,理论上比较内存指针是否相同或者比较坐标是否重合都可以,在这里我们选择遍历比较坐标,代码如下:

//Snake.c
# include"Snake.h"
//自杀
void KillBySelf(pSnake ps)
{
	pSnakeNode cur = ps->_pSnake->next;//从头的下一个开始哈,要不然走一步就寄了
	while (cur)
	{
		if (cur->x == ps->_pSnake->x && cur->y == ps->_pSnake->y)
		{
			ps->_Game_State = KILL_BY_SELF;
		}
		cur = cur->next;
	}
}

注意:cur应当从蛇的头结点的下一个节点开始遍历而不是头结点,否则直接自杀,因为这是恒成立的;且注意结点要不断指向下一个,否则死循环。

至此,在MoveSnake()里的IsNextFood()、EatFood()、NoFood()、KILLBYWALL()、KILLBYSELF()全部完成,GameRun()也完成实现。

效果可以参考上方视频。

六、游戏结束GameEnd()

这里的游戏结束比较简单,就是将之前为蛇malloc动态开辟的所有空间释放掉,并置为空。代码如下:

//Snake.c
# include"Snake.h"
//游戏结束
void GameEnd(pSnake ps)//销毁动态蛇节点内存
{
	pSnakeNode cur = ps->_pSnake;
	pSnakeNode del = cur;
	while (cur)
	{
		del = cur;
		cur = cur->next;
		free(del);
		del = NULL;
	}
}

注意:这里为什么要设置cur和del呢?若不定义del,cur释放置空后就找不到下一个节点了,造成了内存泄漏,因此要cur = cur->next后 再释放del。

七、Snake.c的整理

通过以上三大函数GameStart()、GameRun()、GameEnd()的编写,我们不难将其整理为完整的Snake.c,代码如下:

//Snake.c
# include"Snake.h"

//设置坐标
void SetPos(int x, int y)
{
	COORD pos = { x,y };
	HANDLE hOutput = GetStdHandle(STD_OUTPUT_HANDLE);//获得输出柄,方便设置位置
	SetConsoleCursorPosition(hOutput, pos);//传入操作柄和位置,设置光标位置
}
//隐藏光标
void ConsoleCursorHide()
{
	HANDLE hOutput = GetStdHandle(STD_OUTPUT_HANDLE);
	CONSOLE_CURSOR_INFO console_cursor_info;//定义鼠标结构体,控制光标时是否隐藏
	GetConsoleCursorInfo(hOutput,&console_cursor_info);//获取输出柄和鼠标信息
	console_cursor_info.bVisible = false;//设置光标不可见
	SetConsoleCursorInfo(hOutput, &console_cursor_info);//重新设置鼠标信息
}

//游戏开始
void GameStart(pSnake ps)
{
	//设置窗口大小以及标题
	system("mode con cols=100 lines=30");
	system("title 贪吃蛇");

	//ConsoleCursorHide();//隐藏光标

	WelcomeToGame();//打印欢迎界面

	CreateMap();//打印地图

	InitSnake(ps);//初始化小蛇

	CreateFood(ps);//创建食物
}
//打印欢迎界面
void WelcomeToGame()
{
	//第一张页面
	SetPos(40, 14);
	printf("欢迎来到贪吃蛇!");
	SetPos(40, 15);
	system("pause");//暂停且显示“按任意键继续”
	system("cls");

	//第二张页面
	SetPos(20, 14);
	printf("请使用 ↑ ↓ ← → 键控制蛇的移动");
	SetPos(20, 15);
	printf("F3加速,F4减速,ESC退出,空格暂停");
	SetPos(40, 25);
	system("pause");
	system("cls");
}
//打印地图
void CreateMap()
{
	SetPos(0, 0);//坐标归位
	int i = 0;
	//先打印最上面的墙
	for (i = 0; i <= 56; i += 2)
	{
		wprintf(L"%lc",WALL);
	}
	//再打印左右两边的墙
	for (i = 1; i <= 25; i++)
	{
		SetPos(0, i);
		wprintf(L"%lc", WALL);
		SetPos(56, i);
		wprintf(L"%lc", WALL);
	}
	//最后打印最下面的墙
	SetPos(0, 26);
	for (i = 0; i <= 56; i += 2)
	{
		wprintf(L"%lc", WALL);
	}
}
//初始化小蛇
void InitSnake(pSnake ps)
{
	pSnakeNode cur = NULL;
	int i = 0;
	for (i = 0; i < 5; i++)
	{
		//新建蛇节点
		cur = (pSnakeNode)malloc(sizeof(SnakeNode));
		if (cur == NULL)
		{
			perror("InitSnake()::malloc()");
			return;
		}
		cur->x = POS_X + 2 * i;
		cur->y = POS_Y;
		cur->next = NULL;

		SetPos(cur->x, cur->y);
		wprintf(L"%lc", BODY);//记得要打印
        
		//拼接
		cur->next = ps->_pSnake;
		ps->_pSnake = cur;
		
	}
	ps->_Count = 0;
	ps->_FoodWeight = 10;
	ps->_SleepTime = 300;
	ps->_Dir = RIGHT;
	ps->_Game_State = OK;
	ps->_pFood = NULL;
}
//创建食物
void CreateFood(pSnake ps)
{
	int x = 0, y = 0;
	pSnakeNode pFood = (pSnakeNode)malloc(sizeof(SnakeNode));
	pSnakeNode cur = ps->_pSnake;
	repeat:
	do
	{
		x = rand() % 53 + 2;
		y = rand() % 25 + 1;
	} while (x % 2 != 0);//x必须是偶数

	while (cur)
	{
		if (cur->x == x && cur->y == y)
		{
			goto repeat;
		}
		cur = cur->next;
	}
	pFood->x = x;
	pFood->y = y;
	ps->_pFood = pFood;
	SetPos(x, y);
	wprintf(L"%lc", FOOD);
}

//游戏运行
void GameRun(pSnake ps)
{
	PrintHelpInfo();
	do {
		SetPos(64, 10);
		printf("您的得分为:%05d", ps->_Count);
		SetPos(64, 11);
		printf("单个食物的分数为:%d", ps->_FoodWeight);
		if (KEY_STATE(VK_LEFT) && ps->_Dir != RIGHT)
		{
			ps->_Dir = LEFT;
		}
		else if (KEY_STATE(VK_RIGHT) && ps->_Dir != LEFT)
		{
			ps->_Dir = RIGHT;
		}
		else if (KEY_STATE(VK_UP) && ps->_Dir != DOWN)
		{
			ps->_Dir = UP;
		}
		else if (KEY_STATE(VK_DOWN) && ps->_Dir != UP)
		{
			ps->_Dir = DOWN;
		}
		else if (KEY_STATE(VK_F3))
		{
			if (ps->_SleepTime >= 100)
			{
				ps->_SleepTime -= 30;
				ps->_FoodWeight += 2;
			}
		}
		else if (KEY_STATE(VK_F4))
		{
			if (ps->_SleepTime <= 300)
			{
				ps->_SleepTime += 30;
				ps->_FoodWeight -= 2;
			}
		}
		else if (KEY_STATE(VK_ESCAPE))
		{
			ps->_Game_State = BREAK_NORMAL;
		}
		else if (KEY_STATE(VK_SPACE))
		{
			Pause();
		}
		Sleep(ps->_SleepTime);
		MoveSnake(ps);
	} while (ps->_Game_State == OK);
	
	SetPos(20, 10);
	if (ps->_Game_State == KILL_BY_WALL)
	{
		printf("您撞墙而死,游戏结束!");
	}
	else if (ps->_Game_State == KILL_BY_SELF)
	{
		printf("您自杀而死,游戏结束!");
	}
}
//打印提示信息
void PrintHelpInfo()
{
	SetPos(64, 17);
	printf("1.按 ↑ ↓ ← → 控制蛇的移动");
	SetPos(64, 18);
	printf("2.不能撞墙,不能吃到自己");
	SetPos(64, 19);
	printf("3.按F3加速,F4减速");
	SetPos(64, 20);
	printf("4.按ESC退出,空格暂停");
}
//暂停
void Pause()//空格为暂停
{
	while(1)
	{
		Sleep(200);
		if (KEY_STATE(VK_SPACE))
		{
			break;
		}
	}
}
//蛇的移动
void MoveSnake(pSnake ps)//更新蛇头等
{
	//判断蛇的状态,有没有撞墙,有没有自杀,有没有吃到食物
	pSnakeNode pNext = (pSnakeNode)malloc(sizeof(SnakeNode));
	if (pNext == NULL)
	{
		perror("MoveSnake()::malloc()");
		return;
	}
	//考虑到蛇要走下一步,那么我们要先求出下一步是什么
	switch (ps->_Dir)
	{
	case LEFT:
		pNext->x = ps->_pSnake->x - 2;
		pNext->y = ps->_pSnake->y;
		break;
	case RIGHT:
		pNext->x = ps->_pSnake->x + 2;
		pNext->y = ps->_pSnake->y;
		break;
	case UP:
		pNext->x = ps->_pSnake->x;
		pNext->y = ps->_pSnake->y - 1;
		break;
	case DOWN:
		pNext->x = ps->_pSnake->x;
		pNext->y = ps->_pSnake->y + 1;
		break;
	}//这样就求得下一个点的信息了

	if (IsNextFood(ps, pNext))//如果下一步是食物
	{
		EatFood(ps, pNext);
	}
	else//没有吃到食物
	{
		NoFood(ps, pNext);
	}

	KillByWall(ps);//检查是否撞墙

	KillBySelf(ps);//检查是否自杀了
}
//判断下一个是不是食物
bool IsNextFood(pSnake ps, pSnakeNode pNext)//比较下一个位置和食物的位置
{
	if (ps->_pFood->x == pNext->x && ps->_pFood->y == pNext->y)
	{
		return true;
	}
	return false;
}
//吃掉食物
void EatFood(pSnake ps, pSnakeNode pNext)//覆盖、更新食物
{
	ps->_Count += ps->_FoodWeight;//加上食物的分数

	pSnakeNode cur = NULL;

	pNext->next = ps->_pSnake;
	ps->_pSnake = pNext;//头插

	cur = ps->_pSnake;
	while (cur)
	{
		SetPos(cur->x, cur->y);
		wprintf(L"%lc", BODY);
		cur = cur->next;
	}
	//创建新食物
	CreateFood(ps);
}
//不吃食物
void NoFood(pSnake ps, pSnakeNode pNext)
{
	//头插
	pNext->next = ps->_pSnake;
	ps->_pSnake = pNext;

	pSnakeNode cur = ps->_pSnake;
	while (cur->next->next)
	{
		SetPos(cur->x, cur->y);
		wprintf(L"%lc", BODY);
		cur = cur->next;
	}
	//留下cur->next为尾结点,释放
	SetPos(cur->next->x, cur->next->y);
	free(cur->next);
	cur->next = NULL;
	printf("  ");//打印两个空格补充
}
//撞墙
void KillByWall(pSnake ps)
{
	if (ps->_pSnake->x == 0 || ps->_pSnake->x == 56 || ps->_pSnake->y == 0 || ps->_pSnake->y == 26)
	{
		ps->_Game_State = KILL_BY_WALL;
	}
}
//自杀
void KillBySelf(pSnake ps)
{
	pSnakeNode cur = ps->_pSnake->next;//从头的下一个开始哈,要不然走一步就寄了
	while (cur)
	{
		if (cur->x == ps->_pSnake->x && cur->y == ps->_pSnake->y)
		{
			ps->_Game_State = KILL_BY_SELF;
		}
		cur = cur->next;
	}
}

//游戏结束
void GameEnd(pSnake ps)//销毁动态蛇节点内存
{
	pSnakeNode cur = ps->_pSnake;
	pSnakeNode del = cur;
	while (cur)
	{
		del = cur;
		cur = cur->next;
		free(del);
		del = NULL;
	}
}

八、test.c的整理

还有一个部分是询问玩家要不要继续游戏,这一部分放在主函数里面比较好,如果选择继续玩就重新开始,选择退出就彻底终止程序,因此考虑使用do-while循环,代码如下:

//test.c
# include"Snake.h"
void test();//测试函数
int main()
{
	setlocale(LC_ALL, "");
	srand((unsigned int)time(NULL));
	test();
	return 0;
}
void test()
{
	pSnake ps = (pSnake)malloc(sizeof(Snake));
	if (ps == NULL)
	{
		perror("test()::malloc()");
		return;
	}
	int ch = 0;
	do
	{
        //初始化为0
		ps->_pSnake = NULL;
		ps->_Count = 0;
		ps->_Dir = RIGHT;
		ps->_FoodWeight = 0;
		ps->_pFood = NULL;
		ps->_SleepTime = 0;
		ps->_Game_State = OK;
		//游戏开始
		GameStart(ps);
		//游戏运行
		GameRun(ps);
		//游戏结束
		GameEnd(ps);
		SetPos(20, 12);
		printf("再来一局?(Y/N)");
		SetPos(36, 12);
		ch = getchar();
		getchar();//吸收回车
	} while (ch == 'Y' || ch == 'y');//小写和大写都能够继续玩
	SetPos(0, 27);//方便在最下面打印退出信息,美观
	free(ps);
	ps = NULL;
}

九、总结

完成像贪吃蛇一样的项目,逻辑思维至关重要,只有在实现项目之前明白大概需要哪些函数,定义哪些变量,等等,才能够较为快速的实现代码;本次实现代码,我们大量使用了结构体、指针、动态内存开辟,这些是接下来我们学习数据结构的基础部分;我的代码可能还存在很多可以优化的地方,还请各位大佬指正,谢谢。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值