贪吃蛇——C语言实践

news2024/11/27 11:32:13

目录

1. 游戏效果演示

2. 课程目标

3.项目适合对象

4.技术要点

5. Win32 API介绍

5.1 Win32 API

5.2 控制台程序

5.3 控制台屏幕上的坐标COORD

5.4 GetStdHandle

5.5 GetConsoleCursorInfo

5.5.1 CONSOLE_CURSOR_INFO

5.6 SetConsoleCursorInfo

5.7 SetConsoleCursorPosition

5.8 GetAsyncKeyState

6. 贪吃蛇游戏设计与分析

6.1地图

6.1.1 本地化

6.1.2 类项

6.1.3 setlocale函数

6.1.4 宽字符的打印

6.1.5 地图坐标

6.2 蛇身和食物

6.3 数据结构设计

6.4游戏流程设置

 7. 核心逻辑实现分析

7.1 游戏主逻辑

7.2 游戏开始(GameStart)

7.2.1 打印欢迎界面(WelcomeToGame)

7.2.2 创建地图(CreateMap)

7.2.3 初始化蛇身(InitSnake)

7.2.4 创建第一个食物(CreateFood)

7.3 游戏运行(GameRun)

7.3.1 KEY_PRESS

7.3.2 PrintHelpInfo

7.3.3 蛇身移动(SnakeMove)

7.3.3.1 NextIsFood

7.3.3.2 EatFood

7.3.3.3 NoFood

7.3.3.4 KillByWall

7.3.3.5 KillBySelf

7.4 游戏结束

8. 参考代码


1. 游戏效果演示

 

 

2. 课程目标

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

实现基本的功能:

•   贪吃蛇地图绘制

•   蛇吃食物的功能 (上、下、左、右方向键控制蛇的动作)

•   蛇撞墙死亡

•   蛇撞自身死亡

•   计算得分

•   蛇身加速、减速

•   暂停游戏

3.项目适合对象

C语言学完的同学,有一定的代码能力,初步接触数据结构中的链表

4.技术要点

C语言函数枚举结构体动态内存管理预处理指令链表Win32 API

5. Win32 API介绍

本次实现贪吃蛇会使用到的一些Win32 API知识,接下来我们就学习一下

5.1 Win32 API

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

5.2 控制台程序

平常我们运行起来的黑框程序其实就是控制台程序

若不是则:

点击设置

选择Windows 控制台主机,再点击保存

 我们可以使用cmd命令来设置控制台窗口的长宽:设置控制台窗口的大小,30行,100列

 mode con cols=100 lines=30

注意: cols直接连接=,没有空格,cols与lines用空格隔开,不是用逗号

也可以通过命令设置控制台窗口的名字:

title 贪吃蛇

这些能在控制台窗口执行的命令,也可以调用C语言函数system来执行。例如:

#include <stdio.h>
int main()
{
     //设置控制台窗⼝的⻓宽:设置控制台窗⼝的⼤⼩,30⾏,100列
     system("mode con cols=100 lines=30");
     //设置cmd窗⼝名称
     system("title 贪吃蛇"); 
   return 0;
}

5.3 控制台屏幕上的坐标COORD

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

COORD类型的声明:

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

 给坐标赋值:

COORD pos = { 10, 15 };

5.4 GetStdHandle

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

HANDLE GetStdHandle(DWORD nStdHandle);

实例:

HANDLE houtput = NULL;

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

5.5 GetConsoleCursorInfo

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

BOOL WINAPI GetConsoleCursorInfo(
     HANDLE hConsoleOutput,
     PCONSOLE_CURSOR_INFO lpConsoleCursorInfo
);

PCONSOLE_CURSOR_INFO 是指向 CONSOLE_CURSOR_INFO 结构的指针

实例:

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

hOutput = GetStdHandle(STD_OUTPUT_HANDLE);
CONSOLE_CURSOR_INFO CursorInfo;
GetConsoleCursorInfo(hOutput, &CursorInfo);//获取控制台光标信息
5.5.1 CONSOLE_CURSOR_INFO

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

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

•   dwSize(光标大小),由光标填充的字符单元格的百分比。 此值介于1到100之间。 光标外观会变化,范围从完全填充单元格到单元底部的水平线条。

•   bVisible(光标的可见性),如果光标可见,则此成员为 TRUE,否则为 FALSE

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

5.6 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);//设置控制台光标状态

5.7 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);

SetPos  封装一个设置光标位置的函数:

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

5.8 GetAsyncKeyState

获取按键情况,GetAsyncKeyState的函数原型如下:

SHORT GetAsyncKeyState(
     int vKey
);

将键盘上每个键的虚拟键值传递给函数,函数通过返回值来分辨按键的状态。

如果我们要判断一个键是否被按过,可以检测GetAsyncKeyState返回值的最低值是否为1

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

虚拟键码 (Winuser.h) - Win32 apps | Microsoft Learn参考:虚拟键码 (Winuser.h) - Win32 apps | Microsoft Learn

6. 贪吃蛇游戏设计与分析

6.1地图

