用C语言实现贪吃蛇游戏!!!(破万字)-核心逻辑实现分析

时间:2024-01-27 13:38:39

我们打开VS,创建一个Snake的贪吃蛇工程

在其中创建3个文件:test.c测试文件,snake.c源文件,snake.h头文件

在test.c中我们写游戏的主逻辑:

#define  _CRT_SECURE_NO_WARNINGS
#include "snake.h"


void test()
{
	int ch = 0;
	srand((unsigned int)time(NULL));

	do
	{
		 

         //设置控制台的信息,窗口大小,窗口名
         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 };//创建贪吃蛇

		GameStart(&snake);
		GameRun(&snake);
		GameEnd(&snake);

		SetPos(20, 15);//设置光标的位置

		printf("再来一局吗?(Y/N):");
		ch = getchar();
		getchar();//清理\n

	} while (ch == 'Y');

	SetPos(0, 27);
}



int main()
{
	//修改适配本地中文环境
	setlocale(LC_ALL, "");

	test();//贪吃蛇游戏的测试

	return 0;
}


GameStart

我们先看游戏开始GameStart这个函数,它包含五个函数:


void GameStart(pSnake ps)
{


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

	CreateMap();	//绘制地图

	InitSnake(ps);	//初始化蛇

	CreateFood(ps);//创建食物
	
	PrintHelpInfo();//打印右侧帮助信息
}

先看第一个:打印欢迎信息WelcomeToGame

void WelcomeToGame()
{
	//欢迎信息
	SetPos(35, 10);
	printf("欢迎来到贪吃蛇小游戏\n");

	SetPos(38, 20);
	system("pause");

	system("cls");//清空之前的界面

	//功能介绍信息
	SetPos(15, 10);
	printf("用 ↑ . ↓ . ← . → 来控制蛇的移动,F3是加速,F4是减速");

	SetPos(15, 11);
	printf("加速能得到更高的分数");

	SetPos(38, 20);
	system("pause");

	system("cls");
}


 

接下来是创建地图 CreateMap

#define WALL L'□' //宽字符的定义前面要加上L
void CreateMap()
{
	int i = 0;


	//上
	SetPos(0, 0);
	for (i = 0; i <= 56; i += 2)
	{
		wprintf(L"%lc", WALL);
	}

	//下
	SetPos(0, 25);
	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);
	}

}

下面是初始化蛇InitSnake

蛇最开始为5节,每节对应链表的一个节点,蛇的每一个节点都自己的坐标
创建5个节点,然后将每个节点存放在链表中进行管理创建完蛇身后,将蛇的每一节打印在屏幕上
再设置当前游戏的状态,蛇移动的速度,默认的方向(向右),初始成绩,蛇的状态,每个食物的分数

 

 #define BODY L'●'  

void InitSnake(pSnake ps)
{
	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->next = NULL;
		cur->x = POS_X + i * 2;
		cur->y = POS_Y;

		//头插法
		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"%c", BODY);
		cur = cur->next;
	}
	//初始化贪吃蛇数据
	ps->_SleepTime = 200;
	ps->_Socre = 0;
	ps->_Status = OK; ps->_Dir = RIGHT;
	ps->_foodWeight = 10;
}

GameStart包含的第四个子函数是:CreateFood

随机生成食物的坐标

下面有一些限制:
 x坐标必须是2的倍数
食物的坐标不能和蛇身每个节点的坐标重复

#define FOOD L'★' //食物打印的宽字符
void CreateFood(pSnake ps)
{
	int x = 0;
	int y = 0;
again:
	//产生的x坐标应该是2的倍数,这样才可能和蛇头坐标对齐
	do
	{
		x = rand() % 53 + 2;
		y = rand() % 25 + 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("CreateFood::malloc()");
		return;
	}

	else
	{
		pFood->x = x;
		pFood->y = y;
		SetPos(pFood->x, pFood->y);
		wprintf(L"%c", FOOD);
		ps->_pFood = pFood;
	}
}


GameStart的最后一个函数是PrintHelpInfo,负责打印文字信息

void PrintHelpInfo()
{
	SetPos(64, 15);
	printf("不能穿墙,不能咬到自己\n");
	SetPos(64, 16);
	printf("用↑.↓.←.→分别控制蛇的移动.");
	SetPos(64, 17);
	printf("F3 为加速,F4 为减速\n");
	SetPos(64, 18);
	printf("ESC :退出游戏. 空格键:暂停游戏.");
	SetPos(64, 20);
}


 

GameRun

下面来看一下游戏运行的函数GameRun吧~

