贪吃蛇(C语言版)

在我们学习完C语言 和单链表知识点后 我们开始写个贪吃蛇的代码

目标:使用C语言在Windows环境的控制台模拟实现经典小游戏贪吃蛇

贪吃蛇代码实现的基本功能:

地图的绘制

蛇、食物的创建

蛇的状态(正常  撞墙  撞到自己  正常退出)

蛇移动的方向(上  下  左  右)

蛇的加速 减速

游戏暂停

.......

该代码会运用到函数、枚举、结构体、动态内存管理、预处理指令、链表、Win32 API等

我们会在学习前 会引出一些必要的知识点

Win32 API介绍

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

控制台程序

平常我们运⾏起来的⿊框程序其实就是控制台程序
我们可以使⽤cmd命令来设置控制台窗⼝的⻓宽:设置控制台窗⼝的⼤⼩,30⾏,100列(按住windows+R输入cmd  
我们可以使用cmd命令来设置控制台窗口的长度:如 设置控制台窗口的大小  30行  100列
也可以通过命令设置控制台窗口的名字:
这些能在控制台窗口执行的命令 也可以调用C语言函数system来执行
代码实现:

控制台屏幕上的坐标COORD

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

给坐标赋值:

COORD pos = { 2, 10 };

GetStdHandle

GetStdHandle是⼀个Windows API函数。它⽤于从⼀个特定的标准设备(标准输⼊--键盘、标准输出--屏幕或标准错误)中取得⼀个句柄(⽤来标识不同设备的数值),使⽤这个句柄可以操作设备。(我们可以把句柄想象成一个钥匙  只有拿到钥匙才能打开锁 ---只有得到句柄后才能进行一些操作)
 HANDLE GetStdHandle(DWORD nStdHandle);

在我们贪吃蛇代码中  函数的参数是STD_OUTPUT_HANDLE  标准输出

实例:

HANDLE houtput = NULL;
//获取标准输出的句柄(⽤来标识不同设备的数值)
houtput = GetStdHandle(STD_OUTPUT_HANDLE);

GetConsoleCursorInfo

检索有关指定控制台屏幕缓冲区的光标⼤⼩和可⻅性的信息
在该函数中 有一参数是 PCONSOLE_CURSOR_INFO   其是指向 CONSOLE_CURSOR_INFO 结构的指针,该结构接收有关主机游标 (光标)的信息

实例:

//获得标准输出设备的句柄
HANDLE houtput = NULL;
houtput=GetStdHandle(STD_OUTPUT_HANDLE);//houput获取标准输出的句柄 使得可以操作屏幕

//定义一个光标信息的结构体
CONSOLE_CURSOR_INFO cursor_info = { 0 };

//获取和houtput句柄相关的控制台上的光标信息 并存放在cursor_info中
GetConsoleCursorInfo(houtput, &cursor_info);//获取控制台光标信息

CONSOLE_CURSOR_INFO

这个结构体,包含有关控制台光标的信息
dwSize,由光标填充的字符单元格的百分⽐。 此值介于1到100之间。 光标外观会变化,范围从完
全填充单元格到单元底部的⽔平线条。
bVisible,游标的可⻅性。 如果光标可⻅,则此成员为 TRUE
在前一个代码中我们声明了一个结构体cursor_info
cursor_info.bVisible = false; //隐藏控制台光标

SetConsoleCursorInfo

设置指定控制台屏幕缓冲区的光标的⼤⼩和可⻅性
	//获得标准输出设备的句柄
	HANDLE houtput = NULL;
	houtput=GetStdHandle(STD_OUTPUT_HANDLE);//houput获取标准输出的句柄 使得可以操作屏幕

	//定义一个光标信息的结构体
	CONSOLE_CURSOR_INFO cursor_info = { 0 };

	//获取和houtput句柄相关的控制台上的光标信息 并存放在cursor_info中
	GetConsoleCursorInfo(houtput, &cursor_info);//获取控制台光标信息

	//修改光标占比
	cursor_info.dwSize = 50;
	cursor_info.bVisible = false;
	//设置和houtput句柄相关的控制台上的光标信息
	SetConsoleCursorInfo(houtput, &cursor_info);

SetConsoleCursorPosition

设置指定控制台屏幕缓冲区中的光标位置,我们将想要设置的坐标信息放在COORD类型的pos中,调⽤SetConsoleCursorPosition函数将光标位置设置到指定的位置
实例:
//获得标准输出设备的句柄
HANDLE houtput = NULL;
houtput = GetStdHandle(STD_OUTPUT_HANDLE);//句柄houput获取标准输出的句柄 使得可以操作屏幕

//定位光标的位置
COORD pos = { 10,20 };
SetConsoleCursorPosition(houtput, pos);

SetPos:封装一个设置光标位置的函数  ----贪吃蛇地图  蛇  食物的绘制都需要移动光标

void SetPos(short x, short y)//封装一个函数SetPos 光标位置
{
	HANDLE houtput = NULL;
	//获取标准输出的句柄(⽤来标识不同设备的数值)
	houtput = GetStdHandle(STD_OUTPUT_HANDLE);
	//定位光标位置
	COORD pos = { x, y };
	SetConsoleCursorPosition(houtput, pos);
}

GetAsyncKeyState

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

Win32 API我们就讲到这里

除了Win32 API外 我们还有其他知识需要学习

我这里是用VS2022写的代码  这里还需要对运行窗口做些改变

贪吃蛇游戏设计与分析

地图的绘制

这⾥不得不讲⼀下控制台窗⼝的⼀些知识,如果想在控制台的窗⼝中指定位置输出信息,我们得知道该位置的坐标,所以⾸先介绍⼀下控制台窗⼝的坐标知识。
控制台窗⼝的坐标如下所⽰,横向的是X轴,从左向右依次增⻓,纵向是Y轴,从上到下依次增⻓
在游戏地图上,我们打印墙体使⽤宽字符:□,打印蛇使⽤宽字符●,打印⻝物使⽤宽字符★
普通的字符是占⼀个字节的,这类宽字符是占⽤2个字节。
这⾥再简单的讲⼀下C语⾔的国际化特性相关的知识,过去C语⾔并不适合⾮英语国家(地区)使⽤。
C语⾔最初假定字符都是单字节的。但是这些假定并不是在世界的任何地⽅都适⽤。
C语⾔字符默认是采⽤ASCII编码的,ASCII字符集采⽤的是单字节编码,且只使⽤了单字节中的低7位,最⾼位是没有使⽤的,可表⽰为0xxxxxxx;可以看到,ASCII字符集共包含128个字符,在英语国家中,128个字符是基本够⽤的,但是,在其他国家语⾔中,⽐如,在法语中,字⺟上⽅有注⾳符号,它就⽆法⽤ ASCII 码表⽰。于是,⼀些欧洲国家就决定,利⽤字节中闲置的最⾼位编⼊新的符号。⽐如,法语中的é的编码为130(⼆进制10000010)。这样⼀来,这些欧洲国家使⽤的编码体系,可以表⽰最多256个符号。但是,这⾥⼜出现了新的问题。不同的国家有不同的字⺟,因此,哪怕它们都使⽤256个符号的编码⽅式,代表的字⺟却不⼀样。⽐如,130在法语编码中代表了é,在希伯来语编码中却代表了字⺟Gimel  。 在俄语编码中⼜会代表另⼀个符号。但是不管怎样,所有这 些编码⽅式中,0--127表⽰的符号是⼀样的,不⼀样的只是128--255的这⼀段。
⾄于亚洲国家的⽂字,使⽤的符号就更多了,汉字就多达10万左右。⼀个字节只能表⽰256种符号,肯定是不够的,就必须使⽤多个字节表达⼀个符号。⽐如,简体中⽂常⻅的编码⽅式是 GB2312,使⽤两个字节表⽰⼀个汉字,所以理论上最多可以表⽰ 256 x 256 = 65536 个符号。
后来为了使C语⾔适应国际化,C语⾔的标准中不断加⼊了国际化的⽀持。⽐如:加⼊了宽字符的类型wchar_t 和宽字符的输⼊和输出函数,加⼊了<locale.h>头⽂件,其中提供了允许程序员针对特定地区(通常是国家或者说某种特定语⾔的地理区域)调整程序⾏为的函数。

<locale.h>本地化

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

类项

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

setlocale函数

 char* setlocale (int category, const char* locale);
setlocale 函数⽤于修改当前地区,可以针对⼀个类项修改,也可以针对所有类项。
setlocale 的第⼀个参数可以是前⾯说明的类项中的⼀个,那么每次只会影响⼀个类项,如果第⼀个参数是LC_ALL,就会影响所有的类项。
C标准给第⼆个参数仅定义了2种可能取值: "C" (正常模式)和 " " (本地模式)
在任意程序执行开始 都会隐藏式执行调用正常模式  
当程序运⾏起来后想改变地区,就只能显⽰调⽤setlocale函数。⽤" "作为第2个参数,调⽤setlocale函数就可以切换到本地模式,这种模式下程序会适应本地环境。
⽐如:切换到我们的本地模式后就⽀持宽字符(汉字)的输出等。
setlocale(LC_ALL, " ");//切换到本地环境

宽字符的打印

那如果想在屏幕上打印宽字符,怎么打印呢?
宽字符的字⾯量必须加上前缀“L”,否则 C 语⾔会把字⾯量当作窄字符类型处理。前缀“L”在单引
号前⾯,表⽰宽字符,对应 wprintf() 的占位符为 %lc ;在双引号前⾯,表⽰宽字符串,对应
wprintf() 的占位符为 %ls
代码实例:
从输出结果就可以看出  打印一个普通字符 占一个字符的位置  但是打印一个宽字符  占用2个字符的位置  所以我们在贪吃蛇中使用宽字符时  得处理好地图上坐标的计算

1.绘制地图(CreatMap)

我们假设实现一个27行  58列的棋盘  再围绕地图画出墙

2.蛇身和食物(InitSnake) (CreateFood)

初始化状态 ,假设蛇的⻓度是5,蛇⾝的每个节点是●,在固定的⼀个坐标处,⽐如(24, 5)处开始出现 蛇,连续5个节点
注意:蛇的每个节点的x坐标必须是2的倍数,否则可能会出现蛇的⼀个节点有⼀半⼉出现在墙体中,另外⼀般在墙外的现象,坐标不好对⻬。
关于⻝物,就是在墙体内随机⽣成⼀个坐标(x坐标必须是2的倍数),坐标不能和蛇的⾝体重合,也不能超出墙体,然后打印★。
如下图:

3.数据结构设计(蛇的结点  贪吃蛇的方向 状态  食物分数  总得分  加速  减速)

在游戏运⾏的过程中,蛇每次吃⼀个⻝物,蛇的⾝体就会变⻓⼀节,如果我们使⽤链表存储蛇的信
息,那么蛇的每⼀节其实就是链表的每个节点。每个节点只要记录好蛇⾝节点在地图上的坐标就⾏,所以蛇节点结构如下:
typedef struct Snakenode
{
	//坐标
	int x;
	int y;
	//指向下一个节点的指针
	struct Snakenode* next;
}Snakenode,*pSnakeNode;//结构体指针pSnakeNode
要管理整条贪吃蛇,我们再封装⼀个Snake的结构来维护整条贪吃蛇:
typedef struct Snake
{
	pSnakeNode _pSnake;//指向蛇头的指针
	pSnakeNode _pFood;//指向食物节点的指针
	enum DIRECTION _dir;//蛇的方向
	enum GAME_STATUS _status;//游戏的状态
	int _food_weight;//食物分数
	int _score;//总分数
	int _sleep_time;//休息时间  时间越短 速度越快  时间越长 速度越慢
}Snake,*pSnake;
蛇的⽅向,可以⼀⼀列举,使⽤枚举
enum DIRECTION
{
	UP = 1,
	DOWN,
	LEFT,
	RIGHT
};
游戏状态,可以⼀⼀列举,使⽤枚举
//蛇的状态
//正常 撞墙 撞到自己 正常退出
enum GAME_STATUS 
{
	OK,
	KILL_BY_WALL,
	KILL_BY_SELF,
	END_NORMAL
};

4.游戏流程设计

5.核⼼逻辑实现分析

程序开始就设置程序⽀持本地模式,然后进⼊游戏的主逻辑

主逻辑分为3个过程:
游戏开始(GameStart)完成游戏的初始化
游戏运⾏(GameRun)完成游戏运⾏逻辑的实现
游戏结束(GameEnd)完成游戏结束的说明,实现资源释放
//Test.c
#include<locale.h>
#include"snake.h"
//完成的是游戏的测试逻辑
void test()
{
	int ch = 0;
	do
	{//创建贪吃蛇
		Snake snake = { 0 };
		//初始化游戏 
		//1.打印环境页面
		//2.功能介绍
		//3.绘制地图
		//4.创建蛇
		//5.创建食物
		//6.设置游戏的相关信息
		GameStart(&snake);
		//运行游戏
		GameRun(&snake);
		//结束游戏--善后工作

		GameEnd(&snake);
		SetPos(20, 15);
		printf("再来一局吗?(Y/N):");
		ch = getchar();//按下回车后 其实接收到的是\n 所以需要清理掉\n
		//getchar();//清理掉\n
		while (getchar() != '\n')
			;

	} while (ch=='Y' || ch == 'y');
	SetPos(0, 26);//让代码结束的信息放在地图外 
}
int main()
{
	//设置适配本地环境
	setlocale(LC_ALL, "");//中文 宽字符的打印
	srand((unsigned int)time(NULL));
	test();

	return 0;
}

游戏开始(GameStart)

1.设置控制台窗口大小  名称  隐藏光标

void GameStart(pSnake ps)
{
	//0.先设置窗口的大小 然后把光标隐藏
	system("mode con cols=100 lines=30");//30行 100列
	system("title 贪吃蛇");
	HANDLE houtput=GetStdHandle(STD_OUTPUT_HANDLE);
	//隐藏光标操作
	//定义一个光标信息的结构体
	CONSOLE_CURSOR_INFO cursor_info;
	//获取和houtput句柄相关的控制台上的光标信息 并存放在cursor_info中
	GetConsoleCursorInfo(houtput, &cursor_info);//获取控制台光标信息
 	cursor_info.bVisible = false;
	//设置和houtput句柄相关的控制台上的光标信息
	SetConsoleCursorInfo(houtput, &cursor_info);
	
	//1.打印欢迎页面  //2.功能介绍
	WelcomeToGame();
	//3.绘制地图
	CreatMap();
	//4.创建蛇
	InitSnake(ps);
	//5.创建食物
	CreateFood(ps);
}

2.欢迎页面及功能介绍(WelcomeToGame)

打印欢迎页面

在我们使用光标位置 之前  我们封装一个函数SetPos  

void SetPos(short x, short y)//封装一个函数SetPos 光标位置
{
	HANDLE houtput = NULL;
	//获取标准输出的句柄(⽤来标识不同设备的数值)
	houtput = GetStdHandle(STD_OUTPUT_HANDLE);
	//定位光标位置
	COORD pos = { x, y };
	SetConsoleCursorPosition(houtput, pos);
}
void WelcomeToGame()
{
	SetPos(45, 14);
	wprintf(L"欢迎来到贪吃蛇小游戏\n");
	SetPos(46, 20);
	system("pause");

	system("cls");//清除控制台信息
	SetPos(30, 14);
	wprintf(L"用↑   ↓   ←   → 来控制蛇的移动 按F3加速 F4减速\n");
	SetPos(34, 15);
	wprintf(L"加速能够得到更高的分数");

	SetPos(46, 20);
	system("pause");

	system("cls");//紧接着下面应该是地图绘制
}

代码实现结果:

3.绘制地图(CreatMap)

绘制地图就是将墙体打印出来  因为这里是宽字符打印  所有打印使用wprintf函数  打印格式串前使用L   绘制地图的关键是算好光标的坐标  才能在想要的位置打印墙体

为墙体打印的宽字符写个宏:

#define WALL L'□'

这里的易错点也就是坐标的计算

代码如下:

void CreatMap()
{//27行 58列的棋盘
	//上
	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 <= 25; i++)
	{
		SetPos(0, i);
		wprintf(L"%lc", WALL);
	}

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

4.创建蛇(初始化蛇身)(InitSnake)

蛇最开始⻓度为5节,每节对应链表的⼀个节点,蛇⾝的每⼀个节点都有⾃⼰的坐标。
创建5个节点,然后将每个节点存放在链表中进⾏管理。创建完蛇⾝后,将蛇的每⼀节打印在屏幕上。
蛇的初始位置从 (24,5) 开始。
再设置贪吃蛇的属性
蛇的默认⽅向:RIGHT
初始成绩:0
每个⻝物的分数:10
蛇的移动速度:200毫秒
游戏状态是:OK
void InitSnake(pSnake ps)
{
	int i = 0;
	pSnakeNode cur = NULL;
	for (i = 0; i < 5; i++)
	{
		cur = (pSnakeNode)malloc(sizeof(Snakenode));
		if (cur == NULL)
		{
			perror("InitSnake()::malloc()");
			return;
		}
		cur->next = NULL;
		cur->x =POS_X +i*2;//显示 + 未使用的表达式结果
		cur->y = POS_Y;

		//头插法插入链表
		if (ps->_pSnake == NULL)//空链表  //蛇头
		{
			ps->_pSnake = cur;
		}
		else//非空
		{
			cur->next = ps->_pSnake;
			ps->_pSnake = cur;
		}
	}
	cur = ps->_pSnake;
	while (cur)
	{
		SetPos(cur->x, cur->y);
		wprintf(L"%lc", BODY);
		cur = cur->next;
	}
	//设置贪吃蛇的属性
	ps->_dir = RIGHT;//默认向右
	ps->_score = 0;
	ps->_food_weight = 10;
	ps->_sleep_time = 200;//毫秒
	ps->_status = OK;
	/*getchar();*/为什么我的代码只打印一个●
}
蛇⾝打印的宽字符:
#define BODY L'●'

5.创建食物(CreateFood)

先随机生成食物的坐标(rand和srand函数)  

1.x坐标必须是2的倍数

2.食物的坐标不能和蛇身每个结点的坐标重复  不能越过墙体

创建食物结点  打印食物

食物打印的宽字符:

#define FOOD L'★'

CreateFood函数:

void CreateFood(pSnake ps)
{
	int x = 0;
	int y = 0;
	//x = rand()%53+2;//x:2~54---0~52 //还要保证x是偶数
	//y = rand()%25+1;//y:1~25---0~24
again:
	do
	{
		x = rand() % 53 + 2;//x:2~54---0~52 //还要保证x是偶数
		y = rand()%25+1;//y:1~25---0~24
	} while (x % 2 != 0);
	//x和y的坐标不能跟蛇的身体相冲突
	pSnakeNode cur = ps->_pSnake;
	while (cur)
	{
		if (x == cur->x && y == cur->y)
		{
			goto again;
		}
		cur = cur->next;
	}
	//创捷食物结点
	pSnakeNode pFood=(pSnakeNode)malloc(sizeof(Snakenode));
	if (pFood == NULL)
	{
		perror("CreateFood()::malloc()");
	}
	pFood->x = x;
	pFood->y = y;
	pFood->next = NULL;

	SetPos(x, y);
	wprintf(L"%lc", FOOD);
	ps->_pFood = pFood;
}

游戏运行(GameRun)

1.游戏运⾏期间,右侧打印帮助信息(PrintHelpInfo)

void PrintHelpInfo()
{
	SetPos(64, 10);
	wprintf(L"%ls", L"不能穿墙,不能咬到自己");

	SetPos(64, 11);
	wprintf(L"%ls", L"用↑   ↓   ←   → 来控制蛇的移动 \n");

	SetPos(64, 12);
	wprintf(L"%ls", L"按F3加速 F4减速");

	SetPos(64, 13);
	wprintf(L"%ls", L"按ESC退出游戏  空格暂停游戏");

	SetPos(64, 14);
	wprintf(L"%ls", L"made in LiFeiFei");
}

2.根据游戏状态检查游戏是否继续,如果是状态是OK,游戏继续,否则游戏结束。

游戏有两种状态:i.蛇撞墙而亡    ii.蛇撞到自己

void KillByWall(pSnake ps)
{
	if (ps->_pSnake->x == 0 || ps->_pSnake->x == 56 ||
		ps->_pSnake->y == 0 || ps->_pSnake->y == 26)
	{
		ps->_status = KILL_BY_WALL;
	}
}


void KillBySelf(pSnake ps)
{
	pSnakeNode cur = ps->_pSnake->next;//cur指向蛇身
	while (cur)
	{
		if (cur->x == ps->_pSnake->x && cur->y == ps->_pSnake->y)
		{
			ps->_status = KILL_BY_SELF;
			break;
		}
		cur = cur->next;
	}
}

3.如果游戏继续,就是检测按键情况,确定蛇下⼀步的⽅向,或者是否加速减速,是否暂停或者退出游戏。

//获取按键情况 如果按键最小位为1 说明已按过
#define KEY_PRESS(VK) ( (GetAsyncKeyState(VK) & 0x1) ? 1 : 0 )
需要的虚拟按键的罗列:
上:VK_UP
下:VK_DOWN
左:VK_LEFT
右:VK_RIGHT
空格:VK_SPACE
ESC:VK_ESCAPE
F3:VK_F3
F4:VK_F4

蛇身移动(SnakeMove)

蛇下一步移动方向会有几种情况:

1.下一步不是食物---游戏正常  在打印蛇时  我们还要将最后一个结点原先所在位置打印两个空格 并释放掉原先最后一个结点
2.下一步是食物---吃掉食物---蛇的结点数+1  这里我们使用头插---新增结点位于蛇头  
void NoFood(pSnakeNode pn, pSnake ps)
{
	//头插法
	pn->next = ps->_pSnake;
	ps->_pSnake = pn;

	pSnakeNode cur = ps->_pSnake;
	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);

	//将倒数第二个结点的地址域置为NULL
	cur->next = NULL;
}

