5

贪吃蛇(C语言实现)

 2 years ago
source link: https://blog.csdn.net/chenlong_cxy/article/details/119765659
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

游戏界面当中没有打印相关的按键说明,这里先逐一列出,贪吃蛇游戏按键说明

  1. 按方向键上下左右,可以实现蛇移动方向的改变。
  2. 短时间长按方向键上下左右其中之一,可实现蛇向该方向的短时间加速移动。
  3. 按空格键可实现暂停,暂停后按任意键继续游戏。
  4. 按Esc键可直接退出游戏。
  5. 按R键可重新开始游戏。

除此之外,本游戏还拥有计分系统,可保存玩家的历史最高记录。

游戏效果展示

贪吃蛇游戏当中蛇的移动速度可以进行调整,动图当中把速度调得较慢(速度太快导致动图上蛇身显示不全),下面给出的代码当中将蛇的速度调整到了合适的位置,大家可以试试。
在这里插入图片描述

博友们可以将以下代码复制到自己的编译器当中运行:

#include <stdio.h>
#include <Windows.h>
#include <stdlib.h>
#include <time.h>
#include <conio.h>

#define ROW 22 //游戏区行数
#define COL 42 //游戏区列数

#define KONG 0 //标记空(什么也没有)
#define WALL 1 //标记墙
#define FOOD 2 //标记食物
#define HEAD 3 //标记蛇头
#define BODY 4 //标记蛇身

#define UP 72 //方向键:上
#define DOWN 80 //方向键:下
#define LEFT 75 //方向键:左
#define RIGHT 77 //方向键:右
#define SPACE 32 //暂停
#define ESC 27 //退出

//蛇头
struct Snake
{
	int len; //记录蛇身长度
	int x; //蛇头横坐标
	int y; //蛇头纵坐标
}snake;

//蛇身
struct Body
{
	int x; //蛇身横坐标
	int y; //蛇身纵坐标
}body[ROW*COL]; //开辟足以存储蛇身的结构体数组

int face[ROW][COL]; //标记游戏区各个位置的状态

//隐藏光标
void HideCursor();
//光标跳转
void CursorJump(int x, int y);
//初始化界面
void InitInterface();
//颜色设置
void color(int c);
//从文件读取最高分
void ReadGrade();
//更新最高分到文件
void WriteGrade();
//初始化蛇
void InitSnake();
//随机生成食物
void RandFood();
//判断得分与结束
void JudgeFunc(int x, int y);
//打印蛇与覆盖蛇
void DrawSnake(int flag);
//移动蛇
void MoveSnake(int x, int y);
//执行按键
void run(int x, int y);
//游戏主体逻辑函数
void Game();

int max, grade; //全局变量
int main()
{
#pragma warning (disable:4996) //消除警告
	max = 0, grade = 0; //初始化变量
	system("title 贪吃蛇"); //设置cmd窗口的名字
	system("mode con cols=84 lines=23"); //设置cmd窗口的大小
	HideCursor(); //隐藏光标
	ReadGrade(); //从文件读取最高分到max变量
	InitInterface(); //初始化界面
	InitSnake(); //初始化蛇
	srand((unsigned int)time(NULL)); //设置随机数生成起点
	RandFood(); //随机生成食物
	DrawSnake(1); //打印蛇
	Game(); //开始游戏
	return 0;
}

