贪吃蛇进阶版(单向链表+Windows_API+文件存储游戏数据) (颜色设置详解+API详解+动态内存)

目录

一、电脑终端设置

二、部分核心代码讲解

1.Windows_API讲解

1)GetStdHandle--获取控制台句柄

2)GetConsoleCursorInfo--获取光标信息 和 SetConsoleCursorInfo--设置光标信息

3)SetConsoleCursorPosition--设置光标位置

4)GetAsyncKeyState--获取按键状态

 2.控制台属性设置函数

 3.颜色设置详解(背景色+前景色)

 三、游戏代码讲解

0.光标定位函数

1.游戏坐标设定

 2.主函数逻辑讲解

 3.snake.h头文件中的声明

4.初始化游戏GameStart(&snake); 内容讲解

 1)隐藏光标 HideCurSor();

2)打印环境 + 功能介绍WelcomeToGame();

3)绘制地图CreatMap();

4)创建蛇CreatSnake(ps);

5)创建食物CreatFood(ps);

5.运行游戏GameRun(&snake);内容讲解

6.结束游戏--善后工作--释放空间GameEnd(&snake);内容讲解

 1)WhichCause(ps);游戏结束原因

2)SaveScore(ps);更新最高分到文件

3)DistorySnake(&ps);销毁蛇链表

四、代码与程序展示

0.程序展示

1.text.c 文件

 2.snake.h 头文件

3.snake.c 文件


我的代码中没有对颜色进行修改,用的白色背景色,黑色前景色

如果你想让贪吃蛇界面五彩缤纷,那么就认真学习我在这篇文章中的颜色详解部分,学完你就可以独立设置打印的颜色了!!!让颜色变得简单起来吧!!!

一、电脑终端设置

在所有应用中找到终端打开,在终端黑框上边缘右键单击选择设置,设置为Windows控制台主机

或者随便运行一个代码,在代码运行黑框上边缘右键单击选择设置,设置为Windows控制台主机

e8ad5b4ee3c3414980a254d84372e895.png

 按如上设置之后才能使用system函数对控制台的大小、颜色等属性进行控制

二、部分核心代码讲解

1.Windows_API讲解

下面是贪吃蛇中需要使用的Windous_API函数

Windows_API
	GetStdHandle--获取控制台句柄
	GetConsoleCursorInfo--获取光标信息
	SetConsoleCursorInfo--设置光标信息
	SetConsoleCursorPosition--设置光标位置
	GetAsyncKeyState--获取按键状态

1)GetStdHandle--获取控制台句柄

GetStdHandle是一个Windows API函数。它用于从一个特定的标准设备(标准输入、标准输出或标准错误)中取得一个句柄(用来标识不同设备的数值)。因为很多API函数都需要用到句柄,所以GetStdHandle是一个很重要的函数。

HANDLE WINAPI GetStdHandle(_In_ DWORD nStdHandle);

2)GetConsoleCursorInfo--获取光标信息 和 SetConsoleCursorInfo--设置光标信息

GetConsoleCursorInfo函数:可以获取光标的占空比(即占总高度的多少)也就是高度(dwSize);也可以获取光标的亮灭(bVisible),该变量为bool变量,当它为false时表示光标透明,为true时光标为黑色。

SetConsoleCursorInfo函数:在获取光标信息后对光标信息进行期望的修改后调用函数进行设置。

 _In_ HANDLE hConsoleOutput这个参数就是前面获取的控制台句柄!

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

BOOL WINAPI GetConsoleCursorInfo(
    _In_ HANDLE hConsoleOutput,
    _Out_ PCONSOLE_CURSOR_INFO lpConsoleCursorInfo
    );

BOOL WINAPI SetConsoleCursorInfo(
    _In_ HANDLE hConsoleOutput,
    _In_ CONST CONSOLE_CURSOR_INFO* lpConsoleCursorInfo
    );

3)SetConsoleCursorPosition--设置光标位置

采用结构体储存光标位置坐标,在传入该函数进行光标位置的设置。

 _In_ HANDLE hConsoleOutput这个参数就是前面获取的控制台句柄!

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

BOOL WINAPI SetConsoleCursorPosition(
    _In_ HANDLE hConsoleOutput,
    _In_ COORD dwCursorPosition
    );

4)GetAsyncKeyState--获取按键状态

当某个键按下时该函数返回1,没按下则返回0。其中vKey就是按键的参数值,这里Windows已经定义好了,可以在WinUser.h这个头文件中查看,这里我把要用到的按键列出来

SHORT WINAPI GetAsyncKeyState( _In_ int vKey);

#define VK_LEFT           0x25//左
#define VK_UP             0x26//上
#define VK_RIGHT          0x27//右
#define VK_DOWN           0x28//下
#define VK_SPACE          0x20//空格
#define VK_ESCAPE         0x1B//ESC
#define VK_F3             0x72//F3
#define VK_F4             0x73//F4

 2.控制台属性设置函数

注意这里的颜色设置只能设置全部背景色,在设置前景色的时候会将所有文字同时设置为一个颜色,也就是无法做到同一时刻屏幕上打印多彩的文字

但是!!!这是可以解决的,请看接着的解决办法

设置控制台的相关属性
	system("mode con cols=100 lines=30");//设置大小
	system("title 贪吃蛇");//设置名字
	system("pause");//暂停程序
	system("color 74");//设置颜色:两位数---十位是背景色---个位是前景色
    system("cls");//清屏函数

颜色对应表
		0 = 黑色       8 = 灰色
		1 = 蓝色       9 = 淡蓝色
		2 = 绿色       A = 淡绿色
		3 = 浅绿色     B = 淡浅绿色
		4 = 红色       C = 淡红色
		5 = 紫色       D = 淡紫色
		6 = 黄色       E = 淡黄色
		7 = 白色       F = 亮白色

 3.颜色设置详解(背景色+前景色)

