🏝️专栏:【C语言boss篇】

🌅主页:f狐o狸x


目录

一、 游戏效果展示

二、 游戏逻辑实现分析

        2.1 游戏主体逻辑

        2.2 游戏实现分析

三、贪吃蛇游戏设计

        3.1 前期准备工作

        3.2 数据结构设计

        3.3 地图、蛇身、食物设计

        3.4 游戏主逻辑

        3.5 菜单

        3.6 游戏开始

        3.6.1 打印欢迎信息

        3.6.2 绘制地图

        3.6.3 初始化蛇

        3.6.4创建食物

        3.7 游戏运行

        3.7.1 打印帮助信息

        3.7.2 蛇身移动

3.7.2.1 判断下一步是否有食物

3.7.2.2 吃到食物

3.7.2.3 正常走下一步(没吃到食物)

3.7.2.4 检测是否撞墙

3.7.2.5 检测是否咬到自己

        3.8 游戏结束

        3.8.1 保存分数

四、结语 


         贪吃蛇基本上是我们家喻户晓的小游戏了,在狐狸还在是小学幼儿园时期的时候就经常玩,后来到了初中随着互联网的发展,《贪吃蛇大作战》这个游戏又在众多网游中脱颖而出,因此身为大学生的狐狸,我决定用C语言来实现这个经典的小游戏——贪吃蛇

一、 游戏效果展示

        废话不多说,直接看结果

二、 游戏逻辑实现分析

        2.1 游戏主体逻辑

        2.2 游戏实现分析

        不难看出游戏主逻辑就三个函数,GameStart 、GameRun 、GameEnd。他们分别负责了游戏前的准备工作、游戏运行时贪吃蛇的移动和判断是否结束游戏、游戏结束时的善后工作,那我们话不多说,直接开始

三、贪吃蛇游戏设计

        3.1 前期准备工作

        如果我们想把贪吃蛇小游戏弄得好看一些就需要一些调节颜色的函数,并且打印蛇身、墙体都用到了特殊的字符,因此需要先把修改适配器到中文状态,后面我们生成食物的时候需要的坐标也是随机的,因此还要准备生成随机数

int main()
{
	//修改适配器到中文状态
	setlocale(LC_ALL, "");
	//设置随机数
	srand((unsigned)time(NULL));
	test();
	return 0;
}
void color(int c)
{
	SetConsoleTextAttribute(GetStdHandle(STD_OUTPUT_HANDLE), c); //颜色设置
}

         这里是颜色的十进制对照表:

        这样我们后面只需要在打印前设置好想要的颜色就行了,想了解的朋友可以去:SetConsoleTextAttribute_360百科

        我们还需要获取案件情况来判断当前蛇运行的状态

//监测按键
#define KEY_PRESS(VK)  ( (GetAsyncKeyState(VK) & 0x1) ? 1 : 0 )

         封装一个设置光标位置的函数方便操作

void SetPos(int x, int y)
{
	//获得设备句柄
	HANDLE hanlde = GetStdHandle(STD_OUTPUT_HANDLE);
	//根据句柄设置光标的位置
	COORD pos = { x, y };
	SetConsoleCursorPosition(hanlde, pos);
}

        准备工作完了之后我们就可以开始设计下面的函数了


//颜色设置
void color(int c);

//打印分数
void PrintScore(pSnake ps);

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

//设置光标位置信息
void SetPos(int x, int y);

//打印欢迎信息
void WelcomeToGame();

//绘制地图
void CreateMap();

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

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

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

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

//蛇移动的函数,一次移动一步
void SnakeMove(pSnake ps);

//判断下一步是否有食物
int NextIsFood(pSnake ps, pSnakeNode pnext);

//吃食物
void EatFood(pSnake ps, pSnakeNode pnext);

//没吃到食物正常走一步
void NotEatFood(pSnake ps, pSnakeNode pnext);

//检测是否撞墙
void KillByWall(pSnake ps);

//检测是否撞到自己
void KillBySelf(pSnake ps);

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

//保存分数
void SaveScore(pSnake ps);

        3.2 数据结构设计

        这里我用的是一个链表来管理的蛇身,因为在游戏过程中,贪吃蛇每吃一个食物都会增长一个蛇身节点,正好用链表节点来控制蛇的长度

