目录
一、编译环境
由于windows系统gcc编译器无法形成图像,故在进行贪吃蛇的项目完成时需要在turbo c 环境下运行。使用turbo c可以很容易实现贪吃蛇的显示。
二、知识储备
1、必备函数
1、 在turbo c存在一个头文件conin.h ,在这个头文件下存在三个我们需要的库函数: 1)、clrscr()清除字符窗口函数,这个函数是简称清屏函数,来清除当前屏幕上的所有字符。在本项目中应用于在打印蛇头之前对屏幕上无用字符的清除,以免影响显示结果。
2)、 window()字符窗口函数。此函数用来对贪吃蛇边界判定以及背景颜色的设定。
3)、gotoxy()光标定位函数。用来移动光标实现贪吃蛇的移动、食物的产生以及贪吃蛇的增长。
2、int bioskey (int cmd)函数:bioskey()完成直接键盘操作,cmd的值决定执行什么操作。
cmd = 0:当cmd是0,bioskey()返回下一个在键盘键入的值(它将等待到按下一个键)。它返回一个16位的二进制数,包括两个不同的值。当按下一个普通键时,它的低8位数存放该字符的ASCII码;对于特殊键(如方向键、F1~F12等等),低8位为0,高8位字节存放该键的扫描码。
cmd = 1:当cmd是1,bioskey()查询是否按下一个键,若按下一个键则返回非零值,否则返回0。
cmd = 2:当cmd是2,bioskey()返回Shift、Ctrl、Alt、ScrollLock、NumLock、CapsLock、Insert键的状态。各键状态存放在返回值的低8位字节中。
这个函数的定义与实现存在于bios.h中,我们在头文件中添加完毕后直接调用即可。
2、循环数组
在本项目中使用循环数组进行蛇头以及蛇身的移动,使用循环数组不仅可以降低移动算法的时间复杂度。还可以在之后的增长蛇身的长度中很容易实现。循环数组是对于数组的下标进行一定的运算,以保证仅仅用固定数量的数组空间来进行不定数量的存储。
具体表示 :index = (index + length - 1)% length;
3、发牌算法
发牌算法是一种可以选择出来不相同数据的选择算法。在本项目中主要应用在食物的产生函数中。对于发牌算法的具体介绍如下:
下列有15组数据:
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
15 14 13 12 11 10 9 8 7 6 5 4 3 2 1 0
现在随机取十个,要求不重复,可使用发牌算法,即选出来一个数字之后将此数字同最后一个数字下标交换,同时选取的数字范围减少一位,即将所选取过的数字发出去,最后一个未选取的数字存放在选取数字的下标所指向的空间中。
例证:从0-15中选择数字,现在选到下标为5的数字,
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
15 14 13 12 11 10 9 8 7 6 5 4 3 2 1 0
把10取出,并且与下标为十五的数字交换。
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
15 14 13 12 11 0 9 8 7 6 5 4 3 2 1 10
此时再一次选取数据时将选取范围减一,即从0-14中选择数字。这样做的好处是不会选到重复的数字。类似于玩扑克牌时发牌的原理,发完一张就少一张,且手里的扑克与发出去的牌不存在重复。 即发牌算法。
4、二维数组的转化
为了更好的表示整个贪吃蛇界面,我们可以使用将二维数组化为一维数组的思想,即将二维数组的横纵坐标使用一个坐标来表示,将整个屏幕化作一个地图,每一个点代表一个二维坐标,但是用一维数组来表示。
将整个屏幕作为地图
三、核心代码
1、结构体的介绍
1)、贪吃蛇游戏的主要构成参数:游戏是否正常,游戏是否结束,蛇头的下标位置,蛇的初始设定长度,蛇的当前长度,蛇的移动方向,接收到的按键指令,蛇移动的速度,游戏结束的标志。
typedef struct SNACK_SEC {
boolean ok;
boolean finished;
int headIndex;
int length;
int curlength;
int direct;
int key;
int speed;
boolean gameout;
int map[2000];
}S_SEC;
2)、蛇的位置参数:横坐标,纵坐标以及地图上的坐标
typedef struct SNACK_LONG {//蛇的结构体
int row;
int col;
int t;//地图下的一维坐标
}S_LONG;
3)食物参数:食物的横坐标和食物的纵坐标
typedef struct SNACK_FOOD {
int row;
int col;
}S_FOOD;
2、蛇的产生以及移动
蛇的产生是通过打印蛇头,在主程序中首先会赋初值给结构体S_SEC,如下:
S_SEC arg = {
TRUE, /*boolean ok;*/
FALSE, /*boolean finished;*/
3, /*int headIndex;*/
4, /*int length;*/
1, /*int curlength;*/
RIGHT, /*int direct;*/
0, /*int key;*/
DELAY_SIMPLE, /*int speed;*/
FALSE, /*boolean gameout;*/
0, /*int map[2000];*/
};
根据蛇的初始方向以及初始长度和当前长度来产生一条蛇。即通过打印蛇头的函数即可完成蛇的产生功能。
对于蛇的移动功能,我采用了擦头去尾的方法: 1)、去尾:如果蛇的当前长度小于初始长度不执行去尾程序,反之则去尾。 2)、擦头:把当前的蛇头变为蛇身。 3)、打印蛇头:再次执行打印蛇头程序。 这样便完成了蛇的移动功能!这样移动的好处不仅可以简单实现移动,而且在实现蛇吃食物后只需要不擦尾即可变长。
具体程序实现如下:
void movesnack(S_SEC *arg,S_LONG *lon,S_FOOD *food){
int tailIndex;
int i;
if (arg->curlength < arg->length) {
arg->curlength++;
} else {
tailIndex = (arg->headIndex + 200 - (arg->length) + 1) % 200;
gotoxy(lon[tailIndex].row,lon[tailIndex].col);
printf(" ");
lon[tailIndex].t = (lon[tailIndex].row -1) * COL_COUNT + (lon[tailIndex].row -1);
arg->map[lon[tailIndex].t]= 0;
}
gotoxy(lon[arg->headIndex].row,lon[arg->headIndex].col);
printf("*");
lon[arg->headIndex].t = (lon[arg->headIndex].row -1) * COL_COUNT + (lon[arg->headIndex].row -1);
arg->map[lon[arg->headIndex].t]= 2;
lon[(arg->headIndex + 200 + 1) % 200].row = lon[arg->headIndex].row + delta[arg->direct][0];
lon[(arg->headIndex + 200 + 1) % 200].col = lon[arg->headIndex].col + delta[arg->direct][1];
arg->headIndex = (arg->headIndex + 200 + 1) % 200;
gotoxy(lon[arg->headIndex].row,lon[arg->headIndex].col);
lon[arg->headIndex].t = (lon[arg->headIndex].row -1) * COL_COUNT + (lon[arg->headIndex].row -1);
arg->map[lon[arg->headIndex].t]= 2;
printfHead(arg);
for (i = 0; i < 10; ++i) {
if(food[i].row == lon[arg->headIndex].row && food[i].col == lon[arg->headIndex].col) {
arg->length++;
food[i].row = 0;
food[i].col = 0;
}
}
}
对于循环数组的使用具体图解如下:
3、食物的产生
蛇的食物的产生首先需要产生0到两千2000个随机数,然后用发牌算法选出十个不相同的地图上的点,作为食物产生的点。将一维坐标转化为二维坐标。
准备过程:这多次用到一维数组与二维数组下标转换的关系。
1)、例显示区域为80*25,则先准备一个含有2000个元素的数组,赋值为0-1999。
2)、把蛇的当前的蛇身坐标转换为一维数组的下标,并更改对应数组元素为-1.
3)、定义一个头指针,一个尾指针,分别从下标为0和1999相向遍历,若head对应的元素为-1,停下。若tail对应的元素不为-1,则停下。交换head和tail对应的元素,就这样一直找,知道把所有的-1移到数组的最后。
4)、准备工作已经做好,开始洗牌算法。在数组前面不为-1的元素里面,利用洗牌算法,取出一个随机数作为下标,把对应的元素再转换为坐标输出到屏幕上,即产生的食物。
代码如下:
void foodProduce(S_SEC *arg,S_FOOD *food) {
int i;
int j;
int t;
int k;
int count = 0;
int arr[10];
int number[2000] = {-1};
for(i = 0,j = 0; i < 2000; i++,j++) {
if(arg->map[i] == 0){
number[j] = i;
}
}
for(i = 0; i < 1999; i++){
if (number[i] > 0) {
count++;
}
}
srand(time( NULL ));
for (t = count,i = 0; i < 10 ; --t,++i) {
j = rand()%t;
arr[i] = number[j];
k = number[t];
number[t] = number[j];
number[j] = k;
}
for (i = 0; i < 10; ++i) {
food[i].row = (arr[i] % COL_COUNT) + 1;
food[i].col = (arr[i] / COL_COUNT) + 1;
gotoxy(food[i].row,food[i].col);
printf("*");
arg->map[arr[i]] = 1;
}
}
四、运行截图
五、总结
1、这个贪吃蛇在TC上可完美运行,控制上也较简单。 贪吃蛇是一个即时性要求高的程序,所以这里采用了空间换时间的方法(定义了两个大数组),例如洗牌算法那里时间复杂度最终为O(n)。精妙之处在于循环数组的运用和洗牌算法。
2、这个贪吃蛇程序仅仅为数据结构练手的项目,通过这个练习,明显的感觉到在编程时,对函数的形参与实参的调用,指针的运用等等有了更加深刻的认识。
3、项目=数据结构+算法。选取一个好的算法在特定的地方使用,会使自己的程序在效率上提升很多。在编程时,一定一定要组织好代码的大体框架,逻辑一定要清晰。否则,当程序慢慢变得复杂起来,出现问题时,逻辑不清晰,调试就很难很难,这时候不妨删掉以前的代码,重新组织编写。
4、在这个项目中使用了地图化的思想,将二维坐标转化为一维的点,这个思想不仅可以帮助我们区分是否各种不同的元素,以及时间复杂度更低为O(n),更好的优化了我们的算法,在之后的编程中也应该多使用这样的思想,逐步优化算法,完善程序代码。