当想要实现五颜六色的文字时,我们可以用下面的方法。在这函数中我们发现他是对局部区域前景色和背景色的设置,所以我们要同时设置前景色(FOREGROUND)和背景色(BACKGROUND),在windous提供的颜色库中我们简单运算就发现按16进制理解就是第一位代表前景色(对应十进制中的个位),第二位代表背景色(对应十进制中的十位),所以可以用或这个运算法来表示颜色的总体设置,如前红后白就是0x04|0x70(即4|7*16),因为任何数或上0还是这个数;我们发现这就是前面我给到大家的颜色数字库,只要将颜色乘以16即可得到背景色颜色的数字库。

SetConsoleTextAttribute(GetStdHandle(STD_OUTPUT_HANDLE), FOREGROUND|BACKGROUND);
//这也是一个Windows_API函数

//可以对其进行封装
void color(int FOREGROUND,int BACKGROUND)
{
	SetConsoleTextAttribute(GetStdHandle(STD_OUTPUT_HANDLE), FOREGROUND|BACKGROUND); //颜色设置
	//注:SetConsoleTextAttribute是一个API(应用程序编程接口)
}

#define FOREGROUND_BLUE      0x0001 // text color contains blue.
#define FOREGROUND_GREEN     0x0002 // text color contains green.
#define FOREGROUND_RED       0x0004 // text color contains red.
#define FOREGROUND_INTENSITY 0x0008 // text color is intensified.
#define BACKGROUND_BLUE      0x0010 // background color contains blue.
#define BACKGROUND_GREEN     0x0020 // background color contains green.
#define BACKGROUND_RED       0x0040 // background color contains red.
#define BACKGROUND_INTENSITY 0x0080 // background color is intensified.
#define COMMON_LVB_LEADING_BYTE    0x0100 // Leading Byte of DBCS
#define COMMON_LVB_TRAILING_BYTE   0x0200 // Trailing Byte of DBCS
#define COMMON_LVB_GRID_HORIZONTAL 0x0400 // DBCS: Grid attribute: top horizontal.
#define COMMON_LVB_GRID_LVERTICAL  0x0800 // DBCS: Grid attribute: left vertical.
#define COMMON_LVB_GRID_RVERTICAL  0x1000 // DBCS: Grid attribute: right vertical.
#define COMMON_LVB_REVERSE_VIDEO   0x4000 // DBCS: Reverse fore/back ground attribute.
#define COMMON_LVB_UNDERSCORE      0x8000 // DBCS: Underscore.

#define COMMON_LVB_SBCSDBCS        0x0300 // SBCS or DBCS flag.

这里有趣的地方在于三原色的定义 他们的二进制是 蓝色(0001)绿色(0010)红色(0100),在这三个位置的给1定义使得我们在代码中也可以进行三原色的组合,我们通过 或 运算可以得到各种颜色,就比如 红+蓝=紫(0001|0100=0101=5)。

哈哈,代码是不是很有趣呢?

 三、游戏代码讲解

在经过前面的了解之后,相信大家也具备了对贪吃代码编写的能力,下面就开始代码的编写吧!

0.光标定位函数

这是贯穿全局的函数所以就先讲了

//光标位置设置
void set_pos(short x, short y)
{
	HANDLE houtput = NULL;
	houtput = GetStdHandle(STD_OUTPUT_HANDLE);//获取句柄
	COORD pos = { x,y };//光标位置信息结构体--Windows已经定义好了
	SetConsoleCursorPosition(houtput, pos);//定位光标
}

1.游戏坐标设定

在终端中一个y轴纵向距离是一个x轴横向距离的两倍,后面的代码实现就是对相应坐标的指定打印的过程。

y = 2x
 ---------------------->x col//列
|
|
|
|
|
|
|
\/
y
row//行

 2.主函数逻辑讲解

main函数中 setlocale(LC_ALL, "") 这个函数是进行本地化设置,目的是将编码方式设置为对应的编码方式(因为每个国家的语言有差异,就需要不同的编码方式来打印不同国家的文字)

#include"snake.h"

//游戏逻辑
void game()
{
	char ch;
	do
	{
		system("cls");
		
		Snake snake = { 0 };//创建贪吃蛇
		GameStart(&snake);//初始化游戏
		GameRun(&snake);//运行游戏
		GameEnd(&snake);//结束游戏--善后工作--释放空间

		set_pos(20, 15);
		printf("再来一局吗?(Y/N)(y/n):");

        ch = getchar();//消除缓冲区的回车字符

		while (getchar() != '\n');//消除输入的多余字符

	} while (ch == 'y' || ch == 'Y');

	set_pos(0, row);//定位光标--打印结束语
}

//主函数逻辑
int main()
{
	setlocale(LC_ALL, "");//本地化设置
	srand((unsigned int)time(NULL));//生成随机数
	game();//进入游戏
	return 0;
}

 3.snake.h头文件中的声明

包含蛇信息结构体,蛇身结构体,游戏状态枚举,移动方向枚举

这里宏定义了一个函数:

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

这就是之前说的Windows_API中的按键状态获取函数,再通过三目操作符将状态结果赋给KEY_PRESS(vk)。GetAsyncKeyState()的返回值表示两个内容,一个是最高位bit的值,代表这个键是否被按下,一个是最低位bit的值,代表在上次调用GetAsyncKeyState()后,这个键是否被按下。由于贪吃蛇是在移动中的方向改变,所以我们采用后者(最低位bit的值),所以我们将函数返回值&0x1即可(因为0x1二进制是0000 00001,如 0111 0101 & 0x1=0000 0001,0111 0100 & 0x1=0000 0000),得到最低位状态。

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

#define row 26//坐标轴定义
#define col 29
#define POS_X 24
#define POS_Y 5