//隐藏光标
void HideCursor()
{
	CONSOLE_CURSOR_INFO curInfo; //定义光标信息的结构体变量
	curInfo.dwSize = 1; //如果没赋值的话,光标隐藏无效
	curInfo.bVisible = FALSE; //将光标设置为不可见
	HANDLE handle = GetStdHandle(STD_OUTPUT_HANDLE); //获取控制台句柄
	SetConsoleCursorInfo(handle, &curInfo); //设置光标信息
}
//光标跳转
void CursorJump(int x, int y)
{
	COORD pos; //定义光标位置的结构体变量
	pos.X = x; //横坐标
	pos.Y = y; //纵坐标
	HANDLE handle = GetStdHandle(STD_OUTPUT_HANDLE); //获取控制台句柄
	SetConsoleCursorPosition(handle, pos); //设置光标位置
}
//初始化界面
void InitInterface()
{
	color(6); //颜色设置为土黄色
	for (int i = 0; i < ROW; i++)
	{
		for (int j = 0; j < COL; j++)
		{
			if (j == 0 || j == COL - 1)
			{
				face[i][j] = WALL; //标记该位置为墙
				CursorJump(2 * j, i);
				printf("■");
			}
			else if (i == 0 || i == ROW - 1)
			{
				face[i][j] = WALL; //标记该位置为墙
				printf("■");
			}
			else
			{
				face[i][j] = KONG; //标记该位置为空
			}
		}
	}
	color(7); //颜色设置为白色
	CursorJump(0, ROW);
	printf("当前得分:%d", grade);
	CursorJump(COL, ROW);
	printf("历史最高得分:%d", max);
}
//颜色设置
void color(int c)
{
	SetConsoleTextAttribute(GetStdHandle(STD_OUTPUT_HANDLE), c); //颜色设置
	//注:SetConsoleTextAttribute是一个API(应用程序编程接口)
}
//从文件读取最高分
void ReadGrade()
{
	FILE* pf = fopen("贪吃蛇最高得分记录.txt", "r"); //以只读的方式打开文件
	if (pf == NULL) //打开文件失败
	{
		pf = fopen("贪吃蛇最高得分记录.txt", "w"); //以只写的方式打开文件
		fwrite(&max, sizeof(int), 1, pf); //将max写入文件(此时max为0),即将最高得分初始化为0
	}
	fseek(pf, 0, SEEK_SET); //使文件指针pf指向文件开头
	fread(&max, sizeof(int), 1, pf); //读取文件当中的最高得分到max当中
	fclose(pf); //关闭文件
	pf = NULL; //文件指针及时置空
}
//更新最高分到文件
void WriteGrade()
{
	FILE* pf = fopen("贪吃蛇最高得分记录.txt", "w"); //以只写的方式打开文件
	if (pf == NULL) //打开文件失败
	{
		printf("保存最高得分记录失败\n");
		exit(0);
	}
	fwrite(&grade, sizeof(int), 1, pf); //将本局游戏得分写入文件当中
	fclose(pf); //关闭文件
	pf = NULL; //文件指针及时置空
}
//初始化蛇
void InitSnake()
{
	snake.len = 2; //蛇的身体长度初始化为2
	snake.x = COL / 2; //蛇头位置的横坐标
	snake.y = ROW / 2; //蛇头位置的纵坐标
	//蛇身坐标的初始化
	body[0].x = COL / 2 - 1;
	body[0].y = ROW / 2;
	body[1].x = COL / 2 - 2;
	body[1].y = ROW / 2;
	//将蛇头和蛇身位置进行标记
	face[snake.y][snake.x] = HEAD;
	face[body[0].y][body[0].x] = BODY;
	face[body[1].y][body[1].x] = BODY;
}
//随机生成食物
void RandFood()
{
	int i, j;
	do
	{
		//随机生成食物的横纵坐标
		i = rand() % ROW;
		j = rand() % COL;
	} while (face[i][j] != KONG); //确保生成食物的位置为空,若不为空则重新生成
	face[i][j] = FOOD; //将食物位置进行标记
	color(12); //颜色设置为红色
	CursorJump(2 * j, i); //光标跳转到生成的随机位置处
	printf("●"); //打印食物
}
//判断得分与结束
void JudgeFunc(int x, int y)
{
	//若蛇头即将到达的位置是食物,则得分
	if (face[snake.y + y][snake.x + x] == FOOD)
	{
		snake.len++; //蛇身加长
		grade += 10; //更新当前得分
		color(7); //颜色设置为白色
		CursorJump(0, ROW);
		printf("当前得分:%d", grade); //重新打印当前得分
		RandFood(); //重新随机生成食物
	}
	//若蛇头即将到达的位置是墙或者蛇身,则游戏结束
	else if (face[snake.y + y][snake.x + x] == WALL || face[snake.y + y][snake.x + x] == BODY)
	{
		Sleep(1000); //留给玩家反应时间
		system("cls"); //清空屏幕
		color(7); //颜色设置为白色
		CursorJump(2 * (COL / 3), ROW / 2 - 3);
		if (grade > max)
		{
			printf("恭喜你打破最高记录,最高记录更新为%d", grade);
			WriteGrade();
		}
		else if (grade == max)
		{
			printf("与最高记录持平,加油再创佳绩", grade);
		}
		else
		{
			printf("请继续加油,当前与最高记录相差%d", max - grade);
		}
		CursorJump(2 * (COL / 3), ROW / 2);
		printf("GAME OVER");
		while (1) //询问玩家是否再来一局
		{
			char ch;
			CursorJump(2 * (COL / 3), ROW / 2 + 3);
			printf("再来一局?(y/n):");
			scanf("%c", &ch);
			if (ch == 'y' || ch == 'Y')
			{
				system("cls");
				main();
			}
			else if (ch == 'n' || ch == 'N')
			{
				CursorJump(2 * (COL / 3), ROW / 2 + 5);
				exit(0);
			}
			else
			{
				CursorJump(2 * (COL / 3), ROW / 2 + 5);
				printf("选择错误,请再次选择");
			}
		}
	}
}
//打印蛇与覆盖蛇
void DrawSnake(int flag)
{
	if (flag == 1) //打印蛇
	{
		color(10); //颜色设置为绿色
		CursorJump(2 * snake.x, snake.y);
		printf("■"); //打印蛇头
		for (int i = 0; i < snake.len; i++)
		{
			CursorJump(2 * body[i].x, body[i].y);
			printf("□"); //打印蛇身
		}
	}
	else //覆盖蛇
	{
		if (body[snake.len - 1].x != 0) //防止len++后将(0, 0)位置的墙覆盖
		{
			//将蛇尾覆盖为空格即可
			CursorJump(2 * body[snake.len - 1].x, body[snake.len - 1].y);
			printf("  ");
		}
	}
}
//移动蛇
void MoveSnake(int x, int y)
{
	DrawSnake(0); //先覆盖当前所显示的蛇
	face[body[snake.len - 1].y][body[snake.len - 1].x] = KONG; //蛇移动后蛇尾重新标记为空
	face[snake.y][snake.x] = BODY; //蛇移动后蛇头的位置变为蛇身
	//蛇移动后各个蛇身位置坐标需要更新
	for (int i = snake.len - 1; i > 0; i--)
	{
		body[i].x = body[i - 1].x;
		body[i].y = body[i - 1].y;
	}
	//蛇移动后蛇头位置信息变为第0个蛇身的位置信息
	body[0].x = snake.x;
	body[0].y = snake.y;
	//蛇头的位置更改
	snake.x = snake.x + x;
	snake.y = snake.y + y;
	DrawSnake(1); //打印移动后的蛇
}
//执行按键
void run(int x, int y)
{
	int t = 0;
	while (1)
	{
		if (t == 0)
			t = 3000; //这里t越小,蛇移动速度越快(可以根据次设置游戏难度)
		while (--t)
		{
			if (kbhit() != 0) //若键盘被敲击,则退出循环
				break;
		}
		if (t == 0) //键盘未被敲击
		{
			JudgeFunc(x, y); //判断到达该位置后,是否得分与游戏结束
			MoveSnake(x, y); //移动蛇
		}
		else //键盘被敲击
		{
			break; //返回Game函数读取键值
		}
	}
}
//游戏主体逻辑函数
void Game()
{
	int n = RIGHT; //开始游戏时,默认向后移动
	int tmp = 0; //记录蛇的移动方向
	goto first; //第一次进入循环先向默认方向前进
	while (1)
	{
		n = getch(); //读取键值
		//在执行前,需要对所读取的按键进行调整
		switch (n)
		{
		case UP:
		case DOWN: //如果敲击的是“上”或“下”
			if (tmp != LEFT&&tmp != RIGHT) //并且上一次蛇的移动方向不是“左”或“右”
			{
				n = tmp; //那么下一次蛇的移动方向设置为上一次蛇的移动方向
			}
			break;
		case LEFT:
		case RIGHT: //如果敲击的是“左”或“右”
			if (tmp != UP&&tmp != DOWN) //并且上一次蛇的移动方向不是“上”或“下”
			{
				n = tmp; //那么下一次蛇的移动方向设置为上一次蛇的移动方向
			}
		case SPACE:
		case ESC:
		case 'r':
		case 'R':
			break; //这四个无需调整
		default:
			n = tmp; //其他键无效,默认为上一次蛇移动的方向
			break;
		}
	first: //第一次进入循环先向默认方向前进
		switch (n)
		{
		case UP: //方向键:上
			run(0, -1); //向上移动(横坐标偏移为0,纵坐标偏移为-1)
			tmp = UP; //记录当前蛇的移动方向
			break;
		case DOWN: //方向键:下
			run(0, 1); //向下移动(横坐标偏移为0,纵坐标偏移为1)
			tmp = DOWN; //记录当前蛇的移动方向
			break;
		case LEFT: //方向键:左
			run(-1, 0); //向左移动(横坐标偏移为-1,纵坐标偏移为0)
			tmp = LEFT; //记录当前蛇的移动方向
			break;
		case RIGHT: //方向键:右
			run(1, 0); //向右移动(横坐标偏移为1,纵坐标偏移为0)
			tmp = RIGHT; //记录当前蛇的移动方向
			break;
		case SPACE: //暂停
			system("pause>nul"); //暂停后按任意键继续
			break;
		case ESC: //退出
			system("cls"); //清空屏幕
			color(7); //颜色设置为白色
			CursorJump(COL - 8, ROW / 2);
			printf("  游戏结束  ");
			CursorJump(COL - 8, ROW / 2 + 2);
			exit(0);
		case 'r':
		case 'R': //重新开始
			system("cls"); //清空屏幕
			main(); //重新执行主函数
		}
	}
}

