C语言贪吃蛇(坤)(全解注释-手把手教会)

00简介:

0.1:演示:

C语言贪吃蛇(坤)(全解注释-手把手教会)_第1张图片

0.2:游戏说明:

玩家通过右方的方向键和加速键来控制贪吃坤吃到随机刷新的篮球来尽可能增长贪吃坤的体长,增加分数。

1.项目说明:

1.1项目主题:

本项目的主题是创建一个基于C语言的贪吃蛇游戏,运用双向循环链表来管理蛇的身体节点,实现蛇体的灵活移动。同时,通过碰撞检测机制,让蛇在游戏中与食物、墙壁及自身进行互动,实现游戏的核心逻辑。

1.2技术实现:

项目采用C语言作为开发语言,整个游戏的设计主要包含以下几个方面的知识和技术实现:

  • 数据结构的运用

    • 使用双向循环链表来存储蛇的身体节点,每个节点包含坐标信息(x, y)和指向前后节点的指针。这种结构能够方便地实现蛇的生长、移动和删除操作。
  • 枚举类型的设计

    • 通过定义snakeDir_tsnakeState_t枚举类型,管理蛇的方向和状态,允许程序在不同情况下执行不同的逻辑,使代码更加清晰和易于管理。
  • 游戏逻辑的实现

    • 实现了蛇的移动逻辑、食物的生成与吃食逻辑、蛇的碰撞检测等。特别是通过isNextFoodisSnakeDie函数来判断蛇是否吃到了食物或是否死亡,提高了游戏的智能化。
  • 用户交互

    • 采用触摸屏输入方式,用户通过触碰屏幕控制蛇的移动方向。结合动态刷新界面和动画效果,增强了用户的游戏沉浸感。
  • 多线程与延时处理

    • 为了保证游戏的流畅性,使用usleep()函数进行延时处理,控制蛇的移动速度,确保每次移动都有适当的间隔,避免蛇的移动过快影响游戏体验。

1.3项目框架:

项目的整体框架分为几个模块,包括:

  1. 节点与数据结构模块:负责定义snakeNode_tsnake_t结构体,以及相关的链表操作函数。
  2. 游戏逻辑模块:处理游戏状态的更新,包括蛇的移动、碰撞逻辑、食物生成、以及游戏得分的计算。
  3. 用户交互模块:管理用户的输入,响应触摸事件,并实时更新游戏界面。
  4. 初始化与资源管理模块:负责初始化游戏设置,例如蛇的初始位置和速度,并管理游戏过程中的资源

1.4项目开发工具:

本项目在Ubuntu Linux环境下进行开发,主要使用以下工具和技术:

  1. 开发环境

    • 代码编辑器:使用 Visual Studio Code (VSCode) 作为主要的代码编辑器。VSCode 提供了丰富的插件和调试支持,极大提高了开发效率。
    • 编译工具链:使用 arm-linux-gcc 编译器针对 ARM 架构进行编译。此编译器适用于嵌入式系统,可以将 C 代码编译为适合 ARM 处理器的可执行文件。
  2. 文件管理

    • 项目文件通过 NFS (网络文件系统) 共享到目标开发板。这一方式允许开发者在本地机器上进行开发与测试,同时能够方便地将代码同步到开发板进行实际运行。
    • 在 NFS 的配置中,确保设置合适的权限,以便于无障碍地访问和修改共享的项目文件。

2.项目实现:

2.1项目整体架构:

void snakePlay()

{
    //界面初始化
    
    //蛇重置
   snakeRestart(snake);

    //开始游戏
    while(1)
    {
        //检测触摸判断
        if()
        {
            inputSnakeDir赋值
        }
        
        //蛇移动
        snakeMove();

        //判断下一步是否是食物
        if()
        {
            //是,吃食物(头插)
            eatFood();
            //刷新食物
            creatFood();
        }

        //检测蛇的状态是否死亡
        isSnakeDie();

        //打印蛇身
        snakePrintf(snak);

        //检测是否按下加速按钮
        if()
        {
            //赋值加速后的速度
        
        }else
        {
            //赋值原来的速度
        }
        
        // 延时使蛇不会跑得很快,一节一节一点一点跑
        usleep(snake->snakeSpeed);

        //检测是否按下返回
        if()
        {
        
        }
    }


}

2.2 主要功能模块及API

 2.2.1节点与数据结构:

 定义蛇的各个状态和蛇身节点和蛇的结构体。

/// @brief 蛇的节点设计
typedef struct snakeNode
{
    int x;      //蛇身节点坐标X
    int y;      //蛇身节点坐标X
    struct snakeNode *next; //指向下一个蛇身节点的next指针
    struct snakeNode *prev; //指向上一个蛇身节点的prev指针
} snakeNode_t, *P_snakeNode_t;

/// @brief 枚举定义蛇的每个方向
typedef enum snakeDir
{
    noDir = 0,  //无方向
    up = 1,     //向上
    down = 2,   //向下
    left = 3,   //向左
    right = 4,  //向右
} snakeDir_t;

/// @brief 枚举定义蛇的状态
typedef enum snakeState
{
    alive = 1,          //存活
    KILL_BY_WALL = 2,   //蛇头撞墙被墙杀死
    KILL_BY_SELF = 3,   //蛇头吃到自己被自己杀死
} snakeState_t;

// 蛇的状态设计为一个结构体;
typedef struct snake
{
    P_snakeNode_t snakeHead_P; // 指向蛇头的指针
    snakeDir_t snakeDir;       // 蛇方向
    int snakeLenth;            // 蛇的长度
    int snakeSpeed;            // 蛇的速度
    snakeState_t snakeState;   // 蛇的状态
    P_snakeNode_t snakeFood;   // 蛇的食物
    int foodScore;             // 食物的分数
    int snakeGameScore;        // 贪吃蛇分数

} snake_t, *P_snake_t;

2.2.2游戏初始化

全局变量设置:

// 初始化snake为全局变量
P_snake_t snake = NULL;
// 设置简单难度,中难度,困难难度,以及新速度记录蛇的速度困难级别速度,newSpeed记录蛇加速后的速度(原速度/2)
int snakeEasySpeed = 500000, snakeMiddleSpeed = 250000, snakeDIffcultSpeed = 100000, oldSpeed = 0, newSpeed = 0;
// 初次进入游戏会提示用户设置蛇的速度,之后不会再设置直到退出再次进入游戏
bool isSnakeSetSpeed = false;

游戏初始化:

    //设置为false,在开局设置完速度后设置为true后,直到游戏退出再进入都不会设置速度了
    isSnakeSetSpeed = false;
    // calloc申请蛇的大结构体空间
    snake = (P_snake_t)calloc(1, sizeof(snake_t));
    if (snake == NULL)
    {
        printf("snake calloc is fail.\n");
        perror("snake calloc is fail");
        return;
    }
    // 蛇头初始化,snakeInitX, snakeInitY宏定义蛇头在地图中间
    snake->snakeHead_P = snakeNodeInit(snakeInitX, snakeInitY);
    if (snake->snakeHead_P == NULL)
    {
        printf("snake head calloc is fail.\n");
        perror("snake head calloc is fail");
        free(snake);
        return;
    }
    // 蛇重置初始化
    snake = snakeRestart(snake);
    // 设置蛇// 吃一个食物是五分
    snake->foodScore = 5;
    // 打印用户名
    writeFontProMax(currentUser.name, 580, 15, 100, 40, 0, 0);
    // 打印本局得分
    char scoreBuff[30] = {0};
    memset(scoreBuff, 0, sizeof(scoreBuff));
    sprintf(scoreBuff, "%d%c", snake->snakeGameScore, '\0');
    writeFontProMax(scoreBuff, 630, 154, 100, 40, 0, 0);
    // 初始化键盘输入方向为无
    snakeDir_t inputSnakeDir = noDir;

重置蛇snakeRestart()函数API:

/// @brief 重置蛇
/// @param snake 蛇的大结构体
/// @return 返回重置后的蛇的大结构体
P_snake_t snakeRestart(P_snake_t snake)
{
    // 判断是否设置过蛇的速度(即判断是否是第一次进入游戏)
    if (isSnakeSetSpeed == false)
    {
        // 设置isSnakeSetSpeed为true,之后游戏不会再设置蛇的难度
        isSnakeSetSpeed = true;
        writeFontProMax("请选择难度:1坤(简单),2坤(中),2.5坤(困难)", 0, 225, 480, 40, 0, 0);
        showRandBmp(display(interface_List_head, 609), 40, 270);
        showRandBmp(display(interface_List_head, 610), 110, 270);
        showRandBmp(display(interface_List_head, 611), 240, 270);
        memset(&touchResult, 0, sizeof(touchType));
        // 用户再次选择难度后再开始游戏
        while (1)
        {
            // 简单难度
            if (touchResult.touchX > 40 && touchResult.touchX < 100 && touchResult.touchY > 270 && touchResult.touchY < 330)
            {
                snake->snakeSpeed = snakeEasySpeed;
                memset(&touchResult, 0, sizeof(touchType));
                break;
            } // 中难度
            else if (touchResult.touchX > 110 && touchResult.touchX < 230 && touchResult.touchY > 270 && touchResult.touchY < 330)
            {
                snake->snakeSpeed = snakeMiddleSpeed;
                memset(&touchResult, 0, sizeof(touchType));

                break;
            } // 困难难度
            else if (touchResult.touchX > 240 && touchResult.touchX < 420 && touchResult.touchY > 270 && touchResult.touchY < 330)
            {
                snake->snakeSpeed = snakeDIffcultSpeed;
                memset(&touchResult, 0, sizeof(touchType));

                break;
            }
        }
        // 显示新的贪吃蛇地图
        showRandBmp(display(interface_List_head, 602), 0, 0);
        memset(&touchResult, 0, sizeof(touchType));
        // oldSpeed记录蛇的初始速度,newSpeed记录蛇的加速速度(即(snake->snakeSpeed) / 2)
        oldSpeed = snake->snakeSpeed, newSpeed = (snake->snakeSpeed) / 2;
    }

    // 输出信息让用户点击屏幕开始游戏
    // 清空之前输出的字符串
    writeFontProMax("                           ", 40, 225, 320, 40, 0, 0);
    writeFontProMax("请点击地图开始!", 115, 225, 180, 40, 0, 0);
    // 卡住直到用户点击开始
    memset(&touchResult, 0, sizeof(touchType));
    // 用户再次点击在开始游戏
    while (1)
    {
        if (touchResult.touchX > 0 && touchResult.touchX < 480 && touchResult.touchY > 0 && touchResult.touchY < 480)
        {
            break;
        }
    }
    memset(&touchResult, 0, sizeof(touchType));
    // 释放链表除蛇头节点之外的指针 释放食物 蛇头next指向自己
    destroySnakeBodyNode(snake);
    snake->snakeHead_P->next = snake->snakeHead_P->prev = snake->snakeHead_P;
    // 刷新蛇结构体 刷新蛇的状态
    // 初始化蛇头方向
    snake->snakeDir = left;
    // 初始化蛇身长度为1
    snake->snakeLenth = 1;
    // 初始化蛇为存活状态
    snake->snakeState = alive;
    // //重新随机生成食物
    // snake->snakeFood = creatFood(snake);
    // 初始化蛇头坐标,让蛇头一开始刷在地图中间
    snake->snakeHead_P->x = snakeInitX;
    snake->snakeHead_P->y = snakeInitY;
    // 初始化用户的贪吃蛇分数为0
    snake->snakeGameScore = 0;
    // 打印新的贪吃蛇地图
    showRandBmp(display(interface_List_head, 602), 0, 0); // 打印地图
    // 重新随机刷新生成食物
    snake->snakeFood = creatFood(snake);
    // 打印蛇身
    snakePrintf(snake);
    return snake;
}

2.2.3蛇移动逻辑

