爆肝整理!C 语言贪吃蛇从框架到落地全解析,含加速 / 暂停 / 计分,直接抄作业

欢迎来到 s a y − f a l l 的文章 欢迎来到say-fall的文章 欢迎来到sayfall的文章

在这里插入图片描述


🌈这里是say-fall分享,感兴趣欢迎三连与评论区留言
🔥专栏: 《C语言从零开始到精通》 《C语言编程实战》 《数据结构与算法》 《小游戏与项目》
💪格言:做好你自己,你才能吸引更多人,并与他们共赢,这才是你最好的成长方式。

前言:

本篇博客为大家带来贪吃蛇小游戏的实现思路和分模块化代码,保证大家看完本篇文章以后绝对能自己复刻出来,如果对贪吃蛇小游戏的代码感兴趣欢迎阅读本篇博客~



贪吃蛇源代码:


正文:

一. 实现思路

1. 实现思路

对于贪吃蛇这个小游戏,我们计划先给玩的人一个欢迎界面,然后告诉他们游戏规则,然后正式开始游戏

2. 框架搭建与前置结构

既然如此,我们就先搭建一个框架,主要是,用do-while循环来保证玩家能够选择是否游戏

//测试游戏
void gametest()
{
	char ch = 0;
	setlocale(LC_ALL, "");
	srand((unsigned int)time(NULL));
	do
	{
		Snake snake = { 0 };
		system("cls");
		//游戏准备阶段
		GameStart(&snake);
		//游戏运行阶段
		GameRun(&snake);
		//游戏结束阶段 - 回收资源
		GameEnd(&snake);
		Pos(23, 16);
		printf("再来一局嘛?(Y/N):");
		ch = getchar();
		getchar();
	} while (ch == 'Y' || ch == 'y');
	Pos(0,27);
}


int main()
{
	gametest();
	return 0;
}

而这三个函数传入的参数snake是什么东西呢?我们想要构建贪吃蛇游戏,那就必须要让这个贪吃蛇有规则,而规则是什么呢?

在这里我们假设规则如下:

  • 规则:
    贪吃蛇在一片空间内移动,不能穿墙也不能撞到自己,否则游戏结束;移动过程中空间内会有食物,食物被吃掉以后蛇身变长一截,并且下一个食物随机刷新在任意位置;玩家可以使用 ↑ . ↓ . ← . → 键来控制蛇头的移动方向,按F3键可以加速,按F4键可以减速,按下Space键可以暂停游戏,按下Esc键可以退出游戏;吃到食物可以得分,按下F3键加速可以得到的更高分数

这样子的话我们就能从规则中提取出来一些有用的数据“:蛇的身体、食物、蛇头的方向(上下左右)、游戏状态(正常游戏、撞墙、撞到自己、暂停)、食物的分数、游戏总得分和蛇移动的速度。

我们先来处理一下蛇的身体和食物
蛇的身体有两个关键的要素:坐标如何连接。考虑到连接的问题,这里我们采用链表的方式来构建蛇的身体,而食物只有一个要素就是坐标,我们把食物看作是这个坐标的一种,于是:

//蛇和食物的结构体
typedef struct snakenode 
{
	int x;//节点横坐标
	int y;//节点纵坐标
	struct snakenode* next;//下一个节点的指针
}SnakeNode,* pSnakeNode;

那么接下来我们解决蛇头方向和游戏状态的数据,这两个游戏数据简单来说就是不同的状态嘛,我们采用枚举的方式来处理他们:

enum DIRECTION//方向
{
	up = 1,//上
	down,//下
	left,//左
	right//右
};
enum GAME_STATUS//游戏状态
{
	NORMAL = 1,//存活状态
	KILL_BY_WALL,//撞墙死亡
	KILL_BY_SELF,//撞到自己死亡
	END_NORMAL//Esc正常退出
};

接下来的食物的分数、游戏总得分和蛇移动的速度我们可以看作是三种int类型变量

//贪吃蛇游戏的结构体
typedef struct snake
{
	SnakeNode* _pSnake; //代表蛇头的指针
	SnakeNode* _pFood;   //代表食物的指针
	enum DIRECTION _Dir;//蛇头的方向
	enum GAME_STATUS _Status;//游戏状态
	int _FoodWeight;//食物的分数
	int _Socre;//游戏总得分
	int _SleepTime;//速度 (游戏的休眠时间)
}Snake;

于是我们写一个结构体变量来储存这些数据

二. 前期准备

1. 界面与地图构建

数据都准备好以后,我们要初始化一个好看的界面,这里我们用一些WinAPI函数的知识来处理,不了解WinAPI函数的可以跳转到《WinAPI极简教程》去熟悉一下

//0.界面的处理和光标的隐藏
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);