游戏代码详解

游戏框架构建

首先定义游戏界面的大小,定义游戏区行数和列数。

#define ROW 22 //游戏区行数
#define COL 42 //游戏区列数

这里将蛇活动的区域称为游戏区,将分数提示的区域称为提示区(提示区占一行)。
在这里插入图片描述
此外,我们还需要两个结构体用于表示蛇头和蛇身。蛇头结构体当中存储着当前蛇身的长度以及蛇头的位置坐标。

//蛇头
struct Snake
{
	int len; //记录蛇身长度
	int x; //蛇头横坐标
	int y; //蛇头纵坐标
}snake;

蛇身结构体当中存储着该段蛇身的位置坐标。

//蛇身
struct Body
{
	int x; //蛇身横坐标
	int y; //蛇身纵坐标
}body[ROW*COL]; //开辟足以存储蛇身的结构体数组

同时我们需要一个二维数组来标记游戏区各个位置的状态(空、墙、食物、蛇头以及蛇身)。

int face[ROW][COL]; //标记游戏区各个位置的状态

为了增加代码的可读性,最好运用宏来定义各个位置的状态,而不是在代码中用干巴巴的数字对各个位置的状态进行切换。

#define KONG 0 //标记空(什么也没有)
#define WALL 1 //标记墙
#define FOOD 2 //标记食物
#define HEAD 3 //标记蛇头
#define BODY 4 //标记蛇身