int NextIsFood(pSnakeNode pn, pSnake ps)
{
	return (ps->_pFood->x == pn->x && ps->_pFood->y == pn->y);
}

void EatFood(pSnakeNode pn, pSnake ps)
{
	//头插法
	ps->_pFood->next = ps->_pSnake;
	ps->_pSnake = ps->_pFood;

	//释放下一个位置的结点(我们设置了一个食物结点 和下一个结点 )
	free(pn);
	pn = NULL;
	pSnakeNode cur = ps->_pSnake;
	//打印蛇
	while (cur)
	{
		SetPos(cur->x, cur->y);
		wprintf(L"%lc", BODY);
		cur = cur->next;
	}
	ps->_score += ps->_food_weight;

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

void pause()//暂停
{
	while (1)
	{
		Sleep(200);
		if (KEY_PRESS(VK_SPACE))
		{
			break;
		}
	}
}

在NoFood中 有一个易错点:在打印下一步的蛇身时  也就是将下一个结点当作蛇头  并将之前蛇身最后一个结点打印为空格  释放掉原本蛇身的最后一个结点  在释放掉最后一个结点后  还得将指向(原先蛇身的倒数第二个结点)最后一个结点的指针改为NULL  保证蛇尾打印可以正常结束  不会越界访问

void SnakeMove(pSnake ps)
{
	//创建一个结点 表示蛇即将到的下一个结点
	pSnakeNode pNextNode=(pSnakeNode)malloc(sizeof(Snakenode));
	if (pNextNode == NULL)
	{
		perror("SnakeMove()::malloc()");
		return;
	}
	switch (ps->_dir)
	{
	case UP:
		pNextNode->x = ps->_pSnake->x;
		pNextNode->y = ps->_pSnake->y-1;
		break;
	case DOWN:
		pNextNode->x = ps->_pSnake->x;
		pNextNode->y = ps->_pSnake->y+1;
		break;
	case LEFT:
		pNextNode->x = ps->_pSnake->x-2;
		pNextNode->y = ps->_pSnake->y;
		break;
	case RIGHT:
		pNextNode->x = ps->_pSnake->x+2;
		pNextNode->y = ps->_pSnake->y;
		break;
	}

	//检测下一个坐标处是否是食物
	if (NextIsFood(pNextNode,ps))
	{
		EatFood(pNextNode, ps);
	}
	else
	{
		NoFood(pNextNode, ps);
	}

	//检测蛇是否撞墙
	KillByWall(ps);
	//检测蛇是否撞到自己
	KillBySelf(ps);
}
确定了蛇的⽅向和速度,蛇就可以移动了。
void GameRun(pSnake ps)
{
	//打印帮助信息
	PrintHelpInfo();
	do
	{
		//打印总分数 和食物的分值
		SetPos(64, 8);
		printf("总分数:%d\n", ps->_score);
		SetPos(64, 9);
		printf("当前食物的分值:%2d\n", 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_SPACE))//空格
		{
			pause();
		}
		else if (KEY_PRESS(VK_ESCAPE))//ESC
		{//正常退出游戏
			ps->_status = END_NORMAL;//退出游戏
			break;
		}
		else if (KEY_PRESS(VK_F3))//F3加速---休眠时间减少
		{
			if (ps->_sleep_time >= 80)//初始休眠时间为200-80=120 减4次
			{//最小休眠时间为50
				ps->_sleep_time -= 30;
				ps->_food_weight += 2;//食物初始分值为10  ⼀个⻝物分数最⾼是20分
			}
		}
		else if (KEY_PRESS(VK_F4))//F4减速
		{
			if (ps->_sleep_time < 320)//200 230 260 290 320
			{						  //10   8   6   4   2
				ps->_sleep_time += 30;
				ps->_food_weight -= 2;//⼀个⻝物分数最低是2分
			}
		}

		SnakeMove(ps);//蛇走一步的过程
		Sleep(ps->_sleep_time);
		
	} while (ps->_status == OK);
}

