C语言综合练习6:制作贪吃蛇

news2024/11/29 12:33:05

1 初始化界面

因为还没学QT,我们就使用终端界面替代。
这里我们假设界面中没有障碍物,我们只需要设定界面的高宽就行,这是蛇的移动范围,我们可以写两个宏来规定界面的高宽
新建一个snake.c的文件

#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>

#define WIDE  60
#define HIGH  20

void init_ui()
{
	for (int i = 0; i < HIGH; i++)
	{

		for (int j = 0; j < WIDE; j++)
		{
			printf("#");

		}
		printf("\n");
	}
}

新建一个名为main.c的文件,作为测试用,内容如下:

int main() {
	init_ui();
	return 0;
}

输出
在这里插入图片描述

2 初始化状态

蛇分为蛇头和蛇身,假设最开始的时候,蛇的长度只有两节,一节是蛇头,一节是蛇身。
要把蛇打印到界面上,那么先知道蛇头和蛇身的坐标,这里我们定义一个结构体来保存蛇的每一节的坐标

typedef struct _position
{
	int x;
	int y;
}POSITION;

x和y的增长方向如下图所示
在这里插入图片描述

任意时刻,布局中除了有蛇,还有食物,我们可以把蛇和食物都放进同一结构体里

typedef struct _status
{
	POSITION list[WIDE * HIGH];		//蛇的最大长度为WIDE * HIGH,数组的每个元素都是POSITION类型
	int snake_size;					//蛇的长度
	POSITION food_position;			//食物位置
}STATUS;

现在要定义一个生成食物的函数,因为它是在界面中随机产生,所以我们需要使用随机化函数

void generate_food(STATUS* status)
{
	srand(time(NULL));			//设置随机种子

	//初始化食物
	status->food_position.x = rand() % WIDE;
	status->food_position.y = rand() % HIGH;
}

现在我们可以初始化状态了

void init_status(STATUS* status) {
	//蛇长
	status->snake_size = 2;

	//蛇头
	status->list[0].x = WIDE / 2;
	status->list[0].y = HIGH / 2;

	//蛇身
	status->list[1].x = WIDE / 2 + 1;
	status->list[1].y = HIGH / 2;

	//初始化食物位置
	generate_food(status);
}

3 设置光标位置

在Windows.h文件中,定义了一个名为COORD的类型,内容如下:

typedef struct _COORD {
    SHORT X;
    SHORT Y;
} COORD;

这个类型的变量可以设置光标位置

#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
#include<Windows.h>
int main() {
	COORD coord;

	//行号和列号都是从0开始
	coord.X = 5;			//第6列
	coord.Y = 10;			//第11行
	init_ui();

	//设置光标在第11行、第6列
	SetConsoleCursorPosition(GetStdHandle(STD_OUTPUT_HANDLE), coord);

	//在光标位置打印指定字符串
	printf("12345");

	system("pause");
	return 0;
}

控制台输出
在这里插入图片描述

4 将状态显示

有了COORD,我们在打印食物和蛇的时候就能轻松很多。因为光标的位置经常要设置,所以我们可以在状态结构体中插入一个COORD类型的成员变量,新的结构体如下:

typedef struct _status
{
	POSITION list[WIDE * HIGH];		//蛇的最大长度为WIDE * HIGH,数组的每个元素都是POSITION类型
	int snake_size;					//蛇的长度
	POSITION food_position;			//食物位置
	COORD coord;					//便于设置光标
}STATUS;

我们建立一个显示函数,把蛇和食物打印出来

void show_ui(STATUS* status)
{
	//显示食物
	status->coord.X = status->food_position.x;
	status->coord.Y = status->food_position.y;
	SetConsoleCursorPosition(GetStdHandle(STD_OUTPUT_HANDLE), status->coord);
	printf("#");

	//显示蛇
	for (int i = 0; i < status->snake_size; i++)
	{
		status->coord.X = status->list[i].x;
		status->coord.Y = status->list[i].y;
		SetConsoleCursorPosition(GetStdHandle(STD_OUTPUT_HANDLE), status->coord);

		if (0 == i) 
			printf("@");	//打印蛇头
		else
			printf("*");	//打印蛇身
	}
}

测试函数如下:

int main() {
	STATUS* status = (STATUS*)malloc(sizeof(STATUS));
	init_status(status);
	show_ui(status);
	system("pause");
	return 0;
}

输出
在这里插入图片描述

5 根据蛇的方向更新蛇的位置

蛇是移动的,并且会长大的,所以我们需要及时更新蛇的位置。

为了能够更新谁的位置,我们需要一对变量来规定蛇头移动的方向,可以在状态结构体中增加两个变量

typedef struct _status
{
	POSITION list[WIDE * HIGH];		//蛇的最大长度为WIDE * HIGH,数组的每个元素都是POSITION类型
	int snake_size;					//蛇的长度
	POSITION food_position;			//食物位置
	COORD coord;
	int dx, dy;						//蛇头移动方向
}STATUS;

相应地,需要修改状态初始化函数:

void init_status(STATUS* status) {
	//蛇长
	status->snake_size = 2;

	//蛇头
	status->list[0].x = WIDE / 2;
	status->list[0].y = HIGH / 2;

	//蛇身
	status->list[1].x = WIDE / 2 + 1;
	status->list[1].y = HIGH / 2;

	//蛇头移动方向
	status->dx = -1;
	status->dy = 0;

	//初始化食物位置
	generate_food(status);
}