关于以上代码的由来在《WinAPI极简教程》已经解释过了,这里就不多做解释

1.1 欢迎界面

下面我们开始处理欢迎界面:

我们计划在进入游戏的第一个界面居中打印“欢迎进入贪吃蛇游戏”
那么如何居中打印呢?我们在《WinAPI极简教程》中介绍过可通过函数来调整光标的位置,凭借这个就可以实现居中打印

void Pos(int x, int y)
{
	//获得标准输出设备的句柄
	HANDLE houtput = GetStdHandle(STD_OUTPUT_HANDLE);
	//创建位置pos并修改
	COORD pos = { x,y };
	//设置光标位置
	SetConsoleCursorPosition(houtput, pos);
}

我们来写一下这个欢迎界面的函数

//欢迎界面
void WelcomToGame();
{
	Pos(37, 14);
	printf("欢迎来到贪吃蛇小游戏");
	Pos(39, 28);
	system("pause");
	system("cls");
	Pos(17, 10);
	printf("贪吃蛇在一片空间内移动,不能穿墙也不能撞到自己,否则游戏结束");
	Pos(6, 11);
	printf("移动过程中空间内会有食物,食物被吃掉以后蛇身变长一截 ,并且下一个食物随机刷新在任意位置");
	Pos(7, 12);
	printf(" 玩家可以使用 ↑ . ↓.← . → 键来控制蛇头的移动方向,按F3键可以加速,按F4键可以减速");
	Pos(23, 13);
	printf("按下Space键可以暂停游戏,按下Esc键可以退出游戏");
	Pos(22, 14);
	printf("吃到食物可以得分,按下F3键加速可以得到的更高分数");
	Pos(39, 28);
	system("pause");
	system("cls");
}
  • 效果:
    在这里插入图片描述
    在这里插入图片描述
1.2 地图界面
//地图界面
//地图是一个行数27,列数58(格子数为29)的空间
//蛇实际走的是行数1->25,列数2->54(格子数:1->27)的空间
void CreatMap()
{
	int i = 0;
	//上
	Pos(0, 0);
	for (i = 0; i < 58; i+=2)
	{
		wprintf(WALL);
	}
	//下
	Pos(0, 26);
	for (i = 0; i < 58; i+=2)
	{
		wprintf(WALL);
	}
	//左
	for (i = 1;i <= 25; i++)
	{
		Pos(0, i);
		wprintf(WALL);
	}
	//右
	for (i = 1;i <= 25; i++)
	{
		Pos(56, i);
		wprintf(WALL);
	}
}

总体的原理和欢迎界面的原理相同,注意这里的WALL是一个宏表示:#define WALL L"□"

  • 注意:
    wprintf:处理宽字符,使用wchar_t类型,每个汉字占 2 个字节
  • 效果:
    在这里插入图片描述

2. 蛇的初始化

我们之前已经定义了结构体变量SnakeNode和游戏数据结构体变量Snake,我们通过他们两个结构体来调整整个游戏的数据,下面我们来看初始化函数以及语句作用

//初始化游戏数据
void SnakeInit(pSnake ps)
{
	//蛇的身体数据
	for (int i = 0;i < 5;i++)//利用循环来构建蛇身
	{
		//定义一个pSnakeNode类型指针来放节点数据
		pSnakeNode pcur = (pSnakeNode)malloc(sizeof(SnakeNode));
		if (pcur == NULL)//检查是否动态申请内存成功
		{
			perror("SnakeInit()::malloc()");
			return;
		}
		pcur->next = NULL;//初始化下一个连接的指针为空
		//链表的填充与连接
		pcur->x = Pos_x + 2 * i;
		pcur->y = Pos_y ;
		//链表为空时
		if (pcur == NULL)//空链表就直接让_pSnake成为蛇头
		{
			ps->_pSnake = pcur;
		}
		else
		{
			//非空链表先让新的节点指向蛇头
			pcur->next = ps->_pSnake;
			//再让新节点成为蛇头
			ps->_pSnake = pcur;
		}
		//蛇身的打印
		pcur = ps->_pSnake;
		Pos(pcur->x, pcur->y);
		wprintf(L"●");
		pcur = pcur->next;
	}
	//初始化其他数据
	ps->_Dir = RIGHT;
	ps->_FoodWeight = 10;
	ps->_SleepTime = 200;
	ps->_Socre = 0;
	ps->_Status = NORMAL;
}
  • 效果:
    在这里插入图片描述

3. 食物的初始化

食物的初始化其实和蛇的初始化是很类似的,只要保证创建的食物位置是随机的并且不与蛇身重叠就好了

