手把手教你实现贪吃蛇

 > 作者简介:დ旧言~,目前大二,现在学习Java,c,c++,Python等
> 座右铭:松树千年终是朽,槿花一日自为荣。

> 目标:实现贪吃蛇

> 毒鸡汤:时间并不可真的帮我们去解决哪些问题,它只不过是会把原来怎么也想不通的问题,变得不再重要了。
> 望小伙伴们点赞收藏✨加关注哟 

前言

        作为零零后的我们想必贪吃蛇都玩过吧,博主记得当时还在小霸王里面玩的这款小游戏,十分怀念,记忆深刻,每次都是偷偷躲在被窝里面玩,为了防止别抓,基本上是躲在厕所里面玩,上个厕所上个十年,丑事不必再提。作为一个程序员捏,当然要简单的实现它啦,也是为曾经的自己画上一个圆满的句号吧。那咱们闲话少谈,直接手撕贪吃蛇。

手把手教你实现贪吃蛇_第1张图片

⭐游戏背景

        我们采用的是VS2019编译环境,所以家人们准备好编译环境,当然咱们上手不能直接上代码,可能一开始上代码就是从入门到放弃,我懂大家的捏。咱们先介绍游戏,到时候反手就说,这个博主教我们怎么玩游戏,取关,必须取关,家人们不先知道怎么玩你怎么知道游戏的逻辑,给这些人拖出去斩了。

        不知道大家还记得在C语言中我们实现了两款游戏,一个是三子棋,另一个是扫雷,这些游戏和贪吃蛇是久负盛名的游戏,它也和俄罗斯⽅块,扫雷等游戏位列经典游戏的⾏列。


手把手教你实现贪吃蛇_第2张图片


博主后面这些经典小游戏都会一一实现,刺激

游戏演示

看的是不是很神奇,一点也不神奇,不信你继续向下看。

贪吃蛇

手把手教你实现贪吃蛇_第3张图片

⭐实现目标

使⽤C语⾔在Windows环境的控制台中模拟实现经典⼩游戏贪吃蛇
实现基本的功能:
• 贪吃蛇地图绘制
• 蛇吃⻝物的功能 (上、下、左、右⽅向键控制蛇的动作)
• 蛇撞墙死亡
• 蛇撞⾃⾝死亡
• 计算得分
• 蛇⾝加速、减速
• 暂停游戏

⭐技术要点

        C语⾔、数据结构(链表)、枚举、结构体、动态内存管理、预处理指令、win32 API(这个咱们后面讲解),这些基本上都学过哟,像链表不知道大家能不能手撕链表,不会的拖出去打30大板。
手把手教你实现贪吃蛇_第4张图片

win32 API介绍

        使用Win32 API,应用程序可以充分挖掘Windows的32位操作系统的潜力。 Microsoft的所有32位平台都支持统一的API,包括函数、结构、消息、宏及接口。使用 Win32 API不但可以开发出在各种平台上都能成功运行的应用程序,而且也可以充分利用每个平台特有的功能和属性。

        在具体编程时,程序实现方式的差异依赖于相应平台的底层功能的不同。最显著的差异是某些函数只能在更强大的平台上实现其功能。例如,安全函数只能在Windows NT操作系统下使用。另外一些主要差别就是系统限制,比如值的范围约束,或函数可管理的项目个数等等。

当然啦,咱们不用学习这里面全部知识,不然又要被黑子喷咯。

Win32 API

        有一些函数,并不用于交互,比如管理当前系统正在运行的进程、硬件系统状态的监视等等……这些函数只有一套,但是可以被所有的Windows程序调用(只要这个程序的权限足够高),简而言之,API是为程序所共享的。

        为了达到所有程序能共享一套API的目的,Windows采用了“动态链接库”的办法。之所以叫“动态链接库”,是因为这样的函数库的调用方式是“随用随取”而不是像静态链接库那样“用不用都要带上”。

控制台程序

        像我们运行一个代码,就有一个黑框框,没错它就是控制台程序,Win32 API我们是可以控制它滴,终于有人可以管得住它了,不然它还是一个野孩子。

手把手教你实现贪吃蛇_第5张图片手把手教你实现贪吃蛇_第6张图片

我们可以使⽤cmd命令来设置控制台窗⼝的⻓宽:设置控制台窗⼝的⼤⼩,30⾏,100列

1 mode con cols= 100 lines= 30
也可以通过命令设置控制台窗⼝的名字:
1 title 贪吃蛇
手把手教你实现贪吃蛇_第7张图片
耶嘿这孩子有名字咯。

