基于M5StickC plus制作的小恐龙游戏
2022暑假在家练 M5StickC plus 小恐龙游戏 platformio C语言
标签
2022暑假在家练
M5StickC Plus
pvfcd
更新2022-09-02
北方工业大学
1186

基于M5StickC plus制作的小恐龙游戏

Fvzw8pjP4iPdnz2ueJwEcHKkx4_9

按惯例,先放代码:github

百度网盘 提取码wmx1   

 简介

感谢大家观看我制作的项目。此次项目开发的游戏并没有官方名称,但是大家肯定都知道,就是谷歌Chrome浏览器中的彩蛋项目,那个断网会出现的小恐龙。FkwFu1UMNgH_slnq0qDtrlvjkES0

 

 

硬件分析

这次的硬件比较复杂,电源部分采用了axp192,控制了屏幕的背光,rtc电源,电池充电等等一大堆电源,通过iic总线控制。FlbJGqBKMCBMQ6Pd0vLXg1Cbvzj1

本次项目使用这个硬件调节了屏幕背光亮度。

显示屏采用spi屏幕,刷新速度很快,项目中使用的是tftespi库,这个库刷屏速度也非常快。

FjkHJIZeayuAgjR_qIiwqDs14Fsr

外设还用到了陀螺仪,也挂在iic总线上。

FtKG1SOVwLJGcKwHCvCMnzBdhKP8

 

程序分析

本次项目采用platformIO平台进行开发,使用的是c语言,还使用了rtos进行任务的管理。

platformIO是一个很神奇的开发工具,可以开发上千种mcu,其中也包括有esp32的全系列。前一段乐鑫还和platformio达成了战略合作关系。m5stick官方也在上面传入了它的依赖库,可以直接选择m5stick。

程序框图如下:

FjNFWIG6zXAsgO2hPHH0e29wQsCM

首先,使用platformio创建一个工程,内部直接有m5stick开发板可选,归类于esp32下。环境直接选择arduino,这个好处在于esp32上的arduino是直接包含了freertos的,可以直接运行freertos,无需移植。(在arduino软件上,也可以直接创建线程,无需做任何处理)FgxP-VZMYQ1_HcGuEfnYBVAIwsXP

接下来需要安装m5stack官方的库文件。同样的,在platformio上可以直接安装。

FuFvsaEGAWUCEAjpKGpDirw_iS1q

因为platformio是使用Makefile链接c++文件的(好像是这样,此处存疑),所以还需加入两个freertos的头文件,还有一个arduino的头文件。同时还有两个c标准库的文件,以使用uint16_t这类别名。

#include <Arduino.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include <M5StickCPlus.h>
#include "stdlib.h"
#include "stdint.h"

接下来是创建线程。我创建了4个线程,由于上文提到了,M5StickC plus采用的是esp32作为内核,这是一个双核处理器,所以每个任务都对应至CPU核心,和屏幕有关的都放在了核心1,碰撞检测和陀螺仪处理放在了核心0。

    xTaskCreatePinnedToCore(IMU_ReadPin,"read_pin",4096,NULL,5,NULL,0);//陀螺仪检测
    xTaskCreatePinnedToCore(Show_Oled,"oled_show",1024*10,NULL,1,&oled_show_handle,1);//使用CPU1刷屏
    xTaskCreatePinnedToCore(show_ball,"show_ball",1024*5,NULL,6,&show_ball_handle,1);//刷新移动球
    xTaskCreatePinnedToCore(crush_detect,"crush_detect",1024*6,NULL,4,NULL,0);//碰撞检测

还要创建一个队列以存放陀螺仪线程出来的数据。

QueueHandle_t IMU_Btn_Queue = xQueueCreate(4,sizeof(char));//创建一个队列,长度4,大小20

同时,在setup函数中还要初始化一些引脚。

void setup()
{
      // put your setup code here, to run once:
    m5.begin();
    m5.Lcd.fillScreen(WHITE);//底色为白色
    // m5.Lcd.drawBitmap(0,0,50,45,dino1);
    xTaskCreatePinnedToCore(IMU_ReadPin,"read_pin",4096,NULL,5,NULL,0);//陀螺仪检测
    xTaskCreatePinnedToCore(Show_Oled,"oled_show",1024*10,NULL,1,&oled_show_handle,1);//使用CPU1刷屏
    xTaskCreatePinnedToCore(show_ball,"show_ball",1024*5,NULL,6,&show_ball_handle,1);//刷新移动球
    xTaskCreatePinnedToCore(crush_detect,"crush_detect",1024*6,NULL,4,NULL,0);//碰撞检测
    pinMode(10, OUTPUT);    //初始化灯
    pinMode(37,INPUT_PULLUP);
    digitalWrite(10, HIGH); //设置为高电平
}

其实本质上esp32下的arduino的setup函数和loop函数也是freertos的线程,但是由于优先级低,所以后期无需做过多的处理。

陀螺仪处理

还是freertos的线程,大意如下