当然,为了代码的可读性,我们最好也将需要用到的按键的键值用宏进行定义。

#define UP 72 //方向键:上
#define DOWN 80 //方向键:下
#define LEFT 75 //方向键:左
#define RIGHT 77 //方向键:右
#define SPACE 32 //暂停
#define ESC 27 //退出

隐藏光标比较简单,定义一个光标信息的结构体变量,然后对光标信息进行赋值,最后用这个光标信息的结构体变量进行光标信息设置即可。

//隐藏光标
void HideCursor()
{
	CONSOLE_CURSOR_INFO curInfo; //定义光标信息的结构体变量
	curInfo.dwSize = 1; //如果没赋值的话,光标隐藏无效
	curInfo.bVisible = FALSE; //将光标设置为不可见
	HANDLE handle = GetStdHandle(STD_OUTPUT_HANDLE); //获取控制台句柄
	SetConsoleCursorInfo(handle, &curInfo); //设置光标信息
}

光标跳转,也就是让光标跳转到指定位置进行输出。与隐藏光标的操作步骤类似,先定义一个光标位置的结构体变量,然后设置光标的横纵坐标,最后用这个光标位置的结构体变量进行光标位置设置即可。

//光标跳转
void CursorJump(int x, int y)
{
	COORD pos; //定义光标位置的结构体变量
	pos.X = x; //横坐标
	pos.Y = y; //纵坐标
	HANDLE handle = GetStdHandle(STD_OUTPUT_HANDLE); //获取控制台句柄
	SetConsoleCursorPosition(handle, pos); //设置光标位置
}