控制台屏幕上的坐标COORD

COORD 是Windows API中定义的⼀种结构,表示⼀个字符在控制台屏幕上的坐标
typedef struct _COORD {
	SHORT X;
	SHORT Y;
} COORD, * PCOORD;
给坐标赋值:
COORD pos = { 10 , 15 };

GetStdHandle

        GetStdHandle是⼀个Windows API函数。它⽤于从⼀个特定的标准设备(标准输⼊、标准输出或标准错误)中取得⼀个句柄(⽤来标识不同设备的数值),使⽤这个句柄可以操作设备。简单的来讲就是可以用键盘控制控制台。
HANDLE GetStdHandle(DWORD nStdHandle);
HANDLE hOutput = NULL;
//获取标准输出的句柄(⽤来标识不同设备的数值)

hOutput = GetStdHandle(STD _OUTPUT_HANDLE);

GetConsoleCursorInfo

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

BOOL WINAPI GetConsoleCursorInfo(
 HANDLE hConsoleOutput,
 PCONSOLE_CURSOR_INFO lpConsoleCursorInfo
);

实例:
HANDLE hOutput = NULL ;
// 获取标准输出的句柄 ( ⽤来标识不同设备的数值 )
hOutput = GetStdHandle (STD_OUTPUT_HANDLE);
CONSOLE_CURSOR_INFO CursorInfo;
GetConsoleCursorInfo (hOutput, &CursorInfo); // 获取控制台光标信息

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

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

GetAsyncKeyState

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

贪吃蛇游戏设计与分析

这里我们得了解游戏的逻辑和框架,别慌,一步一步慢慢来!!!

手把手教你实现贪吃蛇_第8张图片

地图设计

咱们看看地图是如何设计的:

手把手教你实现贪吃蛇_第9张图片手把手教你实现贪吃蛇_第10张图片手把手教你实现贪吃蛇_第11张图片

既然要设计地图,必然需要在VS2019下控制台的坐标问题。

        这里不得不讲一下控制台窗口的一些知识,如果想在控制台的窗⼝中指定位置输出信息,我们得知道该位置的坐标,所以首先介绍一下控制台窗⼝的坐标知识。控制台窗口的坐标如下所示,横向的是X轴,从左向右依次增长,纵向是Y轴,从上到下依次增长。
手把手教你实现贪吃蛇_第12张图片
        在游戏地图上,我们打印墙体使⽤宽字符:□,打印蛇使用宽字符●,打印食物使用宽字符★普通的字符是占⼀个字节的,这类宽字符是占用2个字节。这里再简单的讲⼀下C语言的国际化特性相关的知识,过去C语言并不适合非英语国家(地区)使用。C语言最初假定字符都是但自己的。但是这些假定并不是在世界的任何地方都适用。
        后来为了使C语言适应国家化,C语言的标准中不断加入了国际化的支持。比如:加入和宽字符的类型 wchar_t 和宽字符的输⼊和输出函数,加入和头文件,其中提供了允许程序员针对特定 地区(通常是国家或者说某种特定语言的地理区域)调整程序行为的函数。
setlocale函数

char* setlocale (int category, const char* locale);

  1. setlocale 函数用于修改当前地区,可以针对⼀个类项修改,也可以针对所有类项。
  2. setlocale 的第一个参数可以是前面说明的类项中的一个,那么每次只会影响一个类项,如果第一个参数是LC_ALL,就会影响所有的类项。

C标准给第⼆个参数仅定义了2种可能取值:"C"和" "。在任意程序执⾏开始,都会隐藏式执⾏调⽤:

setlocale(LC_ALL, "C");

当地区设置为"C"时,库函数按正常方式执行,小数点是一个点。
当程序运行起来后想改变地区,就只能显⽰调⽤setlocale函数。用" "作为第2个参数,调setlocale
函数就可以切换到本地模式,这种模式下程序会适应本地环境。比如:切换到我们的本地模式后就支持宽字符(汉字)的输出等。
setlocale (LC_ALL, " " ); // 切换到本地环境
我们发现⼀个普通字符占⼀个字符的位置但是打印⼀个汉字字符,占⽤2个字符的位置,那么我们如果要在贪吃蛇中使⽤宽字符,就得处理好地图上坐标的计算。
地图坐标
        我们假设实现一个棋盘27行,58列的棋盘(行和列可以根据自己的情况修改),再围绕地图画出墙, 如下:
手把手教你实现贪吃蛇_第13张图片

蛇身和食物

        初始化状态,假设蛇的长度是5,蛇身的每个节点是●,在固定的一个坐标处,比如(24, 5)处开始出现蛇,连续5个节点。 注意:蛇的每个节点的x坐标必须是2个倍数,否则可能会出现蛇的⼀个节点有一半儿出现在墙体中,另外⼀般在墙外的现象,坐标不好对齐。 关于食物,就是在墙体内随机生成⼀个坐标(x坐标必须是2的倍数),坐标不能和蛇的身体重合,然后打印★。
手把手教你实现贪吃蛇_第14张图片

游戏流程设计

手把手教你实现贪吃蛇_第15张图片

核心逻辑实现分析(重点)

        咱们从三个方面来讲解贪吃蛇的核心逻辑分析:test.c主函数,snake.h包含头文件,snake.c主函数的实现,跟我们玩链表差不多,都是老套路了那贪吃蛇的实现正式上路咯,启程。

手把手教你实现贪吃蛇_第16张图片

手把手教你实现贪吃蛇_第17张图片

snake.h头文件的实现

        像头文件这个东西必然是要包含一些头文件的,当然这只是它的一部分作用,像我们定义的链表,为了在各个源文件都能使用,我们就在头文件实现链表。如果函数很多,就把函数头包函数在头文件中,方便调用,那咱们看看在snake.h中有啥?


手把手教你实现贪吃蛇_第18张图片


//包含头文件
#include 
#include
#include
#include
#include
#include
#include

//定义图案
#define WALL L'□'
#define BODY L'●'
#define FOOD L'★'

//初始化蛇的位置
#define POS_X 24
#define POS_Y 5

//键位的移动
#define KEY_PRESS(VK)  ( (GetAsyncKeyState(VK) & 0x1) ? 1 : 0 )

//枚举键位
enum  DIRECTION
{
	UP = 1,//上
	DOWN,//下
	LEFT,//左
	RIGHT//右
};

//枚举游戏状态
enum GAME_STATUS
{
	OK,//正常运行
	END_NORMAL,//按ESC退出
	KILL_BY_WALL,
	KILL_BY_SELF
};

//贪吃蛇每个节点的描述
typedef struct SnakeNode
{
	int x;
	int y;
	struct SnakeNode* next;//下个节点
}SnakeNode, * pSnakeNode;

//贪吃蛇组成
typedef struct Snake
{
	pSnakeNode _pSnake;//指向贪吃蛇头结点的指针
	pSnakeNode _pFood;//指向食物结点的指针
	int _Score;//贪吃蛇累计的总分
	int _FoodWeight;//一个食物的分数
	int _SleepTime;//每走一步休息的时间,时间越短,速度越快,时间越长,速度越慢
	enum DIRECTION _Dir;//描述蛇的方向
	enum GAME_STATUS _Status;//游戏的状态:正常、退出、撞墙、吃到自己
}Snake, * pSnake;



//游戏开始 - 完成游戏的初始化动作
void GameStart(pSnake ps);

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

//游戏开始的欢迎界面
void WelComeToGame();

//打印地图
void CreateMap();

//初始化贪吃蛇
void InitSnake(pSnake ps);

//创建食物
void CreateFood(pSnake ps);

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

//打印帮助信息
void PrintHelpInfo();


//游戏暂定和恢复
void Pause();

//蛇的移动
void SnakeMove(pSnake ps);

//判断蛇头到达的坐标处是否是食物
int NextIsFood(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);

snake.c源文件的实现

        在这个文件中咱们实现贪吃蛇的函数:实现时当然需要包含snake.h的文件啦,需要用头文件里面的链表等.....

游戏开始 - 初始化游戏

手把手教你实现贪吃蛇_第19张图片


//1. 游戏开始 - 初始化游戏
void GameStart(pSnake ps)
{
	//设置控制台的大小
	system("mode con cols=100 lines=30");
	system("title 贪吃蛇");

	//光标影藏掉
	HANDLE hOutput = GetStdHandle(STD_OUTPUT_HANDLE);
	//影藏光标操作
	CONSOLE_CURSOR_INFO CursorInfo;
	GetConsoleCursorInfo(hOutput, &CursorInfo);//获取控制台光标信息
	CursorInfo.bVisible = false; //隐藏控制台光标
	SetConsoleCursorInfo(hOutput, &CursorInfo);//设置控制台光标状态

	//打印欢迎界面
	WelComeToGame();
	//创建地图
	CreateMap();
	//初始化贪食蛇
	InitSnake(ps);
	//创建食物
	CreateFood(ps);
}
设置光标的坐标

        光标的移动需要多次实现,为了我们方便使用移动光标位置,所以我们直接实现这个函数。不会有人跟我说光标是啥吧,看我打不死你,看到没:

手把手教你实现贪吃蛇_第20张图片

设置光标位置在win32 API已经介绍咯。

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

其实这个函数大家可以自己设置,把欢迎界面整的好看些,像博主就比较懒就简单设计一下咯。

手把手教你实现贪吃蛇_第21张图片

手把手教你实现贪吃蛇_第22张图片

//打印欢迎界面
void WelComeToGame()
{
	//定位光标
	SetPos(40, 14);
	printf("欢迎来到贪吃蛇小游戏");
	SetPos(40, 25);
	system("pause");//pause是暂停
	system("cls");
	SetPos(20, 14);
	printf("使用 ↑ . ↓ . ← . → . 分别控制蛇的移动, F3是加速,F4是减速");
	SetPos(40, 25);
	system("pause");
	system("cls");
}
创建地图/初始化贪食蛇

大家眼睛擦亮点,计算坐标时不要计算错了,可以根据自己设计自己喜欢的图:

手把手教你实现贪吃蛇_第23张图片

        切记一定要把蛇初始状态需要是偶数倍:初始化状态,假设蛇的长度是5,蛇身的每个节点是●,在固定的一个坐标处,比如(24, 5)处开始出现蛇,连续5个节点。注意:蛇的每个节点的x坐标必须是2个倍数,否则可能会出现蛇的⼀个节点有一半儿出现在墙体中,另外⼀般在墙外的现象,坐标不好对齐。

//创建地图
void CreateMap()
{
	//上
	SetPos(0, 0);
	int i = 0;
	for (i = 0; i <= 56; i += 2)
	{
		wprintf(L"%lc", WALL);
	}
	//下
	SetPos(0, 26);
	for (i = 0; i <= 56; i += 2)
	{
		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);
	}
}

//初始化贪食蛇
void InitSnake(pSnake ps)
{
	//初始化蛇身 0 0 0 0 0 
	pSnakeNode cur = NULL;
	//循环5个节点
	
	for (int i = 0; i < 5; i++)
	{
		//开辟空间
		cur = (pSnakeNode)malloc(sizeof(SnakeNode));
		//判断是否开辟成功
		if (cur == NULL)
		{
			perror("InitSnake:malloc");
			exit(-1);
		}

		//初始化蛇的位置在(24,5)
		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;
	}

	//对贪吃蛇的每个组成都初始化
	ps->_Status = OK;//游戏的状态
	ps->_Score = 0;//贪吃蛇累计的总分
	ps->_pFood = NULL;//指向食物结点的指针
	ps->_SleepTime = 200;//每走一步休息的时间
	ps->_FoodWeight = 10;//一个食物的分数
	//(我们默认蛇开始向右走)
	ps->_Dir = RIGHT;//描述蛇的方向
}
创建食物

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

在创建食物时我们需要注意一些事项:小本本拿好咯

  • 食物可能与创建的蛇相同:就必须从新创建食物。(这里采用again)
  • 食物需要开辟空间的:方便和蛇链接嘛,毕竟是单链表
  • 食物的一些初始化
//创建食物
void CreateFood(pSnake ps)
{
	//初始化
	int x = 0;
	int y = 0;
again:
	do
	{
		x = rand() % 53 + 2;
		y = rand() % 25 + 1;
	} while (x % 2 != 0);//x坐标必须是2的倍数

	//坐标不能和蛇的身体冲突
	pSnakeNode cur = ps->_pSnake;
	while (cur)
	{
		//比较坐标
		if (cur->x == x && cur->y == y)
		{
			goto again;
		}
		cur = cur->next;
	}

	//给食物开辟空间
	pSnakeNode pFood = (pSnakeNode)malloc(sizeof(SnakeNode));
	//判断
	if (pFood == NULL)
	{
		perror("CreateFood()::malloc()");
		return;
	}

	//初始化
	pFood->x = x;
	pFood->y = y;
	ps->_pFood = pFood;

	//打印食物
	SetPos(x, y);
	wprintf(L"%lc", FOOD);

}
游戏运行 - 游戏的正常运行过程

手把手教你实现贪吃蛇_第24张图片


//2. 游戏运行 - 游戏的正常运行过程
void GameRun(pSnake ps)
{
	//打印帮助信息
	PrintHelpInfo();

	//循环方向,直到方向正确
	do
	{
		//打印得分
		SetPos(64, 10);
		printf("得分:%05d", ps->_Score);
		SetPos(64, 11);
		printf("每个食物的分数:%2d", ps->_FoodWeight);

		//判断蛇方向,
		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_ESCAPE))
		{
			ps->_Status = END_NORMAL;
			break;
		}
		else if (KEY_PRESS(VK_SPACE))
		{
			//暂停蛇移动
			Pause();
		}
		else if (KEY_PRESS(VK_F3))//加速
		{
			if (ps->_SleepTime >= 80)
			{
				ps->_SleepTime -= 30;
				ps->_FoodWeight += 2;
			}
		}
		else if (KEY_PRESS(VK_F4))//减速
		{
			if (ps->_SleepTime < 320)
			{
				ps->_SleepTime += 30;
				ps->_FoodWeight -= 2;
			}
		}

		//蛇动的时间
		Sleep(ps->_SleepTime);

		//蛇的移动
		SnakeMove(ps);
	} while (ps->_Status == OK);
}
打印帮助信息

这个函数根据自己的需要而写,这个就是看大家的创意咯

手把手教你实现贪吃蛇_第25张图片

//打印帮助信息
void PrintHelpInfo()
{
	SetPos(64, 15);
	printf("1.不能撞墙,不能咬到自己");
	SetPos(64, 16);
	printf("2.使用 ↑.↓.←.→ 分别控制蛇的移动");
	SetPos(64, 17);
	printf("3.F3加速,F4减速");
	SetPos(64, 18);
	printf("4.ESC-退出, 空格-暂停游戏");

	SetPos(64, 20);
	printf("旧言专用版权");

}
判断蛇头到达的坐标处是否是食物

这个函数其实很挫,判断一下x和y坐标就行。

//判断蛇头到达的坐标处是否是食物
int NextIsFood(pSnake ps, pSnakeNode pnext)
{
	if (ps->_pFood->x == pnext->x && ps->_pFood->y == pnext->y)
	{
		return 1;
	}
	else
	{
		return 0;
	}
}
吃掉食物

直接就是头插法:不会的打板子

吃完食物记得创建食物

//吃掉食物
void EatFood(pSnake ps, pSnakeNode pnext)
{
	//头插(把食物当做蛇头)
	pnext->next = ps->_pSnake;
	ps->_pSnake = pnext;

	//打印蛇
	pSnakeNode cur = ps->_pSnake;
	while (cur)
	{
		//找坐标
		SetPos(cur->x, cur->y);
		//打印
		wprintf(L"%lc", BODY);
		//找下一个节点
		cur = cur->next;
	}

	//释放食物空间
	free(ps->_pFood);
	
	//加分数
	ps->_Score += ps->_FoodWeight;

	//创建食物
	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;
	}

	//本来的尾的图案需要覆盖成空格
	SetPos(cur->next->x, cur->next->y);
	printf("  ");
	
	//释放空间
	free(cur->next);
	cur->next = NULL;
}
蛇是否撞墙

这个函数还是比较简单的,判断墙体的坐标是否和蛇移动到的坐标相同

//蛇是否自杀
void KillBySelf(pSnake ps)
{
	//遍历一遍蛇的坐标就行
	pSnakeNode cur = ps->_pSnake->next;
	while (cur)
	{
		if (ps->_pSnake->x == cur->x && ps->_pSnake->y == cur->y)
		{
			//游戏状态为自杀
			ps->_Status = KILL_BY_SELF;
		}
		//找下一个节点
		cur = cur->next;
	}
}
蛇的移动

蛇既然要移动到下一个坐标,所以需要开辟下一个位置的空间,再判断蛇是否到食物。

等到蛇移动到下一个坐标再判断蛇是否撞墙自杀。

//蛇的移动
void SnakeMove(pSnake ps)
{
	//开辟蛇移动到下一个格子的空间
	pSnakeNode pNext = (pSnakeNode)malloc(sizeof(SnakeNode));
	//判断
	if (pNext == NULL)
	{
		perror("SnakeMove()::malloc()");
		exit(-1);
	}
	pNext->next = NULL;

	//蛇移动方向
	switch (ps->_Dir)
	{
	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;
	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;
	}

	//判断蛇头到达的坐标处是否是食物
	if (NextIsFood(ps, pNext))
	{
		//吃掉食物
		EatFood(ps, pNext);
	}
	else
	{
		//不吃食物
		NoFood(ps, pNext);
	}

	//蛇是否撞墙
	KillByWall(ps);

	//蛇是否自杀
	KillBySelf(ps);

}
暂停蛇移动