在游戏地图上,我们打印墙体使⽤宽字符:□,打印蛇使用宽字符●,打印食物使用宽字符★,普通字符是占1个字节的,这类宽字符是占用2个字节

为了使C语言适应国际化,C语言的标准中不断加入了国际化的支持。比如:加入了宽字符的类型 wchar_t 宽字符的输入和输出函数,加入了<locale.h>头文件 ,其中提供了允许程序员针对特定地区(通常是国家或者说某种特定语言的地理区域)调整程序行为的函数。

6.1.1 <locale.h>本地化

依赖地区的部分有以下四项:

• 数字量的格式

• 货币量的格式

• 字符集

• 日期和时间的表示形式

6.1.2 类项

通过修改地区,程序可以改变它的行为来适应世界的不同区域。但地区的改变可能会影响库的许多部分,其中一部分可能是我们不希望修改的。所以C语言支持针对不同的类项进行修改,下面的一个宏, 指定一个类项:

• LC_COLLATE:影响字符串比较函数 strcoll() 和 strxfrm()

• LC_CTYPE:影响字符处理函数的行为

• LC_MONETARY:影响货币格式

• LC_NUMERIC:影响 printf() 的数字格式

• LC_TIME:影响时间格式 strftime() 和 wcsftime()

• LC_ALL - 针对所有类项修改,将以上所有类别设置为给定的语言环境

6.1.3 setlocale函数
char* setlocale (int category, const char* locale);

C标准给第二个参数仅定义了2种可能取值:"C"(正常模式)和" "(本地模式)

在任意程序执行开始,都会隐藏式执行调用:

setlocale(LC_ALL, "C");

切换到我们的本地模式后就支持宽字符(汉字)的输出等 

 setlocale(LC_ALL, " ");//切换到本地环境
6.1.4 宽字符的打印

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

如:

wprintf(L"%lc\n",  L'●');

注意:有两个前缀“L”

6.1.5 地图坐标

我们假设实现一个棋盘27行,58列的棋盘(行和列可以根据自己的情况修改),再围绕地图画出墙, 如下:

6.2 蛇身和食物

初始化状态,假设蛇的长度是5,蛇身的每个节点是●,在固定的一个坐标处,比如(24, 5)处开始出现蛇,连续5个节点

注意:蛇的每个节点的x坐标必须是2的倍数,否则可能会出现蛇的一个节点有一半出现在墙体中, 另外一半在墙外的现象,坐标不好对齐

关于食物,就是在墙体内随机生成一个坐标(x坐标必须是2的倍数),坐标不能和蛇的身体重合,然后打印★

6.3 数据结构设计

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

typedef struct SnakeNode
{
     int x;
     int y;
     struct SnakeNode* next;
}SnakeNode, * pSnakeNode;

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

typedef struct Snake
{
	pSnakeNode _pSnake;//维护整条蛇的指针
	pSnakeNode _pFood;//维护⻝物的指针
	enum DIRECTION _Dir;//蛇头的⽅向,默认是向右
	enum GAME_STATUS _Status;//游戏状态
	int _Socre;//游戏当前获得分数
	int _foodWeight;//默认每个⻝物10分
	int _SleepTime;//每⾛⼀步休眠时间
}Snake, * pSnake;

蛇的方向,可以一 一列举,使用枚举:

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

游戏状态,可以一 一列举,使用枚举:

//游戏状态
enum GAME_STATUS
{
	OK,//正常运⾏
	KILL_BY_WALL,//撞墙
	KILL_BY_SELF,//咬到⾃⼰
	END_NOMAL//正常结束
};

6.4游戏流程设置

 7. 核心逻辑实现分析

7.1 游戏主逻辑

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

主逻辑分为3个过程:

• 游戏开始(GameStart)完成游戏的初始化

• 游戏运行(GameRun)完成游戏运行逻辑的实现

• 游戏结束(GameEnd)完成游戏结束的说明,实现资源释放

#define _CRT_SECURE_NO_WARNINGS 1
#include <locale.h>
#include "Snake.h"

//完成的是游戏的测试逻辑
void test()
{
	int ch = 0;
	do
	{
		system("cls");
	 
		//创建贪吃蛇
		Snake snake = { 0 };
	 
		//初始化游戏
	    //0. 先设置窗口的大小,设置窗口的名字
		//1. 再光标隐藏
		//2. 打印环境界面和功能介绍
		//3. 绘制地图
		//4. 创建蛇
		//5. 创建食物
		GameStart(&snake);

		//运行游戏
		GameRun(&snake);
	 
		//结束游戏 - 善后工作
		GameEnd(&snake);
	 
		SetPos(20, 15);
		printf("再来一局吗?(Y/N):");
	 
		ch = getchar();
		while (getchar() != '\n');
	
	} while (ch == 'Y' || ch == 'y');
	SetPos(0, 27);
}

int main()
{
	//设置适配本地环境
	setlocale(LC_ALL, "");
	srand((unsigned int)time(NULL));
	test();

	return 0;
}

7.2 游戏开始(GameStart)

