首页 > 代码库 > [C入门 - 游戏编程系列] 贪吃蛇篇(五) - 蛇实现

[C入门 - 游戏编程系列] 贪吃蛇篇(五) - 蛇实现

  因为已经写了食物的实现,所以我不知道到底是该先写世界的实现还是蛇的实现。因为世界就是一个窗口,可以立刻在世界中看到食物的样子,对于大多数人来说,如果写完代码立刻就能看到效果,那就再好不过了。可是,我最后还是选择了先写蛇的实现这篇笔记。如果先写世界的实现,我就无法按照现在的思路完完整整的写下去,因为没有蛇,世界部分的代码就不完整,看完食物的效果后,我还是得写蛇的实现,然后又得修改世界部分的代码,来查看蛇的效果。反反复复,实在折腾不起。所以我打算把食物和蛇的实现都写完,最后统一看运行效果。

 

蛇和食物一样,得在世界中创建,所以代码基本差不多。

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语句只有当蛇碰到世界的边界时才会进入,这一段主要是为了实现一个功能:当蛇碰到世界的边界时,蛇无法再向前移动,但是蛇可以再次转弯。

 

未完,待续!

[C入门 - 游戏编程系列] 贪吃蛇篇(五) - 蛇实现