//蛇身结构体的定义
typedef struct SnakeNode
{
	int x;
	int y;
	struct SnakeNode* next;
}SnakeNode,* pSnakeNode;

        另外我们需要用一个结构体来管理贪吃蛇游戏的信息,包括蛇身、食物、总分、当前分数、当前一个食物的分数、蛇的速度、当前游戏状态和当前蛇运动状态(上、下、左、右)

//贪吃蛇结构体定义
typedef struct Snake
{
	pSnakeNode pSnake; //维护蛇的指针,指向蛇头
	pSnakeNode pFood; //指向食物的指针
	int Score;//总分
	int ScoreMax[4];//最高分
	int ScoreSize;//最高分有效个数
	int FoodWeight;//每个食物的分数
	int SleepTime;//蛇的速度
	enum GAME_STATUS status;//游戏当前状态
	enum SNAKE_STATUS dir;//蛇当前走的方向
}Snake,* pSnake;

        这里蛇的运动状态和游戏当前状态都可以一一列举出来,所以他俩可以使用枚举

//游戏状态枚举
enum GAME_STATUS
{
	OK = 1, //游戏正常运行状态
	ESC, //按esc键盘退出游戏,正常退出
	KILL_BY_WALL,//撞墙退出游戏
	KILL_BY_SELF,//咬到自己退出游戏
};

//蛇当前走的方向枚举
enum SNAKE_STATUS
{
	UP=1,//向上
	DOWN,//向下
	LEFT,//向左
	RIGHT//向右
};

        3.3 地图、蛇身、食物设计

        用“ ■ ”表示地图边界、“ ★ ”表示一个食物节点、“ ● ”表示蛇头、“ ○ ”表示蛇身

        3.4 游戏主逻辑

void test()
{
	//设置控制台信息,窗口大小,窗口名
	system("mode con cols=100 lines=30");
	system("title:贪吃蛇");
	//隐藏光标
	HANDLE handle = GetStdHandle(STD_OUTPUT_HANDLE);
	CONSOLE_CURSOR_INFO CursorInfo;
	GetConsoleCursorInfo(handle, &CursorInfo);//获取控制台光标信息
	CursorInfo.bVisible = false;
	SetConsoleCursorInfo(handle, &CursorInfo);

	Snake snake = { 0 };
	int input = 0;
	do 
	{

		system("cls");
		menu();
		scanf("%d", &input);
		switch (input)
		{
		case 1:
			game(&snake);
			SetPos(15, 13);
			printf("再来亿吧~");
			SetPos(15, 14);
			system("pause");
			break;
		case 2:
			PrintScore(&snake);
			break;
		case 0:
			system("cls");
			color(15);//字体颜色 白 
			SetPos(40, 13);
			printf("古德拜拜~~");
			SetPos(0, 27);
			break;
		default:
			SetPos(40, 18);
			printf("输入错误请重新输入");
		}
	} while (input);

}

void game(pSnake ps)
{
	GameStart(ps);//游戏开始前的初始化
	GameRun(ps);//游戏运行
	GameEnd(ps);//游戏结束后的善后工作
}

        3.5 菜单

void menu()
{
	color(6);//墙体颜色 棕 
	SetPos(36, 11);
	int i = 0;
	//上边框
	for (i = 0; i < 28; i += 2)
	{
		wprintf(L"%lc", WALL);
	}
	//下边框
	SetPos(36, 19);
	for (i = 0; i < 28; i += 2)
	{
		wprintf(L"%lc", WALL);
	}
	//左边框
	for (i = 1; i < 8; i++)
	{
		SetPos(36, i + 11);
		wprintf(L"%lc", WALL);
	}
	//右边框
	for (i = 1; i < 8; i++)
	{
		SetPos(62, i + 11);
		wprintf(L"%lc", WALL);
	}
	SetPos(40, 13);
	color(15);//字体颜色 白 
	printf("欢迎来到贪吃蛇小游戏");
	SetPos(40, 14);
	printf("1. 开始游戏");
	SetPos(40, 15);
	printf("2. 查看最高分");
	SetPos(40, 16);
	printf("0. 退出游戏");
	SetPos(40, 17);
	printf("请选择:");


}

        3.6 游戏开始

