1 初始化界面
因为还没学QT,我们就使用终端界面替代。
这里我们假设界面中没有障碍物,我们只需要设定界面的高宽就行,这是蛇的移动范围,我们可以写两个宏来规定界面的高宽
新建一个snake.c的文件
#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>#define WIDE 60
#define HIGH 20void 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("@"); //打印蛇头elseprintf("*"); //打印蛇身}
}
测试函数如下:
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;elseret = 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;}elseret = 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("+");elseprintf(" ");}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("@"); //打印蛇头elseprintf("*"); //打印蛇身}//蛇尾打印空格,防止显示轨迹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("@"); //打印蛇头elseprintf("*"); //打印蛇身}//蛇尾打印空格,防止显示轨迹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 20typedef 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("@"); //打印蛇头elseprintf("*"); //打印蛇身}//蛇尾打印空格,防止显示轨迹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;elseret = 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;}elseret = 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("+");elseprintf(" ");}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;
}