1.判断输入方向:

        // 触摸键盘有效判断
        // inputSnakeDir = 判断用户触摸方向键,触摸事件置零
        // 上方向键 圆半径38 圆心 606,293
        if (((touchResult.touchX - 606) * (touchResult.touchX - 606) + (touchResult.touchY - 293) * (touchResult.touchY - 293)) <= 38 * 38)
        {
            // 设置键盘输入方向为up
            inputSnakeDir = up;
            // 重新初始化触摸事件
            memset(&touchResult, 0, sizeof(touchType));
        }
        // 下方向键 圆半径38 圆心 606,438
        if (((touchResult.touchX - 606) * (touchResult.touchX - 606) + (touchResult.touchY - 438) * (touchResult.touchY - 438)) <= 38 * 38)
        {
            inputSnakeDir = down;
            memset(&touchResult, 0, sizeof(touchType));
        }
        // 左方向键 圆半径38 圆心518 ,365
        if (((touchResult.touchX - 518) * (touchResult.touchX - 518) + (touchResult.touchY - 365) * (touchResult.touchY - 365)) <= 38 * 38)
        {
            inputSnakeDir = left;
            memset(&touchResult, 0, sizeof(touchType));
        }
        // 上方向键 圆半径38 圆心 690,365
        if (((touchResult.touchX - 690) * (touchResult.touchX - 690) + (touchResult.touchY - 365) * (touchResult.touchY - 365)) <= 38 * 38)
        {
            inputSnakeDir = right;
            memset(&touchResult, 0, sizeof(touchType));
        }

移动蛇函数snakeMove()API:

/// @brief 根据蛇头的方向来移动蛇
/// @param snake 蛇的大结构体
/// @param dir 用户触摸键盘输入的蛇的新方向
void snakeMove(P_snake_t snake, snakeDir_t *dir)
{
    // 判断用户是否触摸方向键
    if (*dir)
    {
        // 设置蛇的方向为用户输入的方向
        snake->snakeDir = *dir;
        //*dir重新置零
        *dir = noDir;
    }

    // 临时变量遍历蛇来控制蛇的坐标移动
    P_snakeNode_t tmp = NULL;
    i = 0;
    // 从尾部开始遍历蛇最后再根据蛇头方向更新蛇的头
    for (tmp = snake->snakeHead_P->prev; i < snake->snakeLenth; i++, tmp = tmp->prev)
    {
        // 当tmp遍历到蛇头时
        if (tmp == snake->snakeHead_P)
        {
            // 判断蛇的前进方向更新蛇头坐标
            if (snake->snakeDir == up)
            {
                (snake->snakeHead_P->y)--;
            }
            else if (snake->snakeDir == down)
            {
                (snake->snakeHead_P->y)++;
            }
            else if (snake->snakeDir == left)
            {
                (snake->snakeHead_P->x)--;
            }
            else if (snake->snakeDir == right)
            {
                (snake->snakeHead_P->x)++;
            }
        }
        else
        {
            // 判断tmp节点和上一个节点的坐标关系更新坐标
            if ((tmp->x) == (tmp->prev->x))
            {
                if ((tmp->y) > (tmp->prev->y))
                {
                    (tmp->y)--;
                }
                else
                {
                    (tmp->y)++;
                }
            }
            else if ((tmp->y) == (tmp->prev->y))
            {
                if ((tmp->x) > (tmp->prev->x))
                {
                    (tmp->x)--;
                }
                else
                {
                    (tmp->x)++;
                }
            }
        }
    }
}

2.2.4蛇吃食物逻辑:

1.判断是否是食物:

isNextFood()函数API:

/// @brief 判断蛇是否吃到食物
/// @param snake 蛇的大结构体
/// @return 吃到食物时返回真,没有吃到食物时返回假
bool isNextFood(P_snake_t snake)
{
    // 判断蛇头坐标是不是食物 x,y 是否相同
    return (((snake->snakeHead_P->x) == snake->snakeFood->x) && ((snake->snakeHead_P->y) == (snake->snakeFood->y)));
}

2.吃食物eatFood()函数API:

/// @brief 蛇吃食物
/// @param snake 蛇的大结构体
void eatFood(P_snake_t snake)
{
    // 拓展:吃一下食物就坤一声
    // 打印一个空的地图格子到原来食物格子位置覆盖原食物格子
    showRandBmp(display(interface_List_head, 604), ((snake->snakeFood->x) * 15), ((snake->snakeFood->y) * 15));
    // 食物节点头插 头插后更新该节点坐标
    snake->snakeFood->next = snake->snakeHead_P->next;
    snake->snakeFood->prev = snake->snakeHead_P;
    snake->snakeHead_P->next->prev = snake->snakeFood;
    snake->snakeHead_P->next = snake->snakeFood;
    snake->snakeFood->x = snake->snakeHead_P->x;
    snake->snakeFood->y = snake->snakeHead_P->y;

    // 判断蛇的前进方向来更新蛇头坐标
    if (snake->snakeDir == up)
    {

        (snake->snakeHead_P->y)--;
    }
    else if (snake->snakeDir == down)
    {

        (snake->snakeHead_P->y)++;
    }
    else if (snake->snakeDir == left)
    {

        (snake->snakeHead_P->x)--;
    }
    else if (snake->snakeDir == right)
    {

        (snake->snakeHead_P->x)++;
    }

    // 长度+1
    snake->snakeLenth++;
    // 分数++
    snake->snakeGameScore += snake->foodScore;
    //  刷新分数字库
    char scoreBuff[30] = {0};
    memset(scoreBuff, 0, sizeof(scoreBuff));
    sprintf(scoreBuff, "%d%c", snake->snakeGameScore, '\0');
    writeFontProMax(scoreBuff, 630, 154, 100, 40, 0, 0);
    return;
}

3.蛇吃食物后刷新食物creatFood()函数API:

/// @brief 在地图上随机刷新生成并显示食物
/// @param snake 蛇的大结构体
/// @return 返回刷新后的食物指针
P_snakeNode_t creatFood(P_snake_t snake)
{
    // isRepeatFlag判断随机生成的坐标是否和蛇的坐标重叠
    bool isRepeatFlag = true;
    int x, y;
    P_snakeNode_t tmp = NULL;
    // 寻找地图上除了蛇身之外的坐标来生成食物坐标
    do
    {
        x = randomNum(32);
        y = randomNum(32);
        isRepeatFlag = false;
        for (tmp = snake->snakeHead_P->next; tmp != snake->snakeHead_P; tmp = tmp->next)
        {
            if (tmp->x == x && tmp->y == y)
            {
                isRepeatFlag = true;
                break;
            }
        }
    } while (isRepeatFlag);
    // 初始化新食物
    P_snakeNode_t newFood = snakeNodeInit(x, y);
    if (newFood == NULL)
    {
        printf("new food calloc is fail.\n");
        perror("new food calloc is fail");
        return newFood;
    }
    // 显示随机食物
    showRandBmp(display(interface_List_head, 601), ((newFood->x) * 15), ((newFood->y) * 15));
    // 返回初始化后的随机生成食物
    return newFood;
}

2.2.5蛇状态判断逻辑:

1.判断蛇是否死亡isSnakeDie()函数API:

/// @brief 检测蛇的状态是否死亡
/// @param snake 蛇的大结构体
void isSnakeDie(P_snake_t snake)
{
    // 检测蛇头是否和自己或者墙重叠
    if (snake->snakeHead_P->x < 0 || snake->snakeHead_P->y < 0 || snake->snakeHead_P->x > 31 || snake->snakeHead_P->y > 31)
    {
        snake->snakeState = KILL_BY_WALL;
    }
    P_snakeNode_t tmp = NULL;
    // 检测蛇头坐标是否和自己的蛇身重叠
    for (tmp = snake->snakeHead_P->next; tmp != snake->snakeHead_P; tmp = tmp->next)
    {
        if (snake->snakeHead_P->x == tmp->x && snake->snakeHead_P->y == tmp->y)
        {
            snake->snakeState = KILL_BY_SELF;
        }
    }
    // 判断蛇是否死亡 拓展:蛇头换成死亡图标 放哎哟你干嘛!
    if ((snake->snakeState == KILL_BY_SELF) || (snake->snakeState == KILL_BY_WALL))
    {
        // 卡住
        memset(&touchResult, 0, sizeof(touchType));
        // 判断死亡类型来输出不同提示
        if (snake->snakeState == KILL_BY_SELF)
        {
            writeFontProMax("You were killed by yourself", 40, 225, 320, 40, 0, 0);
        }
        else if (snake->snakeState == KILL_BY_WALL)
        {
            writeFontProMax("You were killed by wall", 80, 225, 260, 40, 0, 0);
        }
        // 用户再次点击在开始游戏
        while ((!touchResult.touchX) && (!touchResult.touchY))
            ;
        memset(&touchResult, 0, sizeof(touchType));

        // 判断分数 如果比最高分数高就写入文件
        if (writeSnakeScoreToFile(snake))
        {
            printf("snakeGameScore write to file success.\n");
        }
        else
        {
            printf("snakeGameScore write to file fail.\n");
        }
        // 重置蛇
        snake = snakeRestart(snake);
    }
    return;
}

2.2.6显示蛇身逻辑:

1.显示蛇身snakePrintf()函数API:

/// @brief 打印蛇,判断是蛇头还是蛇身分别打印不同的图片
/// @param snake 蛇的结构体
void snakePrintf(P_snake_t snake)
{
    // 显示用户的snake->snakeGameScore分数
    char tmpBuff[8];
    sprintf(tmpBuff, "%d%c", snake->snakeGameScore, '\0');
    writeFontProMax(tmpBuff, 615, 220, 80, 30, 0, 0);
    // 生成一张新的贪吃蛇地图来覆盖原来的蛇
    showRandBmp(display(interface_List_head, 602), 0, 0);
    if (snake == NULL || snake->snakeHead_P == NULL)
    {
        printf("snake==NULL || snake->snakeHead_P==NULL.\n");
        return;
    }
    P_snakeNode_t tmp = NULL;
    i = 0;
    // 遍历蛇来根据是蛇头还是蛇身再根据坐标打印不同图片
    for (tmp = snake->snakeHead_P; i < snake->snakeLenth; i++, tmp = tmp->next)
    {
        // 当tmp遍历到蛇头时,根据蛇头的方向来打印不同朝向的蛇头图片
        if (tmp == snake->snakeHead_P)
        {
            if (snake->snakeDir == up)
            {
                showRandBmp(display(interface_List_head, 605), (tmp->x) * 15, (tmp->y) * 15);
            }
            else if (snake->snakeDir == down)
            {
                showRandBmp(display(interface_List_head, 606), (tmp->x) * 15, (tmp->y) * 15);
            }
            else if (snake->snakeDir == left)
            {
                showRandBmp(display(interface_List_head, 607), (tmp->x) * 15, (tmp->y) * 15);
            }
            else if (snake->snakeDir == right)
            {
                showRandBmp(display(interface_List_head, 608), (tmp->x) * 15, (tmp->y) * 15);
            }
        }
        else
        {
            // 打印蛇的节点图片
            showRandBmp(display(interface_List_head, 601), (tmp->x) * 15, (tmp->y) * 15);
        }
    }
    // 打印食物照片
    showRandBmp(display(interface_List_head, 601), ((snake->snakeFood->x) * 15), ((snake->snakeFood->y) * 15));
    return;
}

2.2.7蛇加速逻辑:

        // 判断是否按下加速按钮
        if (((touchResult.touchX - 745) * (touchResult.touchX - 745) + (touchResult.touchY - 272) * (touchResult.touchY - 272)) <= 45 * 45)
        {
            // 加速,newSpeed=(snake->snakeSpeed)/2
            snake->snakeSpeed = newSpeed;
        }
        else
        {
            // 恢复原来正常速度
            snake->snakeSpeed = oldSpeed;
        }
        // 延时使蛇不会跑得很快,一节一节一点一点跑
        usleep(snake->snakeSpeed);

2.2.8游戏退出返回逻辑:

1.检测是否按下返回:

        // 返回检测 时间变量掐掉  音乐变量掐掉 界面标识符设置为3
        if (touchResult.touchX > 700 && touchResult.touchX < 800 && touchResult.touchY > 0 && touchResult.touchY < 80)
        {
            // 触摸事件置零
            memset(&touchResult, 0, sizeof(touchType));
            // 停止显示历史最高得分
            snakeShowHistoryScore = false;
            // 停止播放音乐
            snakeMusciFlag = false;
            // 停止显示时间
            sudokuTimeFlag = false;
            // 摧毁蛇的蛇身节点
            destroySnakeBodyNode(snake);
            // 释放蛇头节点
            free(snake->snakeHead_P);
            // 蛇头节点释放后指向NULL
            snake->snakeHead_P = NULL;
            // 释放蛇的大结构体
            free(snake);
            // 释放蛇的大结构体后snake指向NULL
            snake = NULL;
            break;
        }
    }

2.摧毁蛇的蛇身节点:

/// @brief 摧毁蛇除了蛇头之外的节点
/// @param snake 蛇的大结构体
void destroySnakeBodyNode(P_snake_t snake)
{
    if (snake == NULL || snake->snakeHead_P == NULL)
    {
        printf("snake==NULL || snake->snakeHead_P==NULL.\n");
        return;
    }

    P_snakeNode_t tmpPrev = NULL, tmp = NULL;
    // tmp,tmpPrev遍历蛇来释放蛇的节点
    for (tmp = snake->snakeHead_P->next; tmp != snake->snakeHead_P;)
    {
        tmpPrev = tmp;
        tmp = tmp->next;
        free(tmpPrev);
    }
    // 判断是否食物指针是否存在
    if (snake->snakeFood)
    {
        free(snake->snakeFood);
    }
    return;
}

3.项目源码:

//设计蛇的每个节点为一个双向循环链表节点,x,y记录每个蛇节点的坐标
typedef struct snakeNode
{
    int x;
    int y;
    struct snakeNode *next;
    struct snakeNode *prev;
} snakeNode_t, *P_snakeNode_t;

//enum枚举定义snakeDir的方向
typedef enum snakeDir
{
    noDir = 0,
    up = 1,
    down = 2,
    left = 3,
    right = 4,
} snakeDir_t;

//enum枚举定义蛇的状态
typedef enum snakeState
{
    alive = 1,
    KILL_BY_WALL = 2,
    KILL_BY_SELF = 3,
} snakeState_t;

// 蛇设计为一个大结构体控制
typedef struct snake
{
    P_snakeNode_t snakeHead_P; // 指向蛇头的指针
    snakeDir_t snakeDir;       // 蛇方向
    int snakeLenth;            // 蛇的长度
    int snakeSpeed;            // 蛇的速度
    snakeState_t snakeState;   // 蛇的状态
    P_snakeNode_t snakeFood;    //蛇的食物
    int foodScore;             // 食物的分数
    int snakeGameScore;        // 贪吃蛇分数

} snake_t,*P_snake_t;


// 初始化snake为全局变量
P_snake_t snake = NULL;
// 设置简单难度,中难度,困难难度,以及新速度记录蛇的速度困难级别速度,newSpeed记录蛇加速后的速度(原速度/2)
int snakeEasySpeed = 500000, snakeMiddleSpeed = 250000, snakeDIffcultSpeed = 100000, oldSpeed = 0, newSpeed = 0;
// 初次进入游戏会提示用户设置蛇的速度,之后不会再设置直到退出再次进入游戏
bool isSnakeSetSpeed = false;

/// @brief snakePlay函数
void snakePlay()
{
    isSnakeSetSpeed = false;
    // calloc申请蛇的大结构体空间
    snake = (P_snake_t)calloc(1, sizeof(snake_t));
    if (snake == NULL)
    {
        printf("snake calloc is fail.\n");
        perror("snake calloc is fail");
        return;
    }
    // 蛇头初始化,snakeInitX, snakeInitY宏定义蛇头在地图中间
    snake->snakeHead_P = snakeNodeInit(snakeInitX, snakeInitY);
    if (snake->snakeHead_P == NULL)
    {
        printf("snake head calloc is fail.\n");
        perror("snake head calloc is fail");
        free(snake);
        return;
    }
    // 蛇重置初始化
    snake = snakeRestart(snake);
    // 设置蛇// 吃一个食物是五分
    snake->foodScore = 5;
    // 打印用户名
    writeFontProMax(currentUser.name, 580, 15, 100, 40, 0, 0);
    // 打印本局得分
    char scoreBuff[30] = {0};
    memset(scoreBuff, 0, sizeof(scoreBuff));
    sprintf(scoreBuff, "%d%c", snake->snakeGameScore, '\0');
    writeFontProMax(scoreBuff, 630, 154, 100, 40, 0, 0);
    // 初始化键盘输入方向为无
    snakeDir_t inputSnakeDir = noDir;
    while (1)
    {
        // 触摸键盘有效判断
        // inputSnakeDir = 判断用户触摸方向键,触摸事件置零
        // 上方向键 圆半径38 圆心 606,293
        if (((touchResult.touchX - 606) * (touchResult.touchX - 606) + (touchResult.touchY - 293) * (touchResult.touchY - 293)) <= 38 * 38)
        {
            // 设置键盘输入方向为up
            inputSnakeDir = up;
            // 重新初始化触摸事件
            memset(&touchResult, 0, sizeof(touchType));
        }
        // 下方向键 圆半径38 圆心 606,438
        if (((touchResult.touchX - 606) * (touchResult.touchX - 606) + (touchResult.touchY - 438) * (touchResult.touchY - 438)) <= 38 * 38)
        {
            inputSnakeDir = down;
            memset(&touchResult, 0, sizeof(touchType));
        }
        // 左方向键 圆半径38 圆心518 ,365
        if (((touchResult.touchX - 518) * (touchResult.touchX - 518) + (touchResult.touchY - 365) * (touchResult.touchY - 365)) <= 38 * 38)
        {
            inputSnakeDir = left;
            memset(&touchResult, 0, sizeof(touchType));
        }
        // 上方向键 圆半径38 圆心 690,365
        if (((touchResult.touchX - 690) * (touchResult.touchX - 690) + (touchResult.touchY - 365) * (touchResult.touchY - 365)) <= 38 * 38)
        {
            inputSnakeDir = right;
            memset(&touchResult, 0, sizeof(touchType));
        }
        // 移动蛇
        snakeMove(snake, &inputSnakeDir);
        // 判断蛇头的下一步是不是食物
        if (isNextFood(snake))
        {
            // 是,吃食物(头插)
            eatFood(snake);
            // 食物刷新
            snake->snakeFood = creatFood(snake);
        }
        // 检测蛇的状态
        isSnakeDie(snake);
        // 打印蛇身
        snakePrintf(snake);
        // 判断是否按下加速按钮
        if (((touchResult.touchX - 745) * (touchResult.touchX - 745) + (touchResult.touchY - 272) * (touchResult.touchY - 272)) <= 45 * 45)
        {
            // 加速,newSpeed=(snake->snakeSpeed)/2
            snake->snakeSpeed = newSpeed;
        }
        else
        {
            // 恢复原来正常速度
            snake->snakeSpeed = oldSpeed;
        }
        // 延时使蛇不会跑得很快,一节一节一点一点跑
        usleep(snake->snakeSpeed);
        // 检测暂停按键是否按下
        if (touchResult.touchX > 720 && touchResult.touchX < 800 && touchResult.touchY > 400 && touchResult.touchY < 480)
        {
            memset(&touchResult, 0, sizeof(touchType));
            snakeMusciFlag = false;
            // 显示右下角的暂停游戏按钮
            showRandBmp(display(interface_List_head, 405), 737, 416);
            // 暂停音乐
            system("killall -19 madplay");
            memset(&touchResult, 0, sizeof(touchType));
            // 暂停游戏直到用户再次点击在开始游戏
            while (1)
            {
                if (touchResult.touchX > 720 && touchResult.touchX < 800 && touchResult.touchY > 400 && touchResult.touchY < 480)
                {
                    break;
                }
            }
            memset(&touchResult, 0, sizeof(touchType));
            // 显示右下角的继续游戏按钮
            showRandBmp(display(interface_List_head, 404), 737, 416);
            snakeMusciFlag = true;
            // 继续播放音乐
            system("killall -18 madplay");
        }

        // 返回检测 时间变量掐掉  音乐变量掐掉 界面标识符设置为3
        if (touchResult.touchX > 700 && touchResult.touchX < 800 && touchResult.touchY > 0 && touchResult.touchY < 80)
        {
            // 触摸事件置零
            memset(&touchResult, 0, sizeof(touchType));
            // 停止显示历史最高得分
            snakeShowHistoryScore = false;
            // 界面标识符设置为3,即选择游戏界面
            returnInterfaceFlag.interfaceFlag = 3;
            // 停止播放音乐
            snakeMusciFlag = false;
            // 停止显示时间
            sudokuTimeFlag = false;
            // 摧毁蛇的蛇身节点
            destroySnakeBodyNode(snake);
            // 释放蛇头节点
            free(snake->snakeHead_P);
            // 蛇头节点释放后指向NULL
            snake->snakeHead_P = NULL;
            // 释放蛇的大结构体
            free(snake);
            // 释放蛇的大结构体后snake指向NULL
            snake = NULL;
            break;
        }
    }
}