游戏结束

游戏状态不再是OK的时候  要告知游戏结束的原因  并且释放蛇身结点

void GameEnd(pSnake ps)
{
	SetPos(24, 12);
	switch (ps->_status)
	{
	case END_NORMAL:
		printf("有事 不玩了\n");
		break;
	case KILL_BY_WALL:
		printf("撞个墙玩玩\n");
		break;
	case KILL_BY_SELF:
		printf("吃个自己吧\n");
		break;
	}

	//释放蛇身的链表
	pSnakeNode cur = ps->_pSnake;
	while (cur)
	{
		pSnakeNode del = cur;
		cur = cur->next;
		free(del);
	}
}

在这里我就不给予完整代码了  这篇文章已经快到1.3万字了  我会单独发一篇贪吃蛇代码

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mfbz.cn/a/568791.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

vscode将本地服务转发到外网地址访问

示例中将本地的5500端口&#xff0c;用vscode进行端口转发&#xff0c;在外网地址访问服务 要转发的端口 转发端口 点击转发端口 输入要转发的端口&#xff0c;按下回车 Enter 点击允许&#xff0c;弹出确认界面后点击打开 转发端口已经成功配置上&#xff0c;右键可见性…

Git和Github绑定

天行健&#xff0c;君子以自强不息&#xff1b;地势坤&#xff0c;君子以厚德载物。 每个人都有惰性&#xff0c;但不断学习是好好生活的根本&#xff0c;共勉&#xff01; 文章均为学习整理笔记&#xff0c;分享记录为主&#xff0c;如有错误请指正&#xff0c;共同学习进步。…

