• 设为首页
  • 点击收藏
  • 手机版
    手机扫一扫访问
    迪恩网络手机版
  • 关注官方公众号
    微信扫一扫关注
    公众号

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

原作者: [db:作者] 来自: [db:来源] 收藏 邀请

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

 

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

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

 

未完,待续!


鲜花

握手

雷人

路过

鸡蛋
该文章已有0人参与评论

请发表评论

全部评论

专题导读
上一篇:
C#枚举类型-Enum发布时间:2022-07-14
下一篇:
C语言小知识点汇集发布时间:2022-07-14
热门推荐
阅读排行榜

扫描微信二维码

查看手机版网站

随时了解更新最新资讯

139-2527-9053

在线客服(服务时间 9:00~18:00)

在线QQ客服
地址:深圳市南山区西丽大学城创智工业园
电邮:jeky_zhao#qq.com
移动电话:139-2527-9053

Powered by 互联科技 X3.4© 2001-2213 极客世界.|Sitemap