在《C语言贪吃蛇游戏演示和说明》一节中,我们对贪吃蛇游戏的玩法进行了介绍和演示,这节就来分析一下它的源码。
贪吃蛇源代码下载地址:https://pan.baidu.com/s/1pMk7nlx 密码:2yju
各位读者不妨先将源码下载下来浏览一遍,记住关键的几个函数,整理一下不了解的知识点,做到心中有数。
需要说明的是:贪吃蛇背景地图、食物、贪吃蛇本身都是由特殊字符组成(由 printf() 输出),并不是绘制出来的图形。C语言标准库没有绘图函数,如果绘图的话,就需要使用第三方库,增加了大家的学习成本,所以我们采用了“投机取巧”的办法,用特殊字符来模拟不同的图形。
贪吃蛇背景地图的最终效果如下图所示:
钻红色空心方框表示边框,绿色实心方框表示贪吃蛇的活动区域。实现代码如下:
#include <stdio.h>
#include <conio.h>
#include <windows.h>
int main(){
int width = 30, height = width; //宽度和高度
int x, y; //x、y分别表示当前行和列
HANDLE hConsole = GetStdHandle(STD_OUTPUT_HANDLE);
//设置窗口大小
system("mode con: cols=64 lines=32");
//打印背景,按行输出
for(x=0; x<width; x++){
for(y=0; y<height; y++){
if(y==0 || y==width-1 || x==0 || x==height-1){ //输出边框
SetConsoleTextAttribute(hConsole, 4 );
printf("□");
}else{ //贪吃蛇活动区域
SetConsoleTextAttribute(hConsole, 2 );
printf("■");
}
}
printf("\n");
}
//暂停
getch();
return 0;
}
程序的关键是两层嵌套的循环。x=0 时,内层循环执行30次,输出第0行;x=1 时,内层循环又执行30次,输出1行。以此类推,直到 x=30,外层循环不再执行(内存循环当然也就没机会执行),输出结束。
注意,□和■虽然都是单个字符,但它们不在ASCII码范围内,是宽字符,占用两个字节,用 putchar 等输出ASCII码(一个字节)的函数输出时可能会出现问题,所以作为字符串输出。
接下来,我们来让一条长度为 n 的贪吃蛇移动起来,而且可以用WASD四个键控制移动方向,如下图所示:
其实,移动贪吃蛇并不需要移动所有节点,只需要添加蛇头、删除蛇尾,就会有动态效果,这样会大大提高程序的效率。
我们可以定义一个结构体来表示贪吃蛇一个节点在控制台上的位置(也即所在行和列):
struct POS{
int x; //所在行
int y; //所在列
}
然后再定义一个比贪吃蛇长的数组来保存贪吃蛇的所有节点:
并设置两个变量 headerIndex、tailIndex,分别用来表示蛇头、蛇尾在数组中的下标坐标,这样每次添加蛇头、删除蛇尾时只需要改变两个变量的值就可以。如下图所示:
headerIndex 和 tailIndex 都向前移动,也就是每次减1。如果 headerIndex=0,也就是指向数组的头部,那么下次移动时 headerIndex = arrayLength - 1,也就是指向数组的尾部,就这样一圈一圈地循环,tailIndex 也是如此。这相当于把数组首尾相连成一个圆圈,贪吃蛇在这个圆圈中不停地转圈。
由于这部分的演示代码较长,请大家到百度网盘下载:http://pan.baidu.com/s/1bouZGoZ 提取密码:4g74
1) 贪吃蛇的最大长度为绿色方框的个数,所以我们将容纳贪吃蛇的数组 snakes 的长度定义为(HEIGHT-2) * (WIDTH-2)。
2) □、■、★ 占用两个字符的宽度,所以在 setPosition() 中该变光标位置时,光标的X坐标应该是:
食物的生成是贪吃蛇游戏的难点,因为食物只能在绿色背景(■)部分生成,它不能占用钻红色边框(□)和贪吃蛇本身(★)的位置。
最容易想到的思路是:随机生成一个坐标,然后检测该坐标是不是绿色背景,如果是,那么成功生成,如果不是,继续生成随机数,继续检测。幸运的话,可以一次生成;不幸的话,可能要循环好几次甚至上百次才能生成,这样带来的后果就是程序卡死一段时间,贪吃蛇不能移动。
这种方案的优点就是思路简单,容易实现,缺点就是贪吃蛇移动不流畅,经常会卡顿。
最好的方案是生成的随机数一定会在绿色背景的范围内,这样一次就能成功生成食物。该如何实现呢?
这里我们提供了一种看起来不容易理解却行之有效的方案。
我们不妨将贪吃蛇的活动范围称为“贪吃蛇地图”,而加上边框就称为“全局地图”。首先定义一个二维的结构体数组,用来保存所有的点(也即全局地图):
struct{
char type;
int index;
}globalMap[MAXWIDTH][MAXHEIGHT];
MAXWIDTH 为宽度,也即列数;MAXHEIGHT 为高度,也即行数。成员 type 表示点的类型,它可以是食物、绿色背景、边框和贪吃蛇节点。
直观上讲,应该将 type 定义为int类型,不过int占用四个字节,而节点类型的取值范围非常有限,一个字节就足够了,所以为了节省内存才定义为char类型。
然后再定义一个一维的结构体数组,用来保存贪吃蛇的有效活动范围:
struct{
int x;
int y;
} snakeMap[ (MAXWIDTH-2)*(MAXHEIGHT-2) ];
x、y 表示行和列,也就是 globalMap 数组的两个下标。globalMap 数组中的 index 成员就是 snakeMap 数组的下标。
globalMap 表示了所有节点的信息,而 snakeMap 只表示了贪吃蛇的活动区域。通过 snakeMap 可以定位 globalMap 中的元素,反过来通过 globalMap 也可以找到 snakeMap 中的元素。它们之间的对应关系请看下图:
贪吃蛇向左移动时,headerIndex 指向 404,tailIndex指向 406。
在 snakeMap 数组中,贪吃蛇占用一部分元素,剩下的元素都是绿色的背景,可以随机选取这些元素中的一个作为食物,然后通过 x、y 确定食物的坐标。而这个坐标,一定在绿色背景范围内。
需要注意的是,在贪吃蛇移动过程中需要维护 globalMap 和 snakeMap 的对应关系。
这种方案的另外一个优点就是,贪吃蛇移动时很容易知道下一个节点的类型,不用遍历数组就可以知道是否与自身相撞。