让蛇可以有视觉效果,打印时,休息一会儿。

//暂停蛇移动
void Pause()
{
	while (1)
	{
		Sleep(100);
		if (KEY_PRESS(VK_SPACE))
		{
			break;
		}
	}
}
 游戏结束 - 游戏善后(释放资源)

等于释放内存,这个表示我们熟的来:

//3. 游戏结束 - 游戏善后(释放资源)
void GameEnd(pSnake ps)
{
	//判断游戏状态
	SetPos(20, 12);
	switch (ps->_Status)
	{
	case END_NORMAL:
		printf("您主动退出游戏\n");
		break;
	case KILL_BY_SELF:
		printf("自杀了,游戏结束\n");
		break;
	case KILL_BY_WALL:
		printf("撞墙了,游戏结束\n");
		break;
	}
	//释放蛇身的结点
	pSnakeNode cur = ps->_pSnake;
	while (cur)
	{
		pSnakeNode del = cur;
		cur = cur->next;
		free(del);
	}
	ps->_pSnake = NULL;

}

test.c源文件的实现

直接上代码好叭

//包含头文件
#include"snake.h"

void test()
{
	int ch = 0;
	do
	{
		Snake snake = { 0 };//创建了贪吃蛇
		//1. 游戏开始 - 初始化游戏
		GameStart(&snake);
		//2. 游戏运行 - 游戏的正常运行过程
		GameRun(&snake);
		//3. 游戏结束 - 游戏善后(释放资源)
		GameEnd(&snake);
		SetPos(20, 18);
		printf("再来一局吗?(Y/N):");
		ch = getchar();
		getchar();// 清理掉\n
	} while (ch == 'Y' || ch == 'y');
	SetPos(0, 27);
}

int main()
{
	//设置程序适应本地环境
	setlocale(LC_ALL, "");//设置语言环境
	srand((unsigned int)time(NULL));//游戏睡眠状态


	//调用函数
	test();
	return 0;
}

总代码

snake.c
#define _CRT_SECURE_NO_WARNINGS 1

//包含头文件
#include"snake.h"

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

//打印欢迎界面
void WelComeToGame()
{
	//定位光标
	SetPos(40, 14);
	printf("欢迎来到贪吃蛇小游戏");
	SetPos(40, 25);
	system("pause");//pause是暂停
	system("cls");
	SetPos(20, 14);
	printf("使用 ↑ . ↓ . ← . → . 分别控制蛇的移动, F3是加速,F4是减速");
	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);
	}
	//下
	SetPos(0, 26);
	for (i = 0; i <= 56; i += 2)
	{
		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);
	}
}

//初始化贪食蛇
void InitSnake(pSnake ps)
{
	//初始化蛇身 0 0 0 0 0 
	pSnakeNode cur = NULL;
	//循环5个节点
	
	for (int i = 0; i < 5; i++)
	{
		//开辟空间
		cur = (pSnakeNode)malloc(sizeof(SnakeNode));
		//判断是否开辟成功
		if (cur == NULL)
		{
			perror("InitSnake:malloc");
			exit(-1);
		}

		//初始化蛇的位置在(24,5)
		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;
	}

	//对贪吃蛇的每个组成都初始化
	ps->_Status = OK;//游戏的状态
	ps->_Score = 0;//贪吃蛇累计的总分
	ps->_pFood = NULL;//指向食物结点的指针
	ps->_SleepTime = 200;//每走一步休息的时间
	ps->_FoodWeight = 10;//一个食物的分数
	//(我们默认蛇开始向右走)
	ps->_Dir = RIGHT;//描述蛇的方向
}

//创建食物
void CreateFood(pSnake ps)
{
	//初始化
	int x = 0;
	int y = 0;
again:
	do
	{
		x = rand() % 53 + 2;
		y = rand() % 25 + 1;
	} while (x % 2 != 0);//x坐标必须是2的倍数

	//坐标不能和蛇的身体冲突
	pSnakeNode cur = ps->_pSnake;
	while (cur)
	{
		//比较坐标
		if (cur->x == x && cur->y == y)
		{
			goto again;
		}
		cur = cur->next;
	}

	//给食物开辟空间
	pSnakeNode pFood = (pSnakeNode)malloc(sizeof(SnakeNode));
	//判断
	if (pFood == NULL)
	{
		perror("CreateFood()::malloc()");
		return;
	}

	//初始化
	pFood->x = x;
	pFood->y = y;
	ps->_pFood = pFood;

	//打印食物
	SetPos(x, y);
	wprintf(L"%lc", FOOD);

}

//1. 游戏开始 - 初始化游戏
void GameStart(pSnake ps)
{
	//设置控制台的大小
	system("mode con cols=100 lines=30");
	system("title 贪吃蛇");

	//光标影藏掉
	HANDLE hOutput = GetStdHandle(STD_OUTPUT_HANDLE);
	//影藏光标操作
	CONSOLE_CURSOR_INFO CursorInfo;
	GetConsoleCursorInfo(hOutput, &CursorInfo);//获取控制台光标信息
	CursorInfo.bVisible = false; //隐藏控制台光标
	SetConsoleCursorInfo(hOutput, &CursorInfo);//设置控制台光标状态

	//打印欢迎界面
	WelComeToGame();
	//创建地图
	CreateMap();
	//初始化贪食蛇
	InitSnake(ps);
	//创建食物
	CreateFood(ps);
}

//打印帮助信息
void PrintHelpInfo()
{
	SetPos(64, 15);
	printf("1.不能撞墙,不能咬到自己");
	SetPos(64, 16);
	printf("2.使用 ↑.↓.←.→ 分别控制蛇的移动");
	SetPos(64, 17);
	printf("3.F3加速,F4减速");
	SetPos(64, 18);
	printf("4.ESC-退出, 空格-暂停游戏");

	SetPos(64, 20);
	printf("旧言专用版权");
	//getchar();
}

//判断蛇头到达的坐标处是否是食物
int NextIsFood(pSnake ps, pSnakeNode pnext)
{
	if (ps->_pFood->x == pnext->x && ps->_pFood->y == pnext->y)
	{
		return 1;
	}
	else
	{
		return 0;
	}
}

//吃掉食物
void EatFood(pSnake ps, pSnakeNode pnext)
{
	//头插(把食物当做蛇头)
	pnext->next = ps->_pSnake;
	ps->_pSnake = pnext;

	//打印蛇
	pSnakeNode cur = ps->_pSnake;
	while (cur)
	{
		//找坐标
		SetPos(cur->x, cur->y);
		//打印
		wprintf(L"%lc", BODY);
		//找下一个节点
		cur = cur->next;
	}

	//释放食物空间
	free(ps->_pFood);
	
	//加分数
	ps->_Score += ps->_FoodWeight;

	//创建食物
	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;
	}

	//本来的尾的图案需要覆盖成空格
	SetPos(cur->next->x, cur->next->y);
	printf("  ");
	
	//释放空间
	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 (ps->_pSnake->x == cur->x && ps->_pSnake->y == cur->y)
		{
			//游戏状态为自杀
			ps->_Status = KILL_BY_SELF;
		}
		//找下一个节点
		cur = cur->next;
	}
}

//蛇的移动
void SnakeMove(pSnake ps)
{
	//开辟蛇移动到下一个格子的空间
	pSnakeNode pNext = (pSnakeNode)malloc(sizeof(SnakeNode));
	//判断
	if (pNext == NULL)
	{
		perror("SnakeMove()::malloc()");
		exit(-1);
	}
	pNext->next = NULL;

	//蛇移动方向
	switch (ps->_Dir)
	{
	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;
	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;
	}

	//判断蛇头到达的坐标处是否是食物
	if (NextIsFood(ps, pNext))
	{
		//吃掉食物
		EatFood(ps, pNext);
	}
	else
	{
		//不吃食物
		NoFood(ps, pNext);
	}

	//蛇是否撞墙
	KillByWall(ps);

	//蛇是否自杀
	KillBySelf(ps);

}

//暂停蛇移动
void Pause()
{
	while (1)
	{
		Sleep(100);
		if (KEY_PRESS(VK_SPACE))
		{
			break;
		}
	}
}

//2. 游戏运行 - 游戏的正常运行过程
void GameRun(pSnake ps)
{
	//打印帮助信息
	PrintHelpInfo();

	//循环方向,直到方向正确
	do
	{
		//打印得分
		SetPos(64, 10);
		printf("得分:%05d", ps->_Score);
		SetPos(64, 11);
		printf("每个食物的分数:%2d", ps->_FoodWeight);

		//判断蛇方向,
		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_ESCAPE))
		{
			ps->_Status = END_NORMAL;
			break;
		}
		else if (KEY_PRESS(VK_SPACE))
		{
			//暂停蛇移动
			Pause();
		}
		else if (KEY_PRESS(VK_F3))//加速
		{
			if (ps->_SleepTime >= 80)
			{
				ps->_SleepTime -= 30;
				ps->_FoodWeight += 2;
			}
		}
		else if (KEY_PRESS(VK_F4))//减速
		{
			if (ps->_SleepTime < 320)
			{
				ps->_SleepTime += 30;
				ps->_FoodWeight -= 2;
			}
		}

		//蛇动的时间
		Sleep(ps->_SleepTime);

		//蛇的移动
		SnakeMove(ps);
	} while (ps->_Status == OK);
}