void GameStart(pSnake ps)
{
	//0. 先设置窗口的大小,设置窗口的名字
	system("mode con cols=100 lines=30");//=紧挨着,中间用空格
	system("title 贪吃蛇");
	//getchar();//不让程序结束,检测代码是否执行正确

	//1. 光标隐藏
	HANDLE houtput = GetStdHandle(STD_OUTPUT_HANDLE);//获取 标准输出 的句柄
	CONSOLE_CURSOR_INFO CursorInfo;
	GetConsoleCursorInfo(houtput, &CursorInfo);//获取 控制台 光标信息
	CursorInfo.bVisible = false;//隐藏 控制台 光标
	SetConsoleCursorInfo(houtput, &CursorInfo);//设置 控制台光标状态
	//getchar();//不让程序结束,检测代码是否执行正确

	//2. 打印环境界面和功能介绍
	WelcomeToGame();
	//3. 绘制地图
	CreateMap();
	//4. 创建蛇
	InitSnake(ps);
	//5. 创建食物
	CreateFood(ps);
}
7.2.1 打印欢迎界面(WelcomeToGame)
void WelcomeToGame()
{
	SetPos(36, 14);
	printf("欢迎来到贪吃蛇小游戏");
	SetPos(40, 25);//让按任意键继续的出现的位置好看点
	system("pause");
	system("cls");
	SetPos(20, 10);
	printf("用 ↑ . ↓ . ← . → 分别控制蛇的移动, F3为加速,F4为减速");
	SetPos(20, 11);
	printf("加速将能得到更高的分数");
	SetPos(40, 25);//让按任意键继续的出现的位置好看点
	system("pause");
	system("cls");
}

7.2.2 创建地图(CreateMap)

创建地图就是将墙打印出来,因为是宽字符打印,所有使用wprintf函数,打印格式串前使用L

打印地图的关键是要算好坐标

#define WALL L'□'

易错点:

上:(0,0)到(56,0)

下:(0,26)到(56,26)

左:(0,1)到(0,25)

右:(56,1)到(56,25)

void CreateMap()
{
	int i = 0;
	for (i = 1; i < 30; i++)
	{
		wprintf(L"%lc", WALL);
	}
	SetPos(0, 26);
	for (i = 1; i < 30; i++)
	{
		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();//不让程序结束,检测代码是否执行正确
}
7.2.3 初始化蛇身(InitSnake)

蛇最开始长度为5节,每节对应链表的一个节点,蛇身的每一个节点都有自己的坐标

创建完蛇身后,将蛇的每一节打印在屏幕上

• 蛇的初始位置从 (24,5) 开始

• 游戏状态是:OK

• 蛇的移动速度:200毫秒

• 蛇的默认方向:RIGHT

• 初始成绩:0

• 每个食物的分数:10

蛇身打印的宽字符:

#define BODY L'●'
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 + i * 2;
		cur->y = POS_Y;
		cur->next = NULL;
		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;
	}
	//getchar();//不让程序结束,检测代码是否执行正确

	//初始化贪吃蛇数据
	ps->_dir = RIGHT;
	ps->_food_weight = 10;
	ps->_score = 0;
	ps->_sleep_time = 200;//ms
	ps->_status = OK;
}
7.2.4 创建第一个食物(CreateFood)

• 先随机生成食物的坐标

    ◦ x坐标必须是2的倍数

    ◦ 食物的坐标不能和蛇身每个节点的坐标重复

• 创建食物节点,打印食物

食物打印的宽字符:

#define FOOD L'★'
void CreateFood(pSnake ps)
{
	ps->_pFood = (pSnakeNode)malloc(sizeof(SnakeNode));//申请节点,保存食物的信息
	if (ps->_pFood == NULL)
	{
		perror("CreateFood()::malloc()");
		return;
	}
	ps->_pFood->next = NULL;
	if (ps->_pFood == NULL)
	{
		perror("CreatFood()::malloc()");
		return;
	}
again:
	do
	{
		ps->_pFood->x = rand() % 53 + 2;
		ps->_pFood->y = rand() % 25 + 1;
	} while (ps->_pFood->x % 2 != 0);
	pSnakeNode cur = ps->_pSnake;
	while (cur)
	{
		if (cur->x == ps->_pFood->x && cur->y == ps->_pFood->y)
			goto again;
		cur = cur->next;
	}
	SetPos(ps->_pFood->x, ps->_pFood->y);
	wprintf(L"%lc", FOOD);
}

7.3 游戏运行(GameRun)

游戏运行期间,右侧打印帮助信息,提示玩家,坐标开始位置(64, 15)

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

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

需要的虚拟按键的罗列:

• 上:VK_UP

• 下:VK_DOWN

• 左:VK_LEFT

• 右:VK_RIGHT

• 空格:VK_SPACE

• ESC:VK_ESCAPE

• F3:VK_F3

• F4:VK_F4