爬虫中怎么判断一个网页是否包含ajax请求

1、前言 在用爬虫抓取数据的时候&#xff0c;如果一个网页包含ajax请求&#xff0c;由于数据时动态加载的&#xff0c;直接根据网址是不能获取到想要的数据。因此&#xff0c;在爬虫需要首先判断一个网页是否包含ajax请求数据。 2、ajax请求 2.1 什么是ajax请求 AJAX Asynch…

20240424codeforces刷题题解

240424刷题题解 Walk on Matrix CodeForces - 1332D 思路 构造题&#xff0c;每个 d p i , j dp_{i,j} dpi,j​​​都是由其左上方向中的按位与最大值决定的。 我们需要从使得贪心解与正确解的差值为 k k k。 为了方便获得 k k k&#xff0c;可以考虑构造一个贪心解为 0…

社交媒体数据恢复:Facebook

在使用Facebook的过程中&#xff0c;可能会出现数据丢失的情况&#xff0c;如误删了重要的帖子、照片或其他文件。在这种情况下&#xff0c;你可以尝试以下方法来恢复Facebook的数据。 首先&#xff0c;确保你备份了Facebook的数据。如果你定期备份数据&#xff0c;那么恢复起…

第26天:安全开发-PHP应用模版引用Smarty渲染MVC模型数据联动RCE安全