/// @brief 检测蛇的状态是否死亡
/// @param snake 蛇的大结构体
void isSnakeDie(P_snake_t snake)
{
    // 检测蛇头是否和自己或者墙重叠
    if (snake->snakeHead_P->x < 0 || snake->snakeHead_P->y < 0 || snake->snakeHead_P->x > 31 || snake->snakeHead_P->y > 31)
    {
        snake->snakeState = KILL_BY_WALL;
    }
    P_snakeNode_t tmp = NULL;
    // 检测蛇头坐标是否和自己的蛇身重叠
    for (tmp = snake->snakeHead_P->next; tmp != snake->snakeHead_P; tmp = tmp->next)
    {
        if (snake->snakeHead_P->x == tmp->x && snake->snakeHead_P->y == tmp->y)
        {
            snake->snakeState = KILL_BY_SELF;
        }
    }
    // 判断蛇是否死亡 拓展:蛇头换成死亡图标 放哎哟你干嘛!
    if ((snake->snakeState == KILL_BY_SELF) || (snake->snakeState == KILL_BY_WALL))
    {
        // 卡住
        memset(&touchResult, 0, sizeof(touchType));
        // 判断死亡类型来输出不同提示
        if (snake->snakeState == KILL_BY_SELF)
        {
            writeFontProMax("You were killed by yourself", 40, 225, 320, 40, 0, 0);
        }
        else if (snake->snakeState == KILL_BY_WALL)
        {
            writeFontProMax("You were killed by wall", 80, 225, 260, 40, 0, 0);
        }
        // 用户再次点击在开始游戏
        while ((!touchResult.touchX) && (!touchResult.touchY))
            ;
        memset(&touchResult, 0, sizeof(touchType));

        // 判断分数 如果比最高分数高就写入文件
        if (writeSnakeScoreToFile(snake))
        {
            printf("snakeGameScore write to file success.\n");
        }
        else
        {
            printf("snakeGameScore write to file fail.\n");
        }
        // 重置蛇
        snake = snakeRestart(snake);
    }
    return;
}