void GameStart(pSnake ps)
{

	//打印欢迎信息
	WelcomeToGame();

	//绘制地图
	CreateMap();

	//初始化蛇
	InitSnake(ps);

	//创建食物
	CreateFoods(ps);

}

        3.6.1 打印欢迎信息

        打印一些初始信息方便玩家参考

void WelcomeToGame()
{
	system("cls");
	SetPos(40, 13);
	printf("欢迎来到贪吃蛇小游戏");
	SetPos(44, 26);
	system("pause");
	system("cls");
	SetPos(20, 13);
	printf("用 ↑ . ↓ . ← . → 来控制蛇的移动,F3是加速,F4是减速");
	SetPos(20, 14);
	printf("加速能得到更高的分数");
	SetPos(44, 26);
	system("pause");
	system("cls");
}

        3.6.2 绘制地图

void CreateMap()
{
	int i = 0;
	//控制台主机 
	//上边框
	color(6);//墙体颜色 棕 
	SetPos(0, 0);
	for (i = 0; i < 58; i+=2)
	{
		wprintf(L"%lc", WALL);
	}
	//下边框
	SetPos(0, 26);
	for (i = 0; i < 58; i += 2)
	{
		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);
	}  
}

        3.6.3 初始化蛇

//初始化蛇
void InitSnake(pSnake ps)
{
	//创建5个蛇身节点
	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;
	//打印蛇头
	color(10);//蛇的颜色 绿

	SetPos(cur->x, cur->y);
	wprintf(L"%lc", HEAD);
	cur = cur->next;
	//打印蛇身
	while (cur)
	{
		SetPos(cur->x, cur->y);
		wprintf(L"%lc", BODY);
		cur = cur->next;
	}

	//贪吃蛇其他信息初始化
	ps->dir = RIGHT;
	ps->FoodWeight = 10;
	ps->pFood = NULL;
	ps->Score = 0;
	ps->SleepTime = 200;
	ps->status = OK;

}

        3.6.4创建食物

        食物其实也可以算为是一个蛇的节点

//创建食物
void CreateFoods(pSnake ps)
{
	//随机生成食物坐标
	int x = 0;
	int y = 0;

again:
	do
	{
		x = rand() % 53 + 2;
		y = rand() % 24 + 1;
	} while (x % 2 != 0);

	//防止食物创建到蛇身体上
	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("CreateFoods::malloc");
		return;
	}

	pFood->x = x;
	pFood->y = y;

	ps->pFood = pFood;
	//打印食物
	SetPos(x, y);
	color(12);//食物颜色 红
	wprintf(L"%lc", FOOD);

}

        3.7 游戏运行

//游戏运行
void GameRun(pSnake ps)
{
	//打印帮助信息
	PrintHelpInfo();
	do
	{
		//打印当前分数情况
		color(15);//字体的颜色 白
		SetPos(62, 15);
		printf("当前食物分值:%d", ps->FoodWeight);
		SetPos(62, 16);
		printf("总分:%d", ps->Score);
		SetPos(62, 17);
		printf("最高分:%d", ps->ScoreMax[0]);


		//检测按键
		//上,下,左,右,ESC,空格,F3,F4
		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 = ESC;
			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->FoodWeight > 2)
			{
				ps->SleepTime += 30;
				ps->FoodWeight -= 2;
			}
		}

		//走一步
		SnakeMove(ps);

		//睡眠一下
		Sleep(ps->SleepTime);

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


}

        3.7.1 打印帮助信息

//打印帮助信息
void PrintHelpInfo()
{
	color(15);//字体的颜色 白
	SetPos(62, 12);
	printf("1. 不能穿墙,不能咬到自己");
	SetPos(62, 13);
	printf("2. 用 ↑.↓.←.→ 来控制蛇的移动");
	SetPos(62, 14);
	printf("3. F3是加速,F4是减速");
	SetPos(62, 24);
	printf("制作:狐狸");
	SetPos(62, 25);
	printf("祝大家都好运连连~");

}

        3.7.2 蛇身移动