void pause()//暂停
{
	while (1)
	{
		Sleep(300);
		if (KEY_PRESS(VK_SPACE))//空格键
		     break;
	}
}


void GameRun(pSnake ps)
{
	
	do
	{
		SetPos(64, 10); //每次进来都要对食物分数进行修正
		printf("得分:%d ", ps->_Socre);
		printf("每个食物得分:%d分", 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_SPACE))
		{
			pause();
		}
		else if (KEY_PRESS(VK_ESCAPE))
		{
			ps->_Status = END_NOMAL;
			break;
		}
		else if (KEY_PRESS(VK_F3))
		{
			if (ps->_SleepTime >= 50)
			{
				ps->_SleepTime -= 30;
				ps->_foodWeight += 2;
			}
		}
		else if (KEY_PRESS(VK_F4))
		{
			if (ps->_SleepTime < 350)
			{
				ps->_SleepTime += 30;
				ps->_foodWeight -= 2;
				if (ps->_SleepTime == 350)
				{
					ps->_foodWeight = 1;
				}
			}
		}
		//蛇每次一定之间要休眠的时间,时间短,蛇移动速度就快
		Sleep(ps->_SleepTime);
		SnakeMove(ps);
	} while (ps->_Status == OK);

虽然这个是三个函数最复杂的部分,但我们也是一个一个点,按照设计的流程来~

关于虚拟键检测按键状态,我们封装一个宏~

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

下面是蛇身的移动,这个很关键~

先创建下一个节点,根据移动方向和蛇头的坐标,蛇移动到下⼀个位置的坐标
确定了下⼀个位置后,判断下一个位置是否是食物:NextIsFood

是食物就做:EatFood,如果不是食物:NoFood
蛇身移动后,判断此次移动是否会造成撞墙:KillByWall或者撞上自己:KillBySelf
 

void SnakeMove(pSnake ps)
{

	//创建下一个节点
	pSnakeNode pNextNode = (pSnakeNode)malloc(sizeof(SnakeNode));

	//确定下一个节点的坐标,下一个节点的坐标根据,蛇头的坐标和方向确定
	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);
}

int NextIsFood(pSnakeNode psn, pSnake ps)
{
	return (psn->x == ps->_pFood->x) && (psn->y == ps->_pFood->y);
}

void EatFood(pSnakeNode psn, pSnake ps)
{
	//头插法
	psn->next = ps->_pSnake;
	ps->_pSnake = psn;
	pSnakeNode cur = ps->_pSnake;

	//打印蛇
	while (cur)
	{
		SetPos(cur->x, cur->y);
		wprintf(L"%c", BODY);
		cur = cur->next;
	}
	ps->_Socre += ps->_foodWeight;
	free(ps->_pFood);
	CreateFood(ps);
}

 //将下⼀个节点头插⼊蛇的⾝体,并将之前蛇⾝最后⼀个节点打印为空格,弃掉蛇身的最后⼀个节点
//pSnakeNode psn 是下⼀个节点的地址
//pSnake ps 维护蛇的指针
void NoFood(pSnakeNode psn, pSnake ps)
{
	//头插法
	psn->next = ps->_pSnake;
	ps->_pSnake = psn;
	pSnakeNode cur = ps->_pSnake;
	//打印蛇
	while (cur->next->next)
	{
		SetPos(cur->x, cur->y);
		wprintf(L"%c", BODY);
		cur = cur->next;
	}
	//最后一个位置打印空格,然后释放节点
	SetPos(cur->next->x, cur->next->y);
	printf("  ");
	free(cur->next);
	cur->next = NULL;
}

int 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;
		return 1;
	}
	return 0;
}

int 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;
			return 1;
		}
		cur = cur->next;
	}
	return 0;
}

呼,终于将最核心的逻辑搞定了~最后来看看游戏结束的函数GameEnd吧~

GameEnd

游戏状态不再是OK的时候,要告知游戏结束的原因,并且释放蛇身节点
 

void GameEnd(pSnake ps)
{
	pSnakeNode cur = ps->_pSnake;
	SetPos(24, 12);
	switch (ps->_Status)
	{
	case END_NOMAL:
		printf("您主动退出游戏\n");
		break;
	case KILL_BY_SELF:
		printf("您撞上自己了 ,游戏结束!\n");
		break;
	case KILL_BY_WALL:
		printf("您撞墙了,游戏结束!\n");
		break;
	}
	//释放蛇身的节点
	while (cur)
	{
		pSnakeNode del = cur;
		cur = cur->next;
		free(del);
	}
}

到这,我们整个贪吃蛇游戏的功能就全部实现了(鼓掌鼓掌)

最后是Humble的参考代码,分3个文件