void IMU_ReadPin(void *point)
{
    if (m5.Imu.Init() != 0)//初始化陀螺仪
    {
        while (1)
        {
            Serial.println("IMU_INIT_ERROR!\n");
        }
    }
    uint8_t message;
    for(;;)
    {
        M5.Imu.getGyroData(&acceldata.x,&acceldata.y,&acceldata.z);//读取加速度计
        message = 'E';//先将信息空置
        if(acceldata.x > rate)//大于某个值后才发送信息
        {
            message = 'U';
        }
        if(acceldata.x < -rate)
        {
            message = 'D';
        }
        if(acceldata.z > rate)
        {
            message = 'L';
        }
        if(acceldata.z < -rate)
        {
            message = 'R';
        }
        // Serial.printf("{acceldatax:%f},{acceldatay:%f},{acceldataz:%f}",acceldata.x,acceldata.y,acceldata.z);
        if(message != 'E')//有消息再发送到队列中
        {
            xQueueSend(IMU_Btn_Queue,&message,10);
        }
        
        vTaskDelay(pdMS_TO_TICKS(100));
    }
}

这里没有使用任何滤波器,大于某个值就会输出一个数字。

但是我尝试了许多种滤波器,比如卡尔曼滤波,平滑均值滤波。卡尔曼滤波还需要器还需要调试,均值滤波效果一般般,最关键的是会减小响应速度,增大复杂性,最后发现不加滤波器最好用。

FvCfJbriQUk3e3x7yMniRlRfobzU

刷新小恐龙跳动及物理效果

仅有重力作用的竖直上抛运动是符合公式x= 1/2gt^2的,符合这个公式的竖直上抛运动才看起来更加自然。于是将这一公式带入到单片机中,即可得出小恐龙的运动轨迹。

后期发现,与其让单片机现场算出这些相同的数字,不如直接算好存在数组中,使用时查表即可。于是使用excel算好数字后直接使用。

同时,因为人体从高空落下后还有一个下蹲的动作,实现这一点可以让小恐龙看起来更弹,这个实现很简单,让小恐龙落地后沉入地面一点点就行。

让小恐龙动起来的方法很简单,就是固定y轴,让x轴变化。这样坏处是,会留下残影。一般做法是先在原始图片上画一个白块,再画新图片。但是这样有个问题就是有闪屏而且刷新很慢。于是我可以只在两个图片不重合的地方画一个白块,这样刷新起来会快一点。

还有一点就是小恐龙跑的时候脚在不停的跑动,这个的实现方法就是两个图片一直不停的叠放,看起来就像跑动。

/**
 * @brief 刷新球线程
 *
 * @param point
 */
void show_ball(void *point)
{
    // x= v0t+1/2at^2
    int8_t message = 0;
    uint8_t movement = 0;      //动作标志位
    uint8_t i = 0;
    TickType_t start_time = 0; //开始物理学计算的起始时间
    for (;;)
    {
        //读取按键电平位置
        message = 0;
        if (xQueueReceive(IMU_Btn_Queue, &message, 10) == pdPASS)
        {
            Serial.printf("message = %c", message);
        }
        //绘制球
        if (message == 'L' && movement == 0)
        {
            movement = 'U'; //标志位,标志小球处于向上状态
            start_time = 1;
        }
        if (movement == 'U')
        {
            show_dino(100 - gravity_movement[start_time],200,100 - gravity_movement[start_time - 1],200);
            start_time++;
            if (start_time > 20)
            {
                movement = 0;
            }
            obst_posi.ball_posi = gravity_movement[start_time];
        }
        else if(i == 1)
        {
            m5.Lcd.drawBitmap(100 - gravity_movement[1],200,50,45,dino1);
            i = 0;
        }
        else if (i == 0)
        {
            m5.Lcd.drawBitmap(100 - gravity_movement[1],200,50,45,dino2);
            i = 1;
        }
        vTaskDelay(20);
    }
}
crush_detect_typedef obst_posi; //碰撞检测有关变量
/**
 * @brief 画小恐龙
 * 
 * @param x x位置
 * @param y y位置
 * @param last_x 上次x位置
 * @param last_y 上次y位置
 */
void show_dino(uint16_t x,uint16_t y,uint16_t last_x,uint16_t last_y)
{
    if(x-last_x > 0)
    {
        m5.Lcd.fillRect(last_x+30,last_y,x-last_x + 60,last_y + 45,WHITE);
    }
    else
    {
        m5.Lcd.fillRect(last_x+30,last_y,last_x-x + 60,last_y + 45,WHITE);
    }
    m5.Lcd.drawBitmap(x+30,y,50,45,dino1);
}

刷新背景及随机生成

先定义一个结构体,存储障碍物有关信息。

typedef struct
{
    uint16_t posi;
    uint8_t height;
    uint8_t delay;//延迟生成的时间,以达成随机间距效果
    TickType_t start_time;//记录生成这个障碍物的时间戳,利于delay判定
    uint8_t enable;
}obstruct_typedef;