/// @brief 重置蛇
/// @param snake 蛇的大结构体
/// @return 返回重置后的蛇的大结构体
P_snake_t snakeRestart(P_snake_t snake)
{
    // 判断是否设置过蛇的速度(即判断是否是第一次进入游戏)
    if (isSnakeSetSpeed == false)
    {
        // 设置isSnakeSetSpeed为true,之后游戏不会再设置蛇的难度
        isSnakeSetSpeed = true;
        writeFontProMax("请选择难度:1坤(简单),2坤(中),2.5坤(困难)", 0, 225, 480, 40, 0, 0);
        showRandBmp(display(interface_List_head, 609), 40, 270);
        showRandBmp(display(interface_List_head, 610), 110, 270);
        showRandBmp(display(interface_List_head, 611), 240, 270);
        memset(&touchResult, 0, sizeof(touchType));
        // 用户再次选择难度后再开始游戏
        while (1)
        {
            // 简单难度
            if (touchResult.touchX > 40 && touchResult.touchX < 100 && touchResult.touchY > 270 && touchResult.touchY < 330)
            {
                snake->snakeSpeed = snakeEasySpeed;
                memset(&touchResult, 0, sizeof(touchType));
                break;
            } // 中难度
            else if (touchResult.touchX > 110 && touchResult.touchX < 230 && touchResult.touchY > 270 && touchResult.touchY < 330)
            {
                snake->snakeSpeed = snakeMiddleSpeed;
                memset(&touchResult, 0, sizeof(touchType));

                break;
            } // 困难难度
            else if (touchResult.touchX > 240 && touchResult.touchX < 420 && touchResult.touchY > 270 && touchResult.touchY < 330)
            {
                snake->snakeSpeed = snakeDIffcultSpeed;
                memset(&touchResult, 0, sizeof(touchType));

                break;
            }
        }
        // 显示新的贪吃蛇地图
        showRandBmp(display(interface_List_head, 602), 0, 0);
        memset(&touchResult, 0, sizeof(touchType));
        // oldSpeed记录蛇的初始速度,newSpeed记录蛇的加速速度(即(snake->snakeSpeed) / 2)
        oldSpeed = snake->snakeSpeed, newSpeed = (snake->snakeSpeed) / 2;
    }

    // 输出信息让用户点击屏幕开始游戏
    // 清空之前输出的字符串
    writeFontProMax("                           ", 40, 225, 320, 40, 0, 0);
    writeFontProMax("请点击地图开始!", 115, 225, 180, 40, 0, 0);
    // 卡住直到用户点击开始
    memset(&touchResult, 0, sizeof(touchType));
    // 用户再次点击在开始游戏
    while (1)
    {
        if (touchResult.touchX > 0 && touchResult.touchX < 480 && touchResult.touchY > 0 && touchResult.touchY < 480)
        {
            break;
        }
    }
    memset(&touchResult, 0, sizeof(touchType));
    // 释放链表除蛇头节点之外的指针 释放食物 蛇头next指向自己
    destroySnakeBodyNode(snake);
    snake->snakeHead_P->next = snake->snakeHead_P->prev = snake->snakeHead_P;
    // 刷新蛇结构体 刷新蛇的状态
    // 初始化蛇头方向
    snake->snakeDir = left;
    // 初始化蛇身长度为1
    snake->snakeLenth = 1;
    // 初始化蛇为存活状态
    snake->snakeState = alive;
    // //重新随机生成食物
    // snake->snakeFood = creatFood(snake);
    // 初始化蛇头坐标,让蛇头一开始刷在地图中间
    snake->snakeHead_P->x = snakeInitX;
    snake->snakeHead_P->y = snakeInitY;
    // 初始化用户的贪吃蛇分数为0
    snake->snakeGameScore = 0;
    // 打印新的贪吃蛇地图
    showRandBmp(display(interface_List_head, 602), 0, 0); // 打印地图
    // 重新随机刷新生成食物
    snake->snakeFood = creatFood(snake);
    // 打印蛇身
    snakePrintf(snake);
    return snake;
}

/// @brief 判断蛇是否吃到食物
/// @param snake 蛇的大结构体
/// @return 吃到食物时返回真,没有吃到食物时返回假
bool isNextFood(P_snake_t snake)
{
    // 判断蛇头坐标是不是食物 x,y 是否相同
    return (((snake->snakeHead_P->x) == snake->snakeFood->x) && ((snake->snakeHead_P->y) == (snake->snakeFood->y)));
}

/// @brief 蛇吃食物
/// @param snake 蛇的大结构体
void eatFood(P_snake_t snake)
{
    // 拓展:吃一下食物就坤一声
    // 打印一个空的地图格子到原来食物格子位置覆盖原食物格子
    showRandBmp(display(interface_List_head, 604), ((snake->snakeFood->x) * 15), ((snake->snakeFood->y) * 15));
    // 食物节点头插 头插后更新该节点坐标
    snake->snakeFood->next = snake->snakeHead_P->next;
    snake->snakeFood->prev = snake->snakeHead_P;
    snake->snakeHead_P->next->prev = snake->snakeFood;
    snake->snakeHead_P->next = snake->snakeFood;
    snake->snakeFood->x = snake->snakeHead_P->x;
    snake->snakeFood->y = snake->snakeHead_P->y;

    // 判断蛇的前进方向来更新蛇头坐标
    if (snake->snakeDir == up)
    {

        (snake->snakeHead_P->y)--;
    }
    else if (snake->snakeDir == down)
    {

        (snake->snakeHead_P->y)++;
    }
    else if (snake->snakeDir == left)
    {

        (snake->snakeHead_P->x)--;
    }
    else if (snake->snakeDir == right)
    {

        (snake->snakeHead_P->x)++;
    }

    // 长度+1
    snake->snakeLenth++;
    // 分数++
    snake->snakeGameScore += snake->foodScore;
    //  刷新分数字库
    char scoreBuff[30] = {0};
    memset(scoreBuff, 0, sizeof(scoreBuff));
    sprintf(scoreBuff, "%d%c", snake->snakeGameScore, '\0');
    writeFontProMax(scoreBuff, 630, 154, 100, 40, 0, 0);
    return;
}

/// @brief 在地图上随机刷新生成并显示食物
/// @param snake 蛇的大结构体
/// @return 返回刷新后的食物指针
P_snakeNode_t creatFood(P_snake_t snake)
{
    // isRepeatFlag判断随机生成的坐标是否和蛇的坐标重叠
    bool isRepeatFlag = true;
    int x, y;
    P_snakeNode_t tmp = NULL;
    // 寻找地图上除了蛇身之外的坐标来生成食物坐标
    do
    {
        x = randomNum(32);
        y = randomNum(32);
        isRepeatFlag = false;
        for (tmp = snake->snakeHead_P->next; tmp != snake->snakeHead_P; tmp = tmp->next)
        {
            if (tmp->x == x && tmp->y == y)
            {
                isRepeatFlag = true;
                break;
            }
        }
    } while (isRepeatFlag);
    // 初始化新食物
    P_snakeNode_t newFood = snakeNodeInit(x, y);
    if (newFood == NULL)
    {
        printf("new food calloc is fail.\n");
        perror("new food calloc is fail");
        return newFood;
    }
    // 显示随机食物
    showRandBmp(display(interface_List_head, 601), ((newFood->x) * 15), ((newFood->y) * 15));
    // 返回初始化后的随机生成食物
    return newFood;
}