#define WALL L'□'//中国地区编码方式 L'字符' 表示宽字符,即横宽两格
#define BODY L'●'
#define FOOD L'◆'

#define KEY_PRESS(vk) ((GetAsyncKeyState(vk)&0x1)?1:0)//获取按键状态

typedef enum DIRECTION//方向
{
	UP,//上
	DOWN,//下
	LEFT,//左
	RIGHT//右
}direction;

typedef enum GAME_STATUS//游戏状态
{
	OK,//正常
	KILL_BY_WALL,//撞墙
	KILL_BY_SELF,//吃自己
	END_NORMAL//退出
}game_status;

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

typedef struct Snake//蛇的信息
{
	pSnakeNode _pSnakeHead;//头节点
	pSnakeNode _pFood;//食物
	direction _dir;//移动方向
	game_status _status;//游戏状态
	int _food_weight;//一个食物的分数
	int _score;//总得分
	int _sleep_time;//移动速度--睡眠时间
	int _MAX;//最高得分记录
}Snake,* pSnake;

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

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

//移动
void SnakeMove(pSnake ps);

//判断是否吃到食物
int is_Food(pSnakeNode pn, pSnake ps);

//结束游戏--善后工作--释放空间
void GameEnd(pSnake ps);

//文件保存最高分
int SaveScore(pSnake ps);

//文件下载最高分
int LoadScore(pSnake ps);

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

下面我们对游戏的主要三部分进行讲解:

//初始化游戏
GameStart(&snake);
//运行游戏
GameRun(&snake);
//结束游戏--善后工作--释放空间
GameEnd(&snake);

4.初始化游戏GameStart(&snake); 内容讲解

主要分为四部分:

    //1.打印环境 + 2.功能介绍
    WelcomeToGame();
    //3.绘制地图□
    CreatMap();
    //4.创建蛇
    CreatSnake(ps);
    //5.创建食物
    CreatFood(ps);

加上隐藏光标 HideCurSor();

//初始化游戏
void GameStart(pSnake ps)
{
	/*
			0.设置窗口大小光标隐藏
			1.打印环境
			2.功能介绍
			3.绘制地图
			4.创建蛇
			5.创建食物
			6.设置游戏的相关信息
	*/
	//0.设置窗口大小光标隐藏 + 6.设置游戏的相关信息
	system("mode con cols=100 lines=30");//设置大小
	system("title 贪吃蛇");//设置名字
	HideCurSor();//隐藏光标

	//1.打印环境 + 2.功能介绍
	WelcomeToGame();
	//3.绘制地图□
	CreatMap();
	//4.创建蛇
	CreatSnake(ps);
	//5.创建食物
	CreatFood(ps);
}

 1)隐藏光标 HideCurSor();

其中 GetStdHandle(STD_OUTPUT_HANDLE) 函数就是获取控制台句柄函数,其返回值就是获取到的句柄,所以直接用

//隐藏光标
void HideCurSor()
{
	CONSOLE_CURSOR_INFO cursor_info = { 0 };
	GetConsoleCursorInfo(GetStdHandle(STD_OUTPUT_HANDLE), &cursor_info);//获取光标信息
	cursor_info.bVisible = false;//修改光标信息--透明
	SetConsoleCursorInfo(GetStdHandle(STD_OUTPUT_HANDLE), &cursor_info);//设置光标信息--隐藏光标
}

2)打印环境 + 功能介绍WelcomeToGame();

这 wprintf() 函数就是本地化设置之后的专属打印函数,用于打印本地编码字符,所以要在字符前面加上 L“  ” 来表明打印本地字符(一个汉字占横向两个)。

//1.打印环境 + 2.功能介绍
void WelcomeToGame()
{
	set_pos(40, 14);//定位光标
	wprintf(L"欢迎来到贪吃蛇小游戏\n");
	set_pos(42, 20);//定位光标
	system("pause");//暂停程序
	system("cls");//清屏
	set_pos(25, 14);//定位光标
	wprintf(L"用↑,↓,←,→来控制蛇的移动,按F3加速,按F4减速\n");
	set_pos(40, 16);//定位光标
	wprintf(L"加速可以获得更高的分数\n");
	set_pos(42, 20);//定位光标
	system("pause");//暂停程序
	system("cls");//暂停程序
}

3)绘制地图CreatMap();

打印宽字符用 wprintf() 用法与 printf() 类似

//3.绘制地图□
void CreatMap()
{
	//上
	for (int i = 0; i < col; i++)
		wprintf(L"%lc", WALL);
	//下
	set_pos(0, row - 1);
	for (int i = 0; i < col; i++)
		wprintf(L"%lc", WALL);
	//左
	for (int i = 1; i < row - 1; i++)
	{
		set_pos(0, i);
		wprintf(L"%lc", WALL);
	}
	//右
	for (int i = 1; i < row - 1; i++)
	{
		set_pos(2 * col - 2, i);
		wprintf(L"%lc",WALL);
	}
}

4)创建蛇CreatSnake(ps);

蛇身采用单向链表的数据结构来储存

void SnakePrintf(pSnake ps)
{
	pSnakeNode pcur = ps->_pSnakeHead;//找到头节点
	while (pcur)
	{
		set_pos(pcur->x, pcur->y);//光标定位
		wprintf(L"%lc", BODY);
		pcur = pcur->next;//找到下一个身体节点
	}
}