第二十六天 一、PHP新闻显示-数据库操作读取显示 1.新闻列表 数据库创建新闻存储代码连接数据库读取页面进行自定义显示 二、PHP模版引用-自写模版&Smarty渲染 1.自写模版引用 页面显示样式编排显示数据插入页面引用模版调用触发 2.Smarty模版引用 1.下载&#xff1a…

【C语言回顾】操作符详解

前言1. 操作符分类2. 二进制和进制转换2.1 二进制2.2 进制转换2.2.1 二进制转十进制2.2.2 二进制转八进制2.2.3 二进制转十六进制 3. 原码、反码、补码4. 移位操作符4.1 左移操作符4.2 右移操作符 5. 位操作符6. 单目操作符7. 逗号表达式8. 下标引用操作符9. 函数调用操作符10.…

Linux:进程与计划任务

文章目录 Linux&#xff1a;进程与计划任务一、进程1、进程是什么2、进程状态 二、列出进程命令1、查看静态的进程统计信息——“ps”Play1&#xff1a;“ps aux”Play2:ps -elf 2、查看静态的进程统计信息——“top”段首解析进程信息区解释 三、运行与终止进程3.1、运行进程3…

一致性hash

一、什么是一致性hash 普通的hash算法 (hashcode % size )&#xff0c;如果size发生变化&#xff0c;几乎所有的历史数据都需要重hash、移动&#xff0c;代价非常大&#xff0c;常见的java中的hashmap就是如此。 那如果在hash表扩容或者收缩的时候size能够保持不变&#xff0…

