贪吃蛇实现

一. 相关知识介绍

1.目的

实现贪吃蛇我们用到的基本都是C语言里面所学过的知识点,既是对所学知识的熟练运用,也是复习的一种很好的方式

当然,刚开始我们也会用到一些不属于C语言的函数,我们来一起学学吧,让贪吃蛇变成我们迈出基本的C语言走向更广阔的学府的一条有意义的蛇

2. Win 32 API 介绍

Win 32 API 也就是Microsoft Windows 32位平台的应用程序编程接口

2.1 控制台程序(Console)

平常写代码运行起来的黑框其实就是控制台程序

我们可以使用cmd命令来设置控制台的大小和宽度,例如:

 mode con cols=100 lines=30 

当然lines和cols反着写也是可以的

大家也看到下面还有一个命令,这个呢就是修改我们控制台的标题的命令,这些命令都得在system函数的括号里面才可以实现喔!

而不要忘了任何函数使用都要包含对应的头文件,system函数的头文件就是<windows.h>!

2.2 控制台屏幕上的坐标 COORD
 

COORDWindows API 中定义的一个结构体,表示一个字符在控制台屏幕缓冲区上的坐标,坐标系(0,0)的坐标在缓冲区的顶部左侧单元格,(也就是左上)

来看看COORD的声明

typedef struct _COORD

{

SHORT X,

SHORT Y,

}COORD,*PCOORD;

下面来看使用方法:

2.3GetStdHandle

GetStdHandle是一个Windows API 函数,它用于从一个特定的标准设备中取得一个句柄,使这个句柄可以操作设备

函数声明:

Handle GetStdHandle(DWORD  nStdHandle);

代码段中我们先创建一个类型为Handle的变量,用来接收函数返回的句柄,有了句柄才能对该控制台进行相关的操作

2.4 GetConsoleCursorInfo

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

2.4.1 CONSOLE_CURSOR_INFO
这个结构体,包含有关控制台光标的信息
typedef struct _CONSOLE_CURSOR_INFO {
 DWORD dwSize;
 BOOL bVisible;
} CONSOLE_CURSOR_INFO, *PCONSOLE_CURSOR_INFO;

dwSize是指光标目前的大小占比,bVisible是指光标可见性,默认true为可见,false为不可见

2.4.2 SetConsoleCursorInfo
设置指定控制台屏幕缓冲区的光标的大小和可见性
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);//设置控制台光标状态

我们首先获得本控制台的句柄,然后在找到光标的信息,用GetConsoleCursorInfo函数来找到控制台上的光标的信息,然后把成员bVisible修改为false,默认为不可见 ,这时就可以用SetConsoleCursorInfo函数来修改控制台的光标状态了

2.5 SetConsoleCursorPosition
设置指定控制台屏幕缓冲区中的光标位置,我们将想要设置的坐标信息放在COORD类型的pos中,调用SetConsoleCursorPosition函数将光标位置设置到指定的位置。

BOOL WINAPI SetConsoleCursorPosition
{
HANDLE hConsoleOutput,
COORD pos
};

例如:

COORD pos = { 10, 5};
 HANDLE hOutput = NULL;
 //获取标准输出的句柄(⽤来标识不同设备的数值)
 hOutput = GetStdHandle(STD_OUTPUT_HANDLE);
 //设置标准输出上光标的位置为pos
 SetConsoleCursorPosition(hOutput, pos);

这个函数的第一个参数为句柄,第二个为光标位置,修改以后我们想在控制台上的哪个地方输出字符或者其他,就可以通过修改光标位置来达到,那么这样一看,我们以后还不止一次的会用到它,那么大家应该都心知肚明了,我们来把它封装成一个函数吧,用的时候直接调用就好了,防止代码冗余