//4.创建蛇
void CreatSnake(pSnake ps)
{
	pSnakeNode pcur = NULL;//蛇身节点指针
	for (int i = 0; i < 5; i++)//初始化身体5节长
	{
		pcur = (pSnakeNode)malloc(sizeof(SnakeNode));//动态空间申请
		if (pcur == NULL)
		{
			perror("CreatSnake()::malloc()");//空间申请是否出现错误判断
			return;
		}
		pcur->next = NULL;//下一个节点
		pcur->x = POS_X + 2 * i;//记录初始化位置坐标
		pcur->y = POS_Y;

		//头插法插入链表
		if (ps->_pSnakeHead == NULL)//头节点是否存在的判断,即第一个申请的节点作为头节点
		{
			ps->_pSnakeHead = pcur;
		}
		else//头节点已经存在,对下一个节点进行指向
		{
			pcur->next = ps->_pSnakeHead;
			ps->_pSnakeHead = pcur;
		}
	}
	SnakePrintf(ps);//打印蛇函数
    
    //蛇总信息初始化
	ps->_dir = RIGHT;
	ps->_score = 0;
	ps->_food_weight = 10;
	ps->_sleep_time = 200;//ms
	ps->_status = OK;
	ps->_MAX = 0;

	LoadScore(ps);//读取文件保存的最高分
}
//读取文件保存的最高分
int LoadScore(pSnake ps)
{
	FILE* pf = fopen("MAX_SCORE.txt", "rb");//二进制只读打开文件
	if (pf == NULL)
	{
		perror("fopen fail");//判断打开文件是否出现错误
		return 0;
	}
	fread(&ps->_MAX, sizeof(ps->_MAX), 1, pf);//读取最高分信息
	fclose(pf);//关闭文件
	pf = NULL;//避免野指针
	return 1;
}

5)创建食物CreatFood(ps);

这里主要是食物生成的横坐标问题,要为偶数,因为我们从0开始每两格打印一个字符,所以x表示打印字符的起始位置(偶数)。

//5.创建食物
void CreatFood(pSnake ps)
{
	int x = 0;
	int y = 0;
	again:
	do
	{
		x = rand() % (2 * col -5) + 2;//随机生成x坐标--在墙体内--【2,54】--56,57为墙
		y = rand() % (row - 2) + 1;//随机生成y坐标--在墙体内--【1,24】--25为墙
	} while (x%2);//保证生成的x坐标是偶数

	pSnakeNode pcur = ps->_pSnakeHead;
	while (pcur)//判断食物是否与蛇体重叠
	{
		if (x == pcur->x || y == pcur->y)
		{
			goto again;//重叠则重新生成食物
		}
		pcur = pcur->next;
	}
	pSnakeNode pFood = (pSnakeNode)malloc(sizeof(SnakeNode));//申请食物空间
	if (pFood == NULL)
	{
		perror("CreatFood()::malloc()");//判断空间申请是否出现错误
		return;
	}

    //食物节点也用的是蛇身节点结构体
	pFood->x = x;
	pFood->y = y;
	pFood->next = NULL;
	set_pos(pFood->x, pFood->y);//光标定位
	wprintf(L"%lc", FOOD);//打印食物
	ps->_pFood = pFood;//记录食物到蛇总信息
}

5.运行游戏GameRun(&snake);内容讲解

注意食物也是动态空间,也要申请和释放

注意对尾节点的释放,注意原来的蛇要清除

其他细节见注释

//打印提示信息
void PrintHelp()
{
	set_pos(64, 14);
	wprintf(L"%ls\n", L"不能穿墙,不能咬到自己");
	set_pos(64, 15);
	wprintf(L"%ls\n", L"用↑,↓,←,→来控制蛇的移动");
	set_pos(64, 16);
	wprintf(L"%ls\n", L"按F3加速,按F4减速");
	set_pos(64, 17);
	wprintf(L"%ls\n", L"按ESC退出,按空格暂停");
}

//暂停游戏
void Pause()
{
	while (1)
	{
		Sleep(200);//ms
		if (KEY_PRESS(VK_SPACE))break;//当空格再次按下时继续游戏
	}
}

//判断是否吃到食物
int is_Food(pSnakeNode pn, pSnake ps)
{
	if (ps->_pFood->x == pn->x && ps->_pFood->y == pn->y)//头节点坐标与食物坐标比较
	{
		return 1;
	}
	return 0;
}

//清除屏幕上的蛇--方便移动后的打印
void ClearSnake(pSnake ps)
{
	pSnakeNode pcur = ps->_pSnakeHead;//找到头节点
	while (pcur)
	{
		set_pos(pcur->x, pcur->y);
		printf("  ");//打印两格空格
		pcur = pcur->next;
	}
}

//吃到食物
void EatFood(pSnakeNode pn, pSnake ps)//pn是头移动到的位置
{
	ClearSnake(ps);//清楚屏幕上的蛇
	pn->next = ps->_pSnakeHead;//头移动到的节点变成头节点--所以第二个节点就是原来的头节点
	ps->_pSnakeHead = pn;//头节点移动到的位置
//吃食物,身体变长--尾节点位置不变,释放食物空间
	free(ps->_pFood);//释放食物申请的空间
	ps->_pFood = NULL;//避免野指针
	CreatFood(ps);//重新创建食物
	ps->_score += ps->_food_weight;//加分
}

//没有食物
void NoFood(pSnakeNode pn, pSnake ps)//pn是头移动到的位置
{
	ClearSnake(ps);//清楚屏幕上的蛇
	pn->next = ps->_pSnakeHead;//头移动到的节点变成头节点--所以第二个节点就是原来的头节点
	ps->_pSnakeHead = pn;//头节点移动到的位置
//没有食物,身体不变长,要改变尾节点位置,并释放原来尾节点的空间
	pSnakeNode pcur = ps->_pSnakeHead;
	pSnakeNode del = pcur->next;
	while (del->next)//循环找到原来尾节点
	{
		pcur = del;
		del = del->next;
	}
	free(del);
	del = pcur->next = NULL;//防止野指针--将原倒数第二个节点设为尾节点
}

//移动
void SnakeMove(pSnake ps)
{
	pSnakeNode pNextNode = (pSnakeNode)malloc(sizeof(SnakeNode));
	if (pNextNode == NULL)
	{
		perror("CreatSnake()::malloc()");
		return;
	}
	switch (ps->_dir)//判断方向,并对移动到的下一个节点的坐标进行设置
	{
	case UP:
		pNextNode->x = ps->_pSnakeHead->x;
		pNextNode->y = ps->_pSnakeHead->y - 1;
		break;
	case DOWN:
		pNextNode->x = ps->_pSnakeHead->x;
		pNextNode->y = ps->_pSnakeHead->y + 1;
		break;
	case LEFT:
		pNextNode->x = ps->_pSnakeHead->x - 2;
		pNextNode->y = ps->_pSnakeHead->y;
		break;
	case RIGHT:
		pNextNode->x = ps->_pSnakeHead->x + 2;
		pNextNode->y = ps->_pSnakeHead->y;
		break;
	}
//移动后判断是否吃到食物
	if (is_Food(pNextNode, ps))//吃食物
	{
		EatFood(pNextNode, ps);
	}
	else//没食物
	{
		NoFood(pNextNode, ps);
	}
	SnakePrintf(ps);//再打印蛇身产生移动
}

//判断是否撞墙
void KillByWall(pSnake ps)
{
	if (ps->_pSnakeHead->x == 0 || ps->_pSnakeHead->x == 56 ||
		ps->_pSnakeHead->y == 0 || ps->_pSnakeHead->y == 25)
	{
		ps->_status = KILL_BY_WALL;//撞墙则改变游戏状态为 KILL_BY_WALL
	}
}

//判断是否吃到自己
void KillBySelf(pSnake ps)
{
	pSnakeNode pcur = ps->_pSnakeHead->next;//找到头节点
	while (pcur)
	{
		if (pcur->x == ps->_pSnakeHead->x && pcur->y == ps->_pSnakeHead->y)
		{
			ps->_status = KILL_BY_SELF;//吃到自己则改变游戏状态为 KILL_BY_SELF
			break;
		}
		pcur = pcur->next;
	}
}

//运行游戏
void GameRun(pSnake ps)
{
	//打印提示信息
	PrintHelp();
	do
	{
		set_pos(64, 6);
		printf("历史最高分数:%d\n", ps->_MAX);
		set_pos(64, 10);
		printf("当前得分:%d\n", ps->_score);
		set_pos(64, 11);
		printf("当前食物分数为:%02d\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))//退出
		{
			ps->_status = END_NORMAL;
		}
		else if (KEY_PRESS(VK_F3))//加速
		{
			if (ps->_sleep_time > 80)//速度上限
			{
				ps->_sleep_time -= 30;//减少睡眠时间--即打印移动后的蛇的间隔实间
				ps->_food_weight += 2;//加速--食物分值提高
			}
		}
		else if (KEY_PRESS(VK_F4))//减速
		{
			if (ps->_sleep_time < 320)//速度下限
			{
				ps->_sleep_time += 30;//增加睡眠时间--即打印移动后的蛇的间隔实间
				ps->_food_weight -= 2;//减速--食物分值降低
			}
		}
		SnakeMove(ps);//移动蛇
		ps->_MAX = ps->_score > ps->_MAX ? ps->_score : ps->_MAX;//判断最高分是否更新
		KillByWall(ps);//判断是否撞墙
		KillBySelf(ps);//判断是否吃到自己
		Sleep(ps->_sleep_time);//睡眠时间

	} while (ps->_status == OK);//判断游戏状态
}

6.结束游戏--善后工作--释放空间GameEnd(&snake);内容讲解

三部分:

WhichCause(ps);//游戏结束原因
SaveScore(ps);//更新最高分到文件
DistorySnake(&ps);//销毁蛇链表

//结束游戏--善后工作--释放空间
void GameEnd(pSnake ps)
{
	WhichCause(ps);//游戏结束原因
	SaveScore(ps);//更新最高分到文件
	DistorySnake(&ps);//销毁蛇链表
}

 1)WhichCause(ps);游戏结束原因

找到退出原因并打印相应提示信息

void WhichCause(pSnake ps)
{
	set_pos(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;
	}
}

2)SaveScore(ps);更新最高分到文件

//更新最高分到文件
int SaveScore(pSnake ps)
{
	FILE* pf = fopen("MAX_SCORE.txt", "wb");//二进制只写打开文件
	if (pf == NULL)
	{
		perror("fopen fail");//判断打开文件是否错误
		return 0;
	}
	fwrite(&ps->_MAX, sizeof(ps->_MAX), 1, pf);//更新最高分到文件
	fclose(pf);//关闭文件
	pf = NULL;//避免野指针
	return 1;
}

3)DistorySnake(&ps);销毁蛇链表

//销毁链表--释放空间
void DistorySnake(pSnake* ps)
{
	pSnakeNode del = (*ps)->_pSnakeHead;//找到头节点
	pSnakeNode pcur = del->next;//找到销毁节点的下一个节点
	while (pcur)//从头节点往后依次销毁
	{
		free(del);
		del = pcur;
		pcur = pcur->next;
	}
	(*ps)->_pSnakeHead = del = pcur = NULL;//避免野指针
	*ps = NULL;//蛇总信息指针置空--避免野指针--这也是传入二级指针的原因,对这个指针进行修改
}

四、代码与程序展示

0.程序展示

4e8abdd65c8846938afb4c7fa4a8c325.png

71dccb84d71a4a009300c5f95ed41d98.png b7be593609154ef28848d89a8aea7b7f.png

 924263919fce462cbf2ea6adee1d01d0.png

1.text.c 文件

#include"snake.h"

//游戏逻辑
void game()
{
	char ch;
	do
	{
		system("cls");

		Snake snake = { 0 };//创建贪吃蛇
		GameStart(&snake);//初始化游戏
		GameRun(&snake);//运行游戏
		GameEnd(&snake);//结束游戏--善后工作--释放空间

		set_pos(20, 15);
		printf("再来一局吗?(Y/N)(y/n):");

		ch = getchar();//消除缓冲区的回车字符

		while (getchar() != '\n');//消除输入的多余字符

	} while (ch == 'y' || ch == 'Y');

	set_pos(0, row);//定位光标--打印结束语
}

//主函数逻辑
int main()
{
	setlocale(LC_ALL, "");//本地化设置
	srand((unsigned int)time(NULL));//生成随机数
	game();//进入游戏
	return 0;
}

 2.snake.h 头文件

#pragma once
#define _CRT_SECURE_NO_WARNINGS 1

/*
控制台坐标--windous头文件中
typedef struct _COORD
{
	short x;
	short y;
}COORD;
*/

/*
Windows_API
	GetStdHandle--获取控制台句柄
	GetConsoleCursorInfo--获取光标信息
	SetConsoleCursorInfo--设置光标信息
	SetConsoleCursorPosition--设置光标位置
	GetAsyncKeyState--获取按键状态
	CONSOLE_CURSOR_INFO--光标结构体
*/

/*
设置控制台的相关属性
	system("mode con cols=100 lines=30");//设置大小
	system("title 贪吃蛇");//设置名字
	system("pause");//暂停程序
	system("color 74");//设置颜色:两位数---十位是背景色---个位是前景色
*/

/*
颜色对应表
		0 = 黑色       8 = 灰色
		1 = 蓝色       9 = 淡蓝色
		2 = 绿色       A = 淡绿色
		3 = 浅绿色     B = 淡浅绿色
		4 = 红色       C = 淡红色
		5 = 紫色       D = 淡紫色
		6 = 黄色       E = 淡黄色
		7 = 白色       F = 亮白色
*/


/*
	y = 2x
	 ---------------------->x col
	|
	|
	|
	|
	|
	|
	|
	\/
	y
	row
*/

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

#define row 26//坐标轴定义
#define col 29
#define POS_X 24
#define POS_Y 5

#define WALL L'□'//中国地区编码方式 L'字符' 表示宽字符,即横宽两格
#define BODY L'●'
#define FOOD L'◆'

#define KEY_PRESS(vk) ((GetAsyncKeyState(vk)&0x1)?1:0)//获取按键状态

typedef enum DIRECTION//方向
{
	UP,//上
	DOWN,//下
	LEFT,//左
	RIGHT//右
}direction;

typedef enum GAME_STATUS//游戏状态
{
	OK,//正常
	KILL_BY_WALL,//撞墙
	KILL_BY_SELF,//吃自己
	END_NORMAL//退出
}game_status;

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

typedef struct Snake//蛇的信息
{
	pSnakeNode _pSnakeHead;//头节点
	pSnakeNode _pFood;//食物
	direction _dir;//移动方向
	game_status _status;//游戏状态
	int _food_weight;//一个食物的分数
	int _score;//总得分
	int _sleep_time;//移动速度--睡眠时间
	int _MAX;//最高得分记录
}Snake, * pSnake;

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

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

//移动
void SnakeMove(pSnake ps);

//判断是否吃到食物
int is_Food(pSnakeNode pn, pSnake ps);

//结束游戏--善后工作--释放空间
void GameEnd(pSnake ps);

//文件保存最高分
int SaveScore(pSnake ps);

//文件下载最高分
int LoadScore(pSnake ps);

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

3.snake.c 文件

#include"snake.h"

//隐藏光标
void HideCurSor()
{
	CONSOLE_CURSOR_INFO cursor_info = { 0 };
	GetConsoleCursorInfo(GetStdHandle(STD_OUTPUT_HANDLE), &cursor_info);//获取光标信息
	cursor_info.bVisible = false;//修改光标信息--透明
	SetConsoleCursorInfo(GetStdHandle(STD_OUTPUT_HANDLE), &cursor_info);//设置光标信息--隐藏光标
}

//光标位置设置
void set_pos(short x, short y)
{
	HANDLE houtput = NULL;
	houtput = GetStdHandle(STD_OUTPUT_HANDLE);//获取句柄
	COORD pos = { x,y };//光标位置信息结构体--Windows已经定义好了
	SetConsoleCursorPosition(houtput, pos);//定位光标
}

//1.打印环境 + 2.功能介绍
void WelcomeToGame()
{
	set_pos(40, 14);//定位光标
	wprintf(L"欢迎来到贪吃蛇小游戏\n");
	set_pos(42, 20);//定位光标
	system("pause");//暂停程序
	system("cls");//清屏
	set_pos(25, 14);//定位光标
	wprintf(L"用↑,↓,←,→来控制蛇的移动,按F3加速,按F4减速\n");
	set_pos(40, 16);//定位光标
	wprintf(L"加速可以获得更高的分数\n");
	set_pos(42, 20);//定位光标
	system("pause");//暂停程序
	system("cls");//暂停程序
}

//3.绘制地图□
void CreatMap()
{
	//上
	for (int i = 0; i < col; i++)
		wprintf(L"%lc", WALL);
	//下
	set_pos(0, row - 1);
	for (int i = 0; i < col; i++)
		wprintf(L"%lc", WALL);
	//左
	for (int i = 1; i < row - 1; i++)
	{
		set_pos(0, i);
		wprintf(L"%lc", WALL);
	}
	//右
	for (int i = 1; i < row - 1; i++)
	{
		set_pos(2 * col - 2, i);
		wprintf(L"%lc",WALL);
	}
}

void SnakePrintf(pSnake ps)
{
	pSnakeNode pcur = ps->_pSnakeHead;//找到头节点
	while (pcur)
	{
		set_pos(pcur->x, pcur->y);//光标定位
		wprintf(L"%lc", BODY);
		pcur = pcur->next;//找到下一个身体节点
	}
}

//4.创建蛇
void CreatSnake(pSnake ps)
{
	pSnakeNode pcur = NULL;//蛇身节点指针
	for (int i = 0; i < 5; i++)//初始化身体5节长
	{
		pcur = (pSnakeNode)malloc(sizeof(SnakeNode));//动态空间申请
		if (pcur == NULL)
		{
			perror("CreatSnake()::malloc()");//空间申请是否出现错误判断
			return;
		}
		pcur->next = NULL;//下一个节点
		pcur->x = POS_X + 2 * i;//记录初始化位置坐标
		pcur->y = POS_Y;

		//头插法插入链表
		if (ps->_pSnakeHead == NULL)//头节点是否存在的判断,即第一个申请的节点作为头节点
		{
			ps->_pSnakeHead = pcur;
		}
		else//头节点已经存在,对下一个节点进行指向
		{
			pcur->next = ps->_pSnakeHead;
			ps->_pSnakeHead = pcur;
		}
	}
	SnakePrintf(ps);//打印蛇函数

	//蛇总信息初始化
	ps->_dir = RIGHT;
	ps->_score = 0;
	ps->_food_weight = 10;
	ps->_sleep_time = 200;//ms
	ps->_status = OK;
	ps->_MAX = 0;

	LoadScore(ps);//读取文件保存的最高分
}

///5.创建食物
void CreatFood(pSnake ps)
{
	int x = 0;
	int y = 0;
again:
	do
	{
		x = rand() % (2 * col - 5) + 2;//随机生成x坐标--在墙体内--【2,54】--56,57为墙
		y = rand() % (row - 2) + 1;//随机生成y坐标--在墙体内--【1,24】--25为墙
	} while (x % 2);//保证生成的x坐标是偶数

	pSnakeNode pcur = ps->_pSnakeHead;
	while (pcur)//判断食物是否与蛇体重叠
	{
		if (x == pcur->x || y == pcur->y)
		{
			goto again;//重叠则重新生成食物
		}
		pcur = pcur->next;
	}
	pSnakeNode pFood = (pSnakeNode)malloc(sizeof(SnakeNode));//申请食物空间
	if (pFood == NULL)
	{
		perror("CreatFood()::malloc()");//判断空间申请是否出现错误
		return;
	}

	//食物节点也用的是蛇身节点结构体
	pFood->x = x;
	pFood->y = y;
	pFood->next = NULL;
	set_pos(pFood->x, pFood->y);//光标定位
	wprintf(L"%lc", FOOD);//打印食物
	ps->_pFood = pFood;//记录食物到蛇总信息
}

//初始化游戏
void GameStart(pSnake ps)
{
	/*
			0.设置窗口大小光标隐藏
			1.打印环境
			2.功能介绍
			3.绘制地图
			4.创建蛇
			5.创建食物
			6.设置游戏的相关信息
	*/
	//0.设置窗口大小光标隐藏 + 6.设置游戏的相关信息
	system("mode con cols=100 lines=30");//设置大小
	system("title 贪吃蛇");//设置名字
	HideCurSor();
	//1.打印环境 + 2.功能介绍
	WelcomeToGame();
	//3.绘制地图□
	CreatMap();
	//4.创建蛇
	CreatSnake(ps);
	//5.创建食物
	CreatFood(ps);
}

//打印提示信息
void PrintHelp()
{
	set_pos(64, 14);
	wprintf(L"%ls\n", L"不能穿墙,不能咬到自己");
	set_pos(64, 15);
	wprintf(L"%ls\n", L"用↑,↓,←,→来控制蛇的移动");
	set_pos(64, 16);
	wprintf(L"%ls\n", L"按F3加速,按F4减速");
	set_pos(64, 17);
	wprintf(L"%ls\n", L"按ESC退出,按空格暂停");
}

//暂停游戏
void Pause()
{
	while (1)
	{
		Sleep(200);//ms
		if (KEY_PRESS(VK_SPACE))break;//当空格再次按下时继续游戏
	}
}

//判断是否吃到食物
int is_Food(pSnakeNode pn, pSnake ps)
{
	if (ps->_pFood->x == pn->x && ps->_pFood->y == pn->y)//头节点坐标与食物坐标比较
	{
		return 1;
	}
	return 0;
}

//清除屏幕上的蛇--方便移动后的打印
void ClearSnake(pSnake ps)
{
	pSnakeNode pcur = ps->_pSnakeHead;//找到头节点
	while (pcur)
	{
		set_pos(pcur->x, pcur->y);
		printf("  ");//打印两格空格
		pcur = pcur->next;
	}
}

//吃到食物
void EatFood(pSnakeNode pn, pSnake ps)//pn是头移动到的位置
{
	ClearSnake(ps);//清楚屏幕上的蛇
	pn->next = ps->_pSnakeHead;//头移动到的节点变成头节点--所以第二个节点就是原来的头节点
	ps->_pSnakeHead = pn;//头节点移动到的位置
	//吃食物,身体变长--尾节点位置不变,释放食物空间
	free(ps->_pFood);//释放食物申请的空间
	ps->_pFood = NULL;//避免野指针
	CreatFood(ps);//重新创建食物
	ps->_score += ps->_food_weight;//加分
}

//没有食物
void NoFood(pSnakeNode pn, pSnake ps)//pn是头移动到的位置
{
	ClearSnake(ps);//清楚屏幕上的蛇
	pn->next = ps->_pSnakeHead;//头移动到的节点变成头节点--所以第二个节点就是原来的头节点
	ps->_pSnakeHead = pn;//头节点移动到的位置
	//没有食物,身体不变长,要改变尾节点位置,并释放原来尾节点的空间
	pSnakeNode pcur = ps->_pSnakeHead;
	pSnakeNode del = pcur->next;
	while (del->next)//循环找到原来尾节点
	{
		pcur = del;
		del = del->next;
	}
	free(del);
	del = pcur->next = NULL;//防止野指针--将原倒数第二个节点设为尾节点
}