初始化界面

初始化界面完成游戏区“墙”的打印,和提示区的打印即可。
在这里插入图片描述
在打印过程中需要注意两点

  1. 在cmd窗口中一个小方块占两个单位的横坐标,一个单位的纵坐标。
  2. 光标跳转函数CursorJump接收的是光标将要跳至位置的横纵坐标。

例如,要用CursorJump函数跳转至 i 行 j 列(以一个小方块为一个单位),就等价于让光标跳转至坐标(2*j,i)处。

//初始化界面
void InitInterface()
{
	color(6); //颜色设置为土黄色
	for (int i = 0; i < ROW; i++)
	{
		for (int j = 0; j < COL; j++)
		{
			if (j == 0 || j == COL - 1)
			{
				face[i][j] = WALL; //标记该位置为墙
				CursorJump(2 * j, i);
				printf("■");
			}
			else if (i == 0 || i == ROW - 1)
			{
				face[i][j] = WALL; //标记该位置为墙
				printf("■");
			}
			else
			{
				face[i][j] = KONG; //标记该位置为空
			}
		}
	}
	color(7); //颜色设置为白色
	CursorJump(0, ROW);
	printf("当前得分:%d", grade);
	CursorJump(COL, ROW);
	printf("历史最高得分:%d", max);
}

注意: 在初始化界面的同时,记得对游戏区相应位置的状态进行标记。

颜色设置函数的作用是,将此后输出的内容颜色都更为所指定的颜色,接收的参数c是颜色代码,十进制颜色代码表如下:
在这里插入图片描述

//颜色设置
void color(int c)
{
	SetConsoleTextAttribute(GetStdHandle(STD_OUTPUT_HANDLE), c); //颜色设置
	//注:SetConsoleTextAttribute是一个API(应用程序编程接口)
}

设置颜色函数在其头文件当中的声明如下:
在这里插入图片描述

初始化蛇时将蛇身的长度初始化为2,蛇头的起始位置在游戏区的中央,蛇头向右依次是第0个蛇身、第1个蛇身。
在这里插入图片描述
在初始化蛇的信息后,记得对游戏区该位置的状态进行标记。

//初始化蛇
void InitSnake()
{
	snake.len = 2; //蛇的身体长度初始化为2
	snake.x = COL / 2; //蛇头位置的横坐标
	snake.y = ROW / 2; //蛇头位置的纵坐标
	//蛇身坐标的初始化
	body[0].x = COL / 2 - 1;
	body[0].y = ROW / 2;
	body[1].x = COL / 2 - 2;
	body[1].y = ROW / 2;
	//将蛇头和蛇身位置进行标记
	face[snake.y][snake.x] = HEAD;
	face[body[0].y][body[0].x] = BODY;
	face[body[1].y][body[1].x] = BODY;
}

随机生成食物

随机在游戏区生成食物,需要对生成后的坐标进行判断,只有该位置为空才能在此生成食物,否则需要重新生成坐标。食物坐标确定后,需要对游戏区该位置的状态进行标记。

//随机生成食物
void RandFood()
{
	int i, j;
	do
	{
		//随机生成食物的横纵坐标
		i = rand() % ROW;
		j = rand() % COL;
	} while (face[i][j] != KONG); //确保生成食物的位置为空,若不为空则重新生成
	face[i][j] = FOOD; //将食物位置进行标记
	color(12); //颜色设置为红色
	CursorJump(2 * j, i); //光标跳转到生成的随机位置处
	printf("●"); //打印食物
}

打印蛇与覆盖蛇

打印蛇和覆盖蛇这里直接使用一个函数来实现,若传入参数flag为1,则打印蛇;若传入参数为0,则用空格覆盖蛇。
打印蛇:

  1. 先根据结构体变量snake获取蛇头的坐标,到相应位置打印蛇头。
  2. 然后根据结构体数组body依次获取蛇身的坐标,到相应位置进行打印即可。

覆盖蛇:

  1. 用空格覆盖最后一段蛇身即可。