//设置光标的坐标
void SetPos(short x, short y)
{
 COORD pos = { x, y };
 HANDLE hOutput = NULL;
 //获取标准输出的句柄(⽤来标识不同设备的数值)
 hOutput = GetStdHandle(STD_OUTPUT_HANDLE);
 //设置标准输出上光标的位置为pos
 SetConsoleCursorPosition(hOutput, pos);
}

那么SetPos就是设置光标位置的函数啦,想要修改直接传入两个坐标就可以实现目的啦

2.6 GetAsyncKeyState
这是一个获取按键情况的函数,GetAsyncKeyState的函数原型如下:
SHORT GetAsyncKeyState(
 int vKey
);
将键盘上每个键的虚拟键值传递给函数,函数通过返回值来分辨按键的状态,我们就可以通过按键来判断蛇的走向了
GetAsyncKeyState 的返回值是short类型,在上⼀次调⽤ GetAsyncKeyState 函数后,如果
返回的16位的short数据中,最⾼位是1,说明按键的状态是按下,如果最⾼是0,说明按键的状态是抬起;如果最低位被置为1则说明该按键被按过,否则为0
如果我们要判断⼀个键是否被按过,可以检测GetAsyncKeyState返回值的最低值是否为1,这里用到一个宏
# define KEY_PRESS(VK) ( (GetAsyncKeyState(VK) & 0x1) ? 1 : 0 )

那么这个是什么意思呢?就是看返回的short类型的变量的最后一位二进制位是1还是0,那么我们就可以给它与一个1来检查

画图来看看吧:

与1的话,遇0为0,相同为1,这样就可以很好的检测出最后一位是1还是0了

那么来看看有哪些虚拟键码吧:

键码有很多,就不一 一列举了,大家下来可以自己去搜索,我这里把贪吃蛇用到的上下左右给截出来了  键码网址:虚拟键码 (Winuser.h) - Win32 apps | Microsoft Learn

3. 地图

我们最终的贪吃蛇⼤纲要是这个样⼦,那我们的地图如何布置呢? 先来看看实现好的

这⾥不得不讲⼀下控制台窗⼝的⼀些知识,如果想在控制台的窗⼝中指定位置输出信息,我们得知道 该位置的坐标,所以⾸先介绍⼀下控制台窗⼝的坐标知识
控制台窗⼝的坐标如下所示,横向的是X轴,从左向右依次增⻓,纵向是Y轴,从上到下依次增

在游戏地图上,我们打印墙体使⽤宽字符:□,打印蛇使⽤宽字符●,打印⻝物使⽤宽字符★
普通的字符是占⼀个字节的,这类宽字符是占⽤2个字节,当然我这里把星星改成了 我女朋友的名字 哈哈,大家也可以随意修改的啦!

3.1 <locale.h>本地化
<locale.h>提供的函数⽤于控制C标准库中对于不同的地区会产⽣不⼀样⾏为的部分
在标准中,依赖地区的部分有以下几项:
数字量的格式
货币量的格式
字符集
⽇期和时间的表⽰形式

3.2 类项
通过修改地区,程序可以改变它的⾏为来适应世界的不同区域。但地区的改变可能会影响库的许多部分,其中⼀部分可能是我们不希望修改的 所以C语言支持针对不同的类项进行修改,下⾯的⼀个宏,指定⼀个类项:
LC_COLLATE:影响字符串比较函数 strcoll() strxfrm()
LC_CTYPE:影响字符处理函数的行为。
LC_MONETARY:影响货币格式。
LC_NUMERIC:影响 printf() 的数字格式。
LC_TIME:影响时间格式 strftime() wcsftime()
LC_ALL - 针对所有类项修改,将以上所有类别设置为给定的语⾔环境。

3.3 setlocale函数

声明:

char* setlocale (int category, const char* locale);
setlocale 函数⽤于修改当前地区,可以针对⼀个类项修改,也可以针对所有类项。
setlocale 的第⼀个参数可以是前说明的类项中的⼀个,那么每次只会影响⼀个类项,如果第⼀个参 数是LC_ALL,就会影响所有的类项。
C标准给第二个参数仅定义了2种可能取值: "C" (正常模式)和 " " (本地模式)。
在任意程序执行开始,都会隐藏式执⾏调⽤:
setlocale(LC_ALL, "C");//C语言启动默认模式
setlocale(LC_ALL, ""); //本地模式
用这个函数切换到我们的本地模式后,就支持宽字符(汉字)或特殊符号的输出

3.4 宽字符的打印
宽字符的字面量必须加上前缀“L”,否则 C 语言会把字⾯量当作窄字符类型处理。前缀“L”在单引
号前⾯,表示宽字符,对应 wprintf() 的占位符为 %lc ;在双引号前⾯,表⽰宽字符串,对应
wprintf() 的占位符为 %ls

#include <stdio.h>
#include<locale.h>
int main() {
 setlocale(LC_ALL, "");
 wchar_t ch1 = L'●';
wchar_t ch2 = L'刘';
wchar_t ch3 = L'帅';
wchar_t ch4 = L'★';
printf("%c%c\n", 'a', 'b');
wprintf(L"%lc\n", ch1);
wprintf(L"%lc\n", ch2);
wprintf(L"%lc\n", ch3);
wprintf(L"%lc\n", ch4);
return 0;
}

打印结果: 

ab
●
刘
帅
★

好了,我们要用到的差不多都讲述了,那我们来着手写代码吧,写的过程中我会继续给大家讲述我的思路和相关知识

              

二,贪吃蛇的实现

我们还是老样子,分三个文件来实现:

test.c — 游戏的测试

snake.c — 游戏的功能实现

snake.h — 游戏的函数声明,以及类型声明

下面开始实现,我们游戏一开始就要先配置本地化环境,以方便可以在屏幕上打印宽字符,所以直接放在定义的main函数的刚开始

另外我们后期要生成贪吃蛇的食物,也肯定是一个随机数,现在也先把srand函数声明一下

setlocale需要的是<locale.h>   ,srand需要的是<time.h>

我们接下来捋捋思路

初始化游戏
1.创建初始化环境
2.功能介绍
/3.打印地图
4.创建贪吃蛇
5.创建食物
6.设置游戏相关信息

那么我们的声明都是放在头文件里面的

在游戏运行的过程中,蛇每次吃⼀个食物,蛇的⾝体就会变长⼀节,如果我们使⽤链表存储蛇的信
息,那么蛇的每⼀节其实就是链表的每个节点。每个节点只要记录好蛇身节点在地图上的坐标就行, 所以蛇节点结构如下:

要管理整条贪吃蛇,我们再封装⼀个Snake的结构来维护整条贪吃蛇:

蛇的方向和游戏状态可以一一列举,那么我们使用枚举类型

然后画出总思路一步步实现:

我们这里分为三大类:游戏开始,游戏运行,游戏结束的善后工作

1.游戏开始的实现

这个模块完成游戏的初始化任务:
控制台窗口大小的设置
控制台窗⼝名字的设置
⿏标光标的隐藏
打印欢迎界面
创建地图
初始化蛇
创建第⼀个食物

1.123我们把游戏开始封装为一个函数,开始需要的其他功能也封装为一个个函数来实现

这些我们前面都已经讲了,可以回去看看

目前我们已经实现好前三个任务啦

1.4 接下来就是打印欢迎界面,那么我们就要用到最前面早就封装好的函数SetPos啦

这个函数只需要坐标,就可以在控制台的任意位置输出啦
下面来调用

这个打印界面完成以后就是这个样子啦

在按一下任意键继续就是这样

1.5 接下来就是创建地图啦:

//创建地图
void create_map()
{
	int i = 0;   //横58 竖26

	//上
	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 <=25; i++)
	{
		SetPos(0, i);
		wprintf(L"%lc", wall);
	}

	//右
	for (i = 1; i <=25; i++)
	{
		SetPos(56, i);
		wprintf(L"%lc", wall);
	}
}

