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].x
和status->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;
}