React-editor-js not showing up in a function component

React-editor-js not showing up in a function component react-editor-js 在react 函数组件中显示不出来 真的&#xff0c;我马上就想放弃它了。但是看它周下载量还挺多&#xff0c;我不信别人没遇到过。于是我继续在网络上挖呀挖。只是我一开始的方向错了。我一直以为我的写…

学习Rust第14天:HashMaps

今天我们来看看Rust中的hashmaps&#xff0c;在 std::collections crate中可用&#xff0c;是存储键值对的有效数据结构。本文介绍了创建、插入、访问、更新和迭代散列表等基本操作。通过一个计算单词出现次数的实际例子&#xff0c;我们展示了它们在现实世界中的实用性。Hashm…

安居水站:四大学习法:成为学霸的有效途径

摘要&#xff1a; 本文详细探讨了全球公认的四种高效学习方法——费曼学习法、西蒙学习法、思维导图法和SQ3R阅读法&#xff0c;通过引入相关数据、名人名言以及名人故事&#xff0c;深入分析了这些方法的核心理念、实施步骤及其在学习过程中的关键作用。 一、引言 学习是人…

《QT实用小工具·三十八》QT炫酷的菜单控件

1、概述 源码放在文章末尾 非常飘逸的 Qt 菜单控件&#xff0c;带有各种动画效果&#xff0c;用起来也十分方便。 无限层级&#xff0c;响应键盘、鼠标单独操作&#xff0c;支持单快捷键。 允许添加自定义 widget、layout&#xff0c;当做特殊的 QDialog 使用。 项目demo演示…