结构体中有一部分成员没有用上,这是我之前使用方块代替障碍物时留下的。当初本来是想做高度和宽度都可以自定义的障碍物,后来看到我画图实在是不行,于是就选择了谷歌的素材。
障碍物绘制种类是随机的,采用了arduino函数库内置的随机数生成函数。需要先取一个空引脚的adc值作为随机种子。这里我选择了26引脚。

循环移动的做法就是当障碍物移出屏幕的时候,再把障碍物归位。我定义了一个结构体数组,这样就可以生成很多障碍物。但是因为屏幕太小,所以只写了一个。

/**
 * @brief 刷新障碍物线程
 *
 * @param point 传入参数
 */
void Show_Oled(void *point)
{
    pinMode(26, INPUT);
    randomSeed(analogRead(26));
    obstruct[0].delay = 0;               //第一个障碍物不延迟直接生成
    obstruct[0].height = 1; //高度自定义
    obstruct[0].posi = 0;
    obstruct[0].enable = 1;
    for (;;)
    {
        //障碍物移动
        if (obstruct[0].enable == 1)
        {
            obst_posi.obstruct_posi = obstruct[0].posi - obstruct[0].delay;
            show_obst(90,obstruct[0].posi,0,0,obstruct[0].height);
            obstruct[0].posi += speed;
            //随机数生成
            if (obstruct[0].posi - obstruct[0].delay > 245)
            {
                obstruct[0].posi = 0;
                obstruct[0].enable = 1;
                obstruct[0].height = random(1,3);
            }
        }
        vTaskDelay(30);
    }
}

碰撞检测

碰撞检测还是检测小恐龙和障碍物是否重合,先检测水平距离,之后再检测垂直是否重合。传达信息使用的是全局变量。检测到碰撞后会暂停刷屏线程的调度,之后等待事件组输入复位指令,此处是向内拉。这样直接复位CPU。

/**
 * @brief 碰撞检测
 *
 * @param point
 */
void crush_detect(void *point)
{
    uint8_t message = 0;
    for (;;)
    {
        if (197 < obst_posi.obstruct_posi && obst_posi.obstruct_posi < 203)
        {
            if (70 > obst_posi.ball_posi)
            {

                if (oled_show_handle != NULL)
                {
                    vTaskSuspend(oled_show_handle);
                    oled_show_handle = NULL;
                }
                if (show_ball_handle != NULL)
                {
                    vTaskSuspend(show_ball_handle);
                    show_ball_handle = NULL;
                }
            }
            
        }
        //检测到碰撞后
        if (oled_show_handle == NULL)
        {
            m5.Lcd.drawBitmap(0,0,135,240,end);
            if (xQueueReceive(IMU_Btn_Queue, &message, 10) == pdPASS)
            {
                if (message == 'U')
                {
                    abort();
                }
            }
        }
        vTaskDelay(20);
    }
}

图片生成

图片生成可以直接用取模软件img2lcd,由于我并没有旋转屏幕,于是取模需要一些特殊设置才是正常的图片。设置内容如图所示,图片在pic.cpp文件里面。

FsDD8U5TeEwb-3pDQLaS_5h5LB_G

设置内容如图所示。

使用方法:

下载vscode,安装好platformio内核后(此处有一些麻烦,但是有很多优秀的教程,自行搜索即可)打开代码库中的platformio.ini文件,等待platformio下载完成依赖库后,点击vscode下方的->上传代码至控制板。

将板子横放,m5标志在左,手向上提即可控制恐龙跳跃。死亡后向内拉即可重新开始。

心得体会:

这应该是第三次参加硬禾学堂的活动了,这次群聊真的很冷清,可能也许是因为板子数量比较少的原因吧,只有50份。而且这个假期我有很多比赛,没太多时间做这个项目,拿到板子的第一天点了个灯,后来全程摸鱼,git提交记录忠实的记录了这一切。。。最后呈现的效果也并非很完美。不过好歹是实现了一个看起来能玩,实际上也能玩的效果。

m5stack这个公司做的小开发板很有意思,功能也很多,尤其是文档很全面。乐鑫公司的芯片很好玩,也很便宜,使用也很简便,硬禾学堂提供给我们的这种玩转即退钱的形式也相当的好。这次的小项目也是我本人第一次使用freertos+platformio这种组合,之前要么纯裸机,要么纯arduino加定时器,对我的技能也有很大的提高。

最近会阅读一些大佬的项目,只能说是高山仰止,要走的路还有很多。总之,感谢m5stack,硬禾学堂为我们提供的这次机会。

本次项目没有参考任何其他项目,唯一使用的素材就是谷歌浏览器的小恐龙素材,原版在谷歌浏览器的地址栏中输入chrome://dino/即可打开,图片使用内容元素检查即可查看。这段素材也一并上传至github仓库中了。

 

团队介绍
北方工业大学 自动化21 于承浩
团队成员
pvfcd
评论
0 / 100
查看更多
目录
硬禾服务号
关注最新动态
0512-67862536
info@eetree.cn
江苏省苏州市苏州工业园区新平街388号腾飞创新园A2幢815室
苏州硬禾信息科技有限公司
Copyright © 2023 苏州硬禾信息科技有限公司 All Rights Reserved 苏ICP备19040198号