但需要注意在覆盖前判断覆盖的位置是否为(0,0)位置,因为当得分后蛇身长度增加,需要覆盖当前的蛇(进而打印长度增加后的蛇),而此时新加蛇身还未进行赋值(编译器一般默认初始化为0),我们根据最后一段蛇身获取到的坐标便是(0,0),则会用空格对(0,0)位置的墙进行覆盖,需要看完后面的移动蛇函数的实现后再进行理解。(也可以先将该判断去掉,观察蛇吃到食物后(0,0)位置墙的变化再进行分析)

//打印蛇与覆盖蛇
void DrawSnake(int flag)
{
	if (flag == 1) //打印蛇
	{
		color(10); //颜色设置为绿色
		CursorJump(2 * snake.x, snake.y);
		printf("■"); //打印蛇头
		for (int i = 0; i < snake.len; i++)
		{
			CursorJump(2 * body[i].x, body[i].y);
			printf("□"); //打印蛇身
		}
	}
	else //覆盖蛇
	{
		if (body[snake.len - 1].x != 0) //防止len++后将(0, 0)位置的墙覆盖
		{
			//将蛇尾覆盖为空格即可
			CursorJump(2 * body[snake.len - 1].x, body[snake.len - 1].y);
			printf("  ");
		}
	}
}

移动蛇函数的作用就是先覆盖当前所显示的蛇,然后再打印移动后的蛇。

参数说明:

  • x:蛇移动后的横坐标相对于当前蛇的横坐标的变化。
  • y:蛇移动后的纵坐标相对于当前蛇的纵坐标的变化。

蛇移动后,各种信息需要变化:

  1. 最后一段蛇身在游戏区当中需要被重新标记为空。
  2. 蛇头位置在游戏区当中需要被重新标记为蛇身。
  3. 存储蛇身坐标信息的结构体数组body当中,需要将第i段蛇身的坐标信息更新为第i-1段蛇身的坐标信息,而第0段,即第一段蛇身的坐标信息需要更新为当前蛇头的坐标信息。
  4. 蛇头的坐标信息需要根据传入的参数x和y,进行重新计算。
//移动蛇
void MoveSnake(int x, int y)
{
	DrawSnake(0); //先覆盖当前所显示的蛇
	face[body[snake.len - 1].y][body[snake.len - 1].x] = KONG; //蛇移动后蛇尾重新标记为空
	face[snake.y][snake.x] = BODY; //蛇移动后蛇头的位置变为蛇身
	//蛇移动后各个蛇身位置坐标需要更新
	for (int i = snake.len - 1; i > 0; i--)
	{
		body[i].x = body[i - 1].x;
		body[i].y = body[i - 1].y;
	}
	//蛇移动后蛇头位置信息变为第0个蛇身的位置信息
	body[0].x = snake.x;
	body[0].y = snake.y;
	//蛇头的位置更改
	snake.x = snake.x + x;
	snake.y = snake.y + y;
	DrawSnake(1); //打印移动后的蛇
}

游戏主体逻辑函数

主体逻辑:

  1. 首先第一次进入该函数,默认蛇向右移动,进而执行run函数。
  2. 直到键盘被敲击,再从run函数返回到Game函数进行按键读取。
  3. 读取到键值后需要对读取到的按键进行调整(这是必要的)。
  4. 调整后再进行按键执行,然后再进行按键读取,如此循环进行。

按键调整机制:

  1. 如果敲击的是“上”或“下”键,并且上一次蛇的移动方向不是“左”或“右”,那么将下一次蛇的移动方向设置为上一次蛇的移动方向,即移动方向不变。
  2. 如果敲击的是“左”或“右”键,并且上一次蛇的移动方向不是“上”或“下”,那么将下一次蛇的移动方向设置为上一次蛇的移动方向,即移动方向不变。
  3. 如果敲击的按键是空格、Esc、r或是R,则不作调整。
  4. 其余按键无效,下一次蛇的移动方向设置为上一次蛇的移动方向,即移动方向不变。