如何理解自然语言处理中的位置编码(Positional Encoding)

在自然语言处理和特别是在使用Transformer模型中,位置编码(Positional Encoding)是一个关键的概念。它们的作用是为模型提供序列中各个元素的位置信息。由于Transformer架构本身并不像循环神经网络(RNN)那样具有处理序列的固有能力,位置编码因此显得尤为重要。 为什么需…

MongoDB数据恢复—拷贝MongoDB数据库文件后无法启动服务的数据恢复案例

服务器数据恢复环境&#xff1a; 一台Windows Server操作系统服务器&#xff0c;服务器上部署MongoDB数据库。 MongoDB数据库故障&检测&#xff1a; 工作人员在未关闭MongoDB数据库服务的情况下&#xff0c;将数据库文件拷贝到其他分区。拷贝完成后将原MongoDB数据库所在分…

CCS项目持续集成

​ 因工作需要&#xff0c;用户提出希望可以做ccs项目的持续集成&#xff0c;及代码提交后能够自动编译并提交到svn。调研过jenkins之后发现重新手写更有性价比&#xff0c;所以肝了几晚终于搞出来了&#xff0c;现在分享出来。 ​ 先交代背景&#xff1a; 1. 代码分两部分&am…

Android Studio开发之路(八)Spinner样式设置