//移动
void SnakeMove(pSnake ps)
{
	pSnakeNode pNextNode = (pSnakeNode)malloc(sizeof(SnakeNode));
	if (pNextNode == NULL)
	{
		perror("CreatSnake()::malloc()");
		return;
	}
	switch (ps->_dir)//判断方向,并对移动到的下一个节点的坐标进行设置
	{
	case UP:
		pNextNode->x = ps->_pSnakeHead->x;
		pNextNode->y = ps->_pSnakeHead->y - 1;
		break;
	case DOWN:
		pNextNode->x = ps->_pSnakeHead->x;
		pNextNode->y = ps->_pSnakeHead->y + 1;
		break;
	case LEFT:
		pNextNode->x = ps->_pSnakeHead->x - 2;
		pNextNode->y = ps->_pSnakeHead->y;
		break;
	case RIGHT:
		pNextNode->x = ps->_pSnakeHead->x + 2;
		pNextNode->y = ps->_pSnakeHead->y;
		break;
	}
	//移动后判断是否吃到食物
	if (is_Food(pNextNode, ps))//吃食物
	{
		EatFood(pNextNode, ps);
	}
	else//没食物
	{
		NoFood(pNextNode, ps);
	}
	SnakePrintf(ps);//再打印蛇身产生移动
}

//判断是否撞墙
void KillByWall(pSnake ps)
{
	if (ps->_pSnakeHead->x == 0 || ps->_pSnakeHead->x == 56 ||
		ps->_pSnakeHead->y == 0 || ps->_pSnakeHead->y == 25)
	{
		ps->_status = KILL_BY_WALL;//撞墙则改变游戏状态为 KILL_BY_WALL
	}
}

//判断是否吃到自己
void KillBySelf(pSnake ps)
{
	pSnakeNode pcur = ps->_pSnakeHead->next;//找到头节点
	while (pcur)
	{
		if (pcur->x == ps->_pSnakeHead->x && pcur->y == ps->_pSnakeHead->y)
		{
			ps->_status = KILL_BY_SELF;//吃到自己则改变游戏状态为 KILL_BY_SELF
			break;
		}
		pcur = pcur->next;
	}
}

//运行游戏
void GameRun(pSnake ps)
{
	//打印提示信息
	PrintHelp();
	do
	{
		set_pos(64, 6);
		printf("历史最高分数:%d\n", ps->_MAX);
		set_pos(64, 10);
		printf("当前得分:%d\n", ps->_score);
		set_pos(64, 11);
		printf("当前食物分数为:%02d\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))//退出
		{
			ps->_status = END_NORMAL;
		}
		else if (KEY_PRESS(VK_F3))//加速
		{
			if (ps->_sleep_time > 80)//速度上限
			{
				ps->_sleep_time -= 30;//减少睡眠时间--即打印移动后的蛇的间隔实间
				ps->_food_weight += 2;//加速--食物分值提高
			}
		}
		else if (KEY_PRESS(VK_F4))//减速
		{
			if (ps->_sleep_time < 320)//速度下限
			{
				ps->_sleep_time += 30;//增加睡眠时间--即打印移动后的蛇的间隔实间
				ps->_food_weight -= 2;//减速--食物分值降低
			}
		}
		SnakeMove(ps);//移动蛇
		ps->_MAX = ps->_score > ps->_MAX ? ps->_score : ps->_MAX;//判断最高分是否更新
		KillByWall(ps);//判断是否撞墙
		KillBySelf(ps);//判断是否吃到自己
		Sleep(ps->_sleep_time);//睡眠时间

	} while (ps->_status == OK);//判断游戏状态
}

//更新最高分到文件
int SaveScore(pSnake ps)
{
	FILE* pf = fopen("MAX_SCORE.txt", "wb");//二进制只写打开文件
	if (pf == NULL)
	{
		perror("fopen fail");//判断打开文件是否错误
		return 0;
	}
	fwrite(&ps->_MAX, sizeof(ps->_MAX), 1, pf);//更新最高分到文件
	fclose(pf);//关闭文件
	pf = NULL;//避免野指针
	return 1;
}

//读取文件保存的最高分
int LoadScore(pSnake ps)
{
	FILE* pf = fopen("MAX_SCORE.txt", "rb");//二进制只读打开文件
	if (pf == NULL)
	{
		perror("fopen fail");//判断打开文件是否出现错误
		return 0;
	}
	fread(&ps->_MAX, sizeof(ps->_MAX), 1, pf);//读取最高分信息
	fclose(pf);//关闭文件
	pf = NULL;//避免野指针
	return 1;
}

//销毁链表--释放空间
void DistorySnake(pSnake* ps)
{
	pSnakeNode del = (*ps)->_pSnakeHead;//找到头节点
	pSnakeNode pcur = del->next;//找到销毁节点的下一个节点
	while (pcur)//从头节点往后依次销毁
	{
		free(del);
		del = pcur;
		pcur = pcur->next;
	}
	(*ps)->_pSnakeHead = del = pcur = NULL;//避免野指针
	*ps = NULL;//蛇总信息指针置空--避免野指针--这也是传入二级指针的原因,对这个指针进行修改
}

void WhichCause(pSnake ps)
{
	set_pos(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;
	}
}

//结束游戏--善后工作--释放空间
void GameEnd(pSnake ps)
{
	WhichCause(ps);//游戏结束原因
	SaveScore(ps);//更新最高分到文件
	DistorySnake(&ps);//销毁蛇链表
}

 

 

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

嚴老三

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值