从图中可以看出,整个游戏就是一个大的循环,当判断蛇的生命值为0时就跳出循环游戏结束,否则继续游戏。常用的结构是:
While (1){
……….
//游戏内容
……….
If (……)break;
//满足游戏结束的条件时跳出循环结束游戏
}
接下来就是游戏的具体内容了。
先来看看我们需要哪些变量;
蛇的身体(链表,包含了蛇每一节的坐标,和生命值);
食物(数组,两个元素,包含了食物的坐标);
蛇的运动方向(整型,共四个值);
首先,蛇的身体应该选用什么数据类型来存储呢?数组组是肯定不行的,因为数组的大小是固定的,你不知道你的蛇最终能达到多长(或许有高手能让蛇长到占满屏幕?),而且在数组的头部插入数据是很麻烦的一件事,你必须把所有的数据都往后移动一个元素才行。因此,此处我选用了链表来储存蛇的身体信息,(有的小伙伴要问了:什么是链表啊?这个请自行百度,我的个人看法是,链表就是对结构体的一种应用。话说我们学了一学期的C语言,老师也没讲链表之类的东西,不知道很正常,它应该属于数据结构这一门学科的内容吧)。
链表的一个节点应该包含哪些内容呢?
首先是蛇的坐标x和y,然后是生命值(其实只有第一个节点能用上它,你也可以去掉它,然后另外单独定义一个全局变量如life来判断蛇的生死),最后是指向下一个节点的结构体指针。
定义如下:
struct snk{//蛇身体
int x;
int y;
int life;
structsnk *link;
};
然后是食物:
int food[2];//食物,用food[0]表示x坐标,用food[1]表示y坐标
当然,食物要定义为全局变量,省去了指针操作的麻烦。
当然如果你有需要,还可以定义一个全局整型变量来保存积分:
int fen=0;//积分
需要的主要数据已经定义好,接下来是初始化。
那么,就先来初始化蛇的身体,创建一个链表;
struct snk *snake;//指向蛇链表头(第一个节点)的结构体指针
//声明两个个链表节点,初始化最开始的几节蛇身体
snake=(structsnk *)malloc(sizeof(struct snk));
snake->x=10;snake->y=10;snake->life=1;
snake->link=(struct snk*)malloc(sizeof(struct snk));
snake->link->x=10;snake->link->y=9;
snake->link->x=10;snake->link->link=NULL;
这里我只初始化了两节,你也可以多加几节,但是,最后别忘了把最后一个节点的link指针设为空,它标志着蛇的结尾,至于life,只需把第一个节点初始化为非零值即可。
接下来是初始化食物,为了能够随机生成食物,我们需要用到time.h为我们提供的随机函数,rand()和srand();为了方便后面的食物生成,我们把食物生成单独做成一个函数mkfood(),想一想,它需要哪些参数,它的返回值是什么?
为了防止生成的食物的坐标与蛇的身体重合,所以mkfood函数需要知道蛇的状态,食物是全局变量,因此函数不需要返回值,因此mkfood函数的函数首部应该是
void mkfood(struct snk *p);//产生食物
每次调用它都需要用srand函数重设随机数种子,然后用rand函数产生随机数,分别赋值给food[0]和food[1]作为食物的x坐标和y坐标。在这之后,需要遍历一次蛇的每一个节点,并把它们与食物的坐标比较,如有相同的,既产生的食物和蛇的身体重复了,就必须重新设定食物的坐标,这里我用的是递归调用mkfood函数来重新设定食物坐标。如果你用递归方法来重设食物坐标,不得不提的是当你在设定随机数种子时不要使用time()函数,因为time函数只能精确到秒,一旦食物与蛇身体重复,就会递归调用mkfood函数,此时时间还远远不足一秒,所以导致随机数种子还是和原来的一样,产生的随机数也和原来一样,还是和蛇身体重复,如此循环往复的递归调用,以现在的CPU运算能力,一秒钟之内可以进行的运算次数是相当惊人的,最终导致栈的溢出,程序崩溃。。。。所以应该选用更加精确的函数,比如clock()它返回从程序启动,到调用它时经过的时间,单位是毫秒,或者干脆手动设定一个变量来做种子,每次调用之前更新这个变量。
接下来就是制作一个函数,能够按照蛇身体和食物的信息,把蛇的身体和食物打印(绘制)到屏幕的相应位置。
这个函数需要知道蛇和食物的坐标,食物是全局变量可以直接获取坐标,因此它接受一个结构体指针类型的参数,就是蛇身体的第一个节点,而它不需要返回值。所以他的函数首部应该是:
void drawmap(struct snk *p,int*fd);//画出食物和蛇
至于函数内部,我们可以通过p->x或者p->y来访问蛇的头部的坐标,然后令p=p->link;来继续访问接下来一节蛇的坐标,直到p->link=NULL,这说明蛇到了结尾了。食物是全局变量可以直接获取坐标。获得坐标之后,你可以调用图形库来绘制,也可以在字符界面打印。总之就是遍历一次蛇的身体,根据每一个节点的坐标,分别把它们和食物一起打印在屏幕上就是了。
接下来是蛇的移动问题,怎样让蛇自动的移动呢?
这个只需要主循环while(1){………}每循环一次就改变一次蛇的位置即可,那么如何改变呢?这就体现出我们为什么要用链表来储存蛇的坐标了。我们并不需要改变蛇每一节的坐标,只需要“添头去尾”即可移动蛇,我们只需要在蛇的头部,根据蛇的运动方向添加一个节点,把它设为蛇头,判断此节点的坐标是否超出地图范围,如果超出这修改生命值为死亡,如果没有超出,则继续把它与食物的坐标进行比较,如果不同,则删除蛇的最后一个节点,蛇就向前移动一格了,如果相同,则蛇吃到了食物,此时不删除最后一节蛇的身体,蛇就增加了一节,然后再调用mkfood函数重新生成食物覆盖掉原来食物的坐标。
此处最主要的就是链表的添头去尾操作,添头其实就是新分配一块内存,把它的link指针指向原来的第一个节点,再把现在的第一个节点的地址返回给代表蛇头的结构体指针变量(把新添加的节点设为蛇头),去尾就是把最后一个节点用free()释放掉,把倒数第二个节点的link设为NULL即可。还有就是如何确定添加的头的坐标的问题,你需要根据之前的头的坐标和代表方向的变量的值来做,比如用1234分别代表上下左右方向,当方向为1时,蛇向上运动,那么在原来的蛇头的基础上把x减1,y不变来作为新头的坐标。其他方向同理。
然后是蛇的控制问题,
我们,可以用一个函数来获取输入,改变蛇的前进方向;它需要接收当前的运动方向(整型值),用来判断用户按下的方向是否与其相反(蛇不能向与当前方向相反的方向前进),然后返回一个运动方向,所以他的函数首部应该是:
int keydown(int z)//获取输入
这个函数里,我们可以使用conio.h里的getch来获取键盘输入,为什么不用getchar或者scanf呢?因为getch与getchar()虽然基本功能差不多,差别是getch()直接从键盘获取键值,不等待用户按回车,只要用户按一个键,getch就立刻返回,而getchar则是从输入缓冲区获取键值, getch返回值是用户输入的ASCII码,出错返回-1.输入的字符不会回显在屏幕上。
但是还有一个问题,那就是当程序执行到getch时会暂停,等待用户输入。这样如果我们不按键盘,游戏就无法继续。这个可以用conio.h里提供的kbhit()函数来检测是否有键盘被按下,
函数名:kbhit()(VC++6.0下为_kbhit())
功 能及返回值: 检查当前是否有键盘输入,若有则返回一个非0值,否则返回0
用 法:int kbhit(void);
于是我们可以这样写:
char ch;
if(kbhit()){
ch=getch();
switch(ch){
case’a’:。。。。。。;break;
case’d’:。。。。。。;break;
case’w’:。。。。。。;break;
case’s’:。。。。。。;break;
default:break;
}
}
根据按的键的不同来修改蛇的运动方向。
注意哦!在改变运动方向时不要忘记判断方向是否与当前的方向冲突!我们还可以用它来判断按下的是否是Esc键,如果是,则结束游戏。
至此,我们已经完成了贪吃蛇的最主要两个数据:
Food[2]和snake{
Int x;
Int y;
Int life ;
Struct snake *link ;
}
几个主要的功能函数,
void drawmap(struct snk*p,struct food *fd);//画地图食物和蛇
int keydown(int z);//获取输入
struct snk * mvsnk(struct snk *p,int z,struct food *fd);//移动并更新蛇的坐标
struct food * mkfood(struct snk *p);//产生食物
接下来就是将它们按照开始的程序框图组合起来,一条新鲜的贪吃蛇就出炉了!
当然,你还可以在最外面再套一个大循环以实现多次游戏。
如果你没有ege图形库,只需要稍微更改几个绘制图形的函数,就可以在控制台的字符界面实现了。