C语言·贪吃蛇游戏(下)

上节我们将要完成贪吃蛇游戏所需的前置知识都学完了,那么这节我们就开始动手写代码了

1. 程序规划

首先我们应该规划好我们的代码文件,设置3个文件:snack.h 用来声明游戏中实现各种功能的函数,snack.c 用来实现函数,test.c 用来测试和运行这个游戏

然后我们将游戏的大概流程在test.c文件中写出来,因为我们会用到宽字符,所以要记得适配本地中文环境

2. 游戏数据结构设计

2.1 蛇身数据结构(链表)

我们设计用链表来表示蛇身,链表的每个节点中需要存这节蛇身的坐标 x、y 和下一节点的地址,然后我们给蛇身节点结构体类型改个好写一点的名字

链表的基础知识:数据结构·单链表-CSDN博客

2.2 维护整个游戏的结构体

我们还要创建一个结构体,用来存放当前游戏的种种状态

3. 游戏开始前的初始化

游戏的开始前初始化都在GameStart()函数中实现,那么我们着手开始写这个函数。

首先,我们要先创建维护贪吃蛇游戏的结构体,将它的信息传到GameStart()中去,在snack.h中声明这函数,然后再在snack.c中实现它

我们把这个函数种封装的功能先都展示出来,之后就不重复展示了

3.1 控制台信息设置及光标隐藏

控制台为了美观我们设置成100列30行的,名字就叫做贪吃蛇

运行以下发现控制台设置好了,并且光标也很好的隐藏了起来

3.2 打印欢迎信息

WelcomeToGame()函数内部的大部分东西我们上节其实已经写过了,下面就展示一下

3.3 打印地图

地图大小我们上节已经说过了,要设计一个27行58列的棋盘,然后我们用一个宏把每块墙写出来,方便写打印语句

在打印墙的时候上边界和下边界挨着打印就行,左边界和右边界的时候要在每次打印之前把光标都定一下位

展示打印地图的代码:

3.4 初始化蛇

蛇身我们采用链表的结构进行管理。最开始,我们展示5个蛇身,初始的蛇尾巴从(24,5)的位置开始,向右5节之后是蛇头,这里我们采用头插的方式依次创建节点并加入蛇身中。

同时定义蛇尾初始位置的宏和蛇身形状的宏

​​​​​​​

最后将贪吃蛇的所有状态进行初始化

3.5 创建食物

创建食物时不能随意创建的,要满足一下四点要求:

  1. 食物是随机出现的,就是说坐标是随机的

  2. 坐标必须在墙内 (x:2~54 y:1~25)

  3. 坐标不能在蛇身上

  4. 横坐标必须是偶数

创建的食物其实就相当于蛇的一段身体,所以我们之间用蛇身节点的结构体类型保存食物的()(位置)信息就行,然后食物的创建要满足上面的要求,最后要注意的是,我们想要生成真随机数,就不要忘了在程序开始时给rand()函数种种子srand()

​​​​​​​ ​​​​​​​ ​​​​​​​

4. 游戏的运行过程

下面我们来完成GameRun()函数,首先将这个函数中要实现的功能理出来

​​​​​​​ ​​​​​​​ ​​​​​​​ ​​​​​​​

4.1 打印帮助信息

这个没啥好说的,定好位打印就行

​​​​​​​

4.2 按键逻辑

写一个do···while循环,当游戏状态是OK的情况下就一直循环,持续检测按键的状态,同时在这里把帮助信息也写一下。我们检测按键的时候写一个宏 KEY_PRESS 内容跟上节的一样,这里我就不展示了,最后我会把所有代码都贴出来的

​​​​​​​

pause()函数

4.3 蛇的移动(走一步)

这里我们的策略是在蛇头前面再创建一个节点,用来判断蛇要走的下一个位置是否有食物,这里要注意,当蛇向下走的时候不能直接向上走,向右走的时候不能像左走,以此类推。蛇的下一步的位置信息先全都存在新创建的节点中,等待接入蛇身

​​​​​​​

4.3.1 蛇头下一步是食物

​​​​​​​

4.3.2 蛇头下一步不是食物

​​​​​​​ ​​​​​​​ ​​​​​​​

4.3.3 检测撞墙

撞墙简单,判断一下蛇头横纵坐标有没有跟墙重合就行

4.3.4 检测撞自己

同理,蛇头的横纵坐标没有跟自身重合就行

5. 游戏结束善后的工作

善后工作其实就是打印一下蛇的死亡原因,然后把,malloc的内存都释放掉

​​​​​​​

6. 复玩功能

复玩功能我们之前已经写过很多次了,像扫雷中就有很详细的介绍,这里我就简单写一下了

扫雷请参考:函数·扫雷游戏-CSDN博客

​​​​​​​ ​​​​​​​

7. 完整代码

snack.h

cpp 复制代码
#include<locale.h>
#include<stdlib.h>
#include<stdio.h>
#include<Windows.h>
#include<stdbool.h>
#include<time.h>

#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 GAME_STATUS
{
	OK = 1,//游戏正常运行
	ESC,//玩家按esc要退出
	KILL_BY_WALL,//蛇撞墙死
	KILL_BY_SELF //蛇撞自己死
};

//当前蛇在向哪个方向移动
enum DIRECTION
{
	UP = 1,
	DOWN,
	LEFT,
	RIGHT
};


//蛇身节点
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 GAME_STATUS status;//游戏当前的状态
	enum DIRECTION dir;//蛇当前走的方向
	//···
}Snake, * pSnake;


//修改光标位置
void SetPos(int x, int y);

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

//打印欢迎信息
void WelcomeToGame();
//创建地图
void CreateMap();
//初始化蛇
void InitSnake(pSnake ps);
//创建食物
void CreateFood(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);

snack.c

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


//修改光标位置
void SetPos(int x, int y)
{
	//获得设备句柄
	HANDLE handle = GetStdHandle(STD_OUTPUT_HANDLE);
	//根据句柄设置光标位置
	COORD pos = { x,y };
	SetConsoleCursorPosition(handle, pos);
}
//打印欢迎信息
void WelcomeToGame()
{
	//欢迎界面
	SetPos(38, 12);
	printf("欢迎来到贪吃蛇小游戏\n");
	SetPos(40, 20);
	system("pause");
	system("cls");//清空屏幕

	//操作介绍界面
	SetPos(20, 10);
	printf("用 ↑ . ↓ . ← . → 分别控制蛇的移动, F3为加速,F4为减速\n");
	SetPos(37, 12);
	printf("加速能得到更高的分数\n");
	SetPos(40, 20);
	system("pause");
	system("cls");//清空屏幕

}



//打印地图
void CreateMap()
{
	int i = 0;
	//上边界
	SetPos(0, 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 (int i = 1; i <= 25; i++)
	{
		SetPos(0, i);
		wprintf(L"%lc", WALL);
	}

	//右边界
	for (int i = 1; i <= 25; i++)
	{
		SetPos(56, i);
		wprintf(L"%lc", WALL);
	}
}



//初始化蛇
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->x = POS_X + 2 * i;
		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->dir = RIGHT;
	ps->FoodWeight = 10;
	ps->pFood = NULL;
	ps->Score = 0;
	ps->SleepTime = 200;
	ps->status = OK;
}

//创建食物
void CreateFood(pSnake ps)
{
	int x = 0, y = 0;

again:
	do
	{
		//当x不是偶数就再生成一次
		x = rand() % 53 + 2;
		y = rand() % 24 + 1;
	} while (x % 2 != 0);

	//坐标和蛇的身体的每个节点坐标比较
	pSnakeNode cur = ps->pSnake;
	while (cur)
	{
		if (x == cur->x && y == cur->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);
}


//游戏开始前的初始化
void GameStart(pSnake ps)
{
	//控制台信息设置
	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);

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

	//打印地图
	CreateMap();

	//初始化蛇
	InitSnake(ps);
	
	//创建食物
	CreateFood(ps);
}





void PrintHelpInfo()
{
	SetPos(62,15);
	printf("1. 不能撞墙,不能咬到自己");
	SetPos(62, 16);
	printf("2. 用 ↑ ↓ ← → 控制蛇的移动"); 
	SetPos(62, 17);
	printf("3. F3为加速,F4为减速");

}

void pause()
{
	while (1)
	{
		Sleep(100);
		if (KEY_PRESS(VK_SPACE))
		{
			break;
		}
	}
}


//蛇头下一步要走的坐标是否是食物
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;
	}
	//得分
	ps->Score += ps->FoodWeight;
	//释放被吃掉的食物
	free(ps->pFood);
	//新建一个食物
	CreateFood(ps);
}

//不是食物就走一步
void NotEatFood(pSnake ps, pSnakeNode pNext)
{
	//先把新节点头插进去
	pNext->next = ps->pSnake;
	ps->pSnake = pNext;
	//再释放一个蛇尾
	pSnakeNode pcur = ps->pSnake;
	while (pcur->next->next)
	{
		//这里捎带着把新蛇身打印
		SetPos(pcur->x, pcur->y);
		wprintf(L"%lc", BODY);
		pcur = pcur->next;
	}
	//先将尾节点的位置打印空白字符
	SetPos(pcur->next->x, pcur->next->y);
	printf("  ");//打印两个空格

	//最后free
	free(pcur->next);
	pcur->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 (cur->x == ps->pSnake->x && cur->y == ps->pSnake->y)
		{
			//撞到自己身上了
			ps->status = KILL_BY_SELF;
			return;
		}
		cur = cur->next;
	}
}



//蛇的移动
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;
	}

	//蛇头下一步要走的坐标是否是食物
	if (NextIsFood(ps, pNext))
	{
		//是食物就吃掉
		EatFood(ps, pNext);
	}
	else
	{
		//不是食物就走一步
		NotEatFood(ps, pNext);
	}

	//检测撞墙
	KillByWall(ps);
	//检测撞自己
	KillBySelf(ps);
}

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

	do
	{
		//当前的分数情况
		SetPos(62, 10);
		printf("总分:%d", ps->Score);
		SetPos(62, 11);
		printf("食物的分值:%02d", ps->FoodWeight);
		
		//检测按键
		//上、下、左、右、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;
			}
		}

		//睡眠一下
		Sleep(ps->SleepTime);
		
		//蛇的移动
		SnakeMove(ps);

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





//游戏结束善后的善后工作
void GameEnd(pSnake ps)
{
	SetPos(20, 11);
	switch (ps->status)
	{
	case ESC:
		printf("正常退出\n");

		break;
	case KILL_BY_WALL:
		printf("很遗憾,撞墙了!游戏结束\n");
		break;
	case KILL_BY_SELF:
		printf("很遗憾,自杀了!游戏结束\n");
		break;
	}
	//释放贪吃蛇的链表资源
	pSnakeNode cur = ps->pSnake;
	pSnakeNode del = ps->pSnake;
	while (cur)
	{
		del = cur;
		cur = cur->next;
		free(del);
	}
	ps->pSnake = NULL;

	//释放食物节点
	free(ps->pFood);
	ps->pFood = NULL;

}

test.c

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

void test()
{
	int ch;//getchar()返回的是整形ASCII码值
	do
	{
		//创建贪吃蛇游戏
		Snake snake = { 0 };
		GameStart(&snake);//游戏开始前的初始化
		GameRun(&snake);//游戏的运行过程
		GameEnd(&snake);//游戏结束善后的工作

		SetPos(25, 15);
		printf("再来一局吗?(Y/N):>");
		ch = getchar();
		getchar();//清理缓冲区的\n
	} while (ch == 'Y');
}


int main()
{
	srand((unsigned int)time(NULL));

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

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

	//控制程序退出结语的位置
	SetPos(0, 27);

	return 0;
}

8. 结语

那么到此,贪吃蛇游戏就写好了,当然,这个游戏的功能还有美观度还有待各位共同开发,如果说想要给这个游戏图形化一下,大家可以去 easyX 看看EasyX Graphics Library for C++,把这个库下载下来应该就能在VS上用了

同时,我们的C语言讲解就告一段落了,下一阶段我将着手数据结构的相关知识

相关推荐
朱一头zcy1 小时前
C语言复习第9章 字符串/字符/内存函数
c语言
此生只爱蛋1 小时前
【手撕排序2】快速排序
c语言·c++·算法·排序算法
何曾参静谧1 小时前
「C/C++」C/C++ 指针篇 之 指针运算
c语言·开发语言·c++
lulu_gh_yu2 小时前
数据结构之排序补充
c语言·开发语言·数据结构·c++·学习·算法·排序算法
~yY…s<#>4 小时前
【刷题17】最小栈、栈的压入弹出、逆波兰表达式
c语言·数据结构·c++·算法·leetcode
EricWang13586 小时前
[OS] 项目三-2-proc.c: exit(int status)
服务器·c语言·前端
我是谁??6 小时前
C/C++使用AddressSanitizer检测内存错误
c语言·c++
希言JY6 小时前
C字符串 | 字符串处理函数 | 使用 | 原理 | 实现
c语言·开发语言
午言若6 小时前
C语言比较两个字符串是否相同
c语言
Footprint_Analytics7 小时前
Footprint Analytics 助力 Sei 游戏生态增长
游戏·web3·区块链