void SnakeMove(pSnake ps)
{
	//确定下一步的位置
	pSnakeNode pnext = (pSnakeNode)malloc(sizeof(SnakeNode));
	if (pnext == NULL)
	{
		perror("SnakeMove::malloc");
		return;
	}
	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;
	}

	//判断下一步是否有食物
	int ret = NextIsFood(ps, pnext);

	if (NextIsFood(ps, pnext))
	{
		EatFood(ps, pnext);
	}
	else
	{
		//正常走一步
		NotEatFood(ps, pnext);
	}

	//检测是否撞墙
	KillByWall(ps);

	//检测是否咬到自己
	KillBySelf(ps);
}
3.7.2.1 判断下一步是否有食物
//判断是否有食物
int NextIsFood(pSnake ps, pSnakeNode pnext)
{
	if (ps->pFood->x == pnext->x && ps->pFood->y == pnext->y)
	{
		return 1;
	}
	else
	{
		return 0;
	}
}
3.7.2.2 吃到食物
//吃到食物
void EatFood(pSnake ps, pSnakeNode pnext)
{
	//头插
	pnext->next = ps->pSnake;
	ps->pSnake = pnext;

	//打印蛇
	pSnakeNode cur = ps->pSnake;
	//打印蛇头
	color(10);//蛇的颜色 绿
	SetPos(cur->x, cur->y);
	wprintf(L"%lc", HEAD);
	cur = cur->next;
	//打印蛇身
	while (cur)
	{
		SetPos(cur->x, cur->y);
		wprintf(L"%lc", BODY);
		cur = cur->next;
	}

	//计分
	ps->Score += ps->FoodWeight;

	//释放旧食物
	free(ps->pFood);
	
	//创建新的食物
	CreateFoods(ps);
}
3.7.2.3 正常走下一步(没吃到食物)
void NotEatFood(pSnake ps, pSnakeNode pnext)
{
	//头插
	pnext->next = ps->pSnake;
	ps->pSnake = pnext;
	//删除尾巴
	pSnakeNode cur = pnext;
	//打印蛇头
	color(10);//蛇的颜色 绿
	SetPos(cur->x, cur->y);
	wprintf(L"%lc", HEAD);
	cur = cur->next;
	//打印蛇身
	//打印+找尾
	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;

}
3.7.2.4 检测是否撞墙
//检测是否撞墙
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;
	}
}
3.7.2.5 检测是否咬到自己
//检测是否撞到自己
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;
	}
}

        3.8 游戏结束

void  GameEnd(pSnake ps)
{
	SetPos(15, 12);
	color(15);//字体颜色 白
	switch (ps->status)
	{
	case ESC:
		printf("ESC主动退出游戏,正常退出\n");
		break;
	case KILL_BY_WALL:
		printf("这你都能撞墙??菜啊\n");
		break;
	case KILL_BY_SELF:
		printf("你是真饿了!\n");
		break;
	}

	//保存分数
	SaveScore(ps);


	//释放贪吃蛇的链表资源
	pSnakeNode cur = ps->pSnake;
	pSnakeNode del = NULL;

	while (cur)
	{
		del = cur;
		cur = cur->next;
		free(del);
	}
	free(ps->pFood);
	ps->pSnake = NULL;
	ps = NULL;

}

        3.8.1 保存分数

void SaveScore(pSnake ps)
{

	//写入数据
	int i = 0;
	if (ps->ScoreSize < 3)
	{
		ps->ScoreMax[ps->ScoreSize] = ps->Score;
		ps->ScoreSize++;
	}
	else
	{
		for (i = 0; i < 3; i++)
		{
			if (ps->Score > ps->ScoreMax[i])
			{
				int j = i;
				for (j = i; j < 3; j++)
				{
					ps->ScoreMax[j + 1] = ps->ScoreMax[j];
				}
				ps->ScoreMax[i] = ps->Score;
				break;
			}
		}

	}
	 i = 0;
	for (i = 0; i < 2; i++)
	{
		int j = 0;
		for (j = 0; j < 2 - i; j++)
		{
			if (ps->ScoreMax[j] < ps->ScoreMax[j + 1])
			{
				int tmp = ps->ScoreMax[j];
				ps->ScoreMax[j] = ps->ScoreMax[j + 1];
				ps->ScoreMax[j + 1] = tmp;
			}
		}
	}
}

四、结语 

        不知不觉间,我已经学习三个月的编程了,感谢各位在我学习时的支持与陪伴,希望未来我们能一直坚持学习下去

        “ 请不要相信胜利就像山坡上的蒲公英一样唾手可得,但是请相信生活中总有美好值得我们全力以赴,哪怕粉身碎骨!”———2022卡塔尔世界杯解说贺炜

Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