此时,我们可以根据dx和dy更新蛇的位置了

void move_snake(STATUS* status)
{
	//更新蛇身的坐标
	for (int i = status->snake_size - 1; i >= 1; i--)
	{
		//数组status->list的每一个元素都是结构体变量,因此可以直接赋值
		status->list[i] = status->list[i - 1];	
	}

	//更新蛇头的坐标
	status->list[0].x += status->dx;
	status->list[0].y += status->dy;
}

测试代码如下:

#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
#include<Windows.h>
int main()
{
	STATUS* status = (STATUS*)malloc(sizeof(STATUS));
	init_status(status);
	while (1)
	{
		system("cls");			//清屏
		show_ui(status);
		Sleep(300);				//睡眠300ms(Windows系统中)
		move_snake(status);		//更新蛇的位置
	}
	
	system("pause");
	return 0;
}

这里必须先清屏后显示,否则清屏后延迟300ms,导致看到的屏幕一直是清屏状态,这里有时间可以自己实验一下。

好了,我们的贪吃蛇终于能跑了,但由于我还不知道如何在这里插入gif动图,所以这里就不贴输出了

6 从键盘获得按键信息

既然是游戏,必然需要通过键盘输入获得信息,可以使用下面这段代码从键盘获取信息,当按下键盘时,进入while循环,松开后退出循环

//判断是否按下按键
#include <conio.h>
char  key;
while (_kbhit()) //判断是否按下按键,按下不等于0 
{
	key = _getch();
}

上面的程序需要放在循环里面,因为程序一瞬间就执行完了,while循环不会停下来等你

我们可以测试一下:

#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
#include <conio.h>
int main()
{
	char  key;
	int is_break = 0;
	while (1)
	{
		while (_kbhit()) //判断是否按下按键,按下不等于0 
		{
			key = _getch();
			is_break = 1;
			break;
		}
		if (is_break)
			break;
	}
	printf("%c\n", key);
	return 0;
}

7 使用键盘控制蛇前进的方向

有了_kbhit()_getch(),现在就能用键盘控制蛇的方向了,写一个来实现键盘控制方向

void control_snake(STATUS* status)
{
	char  key = 0;		//这里必须初始化,因为可能不会进入while循环中,导致key未赋值,从而在switch语句中报错
	while (_kbhit())	//判断是否按下按键,按下不等于0 
	{
		key = _getch();
	}

	//使用wsad分别控制上下左右,其它按键无效
	switch (key)
	{
	case 'a':
		status->dx = -1;
		status->dy = 0;
		break;
	case 'w':
		status->dx = 0;
		status->dy = -1;
		break;
	case 's':
		status->dx = 0;
		status->dy = 1;
		break;
	case 'd':
		status->dx = 1;
		status->dy = 0;
		break;
	}
}

测试程序如下:

#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
#include<Windows.h>
int main()
{
	STATUS* status = (STATUS*)malloc(sizeof(STATUS));
	init_status(status);
	while (1)
	{
		system("cls");			//清屏
		show_ui(status);
		Sleep(300);				//睡眠300ms(Windows系统中)
		control_snake(status);	//键盘控制蛇的方向
		move_snake(status);		//更新蛇的位置
	}
	
	system("pause");
	return 0;
}

我们终于可以控制蛇前进的方向了,但这个程序还是有bug的,因为我们这个贪吃蛇居然还能掉头,所以必须修改control_snake,使其不能掉头

void control_snake(STATUS* status)
{
	char  key = 0;		//这里必须初始化,因为可能不会进入while循环中,导致key未赋值,从而在switch语句中报错
	while (_kbhit())	//判断是否按下按键,按下不等于0 
	{
		key = _getch();
	}

	//使用wsad分别控制上下左右,其它按键无效
	switch (key)
	{
	case 'a':
		if (1 == status->dx && 0 == status->dy)		//防止出现调头
			break;
		else
		{
			status->dx = -1;
			status->dy = 0;
			break;
		}
	case 'w':
		if (1 == status->dy) //status->dy和status->dx中,有且只有一个0,因此只需要判断一个
			break;
		else
		{
			status->dx = 0;
			status->dy = -1;
			break;
		}
	case 's':
		if (-1 == status->dy)
			break;
		else
		{
			status->dx = 0;
			status->dy = 1;
			break;
		}
	case 'd':
		if (-1 == status->dx)
			break;
		else
		{
			status->dx = 1;
			status->dy = 0;
			break;
		}
	}
}

测试程序同上,这里不再赘述

8 游戏得分

既然是游戏,就有评价标准,贪吃蛇通过吃了多少个食物来衡量得分。我们需要在状态结构体定义中加入分数变量

typedef struct _status
{
	POSITION list[WIDE * HIGH];		//蛇的最大长度为WIDE * HIGH,数组的每个元素都是POSITION类型
	int snake_size;					//蛇的长度
	POSITION food_position;			//食物位置
	COORD coord;
	int dx, dy;						//蛇头移动方向
	int score;						//游戏得分
}STATUS;

相应的也要修改状态初始化函数

void init_status(STATUS* status) {
	//蛇长
	status->snake_size = 2;

	//蛇头
	status->list[0].x = WIDE / 2;
	status->list[0].y = HIGH / 2;

	//蛇身
	status->list[1].x = WIDE / 2 + 1;
	status->list[1].y = HIGH / 2;

	//蛇头移动方向
	status->dx = -1;
	status->dy = 0;

	//游戏得分
	status->score = 0;

	//初始化食物位置
	generate_food(status);
}

9 检测蛇是否碰到墙

检测碰到墙,可以通过蛇头是否超出边界来判断,这里我们定义一个检测越界的函数

int is_out_range(STATUS* status)
{
	int ret;
	if (status->list[0].x >= 0 && status->list[0].x < WIDE &&
		status->list[0].y >= 0 && status->list[0].y < HIGH)
		ret = 0;
	else
		ret = 1;

	return ret;
}

注意,因为食物的位置,横纵坐标都有可能是0,因此0不能判定为越界,所以要取>=0

测试代码

int main() {
	STATUS* status = (STATUS*)malloc(sizeof(STATUS));
	init_status(status);
	while (1)
	{
		system("cls");			//清屏
		show_ui(status);		
		Sleep(300);				//睡眠300ms(Windows系统中)
		control_snake(status);	//键盘控制蛇的方向
		move_snake(status);		//更新蛇的位置
		if (is_out_range(status))
			break;
	}
	printf("游戏结束,得分为%d\n", status->score);
	system("pause");
	return 0;
}

输出
在这里插入图片描述

现在可以检测越界,并在游戏结束后计算得分,但打印得分的位置有点尴尬,显示完蛇身之后,光标就在蛇最后一节的右边,于是就在这个位置上继续打印。

对测试代码进行如下修改:

int main() {
	STATUS* status = (STATUS*)malloc(sizeof(STATUS));
	init_status(status);
	while (1)
	{
		system("cls");			//清屏
		show_ui(status);		
		Sleep(300);				//睡眠300ms(Windows系统中)
		control_snake(status);	//键盘控制蛇的方向
		move_snake(status);		//更新蛇的位置
		if (is_out_range(status))	//判断蛇头是否越界
			break;
		
	}

	//重新设定光标位置,方面打印得分
	status->coord.X = 5;
	status->coord.Y = HIGH + 1;
	SetConsoleCursorPosition(GetStdHandle(STD_OUTPUT_HANDLE), status->coord);
	printf("游戏结束,得分为%d\n", status->score);
	system("pause");
	return 0;
}

输出
在这里插入图片描述

10 检测蛇是否吃到食物

这里只需要判断蛇头坐标是否和食物坐标重合,如果是则吃到食物,否则没迟到

void eat_food(STATUS* status)
{
	if (status->list[0].x == status->food_position.x &&
		status->list[0].y == status->food_position.y)
	{
		status->snake_size++;			//蛇身增长
		status->score += 10;			//分数增加
		generate_food(status);			//重新生成一个食物
	}
}

这里蛇身增长之后,无需考虑增长的那一节的坐标,只需要更新status->snake_size就行,因为在move_snake函数中,存在下面这一段代码

	//更新蛇身的坐标
	for (int i = status->snake_size - 1; i >= 1; i--)
	{
		//数组status->list的每一个元素都是结构体变量,因此可以直接赋值
		status->list[i] = status->list[i - 1];	
	}

新增的那一节,会在第一轮循环的时候得到原先最后一节的坐标,后面的循环,会使原来的每一节得到前一节的坐标,从而使蛇增长。

另外,我们这里还有个bug,因为生成的食物位置是随机的,有可能生成的位置在蛇身上,因此需要对生成的食物位置进行判断,如果在蛇身上则需要重新生成。

改进后的生成食物代码如下:

void generate_food(STATUS* status)
{
	srand(time(NULL));			//设置随机种子

	//初始化食物
	status->food_position.x = rand() % WIDE;
	status->food_position.y = rand() % HIGH;

	int in_snake = 1;
	while (in_snake)
	{
		for (int i = 0; i < status->snake_size; i++)
		{
			if (status->food_position.x == status->list[i].x &&
				status->food_position.y == status->list[i].y)
			{
				in_snake = 1;
				break;
			}
			in_snake = 0;
		}

		//如果 in_snake==1,表示循环是中途退出的,意味着生成的事物在蛇身上,因此要重新生成食物
		//如果 in_snake==0,表示循环是正常退出的,此时in_snake不再满足while循环的条件
		if (in_snake)
		{
			//重新生成食物
			status->food_position.x = rand() % WIDE;
			status->food_position.y = rand() % HIGH;
		}
	}
}

下面是测试函数

int main() {
	STATUS* status = (STATUS*)malloc(sizeof(STATUS));
	init_status(status);
	while (1)
	{
		system("cls");			//清屏
		show_ui(status);
		Sleep(300);				//睡眠300ms(Windows系统中)
		control_snake(status);	//键盘控制蛇的方向
		eat_food(status);		//判断蛇是否吃到食物
		move_snake(status);		//更新蛇的位置
		if (is_out_range(status))	//判断蛇头是否越界
			break;
	}

	//重新设定光标位置,方面打印得分
	status->coord.X = 5;
	status->coord.Y = HIGH + 1;
	SetConsoleCursorPosition(GetStdHandle(STD_OUTPUT_HANDLE), status->coord);
	printf("游戏结束,得分为%d\n", status->score);
	system("pause");
	return 0;
}

输出
在这里插入图片描述

好了,现在的贪吃蛇可以吃到食物了。

11 检测蛇是否咬到自己

这个只需要判断蛇头的坐标是否和蛇身的某一节坐标相等即可。

int is_eat_body(STATUS* status)
{
	int ret;
	for (int i = 1; i < status->snake_size; i++)
	{
		if (status->list[0].x == status->list[i].x && status->list[0].y == status->list[i].y)
		{
			ret = 1;
			break;
		}
		else
			ret = 0;
	}
	return ret;
}

测试代码:

int main() {
	STATUS* status = (STATUS*)malloc(sizeof(STATUS));
	init_status(status);
	while (1)
	{
		system("cls");			//清屏
		show_ui(status);
		Sleep(300);				//睡眠300ms(Windows系统中)
		control_snake(status);	//键盘控制蛇的方向
		eat_food(status);		//判断蛇是否吃到食物
		move_snake(status);		//更新蛇的位置
		if (is_out_range(status))	//判断蛇头是否越界
			break;
		if (is_eat_body(status))	//判断是否咬到自己
			break;
	}

	//重新设定光标位置,方面打印得分
	status->coord.X = 5;
	status->coord.Y = HIGH + 1;
	SetConsoleCursorPosition(GetStdHandle(STD_OUTPUT_HANDLE), status->coord);
	printf("游戏结束,得分为%d\n", status->score);
	system("pause");
	return 0;
}

这里需要注意的是,if (is_eat_body(status))需要在move_snake(status);后面,假如在move_snake(status);的前面,则是判断上一轮循环中,所更新得到的蛇的位置(即上一轮循环中move_snake的结果),并且此时已经显示把蛇吃到自己的结果显示出来了(蛇头被蛇身覆盖,因为蛇身在蛇头之后打印),这个有时间可以自己去尝试一下。

结果:
在这里插入图片描述

12 隐藏控制台光标

前面的程序,蛇最后一节的右边,还有一个光标,影响蛇的美观
在这里插入图片描述
接下来我们把它去掉。
可以将以下代码放置于main函数的开头,实现光标的隐藏:

//隐藏控制台光标
CONSOLE_CURSOR_INFO  cci;
cci.dwSize = sizeof(cci);
cci.bVisible = FALSE;
SetConsoleCursorInfo(GetStdHandle(STD_OUTPUT_HANDLE), &cci);

为了使main函数精简,将上面的代码段封装成函数

void hide_cur()
{
	//隐藏控制台光标
	CONSOLE_CURSOR_INFO  cci;
	cci.dwSize = sizeof(cci);
	cci.bVisible = FALSE;
	SetConsoleCursorInfo(GetStdHandle(STD_OUTPUT_HANDLE), &cci);
}

测试函数变成下面的形式:

13 建墙

前面的程序,我们是看不到左边界和下边界的,只有撞墙了才知道
现在我们写一个函数来建墙

void init_wall()
{
	for (int i = 0; i <= HIGH; i++)
	{
		for (int j = 0; j <= WIDE; j++)
		{
			if (i == HIGH || j == WIDE)
				printf("+");
			else
				printf(" ");
		}
		printf("\n");
	}
}

测试代码如下:

int main() {
	//隐藏控制台光标
	hide_cur();

	STATUS* status = (STATUS*)malloc(sizeof(STATUS));
	init_status(status);
	while (1)
	{
		system("cls");			//清屏
		init_wall();			//显示边界
		show_ui(status);
		Sleep(300);				//睡眠300ms(Windows系统中)
		control_snake(status);	//键盘控制蛇的方向
		eat_food(status);		//判断蛇是否吃到食物
		move_snake(status);		//更新蛇的位置
		if (is_out_range(status))	//判断蛇头是否越界
			break;
		if (is_eat_body(status))	//判断是否咬到自己
			break;
	}

	//重新设定光标位置,方面打印得分
	status->coord.X = 5;
	status->coord.Y = HIGH + 1;
	SetConsoleCursorPosition(GetStdHandle(STD_OUTPUT_HANDLE), status->coord);
	printf("游戏结束,得分为%d\n", status->score);
	system("pause");
	return 0;
}

效果很好,但是墙总是一闪一闪的,晃眼,因为程序每隔300ms就清屏一次。如果把清屏函数去掉,并且把init_wall();放到while循环外面,那么将导致蛇的轨迹一直留在屏幕上。

解决这个问题,只需要在show_ui函数中,在上一轮蛇尾的位置打印空格键即可,以下是修改后的show_ui函数

void show_ui(STATUS* status)
{
	//显示食物
	status->coord.X = status->food_position.x;
	status->coord.Y = status->food_position.y;
	SetConsoleCursorPosition(GetStdHandle(STD_OUTPUT_HANDLE), status->coord);
	printf("#");

	//显示蛇
	for (int i = 0; i < status->snake_size; i++)
	{
		status->coord.X = status->list[i].x;
		status->coord.Y = status->list[i].y;
		SetConsoleCursorPosition(GetStdHandle(STD_OUTPUT_HANDLE), status->coord);

		if (0 == i) 
			printf("@");	//打印蛇头
		else
			printf("*");	//打印蛇身
	}

	//蛇尾打印空格,防止显示轨迹
	status->coord.X = status->list[status->snake_size].x;
	status->coord.Y = status->list[status->snake_size].y;
	SetConsoleCursorPosition(GetStdHandle(STD_OUTPUT_HANDLE), status->coord);
	printf(" ");
}

最后的测试代码如下:

int main() {
	//隐藏控制台光标
	hide_cur();

	STATUS* status = (STATUS*)malloc(sizeof(STATUS));
	init_status(status);
	init_wall();			//显示边界
	while (1)
	{
		//system("cls");			//清屏
		
		show_ui(status);
		Sleep(300);				//睡眠300ms(Windows系统中)
		control_snake(status);	//键盘控制蛇的方向
		eat_food(status);		//判断蛇是否吃到食物
		move_snake(status);		//更新蛇的位置
		if (is_out_range(status))	//判断蛇头是否越界
			break;
		if (is_eat_body(status))	//判断是否咬到自己
			break;
	}

	//重新设定光标位置,方面打印得分
	status->coord.X = 5;
	status->coord.Y = HIGH + 1;
	SetConsoleCursorPosition(GetStdHandle(STD_OUTPUT_HANDLE), status->coord);
	printf("游戏结束,得分为%d\n", status->score);
	system("pause");
	return 0;
}

输出
在这里插入图片描述
蛇只有在向左移动的时候,轨迹才能去除,原因是下面这段程序并不是在上一个循环中的蛇尾位置上打印空格,而是在一个随机的位置上打印空格(因为status->list[status->snake_size].xstatus->list[status->snake_size].y就是随机值,可以通过debug看到),之所以在想左的时候有效,是因为光标的重新定位不成功(由于是随机值,无法实现定位),于是光标仍然在蛇的最后一节的右边位置,因此能去掉轨迹,但向其他方向就不行了。

	//蛇尾打印空格,防止显示轨迹
	status->coord.X = status->list[status->snake_size].x;
	status->coord.Y = status->list[status->snake_size].y;
	SetConsoleCursorPosition(GetStdHandle(STD_OUTPUT_HANDLE), status->coord);
	printf(" ");

我们需要在状态结构体中,新增一个变量来保存蛇尾位置

typedef struct _status
{
	POSITION list[WIDE * HIGH];		//蛇的最大长度为WIDE * HIGH,数组的每个元素都是POSITION类型
	int snake_size;					//蛇的长度
	POSITION food_position;			//食物位置
	COORD coord;
	int dx, dy;						//蛇头移动方向
	int score;						//游戏得分
	POSITION tail;					//上一拍(即上一轮循环)的蛇尾位置
}STATUS;

初始化函数是否变无所谓

void init_status(STATUS* status) {
	//蛇长
	status->snake_size = 2;

	//蛇头
	status->list[0].x = WIDE / 2;
	status->list[0].y = HIGH / 2;

	//蛇身
	status->list[1].x = WIDE / 2 + 1;
	status->list[1].y = HIGH / 2;

	//蛇头移动方向
	status->dx = -1;
	status->dy = 0;

	//游戏得分
	status->score = 0;

	//蛇尾
	status->tail = status->list[1];

	//初始化食物位置
	generate_food(status);
}

更新蛇位置的函数要变

void move_snake(STATUS* status)
{
	//记录移动前的蛇尾位置
	status->tail = status->list[status->snake_size - 1];

	//更新蛇身的坐标
	for (int i = status->snake_size - 1; i >= 1; i--)
	{
		//数组status->list的每一个元素都是结构体变量,因此可以直接赋值
		status->list[i] = status->list[i - 1];	
	}

	//更新蛇头的坐标
	status->list[0].x += status->dx;
	status->list[0].y += status->dy;
}

当蛇身增长时,status->list[status->snake_size - 1]虽然是蛇尾,但其坐标却是随机值,因为需要在后面的“更新蛇身的坐标”之后,新的蛇尾才有坐标,不过却不影响,原因稍后会讲。

最后是修改show_ui函数

void show_ui(STATUS* status)
{
	//显示食物
	status->coord.X = status->food_position.x;
	status->coord.Y = status->food_position.y;
	SetConsoleCursorPosition(GetStdHandle(STD_OUTPUT_HANDLE), status->coord);
	printf("#");

	//显示蛇
	for (int i = 0; i < status->snake_size; i++)
	{
		status->coord.X = status->list[i].x;
		status->coord.Y = status->list[i].y;
		SetConsoleCursorPosition(GetStdHandle(STD_OUTPUT_HANDLE), status->coord);

		if (0 == i) 
			printf("@");	//打印蛇头
		else
			printf("*");	//打印蛇身
	}

	//蛇尾打印空格,防止显示轨迹
	status->coord.X = status->tail.x;
	status->coord.Y = status->tail.y;
	SetConsoleCursorPosition(GetStdHandle(STD_OUTPUT_HANDLE), status->coord);
	printf(" ");
}

有一种可能,就是在刚刚吃完食物,status->snake_size增长,这种情况下,move_snake函数中status->list[status->snake_size - 1]虽然是蛇尾,但其坐标并未赋值,或者说,此时蛇尾的坐标还是随机值,因为需要在后面的“更新蛇身的坐标”之后,蛇尾才有坐标。不过由于status->tail得到的是随机的坐标,使得show_ui函数中光标重定位失败,进而上一轮的蛇尾位置没能打印出空格,而是保留了#,但由于蛇身本身增长,上一轮蛇尾的位置,本轮依然是蛇尾的位置,因此仍然需要打印#,阴差阳错导致结果正确。

输出
在这里插入图片描述

至此,我们实现了贪吃蛇的基本功能了。

14 总结

贪吃蛇游戏除了main函数外,我们还写了12个函数,其中很多函数都不是一步到位,而是慢慢完善,这也符合软件工程的特点,循序渐进。我们之前写的快译通也是如此,先实现一个简单的,然后再实现复杂的。
贪吃蛇的最终版整体程序如下:

#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
#include<stdlib.h>
#include<Windows.h>
#include<conio.h>
#define WIDE  60
#define HIGH  20

typedef struct _position
{
	int x;
	int y;
}POSITION;

typedef struct _status
{
	POSITION list[WIDE * HIGH];		//蛇的最大长度为WIDE * HIGH,数组的每个元素都是POSITION类型
	int snake_size;					//蛇的长度
	POSITION food_position;			//食物位置
	COORD coord;
	int dx, dy;						//蛇头移动方向
	int score;						//游戏得分
	POSITION tail;					//上一拍(即上一轮循环)的蛇尾位置
}STATUS;

void init_ui()
{
	for (int i = 0; i < HIGH; i++)
	{

		for (int j = 0; j < WIDE; j++)
		{
			printf("#");

		}
		printf("\n");
	}
}

void generate_food(STATUS* status)
{
	srand(time(NULL));			//设置随机种子

	//初始化食物
	status->food_position.x = rand() % WIDE;
	status->food_position.y = rand() % HIGH;

	int in_snake = 1;
	while (in_snake)
	{
		for (int i = 0; i < status->snake_size; i++)
		{
			if (status->food_position.x == status->list[i].x &&
				status->food_position.y == status->list[i].y)
			{
				in_snake = 1;
				break;
			}
			in_snake = 0;
		}

		//如果 in_snake==1,表示循环是中途退出的,意味着生成的事物在蛇身上,因此要重新生成食物
		//如果 in_snake==0,表示循环是正常退出的,此时in_snake不再满足while循环的条件
		if (in_snake)
		{
			//重新生成食物
			status->food_position.x = rand() % WIDE;
			status->food_position.y = rand() % HIGH;
		}
	}
}

void init_status(STATUS* status) {
	//蛇长
	status->snake_size = 2;

	//蛇头
	status->list[0].x = WIDE / 2;
	status->list[0].y = HIGH / 2;

	//蛇身
	status->list[1].x = WIDE / 2 + 1;
	status->list[1].y = HIGH / 2;

	//蛇头移动方向
	status->dx = -1;
	status->dy = 0;

	//游戏得分
	status->score = 0;

	//蛇尾
	status->tail = status->list[1];

	//初始化食物位置
	generate_food(status);
}

void show_ui(STATUS* status)
{
	//显示食物
	status->coord.X = status->food_position.x;
	status->coord.Y = status->food_position.y;
	SetConsoleCursorPosition(GetStdHandle(STD_OUTPUT_HANDLE), status->coord);
	printf("#");

	//显示蛇
	for (int i = 0; i < status->snake_size; i++)
	{
		status->coord.X = status->list[i].x;
		status->coord.Y = status->list[i].y;
		SetConsoleCursorPosition(GetStdHandle(STD_OUTPUT_HANDLE), status->coord);

		if (0 == i) 
			printf("@");	//打印蛇头
		else
			printf("*");	//打印蛇身
	}

	//蛇尾打印空格,防止显示轨迹
	status->coord.X = status->tail.x;
	status->coord.Y = status->tail.y;
	SetConsoleCursorPosition(GetStdHandle(STD_OUTPUT_HANDLE), status->coord);
	printf(" ");
}

void move_snake(STATUS* status)
{
	//记录移动前的蛇尾位置
	status->tail = status->list[status->snake_size - 1];

	//更新蛇身的坐标
	for (int i = status->snake_size - 1; i >= 1; i--)
	{
		//数组status->list的每一个元素都是结构体变量,因此可以直接赋值
		status->list[i] = status->list[i - 1];	
	}

	//更新蛇头的坐标
	status->list[0].x += status->dx;
	status->list[0].y += status->dy;
}

void control_snake(STATUS* status)
{
	char  key = 0;		//这里必须初始化,因为可能不会进入while循环中,导致key未赋值,从而在switch语句中报错
	while (_kbhit())	//判断是否按下按键,按下不等于0 
	{
		key = _getch();
	}

	//使用wsad分别控制上下左右,其它按键无效
	switch (key)
	{
	case 'a':
		if (1 == status->dx && 0 == status->dy)		//防止出现调头
			break;
		else
		{
			status->dx = -1;
			status->dy = 0;
			break;
		}
	case 'w':
		if (1 == status->dy) //status->dy和status->dx中,有且只有一个0,因此只需要判断一个
			break;
		else
		{
			status->dx = 0;
			status->dy = -1;
			break;
		}
	case 's':
		if (-1 == status->dy)
			break;
		else
		{
			status->dx = 0;
			status->dy = 1;
			break;
		}
	case 'd':
		if (-1 == status->dx)
			break;
		else
		{
			status->dx = 1;
			status->dy = 0;
			break;
		}
	}
}

void start_game(STATUS* status)
{
	//蛇的前进方向

}

int is_out_range(STATUS* status)
{
	int ret;
	if (status->list[0].x >= 0 && status->list[0].x < WIDE &&
		status->list[0].y >= 0 && status->list[0].y < HIGH)
		ret = 0;
	else
		ret = 1;

	return ret;
}

int is_eat_body(STATUS* status)
{
	int ret;
	for (int i = 1; i < status->snake_size; i++)
	{
		if (status->list[0].x == status->list[i].x && status->list[0].y == status->list[i].y)
		{
			ret = 1;
			break;
		}
		else
			ret = 0;
	}
	return ret;
}

void eat_food(STATUS* status)
{
	if (status->list[0].x == status->food_position.x &&
		status->list[0].y == status->food_position.y)
	{
		status->snake_size++;			//蛇身增长
		status->score += 10;			//分数增加
		generate_food(status);			//重新生成一个食物
	}
}

void hide_cur()
{
	//隐藏控制台光标
	CONSOLE_CURSOR_INFO  cci;
	cci.dwSize = sizeof(cci);
	cci.bVisible = FALSE;
	SetConsoleCursorInfo(GetStdHandle(STD_OUTPUT_HANDLE), &cci);
}

void init_wall()
{
	for (int i = 0; i <= HIGH; i++)
	{
		for (int j = 0; j <= WIDE; j++)
		{
			if (i == HIGH || j == WIDE)
				printf("+");
			else
				printf(" ");
		}
		printf("\n");
	}
}

int main() {
	//隐藏控制台光标
	hide_cur();

	STATUS* status = (STATUS*)malloc(sizeof(STATUS));
	init_status(status);
	init_wall();			//显示边界
	while (1)
	{
		show_ui(status);
		Sleep(200);				//睡眠200ms(Windows系统中)
		control_snake(status);	//键盘控制蛇的方向
		eat_food(status);		//判断蛇是否吃到食物
		move_snake(status);		//更新蛇的位置
		if (is_out_range(status))	//判断蛇头是否越界
			break;
		if (is_eat_body(status))	//判断是否咬到自己
			break;
	}

	//重新设定光标位置,方面打印得分
	status->coord.X = 5;
	status->coord.Y = HIGH + 1;
	SetConsoleCursorPosition(GetStdHandle(STD_OUTPUT_HANDLE), status->coord);
	printf("游戏结束,得分为%d\n", status->score);
	system("pause");
	return 0;
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/166162.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

快出数量级的性能是怎样炼成的

前言&#xff1a;今天学长跟大家讲讲《快出数量级的性能是怎样炼成的》&#xff0c;废话不多说&#xff0c;直接上干货~我们之前做过一些性能优化的案例&#xff0c;不算很多&#xff0c;还没有失手过。少则提速数倍&#xff0c;多则数十倍&#xff0c;极端情况还有提速上千倍的…

关于IDEA配置本地tomcat部署项目找不到项目工件的问题解答

文章目录一 原因分析二 解决方案三 具体的操作方法3.1 打开项目结构找到工件3.2 添加具体的工件内容3.3 配置本地tomcat一 原因分析 可能是之前的项目再次打开后&#xff0c;没有及时配置项目结构中的工件信息&#xff0c;导致配置tomcat中看不到工件的信息 二 解决方案 解决…

react组件优化,当父组件数据变化与子组件无关时,控制子组件不重新渲染

首先 我们来建立一个场景 我们创建一个react项目 然后创建一个父组件 这里我要叫 record.jsx 参考代码如下 import React from "react"; import Subset from "./subset";export default class record extends React.Component{constructor(props){super(…

工作的同时,我也在这里做副业

文章目录一、什么是独自开&#xff1f;二、独自开能给我们带来什么利益&#xff1f;三、如何使用独自开&#xff1f;3.1、用户任务报价步骤13.2、用户任务报价步骤2四、未来的愿景一、什么是独自开&#xff1f; 独自开&#xff0c;全称独自开发一套系统&#xff0c;是基于商品…

CTP开发(2)行情模块的开发

我在做CTP开发之前&#xff0c;也参考了不少其他的资料&#xff0c;发现他们都是把行情和交易做在同一个工程里的。我呢之前也做过期货相关的交易平台&#xff0c;感觉这种把行情和交易做在一起的方法缺乏可扩展性。比如我开了多个CTP账户&#xff0c;要同时交易&#xff0c;这…

springMVC的学习拦截器之验证用户登录案例

文章目录实现思路关于环境和配置文件pomspring的配置文件关于idea的通病/常见500错误的避坑实现步骤编写登陆页面编写Controller处理请求编写登录成功的页面编写登录拦截器实现思路 有一个登录页面&#xff0c;需要写一个controller访问页面登陆页面提供填写用户名和密码的表单…

UE4c++日记1(允许 创类、蓝图读写/调用/只读、分类、输出日志打印语句)

目录 1允许创建基于xx的蓝图类 2允许蓝图读写/允许蓝图调用/只读 读写调用 只读 3为变量/函数分类 4输出日志打印一段话 1.先创建一个蓝图类 2.构建对象 3.写提示代码&#xff0c;生成解决方案 4.运行&#xff0c;打开“输出日志” 5.总结 创类-实例化对象&#xff08;构建…

2022年个人年终总结(一)

2022年个人年终总结&#xff08;一&#xff09;考研想法的萌生回顾过去一年-考研心路历程基础阶段&#xff08;1-6月&#xff09;强化阶段&#xff08;7-9月&#xff09;冲刺阶段&#xff08;10-12月&#xff09;感受总结特别感谢2022年是做梦的一年&#xff0c;花了一年的时间…

Zookeeper相关操作

Zookeeper概念 •Zookeeper 是 Apache Hadoop 项目下的一个子项目&#xff0c;是一个树形目录服务。 •Zookeeper 翻译过来就是 动物园管理员&#xff0c;他是用来管 Hadoop&#xff08;大象&#xff09;、Hive(蜜蜂)、Pig(小 猪)的管理员。简称zk •Zookeeper 是一个分布式的…

【C++】非递归实现二叉树的前中后序遍历

​&#x1f320; 作者&#xff1a;阿亮joy. &#x1f386;专栏&#xff1a;《吃透西嘎嘎》 &#x1f387; 座右铭&#xff1a;每个优秀的人都有一段沉默的时光&#xff0c;那段时光是付出了很多努力却得不到结果的日子&#xff0c;我们把它叫做扎根 目录&#x1f449;二叉树的…

如何运营个人技术博客

前言 本篇和大家聊聊如何运营个人技术博客&#xff0c;定位下做技术写作的目的&#xff0c;有哪些交流平台和输出方式&#xff0c;如何把控内容质量&#xff0c;整理了一些写作技巧和自己常用的写作工具&#xff0c;最后分享下如何在有限的时间里合理安排保证写作与工作的平衡。…

第九届蓝桥杯省赛 C++ A组 - 付账问题

✍个人博客&#xff1a;https://blog.csdn.net/Newin2020?spm1011.2415.3001.5343 &#x1f4da;专栏地址&#xff1a;蓝桥杯题解集合 &#x1f4dd;原题地址&#xff1a;付账问题 &#x1f4e3;专栏定位&#xff1a;为想考甲级PAT的小伙伴整理常考算法题解&#xff0c;祝大家…

理解CSS

CSS 作为前端技术栈中关键一环&#xff0c;对页面元素及样式呈现起到了直接作用。本节课旨在通过对 CSS 的工作流程及原理、页面中 CSS 使用方法等详细解读&#xff0c;帮助前端新手建立对 CSS 的全面而深刻的认知。 CSS概念 CSS 即 Cascading Style Sheets&#xff0c;是用来…

认识涤生大数据的几个月,彻底改变了我

1自我介绍 大家好&#xff0c;我是泰罗奥特曼&#xff0c;毕业于东北的一所不知名一本大学&#xff0c;学校在一个小城市里面&#xff0c;最热闹的地方是一个四层楼的商城&#xff0c;专业是信息管理与信息系统&#xff0c;由于是调剂的&#xff0c;所以我也不知道这个专业是干…

[JavaEE]阻塞队列

专栏简介: JavaEE从入门到进阶 题目来源: leetcode,牛客,剑指offer. 创作目标: 记录学习JavaEE学习历程 希望在提升自己的同时,帮助他人,,与大家一起共同进步,互相成长. 学历代表过去,能力代表现在,学习能力代表未来! 目录: 1.阻塞队列的概念 2.标准库中的阻塞队列 3.生产者…

1999-2019年全国、各省市直辖区居民收入和消费支出情况面板数据

1999-2019年全国、各省市直辖区居民收入和消费支出情况面板数据 1、时间&#xff1a;1999-2019年 2、指标&#xff1a; 可支配收入、城镇居民家庭平均每人全年消费性支出、食品消费支出、医疗保健消费支出、农村居民家庭人均纯收入、农村居民家庭平均每人生活消费支出、食品…

【Unity URP】设置光源层Light Layers

光源层 (Light Layers) 功能允许配置某些光源仅影响特定的游戏对象。 此功能可以用于加亮在暗处的物体。 1.开启光源层&#xff0c;并设置光源层名称 在URP资源中&#xff0c;点击Lighting右侧的垂直省略号图标 (⋮)&#xff0c;勾选Show Additional Properties&#xff0c…

【已解决】WARNING: Ignoring invalid distribution xxx

问题解决方案解释问题 WARNING: Ignoring invalid distribution -umpy (c:\users\xxx\appdata\roaming\python\python36\site-packages) 解决方案 在报错的路径下(c:\users\xxx\appdata\roaming\python\python36\site-packages)&#xff0c;找到~对应文件夹&#xff0c;此处…

Pytorch实战笔记(1)——BiLSTM 实现情感分析

本文展示的是使用 Pytorch 构建一个 BiLSTM 来实现情感分析。本文的架构是第一章详细介绍 BiLSTM&#xff0c;第二章粗略介绍 BiLSTM&#xff08;就是说如果你想快速上手可以跳过第一章&#xff09;&#xff0c;第三章是核心代码部分。 目录1. BiLSTM的详细介绍2. BiLSTM 的简单…

【三年面试五年模拟】算法工程师的独孤九剑秘籍(第十二式)

Rocky Ding公众号&#xff1a;WeThinkIn写在前面 【三年面试五年模拟】栏目专注于分享AI行业中实习/校招/社招维度的必备面积知识点与面试方法&#xff0c;并向着更实战&#xff0c;更真实&#xff0c;更从容的方向不断优化迭代。也欢迎大家提出宝贵的意见或优化ideas&#xff…