//3. 游戏结束 - 游戏善后(释放资源)
void GameEnd(pSnake ps)
{
	//判断游戏状态
	SetPos(20, 12);
	switch (ps->_Status)
	{
	case END_NORMAL:
		printf("您主动退出游戏\n");
		break;
	case KILL_BY_SELF:
		printf("自杀了,游戏结束\n");
		break;
	case KILL_BY_WALL:
		printf("撞墙了,游戏结束\n");
		break;
	}
	//释放蛇身的结点
	pSnakeNode cur = ps->_pSnake;
	while (cur)
	{
		pSnakeNode del = cur;
		cur = cur->next;
		free(del);
	}
	ps->_pSnake = NULL;

}
snake.h
#pragma once

//包含头文件
#include 
#include
#include
#include
#include
#include
#include

//定义图案
#define WALL L'□'
#define BODY L'●'
#define FOOD L'★'

//初始化蛇的位置
#define POS_X 24
#define POS_Y 5

//键位的移动
#define KEY_PRESS(VK)  ( (GetAsyncKeyState(VK) & 0x1) ? 1 : 0 )

//枚举键位
enum  DIRECTION
{
	UP = 1,//上
	DOWN,//下
	LEFT,//左
	RIGHT//右
};

//枚举游戏状态
enum GAME_STATUS
{
	OK,//正常运行
	END_NORMAL,//按ESC退出
	KILL_BY_WALL,
	KILL_BY_SELF
};

//贪吃蛇每个节点的描述
typedef struct SnakeNode
{
	int x;
	int y;
	struct SnakeNode* next;//下个节点
}SnakeNode, * pSnakeNode;

//贪吃蛇组成
typedef struct Snake
{
	pSnakeNode _pSnake;//指向贪吃蛇头结点的指针
	pSnakeNode _pFood;//指向食物结点的指针
	int _Score;//贪吃蛇累计的总分
	int _FoodWeight;//一个食物的分数
	int _SleepTime;//每走一步休息的时间,时间越短,速度越快,时间越长,速度越慢
	enum DIRECTION _Dir;//描述蛇的方向
	enum GAME_STATUS _Status;//游戏的状态:正常、退出、撞墙、吃到自己
}Snake, * pSnake;



//游戏开始 - 完成游戏的初始化动作
void GameStart(pSnake ps);

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

//游戏开始的欢迎界面
void WelComeToGame();

//打印地图
void CreateMap();

//初始化贪吃蛇
void InitSnake(pSnake ps);

//创建食物
void CreateFood(pSnake ps);

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

//打印帮助信息
void PrintHelpInfo();


//游戏暂定和恢复
void Pause();

//蛇的移动
void SnakeMove(pSnake ps);

//判断蛇头到达的坐标处是否是食物
int NextIsFood(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);
test.c
#define _CRT_SECURE_NO_WARNINGS 1

//包含头文件
#include"snake.h"

void test()
{
	int ch = 0;
	do
	{
		Snake snake = { 0 };//创建了贪吃蛇
		//1. 游戏开始 - 初始化游戏
		GameStart(&snake);
		//2. 游戏运行 - 游戏的正常运行过程
		GameRun(&snake);
		//3. 游戏结束 - 游戏善后(释放资源)
		GameEnd(&snake);
		SetPos(20, 18);
		printf("再来一局吗?(Y/N):");
		ch = getchar();
		getchar();// 清理掉\n
	} while (ch == 'Y' || ch == 'y');
	SetPos(0, 27);
}

int main()
{
	//设置程序适应本地环境
	setlocale(LC_ALL, "");//设置语言环境
	srand((unsigned int)time(NULL));//游戏睡眠状态


	//调用函数
	test();
	return 0;
}

结束语

       今天内容就到这里啦,时间过得很快,大家沉下心来好好学习,会有一定的收获的,大家多多坚持,嘻嘻,成功路上注定孤独,因为坚持的人不多。那请大家举起自己的小说手给博主一键三连,有你们的支持是我最大的动力,回见。

手把手教你实现贪吃蛇_第26张图片

你可能感兴趣的:(c语言)