因为已经写了食物的实现,所以我不知道到底是该先写世界的实现还是蛇的实现。因为世界就是一个窗口,可以立刻在世界中看到食物的样子,对于大多数人来说,如果写完代码立刻就能看到效果,那就再好不过了。可是,我最后还是选择了先写蛇的实现这篇笔记。如果先写世界的实现,我就无法按照现在的思路完完整整的写下去,因为没有蛇,世界部分的代码就不完整,看完食物的效果后,我还是得写蛇的实现,然后又得修改世界部分的代码,来查看蛇的效果。反反复复,实在折腾不起。所以我打算把食物和蛇的实现都写完,最后统一看运行效果。
蛇和食物一样,得在世界中创建,所以代码基本差不多。
Snake * SNK_CreateSnake(World *world, int size, int x, int y) { Snake *snake; if (world == 0) return 0; if ((snake = (Snake *)SDL_malloc(sizeof(Snake))) == 0) return 0; INIT_SNAKE(world, size, x, y); SNK_GrowSnake(snake); return snake; }
宏INIT_SNAKE用于初始化Snake结构体。
SNK_GrowSnake函数用于将蛇的长度加一,因为蛇创建出来后只有蛇头,我必须再次给它加个蛇尾。如果只有蛇头,当然也能运行,这只是模拟,不是真正的生命体。不过这是畸形蛇,不好看。我还是让它正常一点,符合常规思维。
蛇的身体一节一节的,所以可以看到我在头文件中用了一个单向链表表示蛇的身体。所以销毁蛇时,我要遍历整个链表才行,然后依次释放每个身体节点。
void SNK_DestroySnake(Snake *snake) { struct Body *body; if (snake != 0) { if ((body = snake->body)) REMOVE_BODY(body); SDL_free(snake); snake = 0; } }
宏REMOVE_BODY就是用来遍历链表并释放身体节点的,我把它定义为一个宏,这显得有点多次一举。这么做主要是因为我不得不定义一个APPEND_BODY宏来增加蛇的身体节点,所以为了和增加节点相对应,我定义了移除节点这个宏。
移动蛇的位置分为两部,移动蛇头和移动蛇的身体。主要是由于定义的时候我没有把蛇头当作身体的一部分,因为身体可以增长,而蛇头不能增长,所以只能这样了。
void SNK_MoveSnake(Snake *snake) { struct Body *body; if (snake != 0) { MOVE_SNAKE(snake); for (body = snake->body; body; body = body->next) { MOVE_SNAKE(body); body->direction = (body->next != 0) ? body->next->direction : snake->direction; } } }
snake表示蛇头,MOVE_SNAKE(snake)表示移动蛇头的位置,MOVE_SNAKE(body)表示移动身体的位置。
移动身体需要遍历链表,不过这里设置身体方向不知道是否有人看懂? 当前身体节点的方向等于下一个身体节点的方向,仔细想想,这是什么意思?
我对蛇的分析是,蛇只会在尾部追加节点,如果snake->body指向第一个节点first, first指向第二个节点second, 那么我追加第三个节点就要从snake->body开始遍历两次,追加第四个节点就要从snake->body开始遍历三次。所以我改变了这个没有效率的行为,我让snake->body始终指向最后一个身体节点,因此当追加新的身体节点时,直接追加即可,而不用遍历链表。
所以这个for循环其实是从蛇尾向蛇头方向遍历的,当蛇头方向改变时,身体跟着蛇头变化,蛇尾跟着身体变化。这是蛇能随意转弯的关键所在。
接下来就是画出蛇的样子了,和画食物一样,我用一连串的矩形表示蛇。
void SNK_DrawSnake(Snake *snake) { SDL_Rect rect; struct Body *body; if (snake != 0) { rect.x = snake->x; rect.y = snake->y; rect.w = rect.h = snake->size; if (((snake->world != 0) ? (snake->world->render != 0) : 0)) { SDL_SetRenderDrawColor(snake->world->render, snake->color.r, snake->color.g, snake->color.b, snake->color.a); SDL_RenderDrawRect(snake->world->render, &rect); for (body = snake->body; body; body = body->next) { rect.x = body->x; rect.y = body->y; SDL_RenderDrawRect(snake->world->render, &rect); } } } }
对于蛇的增长,有两个意思:没有尾巴时,增长的是尾巴。有尾巴时,增长的是身体。
void SNK_GrowSnake(Snake *snake) { struct Body *body; if (snake != 0) { if ((body = (struct Body *)SDL_malloc(sizeof(struct Body))) == 0) return; if (snake->body == 0) { APPEND_BODY(snake, body); } else { APPEND_BODY(snake->body, body); } } }
接下来是检查碰撞的函数,它主要有两个用途:1. 当参数rect是蛇头位置时,用来检测蛇头是否咬到自己的身体。2. 当参数rect是食物位置时,用来检测身体是否碰到食物。咬到自己或者碰到食物,返回1, 否则返回0。
int SNK_HasIntersection(Snake *snake, SDL_Rect rect) { SDL_Rect bodyrect; struct Body *body; if (snake != 0) { bodyrect.w = bodyrect.h = snake->size; for (body = snake->body; body; body = body->next) { bodyrect.x = body->x; bodyrect.y = body->y; if (SDL_HasIntersection(&bodyrect, &rect) != 0) return 1; } } return 0; }
在头文件中,我定义了蛇的两个状态:已死或者可以移动。这个函数便是用于检测蛇的状态的。返回SNAKE_DIED表示蛇死了;返回SNAKE_MOVABLE表示蛇处于正常状态,可以自由移动;返回0表示蛇碰到世界的边界,不可以移动。我没有实现蛇可以从一边回到另一边这种功能,也没有规定蛇碰到墙就死了。一切尽可能保持简单!
int SNK_GetSnakeStatus(Snake *snake) { SDL_Rect headrect; if (((snake != 0) ? (snake->world != 0) : 0)) { headrect.w = (snake->x > 0 && snake->x < snake->world->w); headrect.h = (snake->y > 0 && snake->y < snake->world->h); if (headrect.w && headrect.h) { headrect.x = snake->x; headrect.y = snake->y; headrect.w = headrect.h = snake->size; if (SNK_HasIntersection(snake, headrect) != 0) return SNAKE_DIED; return SNAKE_MOVABLE; } else { switch (snake->direction) { case SNAKE_UP: headrect.x = (snake->y > 0); break; case SNAKE_DOWN: headrect.x = ((snake->y + snake->size) < snake->world->h); break; case SNAKE_LEFT: headrect.x = (snake->x > 0); break; case SNAKE_RIGHT: headrect.x = ((snake->x + snake->size) < snake->world->w); break; } return ((headrect.x != 0) ? SNAKE_MOVABLE : 0); } } return 0; }
这里switch语句只有当蛇碰到世界的边界时才会进入,这一段主要是为了实现一个功能:当蛇碰到世界的边界时,蛇无法再向前移动,但是蛇可以再次转弯。
未完,待续!