文章目录
前言
小时候都玩过贪吃蛇这个经典的小游戏,在我们的普通手机里似乎都是必备的。它伴随着我们的童年,经历了好多好多时光。它带给我们了许多的乐趣。“贪吃蛇”就是C语言中非常基础的部分,重点需要的知识就是结构体,循环,函数等知识,好在没有让人生畏的指针。
一、游戏效果展示
二、游戏说明
游戏界面当中没有打印相关的按键说明,这里先逐一列出,贪吃蛇游戏按键说明:
1.按方向键上下左右,可以实现蛇移动方向的改变。
2.短时间长按方向键上下左右其中之一,可实现蛇向该方向的短时间加速移动。
3.按空格键可实现暂停,暂停后按任意键继续游戏。
4.按Esc键可直接退出游戏。
5.按R键可重新开始游戏。
除此之外,本游戏还拥有计分系统,可保存玩家的历史最高记录。
三、游戏框架构建
1、游戏界面的大小
首先是创建窗口和给窗口画个背景色,要用到头文件graphics。
graphics的安装:下个Easyx,打开可见exe可执行文件,运行后可以根据自己的VC版本选择适合自己的图形库。
需要用的函数是initgraph和setbkcolor,这里我们设定一个640 * 480的窗口,颜色是蓝色,别忘了要清空一下绘图设备,运行一下,就成了。
initgraph(640,480);
setbkcolor(RGB(14,218,243));//背景色是蓝色
cleardevice();//清空绘图设备
不过运行一下,会出现闪退的情况,这是正常的,因为程序已经结束了。要想显示,可以在main中加一句while(1){}或getchar()
2、创建蛇
2.1定义蛇的结构体
需要定义蛇的节点,长度,方向,坐标,其中坐标可以用POINT来直接定义,坐标里的数值可以用宏定义来处理。
POINT coor[SNAKE_NUM];
看一下POINT的定义:
typedef struct tagPOINT
{
LONG x;
LONG y;
} POINT, *PPOINT, NEAR *NPPOINT, FAR *LPPOINT;
用typedef定义的结构体可以直接用,这样就方便书写,不过为了混淆,最好大写。
2.2、初始化蛇
分两步,一是完成01的过程,二是完成1N的过程,也就是先初始化蛇头,再初始化蛇身。也就是要初始化节数、长度、方向、坐标,刚开始可能没有思路,那就先画一个蛇头:
struct Snake
{
int len; //记录蛇身长度
int x; //蛇头横坐标
int y; //蛇头纵坐标
}snake;
蛇身其实就是三个蛇头,每个蛇头之间10间隔,因此整个蛇就是循环3个蛇头的过程:
蛇身结构体当中存储着该段蛇身的位置坐标
struct Body
{
int x; //蛇身横坐标
int y; //蛇身纵坐标
}body[ROW*COL]; //开辟足以存储蛇身的结构体数组
2.3、画蛇
也是和上面一样的,刚开始没思路可以先画个蛇头,然后用循环画蛇身,这里用到的函数就是solidcircle,里面要输入x坐标,y坐标和半径。
for(int i=0;i<snake.size;i++)
{
solidcircle(snake.coor[i].x,snake.coor[i].y,5);
}
觉得颜色不好看,可以用这个函数改:setfillcolor(WHITE)
注意:运行一下,可以看到有些闪屏,那就在绘制函数中用BeginBatchDraw()和EndBatchDraw(),一头一尾。
3、标记游戏区
同时我们需要一个二维数组来存储游戏区各个位置是什么(该位置为空、墙、食物、蛇头以及蛇身)。
nt 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 //退出
4、菜单栏的设置
void menu() {
system("title 贪吃蛇");//设置窗口标题
system("mode con cols=84 lines=23"); //设置cmd窗口的大小
color(14);//设置文字为淡黄色
printf("*****************************************************************\n");
printf("******************欢迎来到贪吃蛇的游戏里!!!*******************\n");
printf("*****************************************************************\n");
printf("*********按方向键上下左右,可以实现蛇移动方向的改变**************\n");
printf("*****************************************************************\n");
printf("**********按空格键可实现暂停,暂停后按任意键继续游戏*************\n");
printf("*****************************************************************\n");
printf("*********************按Esc键可直接退出游戏***********************\n");
printf("*********************按R键可重新开始游戏*************************\n");
printf("*****************************************************************\n");
system("pause");//暂停程序,按任意键继续
}
5、让蛇动起来
5.1、蛇的移动
先看蛇头再看蛇身,蛇头很好动,就一直向前移动。
snake.coor[0].x++;
而蛇身就是后一个走到前一个位置上,顺序从后往前,从前往后的话那第二个数据谁给它?
这里是易错点,正确思路是2的数据由1给,1的由0给,0是自己++。若是反过来,0自己++,1的数据由0给,然后覆盖了,那这时候2的数据谁给?所以从后往前赋。
for(int i = snake.size-1 ;i > 0 ;i--)
{
snake.coor[i] = snake.coor[i-1];
}
这里会出现闪屏,卡屏的情况,只要在绘制函数中用BeginBatchDraw()和EndBatchDraw(),一头一尾。//这就是双缓存绘图的函数
但是这样做之后,发现全都汇聚到蛇头了,因为没有方向判定。
5.2、表示方向
这就要用到枚举数据类型enum,常规思路是用宏定义,但是上下左右四个宏定义,太多了。于是精简一下,改用enum这样的枚举数据类型。
enum DIR //表示蛇的方向
{
UP,
DOWN,
LEFT,
RIGHT,
};
然后我们初始化一下,就定义从右边开始。因为窗口的坐标系是从左上开始的,我们定义的蛇也是从左上开始的,所以定义方向是右边。
snake.dir = RIGHT;
下面我们要判定方向,先看蛇头的方向,这就要用switch来判定了(这里移动方式是有问题的,但是便于观测,先看蛇头的移动,后面会改动):
switch(snake.dir)
{
case UP:
snake.coor[0].y--;
case DOWN:
snake.coor[0].y++;
case LEFT:
snake.coor[0].x--;
case RIGHT:
snake.coor[0].x++;
}
5.3、控制方向
因为我们需要用键盘来控制这条蛇,所以我们也要用switch语句来控制。其中我们要用conio这个头文件下的_getch()函数,因为这个函数会从控制台读取一个字符,但不显示在屏幕上。
switch(_getch())
{
case 'w':
case 'W':
case '72':
snake.dir=UP;
case 's':
case 'S':
case '80':
snake.dir=DOWN;
case 'a':
case 'A':
case '75':
snake.dir=LEFT;
case 'd':
case 'D':
case '77':
snake.dir=RIGHT;
}
我们运行一下,发现只有我们按一下键盘它才动一下。这个跟_getch()有关,我们要想它自动执行这个循环,那就要没有输入的时候就可以自动执行。这里就可以用到_kbhit()函数,它也是conio.h头文件的,功能是检查控制台是否有输入,有就返回真值。
所以只要把这个switch放到if(_kbhit())循环中就好。
5.4调整
其实上面存在些问题,比如头和身体合并了,移动的话不能向左边移动,或者整节蛇在移动,有点像俄罗斯方块,还有就是超出边界怎么办的处理(穿墙)。
首先调整身体的移动,之前头的代码是这样的:
snake.coor[0].y--;
这就导致后面的会全部聚在头里,因为1单位移动变化小,看起来像是聚在一起的。所以我们优化一下:
snake.coor[0].y-=snake.speed;
这就是根据蛇身的长度来移动的,这么做让头的坐标变化要跟速度保持一致性,头加(减)10,身体也这样才满足。
接下来调整穿墙的问题,也就是从边界出,就能从边界进。就是坐标、半径和边界的关系:
if(snake.coor[0].y+5<=0)//这里以从上面穿墙为例
{
snake.coor[0].y = 480; //480:窗口的宽
}
解决好这个问题,就能解决穿墙和向左移的问题了,但是我们发现它居然还可以调头,这个我们也得解决一下。
最后是调头的问题:
就是键盘输入的方向中,不能出现调头的情况,往上就不能往下。
if(snake.dir != DOWN)
snake.dir=UP;
6、创建食物
随机在游戏区生成食物,需要对生成后的坐标进行判断,只有该位置为空才能在此生成食物,否则需要重新生成坐标。食物坐标确定后,需要对游戏区该位置的状态进行标记。
void createFood()
{
if (snake.x[0] == food.x && snake.y[0] == food.y)//蛇头碰到食物
{
//蛇头碰到食物即为要吃掉这个食物了,因此需要再次生成一个食物
while (1)
{
int flag = 1;
srand((unsigned int)time(NULL));
food.x = rand() % (MAPWIDTH - 4) + 2;
food.y = rand() % (MAPHEIGHT - 2) + 1;
//随机生成的食物不能在蛇的身体上
for (int i = 0; i < snake.len; i++)
{
if (snake.x[i] == food.x && snake.y[i] == food.y)
{
flag = 0;
break;
}
}
//随机生成的食物不能横坐标为奇数,也不能在蛇身,否则重新生成
if (flag && food.x % 2 == 0)
break;
}
//绘制食物
gotoxy(food.x, food.y);
printf("★");
snake.len++;//吃到食物,蛇身长度加1
sorce += 10;//每个食物得10分
snake.speed -= 5;//随着吃的食物越来越多,速度会越来越快
changeFlag = 1;//很重要,因为吃到了食物,就不用再擦除蛇尾的那一节,以此来造成蛇身体增长的效果
}
return;
}
7、打印蛇和覆盖蛇
打印蛇和覆盖蛇这里直接使用一个函数来实现,若传入参数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(" ");
}
}
}
8、移动蛇
移动蛇函数的作用就是先覆盖当前所显示的蛇,然后再打印移动后的蛇。
参数说明:
x:蛇移动后的横坐标相对于当前蛇的横坐标的变化。
y:蛇移动后的纵坐标相对于当前蛇的纵坐标的变化。
蛇移动后,各种信息需要变化:
最后一段蛇身在游戏区当中需要被重新标记为空。蛇头位置在游戏区当中需要被重新标记为蛇身。存储蛇身坐标信息的结构体数组body当中,需要将第i段蛇身的坐标信息更新为第i-1段蛇身的坐标信息,而第0段,即第一段蛇身的坐标信息需要更新为当前蛇头的坐标信息。蛇头的坐标信息需要根据传入的参数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); //打印移动后的蛇
}
9、蛇的运动
void run(char map[ROW_MAX][LINE_MAX], int snake[ROW_MAX][LINE_MAX]){
/*
上 -32 0xffffffe0 72 H
下 -32 0xffffffe0 80 P
左 -32 0xffffffe0 75 K
右 -32 0xffffffe0 77 M
*/
char sh, ch;
while(1){
if(JudgeWall()){
/**********判断键盘是否敲击***********/
if (kbhit()){
ch = getch();
if (ch == -32){
sh = getch();
switch (sh){
case 'H': direct = 'w'; break;
case 'P': direct = 's'; break;
case 'K': direct = 'a'; break;
case 'M': direct = 'd'; break;
}
}
else{
switch (ch){
case 'w':case 'W': direct = 'w'; break;
case 's':case 'S': direct = 's'; break;
case 'a':case 'A': direct = 'a'; break;
case 'd':case 'D': direct = 'd'; break;
}
}
}
/************************************/
/**************蛇的运动******************/
switch (direct){
case 'w':
if(snake[Head_x-1][Head_y] != 0)
return;
snake[Head_x-1][Head_y] = ++Head_v;
Head_x--;
if(EatFood(map))
MoveTail(snake);
else
CreateFood(map, snake);
break;
case 'a':
if(snake[Head_x][Head_y-1] != 0)
return;
snake[Head_x][Head_y-1] = ++Head_v;
Head_y--;
if(EatFood(map))
MoveTail(snake);
else
CreateFood(map, snake);
break;
case 's':
if(snake[Head_x+1][Head_y] != 0)
return;
snake[Head_x+1][Head_y] = ++Head_v;
Head_x++;
if(EatFood(map))
MoveTail(snake);
else
CreateFood(map, snake);
break;
case 'd':
if(snake[Head_x][Head_y+1] != 0)
return;
snake[Head_x][Head_y+1] = ++Head_v;
Head_y++;
if(EatFood(map))
MoveTail(snake);
else
CreateFood(map, snake);
break;
}
system("cls");
TraverseMap(map, snake);
/****************************************/
}
else
return;
}
}
分两部分解释
解释: 键盘是否被敲击,