手撕——贪吃蛇小游戏(下)

引言

上一章介绍了实现贪吃蛇小游戏必备的知识点。

这章,让我们一起开启手搓核弹之旅吧。

先附上贪吃蛇代码的git: 贪吃蛇小游戏_4_23 · Shown_shuai/learn_c - 码云 - 开源中国 (gitee.com)

上一章的窗口:

手撕------贪吃蛇小游戏(上)(前置)-CSDN博客

一、整体思维导图

二、创建贪吃蛇

在游戏开始前,先本地化,为了后面打印的需要

cpp 复制代码
int main()
{
	//设置本地化
	setlocale(LC_ALL, "");


	return 0;
}

用链表来维护贪吃蛇

在snack.h中进行类型的声明

1. 蛇身的节点

cpp 复制代码
//蛇身体的节点
typedef struct SnakeNode
{
	//坐标
	int x;
	int y;
	//指向下一个节点的指针
	struct SnakeNode* nest;
}SnakeNode,* pSnakeNode;
//pSnakeNode是指向节点的指针类型
//等价于:typedef struct SnakeNode* pSnakeNode;

2. 创建贪吃蛇

贪吃蛇有许多需要维护的信息,将贪吃蛇创建为一个结构体。

cpp 复制代码
//蛇的方向
enum DIRECTION
{
	UP = 1, //上
	DOWN,   //下
	LEFT,   //左
	RIGHT   //右
};
//蛇的状态
//游戏正常,撞墙,撞到自己,正常退出
enum GAME_STATUS
{
	OK,            //正常
	KILL_BY_WALL,  //撞墙
	KILL_BY_SELF,	//撞到自己
	END_NORMAL     //正常退出
};


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

3.用枚举结构体,来枚举蛇属性的状态:

cpp 复制代码
//蛇的方向
enum DIRECTION
{
	UP = 1, //上
	DOWN,   //下
	LEFT,   //左
	RIGHT   //右
};

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

4. 定义特殊的符号,和特殊的数据

cpp 复制代码
#define POS_X 24 //游戏刚开始的时候,蛇头的位置
#define POS_Y 5  //游戏刚开始的时候,蛇头的位置
#define WALL L'□'
#define BOOY L'●'
#define FOOD L'★'

三、开始游戏

1.设置游戏窗口大小和名字,隐藏光标

cpp 复制代码
//1.设置窗口的大小,光标隐藏
system("mode con cols=110 lines=35");
system("title 贪吃蛇");

HANDLE houtput = GetStdHandle(STD_OUTPUT_HANDLE);//获取标准输出的句柄(通过这个句柄来对控制台进行操作)

CONSOLE_CURSOR_INFO curInfo;                     //存有光标信息的结构体
//获得和hOutput句柄相关的控制台上的光标信息,存放在curInfo中
GetConsoleCursorInfo(houtput, &curInfo); 

curInfo.bVisible = false;  //修改光标信息

//设置获得和hOutput句柄相关的控制台上的光标信息
SetConsoleCursorInfo(houtput, &curInfo); 

2. 打印欢迎界面和功能介绍(上一章的知识点):

cpp 复制代码
//光标定位
void SetPos(int x, int y)
{
	//获取标准输出的句柄(通过这个句柄来对控制台进行操作)
	HANDLE hOutput = NULL;
	hOutput = GetStdHandle(STD_OUTPUT_HANDLE);

	//定位光标的位置
	COORD pos = { x, y };
	SetConsoleCursorPosition(hOutput, pos);
}

//打印欢迎界面和功能介绍
void WelcomeToGame()
{
	SetPos(45, 16);
	printf("欢迎来的贪吃蛇小游戏\n");
	SetPos(48, 20);
	system("pause");
	system("cls"); //清空屏幕
	SetPos(35, 16);
	wprintf(L"用↑,↓,←,→来控制小蛇的移动,按F3加速,F4减速\n");
	SetPos(43, 18);
	wprintf(L"加速可以获得更高的分数");

	SetPos(45, 23);
	system("pause");
	system("cls");
}

3.绘制地图:

cpp 复制代码
//3.绘制地图
void CreateMap()
{
	int i = 0;
	//上
	for (int i = 0; i < 35; i++)
	{
		wprintf(L"%lc", WALL);
	}
	//下
	SetPos(0, 30);
	for (i = 0; i < 35; i++)
	{
		wprintf(L"%lc", WALL);
	}
	//左
	for (i = 1; i < 30; i++)
	{
		SetPos(0, i);
		wprintf(L"%lc\n", WALL);
	}
	//右
	for (i = 1; i < 30; i++)
	{
		SetPos(68, i);
		wprintf(L"%lc\n", WALL);
	}
	SetPos(80, 0);
}

4.创建蛇

申请5个节点,使用头插,将蛇链接起来