//游戏主体逻辑函数
void Game()
{
	int n = RIGHT; //开始游戏时,默认向后移动
	int tmp = 0; //记录蛇的移动方向
	goto first; //第一次进入循环先向默认方向前进
	while (1)
	{
		n = getch(); //读取键值
		//在执行前,需要对所读取的按键进行调整
		switch (n)
		{
		case UP:
		case DOWN: //如果敲击的是“上”或“下”
			if (tmp != LEFT&&tmp != RIGHT) //并且上一次蛇的移动方向不是“左”或“右”
			{
				n = tmp; //那么下一次蛇的移动方向设置为上一次蛇的移动方向
			}
			break;
		case LEFT:
		case RIGHT: //如果敲击的是“左”或“右”
			if (tmp != UP&&tmp != DOWN) //并且上一次蛇的移动方向不是“上”或“下”
			{
				n = tmp; //那么下一次蛇的移动方向设置为上一次蛇的移动方向
			}
		case SPACE:
		case ESC:
		case 'r':
		case 'R':
			break; //这四个无需调整
		default:
			n = tmp; //其他键无效,默认为上一次蛇移动的方向
			break;
		}
	first: //第一次进入循环先向默认方向前进
		switch (n)
		{
		case UP: //方向键:上
			run(0, -1); //向上移动(横坐标偏移为0,纵坐标偏移为-1)
			tmp = UP; //记录当前蛇的移动方向
			break;
		case DOWN: //方向键:下
			run(0, 1); //向下移动(横坐标偏移为0,纵坐标偏移为1)
			tmp = DOWN; //记录当前蛇的移动方向
			break;
		case LEFT: //方向键:左
			run(-1, 0); //向左移动(横坐标偏移为-1,纵坐标偏移为0)
			tmp = LEFT; //记录当前蛇的移动方向
			break;
		case RIGHT: //方向键:右
			run(1, 0); //向右移动(横坐标偏移为1,纵坐标偏移为0)
			tmp = RIGHT; //记录当前蛇的移动方向
			break;
		case SPACE: //暂停
			system("pause>nul"); //暂停后按任意键继续
			break;
		case ESC: //退出
			system("cls"); //清空屏幕
			color(7); //颜色设置为白色
			CursorJump(COL - 8, ROW / 2);
			printf("  游戏结束  ");
			CursorJump(COL - 8, ROW / 2 + 2);
			exit(0);
		case 'r':
		case 'R': //重新开始
			system("cls"); //清空屏幕
			main(); //重新执行主函数
		}
	}
}

参数说明:

  • x:蛇移动后的横坐标相对于当前蛇的横坐标的变化。
  • y:蛇移动后的纵坐标相对于当前蛇的纵坐标的变化。

给定一定的时间间隔,若在该时间间隔内键盘被敲击,则退出run函数,返回Game函数进行按键读取。若未被敲击,则先判断蛇到达移动后的位置后是否得分或是游戏结束,然后再移动蛇的位置。
若键盘一直未被敲击,则就会一直执行run函数当中的while函数,蛇就会一直朝一个方向移动,直到游戏结束。

//执行按键
void run(int x, int y)
{
	int t = 0;
	while (1)
	{
		if (t == 0)
			t = 3000; //这里t越小,蛇移动速度越快(可以根据次设置游戏难度)
		while (--t)
		{
			if (kbhit() != 0) //若键盘被敲击,则退出循环
				break;
		}
		if (t == 0) //键盘未被敲击
		{
			JudgeFunc(x, y); //判断到达该位置后,是否得分与游戏结束
			MoveSnake(x, y); //移动蛇
		}
		else //键盘被敲击
		{
			break; //返回Game函数读取键值
		}
	}
}

判断得分与结束

判断得分:
若蛇头即将到达的位置是食物,则得分。得分后需要将蛇身加长,并且更新当前得分,除此之外,还需要重新生成食物。

判断结束:
若蛇头即将到达的位置是墙或者蛇身,则游戏结束。游戏结束后比较本局得分和历史最高得分,给出相应的提示语句,并且询问玩家是否再来一局,可自由发挥。