void GameRun(pSnake ps)
{
	//打印右侧帮助信息
	PrintHelpInfo();

	do
	{
		//打印总分数和食物的分值
		SetPos(62, 11);
		printf("总分数:>%d",ps->_score);
		SetPos(62, 13);
		printf("当前食物的分数:>%2d", 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))
		{
			//正常退出游戏
			ps->_status = END_NORMAL;
		}
		else if (KEY_PRESS(VK_F3))
		{
			//F3加速
			if (ps->_sleep_time > 100)
			{
				ps->_sleep_time -= 30;//ms
				ps->_food_weight += 2;
			}
		}
		else if (KEY_PRESS(VK_F4))
		{
			//F4减速
			if (ps->_sleep_time < 300)
			{
				ps->_sleep_time += 30;//ms
				ps->_food_weight -= 2;
			}
		}

		SnakeMove(ps);//蛇走一步的过程

		Sleep(ps->_sleep_time);

	} while (ps->_status == OK);
}
7.3.1 KEY_PRESS

检测按键状态,我们封装了一个宏

 #define KEY_PRESS(VK) ((GetAsyncKeyState(VK)&0x1) ? 1 : 0)
7.3.2 PrintHelpInfo
void PrintHelpInfo()
{
	SetPos(62, 16);
	printf("不能穿墙,不能咬到自己");
	SetPos(62, 17);
	printf("用 ↑ . ↓ . ← . → 分别控制蛇的移动");
	SetPos(62, 18);
	printf("F3为加速,F4为减速");
	SetPos(62, 19);
	printf("按ESC退出游戏,按空格暂停游戏");

	SetPos(62, 24);
	printf("Lzc制作@版权");
}
7.3.3 蛇身移动(SnakeMove)
void SnakeMove(pSnake ps)
{
	//创建一个结点,表示蛇即将到的下一个节点
	pSnakeNode pNextNode = (pSnakeNode)malloc(sizeof(SnakeNode));
	if (pNextNode == NULL)
	{
		perror("SnakeMove()::malloc()");
		return;
	}

	if (ps->_dir == UP)
	{
		pNextNode->x = ps->_pSnake->x;
		pNextNode->y =  ps->_pSnake->y - 1;
	}
	else if (ps->_dir == DOWN)
	{
		pNextNode->x = ps->_pSnake->x;
		pNextNode->y = ps->_pSnake->y + 1;
	}
	else if (ps->_dir == LEFT)
	{
		pNextNode->x = ps->_pSnake->x - 2;
		pNextNode->y = ps->_pSnake->y;
	}
	else if (ps->_dir = RIGHT)
	{
		pNextNode->x = ps->_pSnake->x + 2;
		pNextNode->y = ps->_pSnake->y;
	}

	//判断下个节点是不是食物
	if (NextIsFood(pNextNode, ps))
	{
		EatFood(ps);

		//释放pNextNode
		free(pNextNode);
		pNextNode = NULL;
	}
	else
	{
		NoFood(pNextNode, ps);
	}

	KillByWall(ps);

	KillBySelf(ps);
}
7.3.3.1 NextIsFood
int NextIsFood(pSnakeNode pn, pSnake ps)
{
	//&& 如果条件判断结果为 true 就返回第二个操作数的值,如果为 false 就返回第一个操作数的值。
	return (pn->x == ps->_pFood->x && pn->y == ps->_pFood->y);
}
7.3.3.2 EatFood
void EatFood(pSnake ps)
{
	//头插
	ps->_pFood->next = ps->_pSnake;
	ps->_pSnake = ps->_pFood;

	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);
}
7.3.3.3 NoFood

易错点:这里最容易错误的是,释放最后一个结点后,还得将指向在最后一个结点的指针改为NULL, 保证蛇尾打印可以正常结束,不会越界访问。

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

	pSnakeNode cur = ps->_pSnake;

	//打印蛇头
	SetPos(cur->x, cur->y);
	wprintf(L"%lc", BODY);

	//得到倒数第二个节点
	while (cur->next->next != NULL)
	{
		cur = cur->next;
	}

	//先把这个位置打印成空格,因为已经打印了BODY
	SetPos(cur->next->x, cur->next->y);
	printf("  ");

	//free最后一个节点
	free(cur->next);
	cur->next = NULL;
}
7.3.3.4 KillByWall
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;
	}
}
7.3.3.5 KillBySelf
void KillBySelf(pSnake ps)
{
	pSnakeNode cur = ps->_pSnake->next;
	while (cur)
	{
		if (cur->x == ps->_pSnake->x && cur->y == ps->_pSnake->y)
		{
			ps->_status = KILL_BY_SELF;
			return;
		}
		cur = cur->next;
	}
}

7.4 游戏结束

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

void GameEnd(pSnake ps)//游戏善后的工作
{
	pSnakeNode cur = ps->_pSnake;
	SetPos(24, 12);
	if (ps->_status == KILL_BY_WALL)
	{
		printf("您撞墙,游戏结束");
		return;
	}
	else if (ps->_status == KILL_BY_SELF)
	{
		printf("您吃到自己了,游戏结束");
		return;
	}
	else if (ps->_status == END_NORMAL)
	{
		printf("您主动退出了游戏");
		return;
	}

	//销毁节点,cur跳出就为NULL
	while (cur)
	{
		pSnakeNode del = cur;
		cur = cur->next;
		free(del);
	}
}

8. 参考代码

test.c

#define _CRT_SECURE_NO_WARNINGS 1
#include <locale.h>
#include "Snake.h"

//完成的是游戏的测试逻辑
void test()
{
	int ch = 0;
	do
	{
		system("cls");
	 
		//创建贪吃蛇
		Snake snake = { 0 };
	 
		//初始化游戏
	    //0. 先设置窗口的大小,设置窗口的名字
		//1. 再光标隐藏
		//2. 打印环境界面和功能介绍
		//3. 绘制地图
		//4. 创建蛇
		//5. 创建食物
		GameStart(&snake);

		//运行游戏
		GameRun(&snake);
	 
		//结束游戏 - 善后工作
		GameEnd(&snake);
	 
		SetPos(20, 15);
		printf("再来一局吗?(Y/N):");
	 
		ch = getchar();
		while (getchar() != '\n');
	
	} while (ch == 'Y' || ch == 'y');
	SetPos(0, 27);
}

int main()
{
	//设置适配本地环境
	setlocale(LC_ALL, "");
	srand((unsigned int)time(NULL));
	test();

	return 0;
}

Snake.h

#pragma once

#include <windows.h>
#include <stdbool.h>
#include <stdlib.h>
#include <stdio.h>
#include <time.h>
#include <locale.h>

#define POS_X 24
#define POS_Y 5

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

//类型的声明

//蛇的方向
enum DIRECTION
{
	UP = 1,
	DOWN,
	LEFT,
	RIGHT
};

//蛇的状态
//正常、撞墙、撞到自己、正常退出
enum GAME_STATUS
{
	OK, //正常
	KILL_BY_WALL, //撞墙
	KILL_BY_SELF, //撞到自己
	END_NORMAL //正常退出
};

//蛇身的节点类型
typedef struct SnakeNode
{
	//坐标
	int x;
	int y;
	//指向下一个节点的指针
	struct SnakeNode* next;
}SnakeNode, * pSnakeNode;

//typedef struct SnakeNode* pSnakeNode;


//贪吃蛇
typedef struct Snake
{
	pSnakeNode _pSnake;//指向蛇头的指针
	pSnakeNode _pFood;//指向食物节点的指针
	enum DIRECTION _dir;//蛇的方向
	enum GAME_STATUS _status;//游戏的状态
	int _food_weight;//一个食物的分数
	int _score;      //总成绩
	int _sleep_time; //休息时间,时间越短,速度越快,时间越长,速度越慢
}Snake, * pSnake;

//函数的声明

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

//游戏的初始化
void GameStart(pSnake ps);

    //欢迎界面的打印
    void WelcomeToGame();
    //创建地图
    void CreateMap();
    //初始化蛇身
    void InitSnake(pSnake ps);
    //创建食物
    void CreateFood(pSnake ps);

//游戏运行的逻辑
void GameRun(pSnake ps);

    //蛇的移动-走一步
    void SnakeMove(pSnake ps);
    //判断下一个坐标是否是食物
    int NextIsFood(pSnakeNode pn, pSnake ps);
    //下一个位置是食物,就吃掉食物
    void EatFood(pSnake ps);
    //下一个位置不是食物
    void NoFood(pSnakeNode pn, pSnake ps);
    //检测蛇是否撞墙
    void KillByWall(pSnake ps);
    //检测蛇是否撞到自己
    void KillBySelf(pSnake ps);

//游戏善后的工作
void GameEnd(pSnake ps);

Snake.c

#define _CRT_SECURE_NO_WARNINGS 1
#include "Snake.h"
#define KEY_PRESS(VK) ( (GetAsyncKeyState(VK) & 0x1) ? 1 : 0 )

void SetPos(short x, short y)
{
	COORD pos = { x,y }; //⼀个字符在控制台屏幕缓冲区上的坐标
	HANDLE houtput = GetStdHandle(STD_OUTPUT_HANDLE);//获取 标准输出 的句柄
	SetConsoleCursorPosition(houtput, pos);//设置 标准输出上 光标的位置为pos
}

void WelcomeToGame()
{
	SetPos(36, 14);
	printf("欢迎来到贪吃蛇小游戏");
	SetPos(40, 25);//让按任意键继续的出现的位置好看点
	system("pause");
	system("cls");
	SetPos(20, 10);
	printf("用 ↑ . ↓ . ← . → 分别控制蛇的移动, F3为加速,F4为减速");
	SetPos(20, 11);
	printf("加速将能得到更高的分数");
	SetPos(40, 25);//让按任意键继续的出现的位置好看点
	system("pause");
	system("cls");
}

void CreateMap()
{
	int i = 0;
	for (i = 1; i < 30; i++)
	{
		wprintf(L"%lc", WALL);
	}
	SetPos(0, 26);
	for (i = 1; i < 30; i++)
	{
		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();//不让程序结束,检测代码是否执行正确
}

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 + i * 2;
		cur->y = POS_Y;
		cur->next = NULL;
		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;
	}
	//getchar();//不让程序结束,检测代码是否执行正确

	//初始化贪吃蛇数据
	ps->_dir = RIGHT;
	ps->_food_weight = 10;
	ps->_score = 0;
	ps->_sleep_time = 200;//ms
	ps->_status = OK;
}

void CreateFood(pSnake ps)
{
	ps->_pFood = (pSnakeNode)malloc(sizeof(SnakeNode));//申请节点,保存食物的信息
	if (ps->_pFood == NULL)
	{
		perror("CreateFood()::malloc()");
		return;
	}
	ps->_pFood->next = NULL;
	if (ps->_pFood == NULL)
	{
		perror("CreatFood()::malloc()");
		return;
	}
again:
	do
	{
		ps->_pFood->x = rand() % 53 + 2;
		ps->_pFood->y = rand() % 25 + 1;
	} while (ps->_pFood->x % 2 != 0);
	pSnakeNode cur = ps->_pSnake;
	while (cur)
	{
		if (cur->x == ps->_pFood->x && cur->y == ps->_pFood->y)
			goto again;
		cur = cur->next;
	}
	SetPos(ps->_pFood->x, ps->_pFood->y);
	wprintf(L"%lc", FOOD);
}

void GameStart(pSnake ps)
{
	//0. 先设置窗口的大小,设置窗口的名字
	system("mode con cols=100 lines=30");//=紧挨着,中间用空格
	system("title 贪吃蛇");
	//getchar();//不让程序结束,检测代码是否执行正确

	//1. 光标隐藏
	HANDLE houtput = GetStdHandle(STD_OUTPUT_HANDLE);//获取 标准输出 的句柄
	CONSOLE_CURSOR_INFO CursorInfo;
	GetConsoleCursorInfo(houtput, &CursorInfo);//获取 控制台 光标信息
	CursorInfo.bVisible = false;//隐藏 控制台 光标
	SetConsoleCursorInfo(houtput, &CursorInfo);//设置 控制台光标状态
	//getchar();//不让程序结束,检测代码是否执行正确

	//2. 打印环境界面和功能介绍
	WelcomeToGame();
	//3. 绘制地图
	CreateMap();
	//4. 创建蛇
	InitSnake(ps);
	//5. 创建食物
	CreateFood(ps);
}

void PrintHelpInfo()
{
	SetPos(62, 16);
	printf("不能穿墙,不能咬到自己");
	SetPos(62, 17);
	printf("用 ↑ . ↓ . ← . → 分别控制蛇的移动");
	SetPos(62, 18);
	printf("F3为加速,F4为减速");
	SetPos(62, 19);
	printf("按ESC退出游戏,按空格暂停游戏");

	SetPos(62, 24);
	printf("Lzc制作@版权");
}

void Pause()
{
	while (1)
	{
		Sleep(200);//ms
		if (KEY_PRESS(VK_SPACE))
		{
			break;
		}
	}
}

int NextIsFood(pSnakeNode pn, pSnake ps)
{
	//&& 如果条件判断结果为 true 就返回第二个操作数的值,如果为 false 就返回第一个操作数的值。
	return (pn->x == ps->_pFood->x && pn->y == ps->_pFood->y);
}

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

	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 NoFood(pSnakeNode pn, pSnake ps)
{
	//头插
	pn->next = ps->_pSnake;
	ps->_pSnake = pn;

	pSnakeNode cur = ps->_pSnake;

	//打印蛇头
	SetPos(cur->x, cur->y);
	wprintf(L"%lc", BODY);

	//得到倒数第二个节点
	while (cur->next->next != NULL)
	{
		cur = cur->next;
	}

	//先把这个位置打印成空格,因为已经打印了BODY
	SetPos(cur->next->x, cur->next->y);
	printf("  ");

	//free最后一个节点
	free(cur->next);
	cur->next = NULL;
}

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;
	while (cur)
	{
		if (cur->x == ps->_pSnake->x && cur->y == ps->_pSnake->y)
		{
			ps->_status = KILL_BY_SELF;
			return;
		}
		cur = cur->next;
	}
}

void SnakeMove(pSnake ps)
{
	//创建一个结点,表示蛇即将到的下一个节点
	pSnakeNode pNextNode = (pSnakeNode)malloc(sizeof(SnakeNode));
	if (pNextNode == NULL)
	{
		perror("SnakeMove()::malloc()");
		return;
	}

	if (ps->_dir == UP)
	{
		pNextNode->x = ps->_pSnake->x;
		pNextNode->y =  ps->_pSnake->y - 1;
	}
	else if (ps->_dir == DOWN)
	{
		pNextNode->x = ps->_pSnake->x;
		pNextNode->y = ps->_pSnake->y + 1;
	}
	else if (ps->_dir == LEFT)
	{
		pNextNode->x = ps->_pSnake->x - 2;
		pNextNode->y = ps->_pSnake->y;
	}
	else if (ps->_dir = RIGHT)
	{
		pNextNode->x = ps->_pSnake->x + 2;
		pNextNode->y = ps->_pSnake->y;
	}

	//判断下个节点是不是食物
	if (NextIsFood(pNextNode, ps))
	{
		EatFood(ps);

		//释放pNextNode
		free(pNextNode);
		pNextNode = NULL;
	}
	else
	{
		NoFood(pNextNode, ps);
	}

	KillByWall(ps);

	KillBySelf(ps);
}

void GameRun(pSnake ps)
{
	//打印右侧帮助信息
	PrintHelpInfo();

	do
	{
		//打印总分数和食物的分值
		SetPos(62, 11);
		printf("总分数:>%d",ps->_score);
		SetPos(62, 13);
		printf("当前食物的分数:>%2d", 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))
		{
			//正常退出游戏
			ps->_status = END_NORMAL;
		}
		else if (KEY_PRESS(VK_F3))
		{
			//F3加速
			if (ps->_sleep_time > 100)
			{
				ps->_sleep_time -= 30;//ms
				ps->_food_weight += 2;
			}
		}
		else if (KEY_PRESS(VK_F4))
		{
			//F4减速
			if (ps->_sleep_time < 300)
			{
				ps->_sleep_time += 30;//ms
				ps->_food_weight -= 2;
			}
		}

		SnakeMove(ps);//蛇走一步的过程

		Sleep(ps->_sleep_time);

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

void GameEnd(pSnake ps)//游戏善后的工作
{
	pSnakeNode cur = ps->_pSnake;
	SetPos(24, 12);
	if (ps->_status == KILL_BY_WALL)
	{
		printf("您撞墙,游戏结束");
		return;
	}
	else if (ps->_status == KILL_BY_SELF)
	{
		printf("您吃到自己了,游戏结束");
		return;
	}
	else if (ps->_status == END_NORMAL)
	{
		printf("您主动退出了游戏");
		return;
	}

	//销毁节点,cur跳出就为NULL
	while (cur)
	{
		pSnakeNode del = cur;
		cur = cur->next;
		free(del);
	}
}

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Linux系统——VIM编辑工具

vi/vim vi是一个文本编辑器&#xff0c;用于撰写文档&#xff0c;或者开发程序。vim是vi的增强版 功能一致&#xff0c;可视化效果更好一些。去鼠标化 编辑更加方便 可定制化。 vim编辑器是一个模式化文本编辑器 模式以&#xff1a;编辑模式 进入文档后默认的模式 作用&am…

Midjourney与Stable Diffusion大比拼:AI绘画技术的未来

在当今快速发展的人工智能技术浪潮中&#xff0c;AI绘画软件成为了艺术和技术交汇的新领域。两大巨头——Midjourney和Stable Diffusion&#xff0c;在这一领域中引领风骚&#xff0c;它们以其独特的功能和强大的生成能力&#xff0c;让创作者能够将想象力化为现实。本文将深入…

在Ubuntu 24.04 LTS (Noble Numbat)上安装nfs server以及nfs client

在Ubuntu 24.04 LTS (Noble Numbat)上,我使用的是最小化安装, 当然server版本的Ubuntu在安装的时候可能会有网络不通的问题,解决办法见如下文章: ubuntu 24.04 server 仅NAT模式上网设置静态IP设置-CSDN博客文章浏览阅读489次,点赞9次,收藏3次。在Ubuntu 24.04 上设置网…

YApi的在IDEA中的使用

1 IDEA中插件下载 2 misc.xml的配置 <component name"yapi"><option name"projectToken">XXXXXXXXXX</option><option name"projectId">47</option><option name"yapiUrl">http://XXXX:3000<…

Appium 2.x 安装及使用

由于安全问题&#xff0c;Appium 1.x 版本不再被维护&#xff0c;但想要继续使用Appium进行自动化可以使用 Appium 2.x。 1. 安装Appium 2.x 在过往文章中有介绍过Appium 1.x 的安装&#xff0c;所以一些必备的软件(如&#xff1a;JDK、SDK、node.js、Python)安装就不再细嗦&…

RWA会成为下一个风口吗?有哪些值得关注的项目?

随着加密货币市场的迅速发展和成熟&#xff0c;现实世界资产&#xff08;Real World Assets&#xff0c;RWA&#xff09;正逐渐引起人们的关注&#xff0c;并有望成为下一个加密货币领域的风口。本文将探讨RWA的潜力&#xff0c;以及当前值得关注的项目。 RWA的潜力 RWA代表着…

docker(五):DockerFile

文章目录 DockerFile1、Dockerfile构建过程解析2、DockerFile常用保留字命令FROMMAINTAINERRUNEXPOSEWORKDIRUSERENVADDCOPYVOLUMECMDENTRYPOINT总结 3、案例 DockerFile 1、Dockerfile构建过程解析 官网文档&#xff1a;https://docs.docker.com/reference/dockerfile/ Dock…

SpringBoot3集成WebSocket

标签&#xff1a;WebSocket&#xff0c;Session&#xff0c;Postman。 一、简介 WebSocket通过一个TCP连接在客户端和服务器之间建立一个全双工、双向的通信通道&#xff0c;使得客户端和服务器之间的数据交换变得更加简单&#xff0c;允许服务端主动向客户端推送数据&#xf…

有没有适合女生或者宝妈下班后可以做的副业?

宝妈与上班族女生的新篇章&#xff1a;水牛社副业兼职之旅 在繁忙的职场和温馨的家庭之间&#xff0c;不少女性渴望找到一种既能兼顾家庭又能实现自我价值的兼职方式。对于上班族女生和宝妈们来说&#xff0c;水牛社这样的线上任务平台为她们提供了一个全新的选择。 上班族女…

c++ map,set封装

map 是一个 kv 结构&#xff0c; set 是 k结构。 我们前面模拟实现了 红黑树&#xff0c;但是我们实现的红黑树把 kv 结构写死了&#xff0c;怎么样才能用泛型编程的思想来实现map和set呢 我们先简单看一下原码中是怎么实现的 1.原码实现逻辑 我们打开这里的 stl_set.h 通过…

数据结构(Java实现):List接口的介绍

1.什么是List 在集合框架中&#xff0c;List是一个接口&#xff0c;继承自Collection。 Collection也是一个接口&#xff0c;该接口中规范了后序容器中常用的一些方法&#xff0c;具体如下所示&#xff1a; Iterable也是一个接口&#xff0c;表示实现该接口的类是可以逐个元…

达梦数据库连接失败:Connect Failure! “Encryption module failed to load“

初次安装达梦数据库&#xff1a;V7 QT5.12.12版本开发调用数据库&#xff0c;最基础的原型调用&#xff1a; { //执行查询语句或则执行sql语句 QSqlDatabase qDb; QSqlDatabase db QSqlDatabase::addDatabase("QDM"); db.setHostName("192.168.2…

【大数据】HDFS

文章目录 [toc]HDFS 1.0NameNode维护文件系统命名空间存储元数据解决NameNode单点问题 SecondaryNameNode机架感知数据完整性校验校验和数据块检测程序DataBlockScanner HDFS写流程HDFS读流程HDFS与MapReduce本地模式Block大小 HDFS 2.0NameNode HANameNode FederationHDFS Sna…

红黑树的理解和简单实现

目录 1. 红黑树的概念和性质 2. 红黑树的插入 2.1. 情况一&#xff1a;新增节点的父亲为空 2.2. 情况二&#xff1a;新增节点的父亲非空且为黑色节点 2.3. 情况三&#xff1a;当父亲为红节点&#xff0c;叔叔存在且为红 2.3.1. 当祖父为根节点的时候 2.3.2. 当祖父不是根…

揭秘高效引流获客的艺术:转化技巧大公开

在数字化营销的海洋中&#xff0c;每个企业都如同一艘努力航行的船&#xff0c;而流量便是推动船只前行的风帆。如何有效吸引并获取潜在客户&#xff0c;即所谓的“引流获客”&#xff0c;已成为企业市场营销策略中不可或缺的一环。本文将详细探讨几种实用且高效的引流获客技巧…

【RAG 论文】AAR:训练一个LLM喜欢的检索器来做RAG

论文&#xff1a;Augmentation-Adapted Retriever Improves Generalization of Language Models as Generic Plug-In ⭐⭐⭐ ACL 2023, Tsinghua & Microsoft&#xff0c;arXiv:2305.17331 论文速读 以往 RAG 的工作通常联合微调 retriever 和 LLM 导致紧密耦合&#xff0…

实验0.0 Visual Studio 2022安装指南

Visual Studio 2022 是一个功能强大的开发工具&#xff0c;对于计算机专业的学生来说&#xff0c;它不仅可以帮助你完成学业项目&#xff0c;还能为你将来的职业生涯打下坚实的基础。通过学习和使用 Visual Studio&#xff0c;你将能够更高效地开发软件&#xff0c;并在编程领域…

公有云Linux模拟UDP端口并抓包

目录 写在前面操作步骤服务端开启UDP端口并监听客户端连接Wireshark抓包查看 写在前面 关于具体的操作&#xff0c;请参考我的上一篇文章 公有云Linux模拟TCP三次挥手与四次握手&#xff08;Wireshark抓包验证版&#xff09; 在本文&#xff0c;仅介绍与上一篇不同的地方。 操…

Matlab-粒子群优化算法实现

文章目录 一、粒子群优化算法二、相关概念和流程图三、例题实现结果 一、粒子群优化算法 粒子群优化算法起源于鸟类觅食的经验&#xff0c;也就是一群鸟在一个大空间内随机寻找食物&#xff0c;目标是找到食物最多的地方。以下是几个条件: (1) 所有的鸟都会共享自己的位置以及…

泰达克仿钻点水晶饰品包装印刷防滑UV胶特性及应用场景

仿钻点UV滴胶是一种特殊的胶水 常用于模拟钻石的效果 它是一种透明的胶水 具有高光泽度和折射率 可以在物体表面形成类似钻石的亮闪效果 仿钻点UV滴胶通常由紫外线固化胶组成 需要通过紫外线照射来固化和硬化 它具有以下特点&#xff1a; 1. 透明度&#xff1a;仿钻点UV滴胶具有…