cpp 复制代码
//初始化蛇身
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("InitSanke(): malloc fail!");
			return;
		}
		cur->nest = NULL;
		cur->x = POS_X + 2 * i;
		cur->y = POS_Y;
		if (ps->_pSnake == NULL)
		{
			ps->_pSnake = cur;
		}
		else
		{
			cur->nest = ps->_pSnake;
			ps->_pSnake = cur;
		}
	}
	//打印:
	cur = ps->_pSnake;
	while (cur)
	{
		SetPos(cur->x, cur->y);
		wprintf(L"%lc", BOOY);
		cur = cur->nest;
	}
	//设置贪吃蛇的属性
	ps->_dir = RIGHT;
	ps->_score = 0;
	ps->_food_weight = 10;
	ps->_sleep_time = 200;
	ps->_status = OK;
}

5.创建食物

cpp 复制代码
//创建食物
void CreateFood(pSnake ps)
{
	int x = 0;  //取值范围:2 ~ 66  0~64
	int y = 0;  //          1 ~ 30  0~29
again:
	do
	{
		x = rand() % 65 + 2;
		y = rand() % 29 + 1;
	} while (x % 2 != 0);   //x 必须是 2 的倍数
	//x 和 y 的坐标不能和蛇的坐标相同
	pSnakeNode cur = ps->_pSnake;
	while (cur)
	{
		if (x == cur->x && y == cur->y)
		{
			goto again;
		}
		cur = cur->nest;
	}
	//创建食物的节点:
	pSnakeNode pFood = (pSnakeNode)malloc(sizeof(SnakeNode));
	if (pFood == NULL)
	{
		perror("CreaterFood():malloc fail");
		return;
	}
	pFood->x = x;
	pFood->y = y;
	pFood->nest = NULL;
	SetPos(x, y);
	wprintf(L"%lc", FOOD);
	ps->_pFood = pFood;

}

6.初始化游戏的所以代码:

cpp 复制代码
//游戏的初始化
void GameStart(pSnake ps)
{
	//0.设置窗口的大小,光标隐藏
	system("mode con cols=110 lines=35");
	system("title 贪吃蛇");
	HANDLE houtput = GetStdHandle(STD_OUTPUT_HANDLE);//获取标准输出的句柄(通过这个句柄来对控制台进行操作)
	CONSOLE_CURSOR_INFO curInfo;                     //存有光标信息的结构体
	//获得和hOutput句柄相关的控制台上的光标信息,存放在curInfo中
	GetConsoleCursorInfo(houtput, &curInfo); 
	curInfo.bVisible = false;  //修改光标信息
	//设置获得和hOutput句柄相关的控制台上的光标信息
	SetConsoleCursorInfo(houtput, &curInfo); 
	//1.打印欢迎界面
	//2.功能介绍
	WelcomeToGame();
	//3.绘制地图
	CreateMap();
	//4.创建蛇
	InitSnake(ps);
	//5.创建食物
	CreateFood(ps);
}

四、运行游戏

1. 打印帮助信息

cpp 复制代码
void PrintHelpInfo()
{
	SetPos(74, 15);
	wprintf(L"%s", L"不能穿墙,不能咬到自己");
	SetPos(71, 16);
	wprintf(L"%s", L"用↑,↓,←,→来控制小蛇的移动");
	SetPos(74, 17);
	wprintf(L"%s", L"按F3加速,F4减速\n");
	SetPos(71, 18);
	wprintf(L"%s", L"ESC:退出游戏,space:暂停游戏");
	SetPos(74, 20);
	wprintf(L"%s", L"制作者:@Run_Teenage");
}

2.定义一个宏,检测按键是否被按过

上一章有讲,可以去看一下

cpp 复制代码
#define KEY_PRESS(kv) ((GetAsyncKeyState(kv)&1) ? 1:0)

3.游戏运行时,整体结构:

cpp 复制代码
//游戏的运行:
void GameRun(pSnake ps)
{
	//打印帮助信息
	PrintHelpInfo();
	do
	{
		//打印总分数和食物的分数
		SetPos(74,10);
		printf("总分数:%d\n", ps->_score);
		SetPos(74, 11);
		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_DOWN) && ps->_dir != UP)//按下键时,并且向上走,不会有反应
		{
			ps->_dir = DOWN;
		}
		else if (KEY_PRESS(VK_SPACE))
		{
			//暂停
			Pause();
		}
		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->_food_weight > 2)
			{
				ps->_sleep_time += 30;
				ps->_food_weight -= 2;
			}
		}
		else if (KEY_PRESS(VK_ESCAPE))
		{
			//正常退出游戏
			ps->_status = END_NORMAL;
		}
		//贪吃蛇走
		SnakeMove(ps);  //蛇走一步的过程
		Sleep(ps->_sleep_time);

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

4.实现暂停模块

cpp 复制代码
//暂停
void Pause()
{
	while (1)
	{

		Sleep(200);
		if (KEY_PRESS(VK_SPACE))
		{
			break;
		}
	}
}

5.实现贪吃蛇走一步的过程

cpp 复制代码
//贪吃蛇走
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);
}

5.1检查下一个位置是不是食物

cpp 复制代码
//检测下一个坐标是不是食物
int NextIsFood(pSnakeNode pn, pSnake ps)
{
	return (pn->x == ps->_pFood->x && pn->y == ps->_pFood->y);
}