//判断得分与结束
void JudgeFunc(int x, int y)
{
	//若蛇头即将到达的位置是食物,则得分
	if (face[snake.y + y][snake.x + x] == FOOD)
	{
		snake.len++; //蛇身加长
		grade += 10; //更新当前得分
		color(7); //颜色设置为白色
		CursorJump(0, ROW);
		printf("当前得分:%d", grade); //重新打印当前得分
		RandFood(); //重新随机生成食物
	}
	//若蛇头即将到达的位置是墙或者蛇身,则游戏结束
	else if (face[snake.y + y][snake.x + x] == WALL || face[snake.y + y][snake.x + x] == BODY)
	{
		Sleep(1000); //留给玩家反应时间
		system("cls"); //清空屏幕
		color(7); //颜色设置为白色
		CursorJump(2 * (COL / 3), ROW / 2 - 3);
		if (grade > max)
		{
			printf("恭喜你打破最高记录,最高记录更新为%d", grade);
			WriteGrade();
		}
		else if (grade == max)
		{
			printf("与最高记录持平,加油再创佳绩", grade);
		}
		else
		{
			printf("请继续加油,当前与最高记录相差%d", max - grade);
		}
		CursorJump(2 * (COL / 3), ROW / 2);
		printf("GAME OVER");
		while (1) //询问玩家是否再来一局
		{
			char ch;
			CursorJump(2 * (COL / 3), ROW / 2 + 3);
			printf("再来一局?(y/n):");
			scanf("%c", &ch);
			if (ch == 'y' || ch == 'Y')
			{
				system("cls");
				main();
			}
			else if (ch == 'n' || ch == 'N')
			{
				CursorJump(2 * (COL / 3), ROW / 2 + 5);
				exit(0);
			}
			else
			{
				CursorJump(2 * (COL / 3), ROW / 2 + 5);
				printf("选择错误,请再次选择");
			}
		}
	}
}

注意: 若本局得分大于历史最高得分,需要更新最高分到文件。

从文件读取最高分

首先需要使用fopen函数打开“贪吃蛇最高得分记录.txt”文件,若是第一次运行该代码,则会自动创建该文件,并将历史最高记录设置为0,之后再读取文件当中的历史最高记录存储在max变量当中,并关闭文件即可。
在这里插入图片描述

//从文件读取最高分
void ReadGrade()
{
	FILE* pf = fopen("贪吃蛇最高得分记录.txt", "r"); //以只读的方式打开文件
	if (pf == NULL) //打开文件失败
	{
		pf = fopen("贪吃蛇最高得分记录.txt", "w"); //以只写的方式打开文件
		fwrite(&max, sizeof(int), 1, pf); //将max写入文件(此时max为0),即将最高得分初始化为0
	}
	fseek(pf, 0, SEEK_SET); //使文件指针pf指向文件开头
	fread(&max, sizeof(int), 1, pf); //读取文件当中的最高得分到max当中
	fclose(pf); //关闭文件
	pf = NULL; //文件指针及时置空
}

更新最高分到文件

首先使用fopen函数打开“贪吃蛇最高得分记录.txt”,然后将本局游戏的分数grade写入文件当中即可(覆盖式)。

//更新最高分到文件
void WriteGrade()
{
	FILE* pf = fopen("贪吃蛇最高得分记录.txt", "w"); //以只写的方式打开文件
	if (pf == NULL) //打开文件失败
	{
		printf("保存最高得分记录失败\n");
		exit(0);
	}
	fwrite(&grade, sizeof(int), 1, pf); //将本局游戏得分写入文件当中
	fclose(pf); //关闭文件
	pf = NULL; //文件指针及时置空
}

有了以上函数的支撑,写出主函数是相当简单的,但需要注意以下三点:

  1. 全局变量grade需要在主函数内初始化为0,不能在全局范围初始化为0,因为当玩家按下R键进行重玩时我们需要将当前分数grade重新设置为0。
  2. 随机数的生成起点建议设置在主函数当中。
  3. 主函数当中的#pragma语句是用于消除类似以下警告的:

在这里插入图片描述

int max, grade; //全局变量
int main()
{
#pragma warning (disable:4996) //消除警告
	max = 0, grade = 0; //初始化变量
	system("title 贪吃蛇"); //设置cmd窗口的名字
	system("mode con cols=84 lines=23"); //设置cmd窗口的大小
	HideCursor(); //隐藏光标
	ReadGrade(); //从文件读取最高分到max变量
	InitInterface(); //初始化界面
	InitSnake(); //初始化蛇
	srand((unsigned int)time(NULL)); //设置随机数生成起点
	RandFood(); //随机生成食物
	DrawSnake(1); //打印蛇
	Game(); //开始游戏
	return 0;
}

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK