目录
一:实现环境
二:程序优点
1、洗牌算法:
2、循环数组:
3、二维坐标与一维坐标的转化:
三:准备过程(开始之前务必熟悉)
四:结构体的定义
五:主要函数的实现
1、移动蛇的函数:
2、随机产生食物函数:
3、边界,障碍物,食物,头撞到身体的判断
六:运行截图
七:总结
八:源码地址
Sublime Text 3,TC 2.0
之前在网上看过很多贪吃蛇的博客,代码。产生随机食物的方法大多都是利用随机函数产生,然后用循环判断食物是否落到边界,蛇身等,如果是,则再调用随机函数产生食物。说实话这样的方法逻辑是很简单,但是,贪吃蛇是一个实时性要求较高的小游戏,如果随机产生的食物一直落到边界,那就一直调用随机函数吗?而且这样做也增加了时间复杂度。看一段代码(网上找的)。
unsigned char generate_food(void)
{
unsigned char food_,fx,fy;
int in_snake=0,i;
srand((unsigned int)time(NULL));
do {
food_ = rand() % 255;
tran(food_, &fx, &fy);
for (i = 0; i < len; i++)
if (food_ == snake[i])
in_snake = 1;
} while (fx == 0 || fx == 16 || fy == 0 || fy == 16 || in_snake);
return food_;
}
一个do-while循环,大大提高了时间复杂度。所以,我的方法是利用洗牌算法完成。利用洗牌算法, 产生的随机食物一定不会出现在边界和蛇身上。
之前考虑过用链表实现,但是,使用链表的方式效率不是很好。因为,贪吃蛇的移动需要找蛇尾,蛇头,使用链表的话,就需要每次从链表的表头查找,移动一下,就找一次,不觉得很麻烦吗?但是,利用循环数组,就解决了这样的问题,利用下标运算,可以任意定位到蛇身,蛇尾,蛇头的每一个点,提高了时间复杂度。
二维数组的本质就是一维数组。一维数组操作简单,当然首选的就是一维数组。由于屏幕的坐标是二维的,所以,两者之间的转化是一个很重要的过程。并且,定义一个和屏幕一样大的数组,把蛇的身体,障碍物,食物,边界的坐标都转化为一维数组的下标存进去,写一个专门的check函数,在函数内部进行各种判断,程序的逻辑也就变得简单明了。
定义一个大的数组,采用的是空间换时间思想。对于一个算法来说,空间复杂度和时间复杂度往往是相互影响的。当追求一个较好的时间复杂度时,可能会使空间复杂度的性能变差,即可能导致占用较多的存储空间;反之,当追求一个较好的空间复杂度时,可能会使时间复杂度的性能变差,即可能导致占用较长的运行时间。有时我们可以用空间来换取时间以达到目的。
void gotoxy(int x, int y)函数:实现光标的移动。注意,TC中第一个参数为列坐标,第二个参数为行坐标。
typedef struct DELTA_MOVE {
int deltaCol;
int deltaRow;
} DELTA_MOVE;
const DELTA_MOVE deltaMove[4] = {{0, -1}, {0, 1}, {-1, 0}, {1, 0}};
存储移动方向上,下,左,右的增量。
typedef struct SNAKE_BODY {
int bodyCol;
int bodyRow;
} SNAKE_BODY;
存储蛇身体的坐标。
typedef struct FOOD_POINT {
int foodCol;
int foodRow;
} FOOD_POINT;
存储食物的坐标。
typedef struct SNAKE {
int head;
int len;
int curLen;
int maxLen;
int direct;
SNAKE_BODY snakeBody[MAX_SANKE_LEN];
} SNAKE;
存储蛇的各项信息。curLen,len两个属性为以后蛇的移动,增长提供了非常大的便利。
/*移动蛇*/
/*
使用循环数组来存储蛇身体每个点的行,列坐标
初始显示一个蛇头,慢慢增多,知道等于设定的蛇的长度
蛇移动的过程:
1、先在蛇头的位置输出一个蛇的身体
2、再在之前蛇头的下一个位置输出一个蛇头
3、消去尾部
*/
void moveSnake(SNAKE *snake, int directionIndex) {
int oldSnakeHeadPoint;
int removeOphiurid;
SNAKE_BODY *snakeBody = snake->snakeBody;
if(snake->curLen > 1) {
gotoxy(snakeBody[snake->head].bodyCol, snakeBody[snake->head].bodyRow);
putchar('*');
}
oldSnakeHeadPoint = snake->head;
snake->head = (snake->head + 1) % snake->maxLen;
changeSnakeHeadPoint(&snakeBody[snake->head], snakeBody[oldSnakeHeadPoint], directionIndex);
gotoxy(snakeBody[snake->head].bodyCol, snakeBody[snake->head].bodyRow);
putchar(showSnakeHead[directionIndex]);
if(snake->curLen >= snake->len) {
removeOphiurid = (snake->head - snake->len + snake->maxLen) % snake->maxLen;
gotoxy(snakeBody[removeOphiurid].bodyCol, snakeBody[removeOphiurid].bodyRow);
putchar(32);
/*把上一次蛇尾在barrierArray中的数据清零*/
barrierArray[(snakeBody[removeOphiurid].bodyRow - 1) * 65 + snakeBody[removeOphiurid].bodyCol - 1] = 0;
} else {
(snake->curLen)++;
}
gotoxy(snakeBody[snake->head].bodyCol, snakeBody[snake->head].bodyRow);
}
主要思路:当蛇的当前长度curLen等于1时,只输出一个蛇头。
当蛇的当前长度curLen大于1,小于蛇的长度len时,先在蛇头的位置输出一个蛇身,再找到下一个蛇头的位置输出蛇头,直到等于蛇的长度len(刚开始运行阶段)。
当蛇的当前长度curLen大于等于蛇的长度len时,先在蛇头的位置输出一个蛇身,再找到下一个蛇头的位置输出蛇头,最后找到蛇尾,输出空格即可(这就是移动)。
实现蛇的移动,循环数组是关键。利用循环数组, 可以实现蛇身上任意一个点的快速定位。
蛇的最大长度就为循环数组的长度,循环数组里存着每个蛇身的坐标。
/*产生随机食物过程:
1、定义一个能存储全屏幕坐标的一维数组,大小为80*25,初始化为0-1999
2、根据蛇的当前长度,利用蛇头找到蛇的每个身体的坐标,把蛇身体坐标转换为一维数组的下标,此下标元素赋值为-1
3、定义一个头指针,一个尾指针,分别从下标为0和1999相向遍历,若head对应的元素为-1,停下。若tail对应的元素不为-1,则停下
4、交换head和tail对应的元素,就这样一直找,知道把所有的-1移到数组的最后
5、在数组前面不为-1的元素里面,利用洗牌算法,取出一个随机数作为下标,把对应的元素再转换为坐标输出到屏幕上,即产生的食物*/
/*显示的区域:63列,23行*/
void makeFood(SNAKE snake, int foodCount, FOOD_POINT *foodPoint) {
short screen[1449];
int head = 0;
int tail = 1448;
int i;
int y;
int x;
int index;
int randIndex;
int count = 1449;
int foodCol;
int foodRow;
for(i = 0; i < 1449; i++) {
screen[i] = i;
}
for(i = 0; i < snake.curLen; i++) {
index = (snake.head - i + snake.maxLen) % snake.maxLen;
y = snake.snakeBody[index].bodyCol;
x = snake.snakeBody[index].bodyRow;
screen[(x - 2) * 63 + y - 2] = -1;
count--;
}
while(head <= tail) {
while(screen[head] != -1) {
head++;
}
while(screen[tail] == -1) {
tail--;
}
if(head <= tail) {
swap(&screen[head], &screen[tail]);
}
}
srand(time(0));
for(i = 0; i < foodCount; i++) {
randIndex = rand() % ((count--) + 1);
foodPoint[i].foodCol = screen[randIndex] % 63 + 2;
foodPoint[i].foodRow = screen[randIndex] / 63 + 2;
gotoxy(foodPoint[i].foodCol, foodPoint[i].foodRow);
putchar('O');
/*食物的二维坐标转换为数组的一维下标存起来,为4*/
barrierArray[(foodPoint[i].foodRow - 1) * 65 + foodPoint[i].foodCol - 1] = 4;
swap(&screen[randIndex], &screen[count]);
}
}
主要思路:洗牌算法。
准备过程:这多次用到一维数组与二维数组下标转换的关系。
1、例显示区域为80*25,则先准备一个含有2000个元素的数组,赋值为0-1999。
2、把蛇的当前的蛇身坐标转换为一维数组的下标,并更改对应数组元素为-1.
3、定义一个头指针,一个尾指针,分别从下标为0和1999相向遍历,若head对应的元素为-1,停下。若tail对应的元素不为-1,则停下。交换head和tail对应的元素,就这样一直找,知道把所有的-1移到数组的最后。
4、准备工作已经做好,开始洗牌算法。在数组前面不为-1的元素里面,利用洗牌算法,取出一个随机数作为下标,把对应的元素再转换为坐标输出到屏幕上,即产生的食物。
void checkBarrier(SNAKE *snake, int *finished, int *foodCount, FOOD_POINT *foodPoint) {
int col = snake->snakeBody[snake->head].bodyCol;
int row = snake->snakeBody[snake->head].bodyRow;
int snakeHeadElement = barrierArray[(row - 1) * 65 + col - 1];
int i;
if(snakeHeadElement == 1 || snakeHeadElement == 2 || snakeHeadElement == 3) {
*finished = 1;
}
if(snakeHeadElement == 4) {
(snake->len)++;
--(*foodCount);
}
for(i = 0; i < *foodCount; i++) {
if((foodPoint[i].foodRow - 1) * 65 + foodPoint[i].foodCol - 1 == 3) {
makeFood(*snake, *foodCount, foodPoint);
}
}
}
主要思路:把边界,障碍物,蛇身,食物的二维坐标转换为数组的一维下标存起来,分别对对应的元素赋值为1, 2, 3, 4。把蛇头的坐标转换为数组下标,比较对应的元素是不是1,2,3,若是,则游戏结束;若为4,则表明吃到食物, 蛇变长。若食物落到障碍物上,则重新随机产生食物。
这个贪吃蛇在TC上可完美运行,控制上也较简单。 贪吃蛇是一个即时性要求高的程序,所以这里采用了空间换时间的方法(定义了两个大数组),例如洗牌算法那里时间复杂度最终为O(n)。精妙之处在于循环数组的运用和洗牌算法。
不足之处是每次随机出现食物只能出现一个固定的值,我觉得每次给食物的数量加一是个不错的办法,即随机产生食物坐标的时候多产生一个即可,设置一个食物增加的增量。
发挥想象,还可以再增加一个蛇, 加入wasd按键控制,实现双人对战。即再定义一个蛇的结构体变量,函数调用多传一个参数等等,把细节考虑到位即可。
这个贪吃蛇程序仅仅为数据结构练手的项目,通过这个练习,明显的感觉到在编程时,对函数的形参与实参的调用,指针的运用等等有了更加深刻的认识。
程序=数据结构+算法。选取一个好的算法在特定的地方使用,会使自己的程序在效率上大大提升。
在编程时,一定一定要组织好代码的大体框架,逻辑一定要清晰。否则,当程序慢慢变得复杂起来,出现问题时,逻辑不清晰,调试就很难很难,这时候不妨删掉以前的代码,重新组织编写。
单人版:
https://github.com/yangchaoy259189888/Gluttonous-Snake/
双人版(做了一点点优化):
https://github.com/yangchaoy259189888/Gluttonous-Snake-Double-against/