/// @brief 打印蛇,判断是蛇头还是蛇身分别打印不同的图片
/// @param snake 蛇的结构体
void snakePrintf(P_snake_t snake)
{
    // 显示用户的snake->snakeGameScore分数
    char tmpBuff[8];
    sprintf(tmpBuff, "%d%c", snake->snakeGameScore, '\0');
    writeFontProMax(tmpBuff, 615, 220, 80, 30, 0, 0);
    // 生成一张新的贪吃蛇地图来覆盖原来的蛇
    showRandBmp(display(interface_List_head, 602), 0, 0);
    if (snake == NULL || snake->snakeHead_P == NULL)
    {
        printf("snake==NULL || snake->snakeHead_P==NULL.\n");
        return;
    }
    P_snakeNode_t tmp = NULL;
    i = 0;
    // 遍历蛇来根据是蛇头还是蛇身再根据坐标打印不同图片
    for (tmp = snake->snakeHead_P; i < snake->snakeLenth; i++, tmp = tmp->next)
    {
        // 当tmp遍历到蛇头时,根据蛇头的方向来打印不同朝向的蛇头图片
        if (tmp == snake->snakeHead_P)
        {
            if (snake->snakeDir == up)
            {
                showRandBmp(display(interface_List_head, 605), (tmp->x) * 15, (tmp->y) * 15);
            }
            else if (snake->snakeDir == down)
            {
                showRandBmp(display(interface_List_head, 606), (tmp->x) * 15, (tmp->y) * 15);
            }
            else if (snake->snakeDir == left)
            {
                showRandBmp(display(interface_List_head, 607), (tmp->x) * 15, (tmp->y) * 15);
            }
            else if (snake->snakeDir == right)
            {
                showRandBmp(display(interface_List_head, 608), (tmp->x) * 15, (tmp->y) * 15);
            }
        }
        else
        {
            // 打印蛇的节点图片
            showRandBmp(display(interface_List_head, 601), (tmp->x) * 15, (tmp->y) * 15);
        }
    }
    // 打印食物照片
    showRandBmp(display(interface_List_head, 601), ((snake->snakeFood->x) * 15), ((snake->snakeFood->y) * 15));
    return;
}

/// @brief 根据蛇头的方向来移动蛇
/// @param snake 蛇的大结构体
/// @param dir 用户触摸键盘输入的蛇的新方向
void snakeMove(P_snake_t snake, snakeDir_t *dir)
{
    // 判断用户是否触摸方向键
    if (*dir)
    {
        // 设置蛇的方向为用户输入的方向
        snake->snakeDir = *dir;
        //*dir重新置零
        *dir = noDir;
    }

    // 临时变量遍历蛇来控制蛇的坐标移动
    P_snakeNode_t tmp = NULL;
    i = 0;
    // 从尾部开始遍历蛇最后再根据蛇头方向更新蛇的头
    for (tmp = snake->snakeHead_P->prev; i < snake->snakeLenth; i++, tmp = tmp->prev)
    {
        // 当tmp遍历到蛇头时
        if (tmp == snake->snakeHead_P)
        {
            // 判断蛇的前进方向更新蛇头坐标
            if (snake->snakeDir == up)
            {
                (snake->snakeHead_P->y)--;
            }
            else if (snake->snakeDir == down)
            {
                (snake->snakeHead_P->y)++;
            }
            else if (snake->snakeDir == left)
            {
                (snake->snakeHead_P->x)--;
            }
            else if (snake->snakeDir == right)
            {
                (snake->snakeHead_P->x)++;
            }
        }
        else
        {
            // 判断tmp节点和上一个节点的坐标关系更新坐标
            if ((tmp->x) == (tmp->prev->x))
            {
                if ((tmp->y) > (tmp->prev->y))
                {
                    (tmp->y)--;
                }
                else
                {
                    (tmp->y)++;
                }
            }
            else if ((tmp->y) == (tmp->prev->y))
            {
                if ((tmp->x) > (tmp->prev->x))
                {
                    (tmp->x)--;
                }
                else
                {
                    (tmp->x)++;
                }
            }
        }
    }
}

/// @brief 初始化一个蛇的节点并返回
/// @param newSnakeNodeX 初始化的节点坐标X
/// @param newSnakeNodeY 初始化的节点坐标Y
/// @return 返回初始化后的节点
P_snakeNode_t snakeNodeInit(int newSnakeNodeX, int newSnakeNodeY)
{
    // 申请新的堆内存空间给新的蛇节点
    P_snakeNode_t newSnakeNode = (P_snakeNode_t)calloc(1, sizeof(snakeNode_t));
    if (newSnakeNode == NULL)
    {
        printf("snakeNewNode calloc is fail.\n");
        perror("snakeNewNode calloc is fail");
        return NULL;
    }
    newSnakeNode->x = newSnakeNodeX;
    newSnakeNode->y = newSnakeNodeY;
    newSnakeNode->next = newSnakeNode->prev = newSnakeNode;

    return newSnakeNode;
}

/// @brief 摧毁蛇除了蛇头之外的节点
/// @param snake 蛇的大结构体
void destroySnakeBodyNode(P_snake_t snake)
{
    if (snake == NULL || snake->snakeHead_P == NULL)
    {
        printf("snake==NULL || snake->snakeHead_P==NULL.\n");
        return;
    }

    P_snakeNode_t tmpPrev = NULL, tmp = NULL;
    // tmp,tmpPrev遍历蛇来释放蛇的节点
    for (tmp = snake->snakeHead_P->next; tmp != snake->snakeHead_P;)
    {
        tmpPrev = tmp;
        tmp = tmp->next;
        free(tmpPrev);
    }
    // 判断是否食物指针是否存在
    if (snake->snakeFood)
    {
        free(snake->snakeFood);
    }
    return;
}

4.结语:

        该项目是通过C语言写,在开发板上跑的,如果有同学是要写的文本形式,大概逻辑也差不多,只要改一些显示蛇的代码等等就可以了。

你可能感兴趣的:(c语言,数据结构)