一、需求 白色背景显示下拉框按钮 问题&#xff1a; 设置Spinner的背景可以通过设置background&#xff1a; android:background"color/white",但是一旦设置了这个值&#xff0c;右侧的下拉按钮就会消失 方法一、自定义一个style&#xff08;不成功&#xff09; …

大模型推理框架Vllm和TensorRT-LLM在ChatGLM2-6B模型的推理速度对比

目录 一、框架的特点简介 1、vllm pagedAttention Continuous batching 2、TensorRT-LLM WOQ——W4A16、W8A16 SQ——SmoothQuant AWQ——Activation-aware Weight Quantization 二、web推理服务 vllm_service tensortllm_service 三、推理速度对比 1、非业务数据 …

第48期|GPTSecurity周报

GPTSecurity是一个涵盖了前沿学术研究和实践经验分享的社区&#xff0c;集成了生成预训练Transformer&#xff08;GPT&#xff09;、人工智能生成内容&#xff08;AIGC&#xff09;以及大语言模型&#xff08;LLM&#xff09;等安全领域应用的知识。在这里&#xff0c;您可以找…

游戏陪玩系统app

游戏陪玩系统APP为用户提供了一个便捷的平台&#xff0c;让他们能够轻松找到合适的陪玩者&#xff0c;一同享受游戏的乐趣。以下是对您提到的功能的详细解释&#xff1a; 游戏约玩&#xff1a; 在陪玩APP上&#xff0c;用户可以浏览陪玩者的信息&#xff0c;包括他们的游戏技能…
最新文章