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

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;
}
  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值