//初始化食物数据
void CreateFood(pSnake ps)
{
	int x = 0;
	int y = 0;
	//生成x是2的倍数,并且在地图里面
again:
	do
	{
		x = 2 + rand() % 53;
		y = 1 + rand() % 25;
	} while (x % 2 != 0);//随机生成食物并且只有是2的倍数才成功
	pSnakeNode pcur = ps->_pSnake;
	while (pcur)
	{
		//如果和蛇身重叠,重新生成
		if (pcur->x == x && pcur->y == y)
		{
			goto again;
		}
		pcur = pcur->next;
	}
	//创建食物节点
	pSnakeNode pFood = (pSnakeNode)malloc(sizeof(SnakeNode));
	pFood->x = x;
	pFood->y = y;
	pFood->next = NULL;
	ps->_pFood = pFood;
	//食物的打印
	Pos(x, y);
	wprintf(FOOD);
}
  • 演示:
    在这里插入图片描述

二. 游戏的运行

在游戏运行以前,我们在侧面打印我们的得分情况和游戏规则帮助玩家开始游戏

然后我们检测按键被按下的情况:

  • 如果不是我们蛇头的反方向,修改他的方向
  • 按下space就暂停游戏
  • 按下Esc键就退出游戏
  • 按下F3键为食物加分并加速
  • 按下F4键为食物减分并减速
//游戏运行阶段
void GameRun(pSnake ps)
{
	do
	{
		//记录得分情况
		Pos(60, 6);
		printf("当前得分:  %d", ps->_Socre);
		Pos(60, 7);
		printf("食物分数:  %d", ps->_FoodWeight);
		//规则界面
		Pos(60, 12);
		printf("使用↑ . ↓.← . → 键来控制移动方向");
		Pos(60, 13);
		printf("按F3键可以加速,按F4键可以减速");
		Pos(60, 14);
		printf("吃到食物可以得分,加速可以得更高分");
		Pos(60, 15);
		printf("按space键暂停/开始游戏,Esc键退出游戏");
		Pos(68, 20);
		printf("say-fall分享");
		//检测按键被是否按下
		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_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;
			}
		}
		else if (KEY_PRESS(VK_ESCAPE))
		{
			ps->_Status = END_NORMAL;
		}
		//蛇的移动和检测
		SnakeMove(ps);
		Sleep(ps->_SleepTime);
	} while (ps->_Status == NORMAL);
}

1. 打印得分与规则

  • 代码块:
//记录得分情况
		Pos(60, 6);
		printf("当前得分:  %d", ps->_Socre);
		Pos(60, 7);
		printf("食物分数:  %d", ps->_FoodWeight);
		//规则界面
		Pos(60, 12);
		printf("使用↑ . ↓.← . → 键来控制移动方向");
		Pos(60, 13);
		printf("按F3键可以加速,按F4键可以减速");
		Pos(60, 14);
		printf("吃到食物可以得分,加速可以得更高分");
		Pos(60, 15);
		printf("按space键暂停/开始游戏,Esc键退出游戏");
		Pos(68, 20);
		printf("say-fall分享");
  • 效果:
    在这里插入图片描述

2. 按键的检测

我们这里检测被按下的最后一个键位,然后做出相应的改变

  • 代码块:
//检测按键被是否按下
		//方向键:如果不为按键的反方向按键就改变方向
		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();
		}
		//按下 F3/F4键 分别 加速/减速
		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;
			}
		}
		//按下Esc就更换游戏状态,即退出了游戏运行函数的循环
		else if (KEY_PRESS(VK_ESCAPE))
		{
			ps->_Status = END_NORMAL;
		}

3. 蛇的移动与休眠

我们采用蛇走一步休眠并且检测状态是否正常存活的方式使游戏动起来

//蛇的移动和检测
SnakeMove(ps);
Sleep(ps->_SleepTime);

下面我们来分析蛇移动函数

大概思路是:根据最后一个被按的方向键决定下一个节点出现在哪个坐标,并且判断下一个节点会不会有食物,如果有食物就将食物节点释放掉并且在其他地方重新创建食物节点

//蛇走一步
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 (IsFood(ps,pNextNode))
	{
		EatFood(ps,&pNextNode);
	}
	else
	{
		NoFood(ps,&pNextNode);
	}
	//判断是否撞墙
	IsKillByWall(ps);
	//是否撞到自己
	IsKillBySelf(ps);
}
3.1 是否有食物

我们写一个函数来接收有无食物的结果:

//是否吃到了食物
int IsFood(pSnake ps,pSnakeNode pNextNode)
{
	return (pNextNode->x == ps->_pFood->x) && (pNextNode->y == ps->_pFood->y);
}

如果有食物返回结果为真,执行吃食物操作,否则不吃食物

3.2 吃食物

吃到食物的时候,加分并且将食物的节点加到蛇头上,释放掉之前申请的新节点,并且尾节点不释放,表现为蛇身长度加一,然后创建一个新食物