这里我们也是用到了SetPos函数,刚开始从坐标为0开始它会一直下向后打印,我们这里就打印一个58*26的地图吧,因为我们横着打印的是宽字符,一个宽字符占两个字节,所以58我们只需要打印29次就可以刚好全部占满

□□□□□□□□□□□□□□□□□□□□□□□□□□□□□

我们第一个循环打印了第一行围墙,接下来同样在(0,26)的位置,也就是26行的位置在打印29个宽字符的围墙,打印好以后就是这样:

□□□□□□□□□□□□□□□□□□□□□□□□□□□□□










□□□□□□□□□□□□□□□□□□□□□□□□□□□□□

横的两行好打印,那么竖着的我们应该怎么办呢?也不难

我们从x为0和x为56的位置开始打印,每次循环都让y+1,这样就可以一直向下打印啦,那么都完了以后我们的地图就打印好啦:

1.6 接下来初始化蛇

//初始化贪吃蛇
void init_snake(Psnake ps)
{
	psnake cur = NULL;

	int i = 0;
	for (i = 0; i < 5; i++)  //整个循环既可以创建五个结点的空间,也可以把五个结点链接起来
	{
		cur = (psnake)malloc(sizeof(psnake));//申请蛇的结点空间
		if (cur == NULL)
		{
			perror("malloc");
				return;
		}
		cur->next = NULL;  //刚开始谁都不要指向谁

		cur->x = POS_x + 2 * i;  //设置坐标
		cur->y = 5;


		//下面也是用循环顺便链接起来整个链表

		//采用头插法链接蛇的身体
		if (ps->snakehead==NULL) //为空的时候放进一个结点
		{
			ps->snakehead = cur;
		}
		else //不为空的时候,头插进链表,然后将头指针指向这个结点
		{
			cur->next = ps->snakehead;
			ps->snakehead = cur;
		}
	}

	cur = ps->snakehead; //先将蛇的头指针给了cur,然后遍历蛇整个身体链表
	//打印蛇的身体,坐标为蛇的两个结点,每次将光标移动到下一个结点上,然后打印
	while (cur)  
	{
		SetPos(cur->x, cur->y);
		wprintf(L"%lc",body);
		cur = cur->next;
	}

	//设置关于蛇的一些情况
	ps->dir = right;  //蛇默认向右走
	ps->status = ok;   //情况默认为ok
	ps->food_weight = 10;  //每个食物分数为10
	ps->score = 0;        //总分数初始化为0
	ps->sleep_time = 200; //以毫秒为单位
}

我们首先为蛇申请五个结点的空间作为刚开始的长度或链表大小,并且在循环里把坐标赋好,首先蛇肯定是一条,所以y坐标不能动,而因为要打印的是宽字符,所以x坐标必须为2的倍数,才不至于蛇出现一半在墙里面,一半在墙外面的情况

然后在循环里把五个结点头插,连接成一个链表,每次申请的cur就是一个新节点,然后一直头插,最后头指针放在最后一个结点,就像这样:

最后把我们初始化好的蛇打印出来,每次的位置就是蛇结点的坐标,在将一些游戏的情况设置一下,我们的蛇默认向右走,游戏运行情况为ok,每个食物权重为10分,总分数为0,蛇每走一步就会停一下,我们用肉眼是很难看出来的,每走一步暂停的时间为200毫秒,到了后面我们要让蛇减速的话就可以把时间增多,加速的话就把时间减少

1.7 创建食物

大概是这个思路:

先随机生成食物的坐标
x坐标必须是2的倍数
食物的坐标不能和蛇身每个节点的坐标重复
创建食物节点,打印食物
//创建食物的随机函数
void create_food(Psnake ps)
{         
	int x = 0;                          
	int y = 0;                          

	again:
	do
	{
		x = rand() % 53 + 2;      //还得在墙体范围内
		y = rand() % 25 + 1;
	} while (x%2!=0);    //食物x坐标为偶数,否则蛇吃不到,所以得判断
	


	psnake cur = ps->snakehead;  //接收蛇头指针

	while (cur)//遍历判断食物和蛇身是不是重复,是的话goto重新生成
	{
		if (x == cur->x && y == cur->y)
		{
			goto again;
		}
		cur = cur->next;
	}


	//创建食物的结点
	psnake food_=(snakeNode*)malloc(sizeof(snakeNode));
	if (food_ == NULL)
	{
		perror("creat_food:");
		return;
	}
	food_->x = x;
	food_->y = y;
	food_->next = NULL;

	SetPos(x, y);
	wprintf(L"%lc", food);
	ps->snakefood = food_;
}

我们随机生成的食物首先必须在地图里面,所以需要设置一些限制条件,而且食物的坐标x和蛇的一样,也不能是单数,必须为2的倍数,然后用循环遍历蛇身,如果有食物的坐标x和y都和蛇的相等,那就goto到随机数之前重新生成,这些条件都判断完了之后,我们就放心的给食物申请结点空间,将坐标赋给食物结点的x和y,并且定位光标到随机坐标的位置,然后用wprintf打印食物,最后将贪吃蛇结构体食物的结点指向食物,我们这里为了避免麻烦,打印字符都用宏代替啦

我们这里完了以后,游戏开始的实现就完成啦,来看看吧:

右边的我们在第二部分去实现

2.游戏运行的实现

2.1.打印帮助信息

//打印右侧帮助信息
void print_help()
{
	SetPos(65, 10);
	wprintf(L"%ls", L"不能穿墙,不能咬到自己");
	SetPos(65, 12);
	wprintf(L"%ls", L"用↑.↓.←.→.来控制蛇移动");
	SetPos(65, 14);
	wprintf(L"%ls", L" 按F3加速,F4减速");
	SetPos(65, 16);
	wprintf(L"%ls", L"按ESC退出,按spacce暂停");

	SetPos(80, 25);
	wprintf(L"%ls", L"《-刘帅制作-》");
}

打印完以后就正式进入到了功能实现的门槛

2.2游戏逻辑运行

//游戏运行的逻辑
void Gamerun(Psnake ps)
{
	//打印帮助信息
	print_help();
	do
	{
		//打印总分数和食物分值
		SetPos(65, 8);
		printf("食物分数:%d   总分数:%d \n", ps->food_weight, ps->score);

		//看上下左右键有没有被按
		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_SPACE))
		{
			//暂停
			Pause();
		}
		else if (KEY_PRESS(VK_ESCAPE))
		{
			//正常退出游戏
			ps->status = Esc;
		}
		else if (KEY_PRESS(VK_F3))
		{
			//加速
			if (ps->sleep_time > 80) //用休眠时间来做判断,休眠最大200毫秒,只能加速四次
			{
				ps->sleep_time -= 30;
				ps->food_weight += 2;
			}
		}
		else if (KEY_PRESS(VK_F4))
		{
			//减速
			if (ps->food_weight > 2)  //用食物分数来做减速的条件,最多分数到2的时候就不能减速了
			{
				ps->sleep_time += 30;
				ps->food_weight -= 2;
			}
		}

		snakeMove(ps); //蛇每次走一步的函数
		Sleep(ps->sleep_time); //每走一步的休眠时间

	} while (ps->status == ok); //情况为ok就可以正常运行游戏
}

我们将食物分数和总分数的打印放进循环里,因为他们会随着游戏情况变化,一进循环我们就来判断一下玩家的按键有没有按压,我们需要刚开始讲的那个宏

//定义按键有没有被按的宏
#define  KEY_PRESS(vk)  ((GetAsyncKeyState(vk)&1)?1:0)

将找好的虚拟键值放进宏里面去测试,这里我们还要判断一下,因为我们不能走相反的方向,比如蛇现在向右走,就不能向左走,其他三个按键也是一样都判断一下,如果按压了哪个键,那么我们就把蛇的方向改变为那个键指的方向

接下来判断其他按键,空格要暂停的话其实也很简单,我们只需要一个函数,然后死循环的用Sleep函数进行休眠就好了,可是我们不会一直暂停呀,所以我们需要做一个跳出死循环的条件,那就是空格下次如果被按了,那说明就是不要暂停啦,我们直接break跳出死循环:

//游戏暂停函数
void Pause()
{
	while (1)
	{
		Sleep(200);
		if (KEY_PRESS(VK_SPACE))
		{
			break;
		}
	}
}

接下来其他键我就一起说啦,判断Esc退出键,我们就把status设置为Esc,因为循环的条件就是status=ok,所以就可以退出游戏啦

而判断F3和F4加速减速也很简单,我们加减蛇每走一步的休眠时间就好啦,然后每次相应的也加减分数,但我们可不能让玩家一直加速或者减速,所以也得判断,加速做多可以有四次,休眠时间小于80就不可以加速啦,而减速每次减2的食物分数,当食物分数小于2时就不可以减啦

2.3 蛇每走一步函数

先上代码:

//蛇每走一步的函数
void snakeMove(Psnake ps)
{

	//创建一个结点,表示蛇的下一个结点
	psnake pnext = (psnake)malloc(sizeof(snakeNode));
	if (pnext == NULL)
	{
		perror("snakeMove:");
		return;
	}

	//判断蛇的方向以及下一个结点的坐标
	switch (ps->dir)
	{
	case up:
		pnext->x=ps->snakehead->x;
		pnext->y=ps->snakehead->y - 1;
		break;
	case down:
		pnext->x = ps->snakehead->x;
		pnext->y = ps->snakehead->y + 1;
		break;
	case left:
		pnext->x = ps->snakehead->x-2;
		pnext->y = ps->snakehead->y;
		break;
	case right:
		pnext->x = ps->snakehead->x+2;
		pnext->y = ps->snakehead->y;
		break;
	}


	//判断下一个结点是不是食物
	if (is_food(pnext,ps))
	{
		//吃掉食物
		Eatfood(pnext,ps);
		pnext = NULL;
	}
	else
	{
		//不是食物
		Nofood(pnext, ps);
	}



	//检测蛇是否撞墙
	kill_bywall(ps);

	//检测蛇是否撞到自己
	kill_byself(ps);
}
2.3.1我们得先创建下⼀个节点,根据移动方向和蛇头的坐标,蛇移动到下⼀个位置的坐标,有四种可能性,蛇向四个方向走,分别判断:

2.3.2判断完以后,将坐标赋给蛇的下一个结点,然后继续判断这个结点是不是食物

只有两种可能,是或者不是

//判断是不是食物
int is_food(psnake pn, Psnake ps)
{
	//是的话返回1,不是返回0
	return (ps->snakefood->x == pn->x && ps->snakefood->y == pn->y);
}

我们直接封装一个函数,看食物的结点坐标等不等于pn的坐标,等于就是食物,返回1,不等于返回0

当回到判断里面以后,又会产生出了两个问题,是食物我们就吃掉,不是食物就作为蛇的下一个结点,头插进去然后准备打印

那么我们又会封装两个函数,是不是感觉一直在嵌套函数哈哈,不要绕晕哦

吃掉食物函数:

//吃掉食物函数
void Eatfood(psnake pn, Psnake ps)
{
	//头插使食物指针指向蛇头指针,头插就会使蛇每次吃完食物变长
	ps->snakefood->next = ps->snakehead;
	ps->snakehead = ps->snakefood;
	//free(pn);
	//pn = NULL; //将食物指针头插到蛇链表上,然后释放掉另一个pn结点


	//遍历打印贪吃蛇
	psnake cur = ps->snakehead;//找到蛇头指针
	while (cur)
	{
		SetPos(cur->x, cur->y); //打印之前先定位坐标
		wprintf(L"%lc", body);
		cur = cur->next;
	}
	//每次吃完食物,分数也加
	ps->score += ps->food_weight;
	
	//吃完并重新创建一个食物,调用之前的函数就好
	create_food(ps);
}

如果下一个结点是食物,我们就把食物的结点指针头插指向链表,然后将蛇头指针指向刚头插进来的第一个结点,这样蛇每次吃掉食物就会变长一个结点,然后我们创建一个cur结点指向蛇头结点,遍历打印贪吃蛇,每次打印前都把要打印的贪吃蛇的每个结点的坐标定位好,每次吃完食物,总分数就加上一个食物的分数,最后我们既然吃掉一个食物,那肯定就要生成下一个随机食物,我们这里就直接调用上次的创建食物函数

不是食物函数:

//下一个位置不是食物函数
void Nofood(psnake pn, Psnake ps)
{
	//头插下一个结点链接蛇头指针
	pn->next = ps->snakehead;
	ps->snakehead = pn;
	
	//指向蛇头指针,开始遍历打印
	psnake cur = ps->snakehead;
	while(cur->next->next!=NULL)  //当遍历到蛇尾巴的前一个结点的时候跳出循环
	{
		SetPos(cur->x, cur->y);
		wprintf(L"%lc", body);
		cur = cur->next;
	}
	//把光标定位到蛇尾巴的结点,打印两个空格(正好对应宽字符的两个字节),
	// 要不然那个圆圈会一直在原地,就算蛇不吃食物也会变长
	SetPos(cur->next->x, cur->next->y);
	printf("  ");
	free(cur->next->next);//释放尾巴结点
	cur->next = NULL; //将目前蛇的最后一个结点的next置为NULL
}

如果下一个结点不是食物,那我们就把下一个结点头插进链表,然后使新节点成为蛇头指针,然后就开始打印贪吃蛇,但我们要想一个问题,蛇一直往前走会不会一直变长,每次不是食物都会向前走,但每次加一个结点是不是不合适呢?所以我们每次不是食物就加一个结点,然后减一个尾结点,这样保证贪吃蛇不吃食物的前提下,向前走一直都是原始的五个结点,那么我们就要在循环中判断啦,当遍历的蛇身结点的下一个结点的next为NULL时,我们就跳出循环,这时我们刚好打印了五个结点,但尾巴结点还在屏幕上呢 像下面这样:

所以我们需要定位到目前指针的下一个位置的坐标(尾巴结点坐标),然后打印空格,把贪吃蛇的结点●覆盖掉,这样蛇每次没吃食物移动就会保持原始长度啦!不要忘记最后把尾巴结点置为NULL喔!!!

2.3.3 判断蛇运行情况

现在我们的贪吃蛇就可以移动起来啦,但我们移动的过程中又会想到,贪吃蛇怎样算挂掉,然后退出游戏呢?那当然是撞到墙和撞到自己的身体

所以我分别封装两个函数来判断:

撞墙:

/判断撞墙的函数 
// 拿头指针的坐标判断是否与墙有重合
void kill_bywall(Psnake ps)
{
	if (ps->snakehead->x == 0 || ps->snakehead->x == 56
		||ps->snakehead->y ==0 || ps->snakehead->y == 26)
	{
		ps->status = die_wall;
	}
}

我们就拿蛇头指针的坐标来判断,如果跟墙的坐标重合,那肯定就是撞墙啦,但我们的地图是58*26的,其实x坐标得设置为等于56才算挂掉,还记得吗,我们打印的是宽字符,所以得是2的倍数,而58的话已经处于墙的最外面啦,所以得减2是56,我们判断完以后,就会用到之前设置的判断游戏情况的枚举啦,我们这时就把枚举变量设置为die_wall,就表示撞墙挂掉啦,然后do while循环就会检测到status不等于ok,然后就会退出游戏        

撞自己:

//检测蛇是否撞到自己  
// 遍历蛇身链表,然后看有没有和蛇头重复的坐标
void kill_byself(Psnake ps)
{
	psnake cur = ps->snakehead->next;
	while (cur)
	{
		if (ps->snakehead->x == cur->x &&
			ps->snakehead->y == cur->y)
		{
			ps->status = die_self;
			break;
		}
		cur = cur->next;
	}
}

撞自己的话,我们采用遍历蛇链表的方式来检测,每走一步就看看蛇头指针等不等于蛇身的结点坐标,必须得x和y的坐标都相等,这样才说明撞到自己啦,然后我们直接把游戏情况的枚举变量设置为die_self,然后break跳出循环就不判断了,因为这时贪吃蛇已经挂掉啦

2.4 每步的休眠时间

我们这里非常简单,直接调用<Windows.h>里的Sleep函数,休眠时间就为ps->sleep_time,也就是200毫秒,

3. 游戏结束的善后工作

//游戏结束-善后工作
void Gameover(Psnake ps)
{
	SetPos(24, 13);
	//根据游戏状况总结话语
	switch (ps->status)
	{
	case die_wall:
		printf("您撞到墙了,游戏结束\n");
		break;
	case die_self:
		printf("您撞到自己了,游戏结束\n");
		break;
	case Esc:
		printf("主动退出\n");
		break;
	}
}
3.1 打印话语

我们最后用switch语句,根据定义的枚举变量来决定打印什么话语,然后回到测试函数里面

3.2 重新开始游戏

回来以后就可以问玩家要不要再来一把,那么这肯定又是一个循环我们在测试函数里面写一下:

//完成游戏逻辑
void test()
{
	int ch = 0;
	do
	{
		system("cls");//清屏,防止y一直在屏幕上
		//创建贪吃蛇
		Snake snake = { 0 };

		//初始化游戏
		//1.创建初始化环境
		//2.功能介绍
		// 3.打印地图
		//4.创建贪吃蛇
		//5.创建食物
		//6.设置游戏相关信息

		//游戏初始化
		Gamestart(&snake);

		//运行游戏
		Gamerun(&snake);


		//游戏结束-善后工作
		Gameover(&snake);

		//释放贪吃蛇
		realese_snake(&snake);

		SetPos(24, 14); //坐标
		printf("还要再来一局吗?(Y/N):");
		ch=getchar(); 
		while (getchar()!='\n');  //防止一直输入的情况
	} while (ch=='y' || ch=='Y');  
	SetPos(0, 27);  //最后结束的时候把信息放在墙体下面
}

测试函数里面包括了我们贪吃蛇的所有功能实现,把这个整体作为一个循环,根据用户输入来判断是否重新开始游戏,那么循环条件就是输入值啦,有大Y也有小y,然后在输入下面加一个循环,防止用户乱输入,当用户输入选项以后,不管输入什么,我们都根据回车才做出反应

3.3 释放贪吃蛇

当然,我们每次游戏一结束,就立马释放贪吃蛇链表,然后在问玩家要不要继续开始游戏,继续开始的话就输入y或Y,然后进入三个大函数,游戏开始,运行和结束

三, C语言总结

这个贪吃蛇作为C语言基础学完的一个小项目,运用到了我们学习C语言中的许多知识,思想,还涉及了数据结构中的一些链表知识,对刚学完C语言的同学来说,是一个总结和复习自己学过知识的好机会,这是我们写的第一个小项目,只有500多行代码,但绝对不是我们的最后一个项目,我们要更加努力的去学习,写出更多的项目

少年的书桌上没有虚度的光阴,程序员是一个需要终身学习的职业,愿我们保持激情,继续奔赴下一个人才辈出的时代!

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值