5.2下一个是食物

cpp 复制代码
//下一个位置是食物,吃掉食物
void EatFood(pSnakeNode pn, pSnake ps)
{
	//头插
	ps->_pFood->nest = ps->_pSnake;
	ps->_pSnake = ps->_pFood;

	//释放next节点
	free(pn);
	pn = NULL;
	pSnakeNode cur = ps->_pSnake;
	while (cur)
	{
		SetPos(cur->x, cur->y);
		wprintf(L"%c", BOOY);
		cur = cur->nest;
	}
	ps->_score += ps->_food_weight;

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

5.3下一个位置不是食物

cpp 复制代码
//下一个位置不是食物
void NoFood(pSnakeNode pn, pSnake ps)
{
	//头插
	pn->nest = ps->_pSnake;
	ps->_pSnake = pn;
	pSnakeNode cur = ps->_pSnake;
	while (cur->nest->nest != NULL)
	{
		SetPos(cur->x, cur->y);
		wprintf(L"%c", BOOY);
		cur = cur->nest;
	}
	//把最后一个位置的节点打印成空格
	SetPos(cur->nest->x , cur->nest->y);
	printf("  ");
	//释放最后一个位置的节点
	free(cur->nest);
	//把倒数第二的节点的地址置为NULL
	cur->nest = NULL;
}

5.4检查蛇是否撞到墙

cpp 复制代码
//检测蛇是否撞墙
void KillByWall(pSnake ps)
{
	if (ps->_pSnake->x == 0 || ps->_pSnake->x == 68 ||
		ps->_pSnake->y == 0 || ps->_pSnake->y == 29)
	{
		ps->_status = KILL_BY_WALL;
	}
}

5.5检查蛇是否撞到自己

遍历蛇头后面的节点的坐标是否和蛇的头节点重合,重合,则说明自己撞到了自己

cpp 复制代码
//检测蛇是否撞到自己
void KillBySelf(pSnake ps)
{
	pSnakeNode cur = ps->_pSnake->nest;
	while (cur)
	{
		if (cur->x == ps->_pSnake->x && cur->y == ps->_pSnake->y)
		{
			ps->_status = KILL_BY_SELF;
			break;
		}
		cur = cur->nest;
	}
}

五、游戏结束 (善后工作)

打印游戏是为什么结束的,释放贪吃蛇的节点

cpp 复制代码
//结束游戏 ------ 善后工作(释放内存之类的)
void GameEnd(pSnake ps)
{
	SetPos(25, 15);
	switch (ps->_status)
	{
	case END_NORMAL:
		printf("正常结束游戏");
		break;
	case KILL_BY_WALL:
		printf("撞上墙了,游戏结束");
		break;
	case KILL_BY_SELF:
		printf("咬到自己了,游戏结束");
		break;
	}
	//释放蛇身节点
	pSnakeNode cur = ps->_pSnake;
	while (cur)
	{
		pSnakeNode del = cur;
		cur = cur->nest;
		free(del);

	}
}

六、合并游戏的全部流程,处理细节问题

cpp 复制代码
#include"snack.h"

void test()
{
	int ch = 0;
	do
	{
		system("cls");
		//创建贪吃蛇
		Snake snake = { 0 };
		//初始化游戏
		//1.打印欢迎界面
		//2.功能介绍
		//3.绘制地图
		//4.创建蛇
		//5.创建食物
		//6.设置游戏的相关信息
		GameStart(&snake);
		//运行游戏
		GameRun(&snake);
		//结束游戏 ------ 善后工作(释放内存之类的)
		GameEnd(&snake);
		while (_kbhit())
		{
			_getch();    //吸收蛇动时,按键盘的键值带来的影响,不然游戏会有小bug
		}
		SetPos(24, 16);
		printf("再来一局吗?(Y/N)");
		ch = getchar();
		while (getchar() != '\n');

	} while (ch == 'Y' || ch == 'y');
	SetPos(0, 32);
}
int main()
{
	//设置本地化
	setlocale(LC_ALL, "");
	srand((unsigned int )time(NULL));
	test();


	return 0;
}

七、所以代码可以在引言的git中拿取

相关推荐
犯困的土子哥7 小时前
VS Code搭建C/C++开发环境
c语言
charade3127 小时前
【C语言】内存分配的理解
c语言·开发语言·c++
流影ng8 小时前
C语言HashTable基本理解
c语言·哈希算法
2401_867021908 小时前
文件缓冲区(IO与文件 ·III)(linux/C)
linux·运维·服务器·c语言
前端_学习之路8 小时前
javaScript--数据结构和算法
javascript·数据结构·算法
8RTHT11 小时前
数据结构(七)---链式栈
数据结构
Fency咖啡13 小时前
《代码整洁之道》第9章 单元测试 - 笔记
数据结构·b树
2501_9063143213 小时前
优化无头浏览器流量:使用Puppeteer进行高效数据抓取的成本降低策略
开发语言·数据结构·数据仓库
C1829818257513 小时前
项目中数据结构为什么用数组,不用List
数据结构