//吃到食物
void EatFood(pSnake ps,pSnakeNode* ppNextNode)
{
	//加分
	ps->_Socre += ps->_FoodWeight;
	//让食物的位置成为蛇头
	ps->_pFood->next = ps->_pSnake;
	ps->_pSnake = ps->_pFood;
	//把刚才新申请的节点释放掉
	free(*ppNextNode);
	*ppNextNode = NULL;
	//打印新蛇
	pSnakeNode pcur = ps->_pSnake;
	while (pcur)
	{
		Pos(ps->_pSnake->x, ps->_pSnake->y);
		wprintf(BODY);
		pcur = pcur->next;
	}
	//创建新食物
	CreateFood(ps);
}
3.3 没吃到食物

没吃到食物的话分数不变,只需要让新节点成为蛇头,再把蛇尾节点给释放掉并且在原位置上打印空格来覆盖原来的蛇尾图案

//没吃到食物
void NoFood(pSnake ps, pSnakeNode* ppNextNode)
{
	//让新节点成为蛇头
	(*ppNextNode)->next = ps->_pSnake;
	ps->_pSnake = *ppNextNode;
	//打印新蛇
	pSnakeNode pcur = ps->_pSnake;
	while (pcur->next->next)
	{
		Pos(pcur->x, pcur->y);
		wprintf(BODY);
		pcur = pcur->next;
	}
	//在蛇尾位置打印空格覆盖旧蛇
	Pos(pcur->next->x, pcur->next->y);
	printf("  ");
	//释放后面的蛇尾节点
	free(pcur->next);
	pcur->next = NULL;
}
3.4 是否撞到墙

我们通过检测蛇头的坐标和墙体的坐标是否重合来判断是否撞到墙体,如果撞到就改变状态

//是否撞到墙
void IsKillByWall(pSnake ps)
{
	if (ps->_pSnake->x == 0 || ps->_pSnake->x == 56
		|| ps->_pSnake->y == 0 || ps->_pSnake->y == 26)
	{
		ps->_Status = KILL_BY_WALL;
	}
}

3.5 是否撞到自己

同理,我们通过检测蛇头和蛇身的坐标是否重合来判断是否撞到自己,然后改变状态

//是否撞到自己
void IsKillBySelf(pSnake ps)
{
	pSnakeNode pcur = ps->_pSnake->next;
	while (pcur)
	{
		if (pcur->x == ps->_pSnake->x && pcur->y == ps->_pSnake->y)
		{
			ps->_Status = KILL_BY_SELF;
		}
		pcur = pcur->next;
	}
}

通过遍历来判断每一个节点是否和蛇头同坐标

再走完一步以后休眠一下,表示闪烁,视觉表现为蛇在动

三. 游戏结束后的报告原因和回收资源

  • 代码块:
//游戏结束阶段 - 回收资源
void GameEnd(pSnake ps)
{
	switch (ps->_Status)
	{
	case KILL_BY_SELF:
	{
		Pos(23, 14);
		printf("您撞到了自己,游戏结束");
		break;
	}
	case KILL_BY_WALL:
	{
		Pos(23, 14);
		printf("您撞到了墙体,游戏结束");
		break;
	}
	case END_NORMAL:
	{
		Pos(23, 14);
		printf("您主动退出游戏,游戏结束");
		break;
	}
	}
	free(ps->_pFood);
	ps->_pFood = NULL;
	pSnakeNode pcur = ps->_pSnake;
	while (pcur)
	{
		pSnakeNode pdel = pcur;
		pcur = pcur->next;
		free(pdel);
		pdel = NULL;
	}
}

1. 报告死亡原因

除了正常存活以外的三种状态都会使游戏退出,那么我们用switch语句使系统做出不同的操作:

switch (ps->_Status)
	{
	case KILL_BY_SELF:
	{
		Pos(23, 14);
		printf("您撞到了自己,游戏结束");
		break;
	}
	case KILL_BY_WALL:
	{
		Pos(23, 14);
		printf("您撞到了墙体,游戏结束");
		break;
	}
	case END_NORMAL:
	{
		Pos(23, 14);
		printf("您主动退出游戏,游戏结束");
		break;
	}

2. 回收资源

在我们游戏中用到的内存主要是蛇的身体和食物的节点这两个,那么我们先释放掉食物节点,然后遍历释放蛇的身体节点,直到完全为空

free(ps->_pFood);
	ps->_pFood = NULL;
	pSnakeNode pcur = ps->_pSnake;
	while (pcur)
	{
		pSnakeNode pdel = pcur;
		pcur = pcur->next;
		free(pdel);
		pdel = NULL;
	}

至此,我们整个贪吃蛇小游戏就写完了,如果喜欢say-fall的话,欢迎留言关注


  • 本节完…
评论 31
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值