基于M5StickC plus制作的小恐龙游戏
按惯例,先放代码:github
简介
感谢大家观看我制作的项目。此次项目开发的游戏并没有官方名称,但是大家肯定都知道,就是谷歌Chrome浏览器中的彩蛋项目,那个断网会出现的小恐龙。
硬件分析
这次的硬件比较复杂,电源部分采用了axp192,控制了屏幕的背光,rtc电源,电池充电等等一大堆电源,通过iic总线控制。
本次项目使用这个硬件调节了屏幕背光亮度。
显示屏采用spi屏幕,刷新速度很快,项目中使用的是tftespi库,这个库刷屏速度也非常快。
外设还用到了陀螺仪,也挂在iic总线上。
程序分析
本次项目采用platformIO平台进行开发,使用的是c语言,还使用了rtos进行任务的管理。
platformIO是一个很神奇的开发工具,可以开发上千种mcu,其中也包括有esp32的全系列。前一段乐鑫还和platformio达成了战略合作关系。m5stick官方也在上面传入了它的依赖库,可以直接选择m5stick。
程序框图如下:
首先,使用platformio创建一个工程,内部直接有m5stick开发板可选,归类于esp32下。环境直接选择arduino,这个好处在于esp32上的arduino是直接包含了freertos的,可以直接运行freertos,无需移植。(在arduino软件上,也可以直接创建线程,无需做任何处理)
接下来需要安装m5stack官方的库文件。同样的,在platformio上可以直接安装。
因为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));
}
}
这里没有使用任何滤波器,大于某个值就会输出一个数字。
但是我尝试了许多种滤波器,比如卡尔曼滤波,平滑均值滤波。卡尔曼滤波还需要器还需要调试,均值滤波效果一般般,最关键的是会减小响应速度,增大复杂性,最后发现不加滤波器最好用。
刷新小恐龙跳动及物理效果
仅有重力作用的竖直上抛运动是符合公式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文件里面。
设置内容如图所示。
使用方法:
下载vscode,安装好platformio内核后(此处有一些麻烦,但是有很多优秀的教程,自行搜索即可)打开代码库中的platformio.ini文件,等待platformio下载完成依赖库后,点击vscode下方的->上传代码至控制板。
将板子横放,m5标志在左,手向上提即可控制恐龙跳跃。死亡后向内拉即可重新开始。
心得体会:
这应该是第三次参加硬禾学堂的活动了,这次群聊真的很冷清,可能也许是因为板子数量比较少的原因吧,只有50份。而且这个假期我有很多比赛,没太多时间做这个项目,拿到板子的第一天点了个灯,后来全程摸鱼,git提交记录忠实的记录了这一切。。。最后呈现的效果也并非很完美。不过好歹是实现了一个看起来能玩,实际上也能玩的效果。
m5stack这个公司做的小开发板很有意思,功能也很多,尤其是文档很全面。乐鑫公司的芯片很好玩,也很便宜,使用也很简便,硬禾学堂提供给我们的这种玩转即退钱的形式也相当的好。这次的小项目也是我本人第一次使用freertos+platformio这种组合,之前要么纯裸机,要么纯arduino加定时器,对我的技能也有很大的提高。
最近会阅读一些大佬的项目,只能说是高山仰止,要走的路还有很多。总之,感谢m5stack,硬禾学堂为我们提供的这次机会。
本次项目没有参考任何其他项目,唯一使用的素材就是谷歌浏览器的小恐龙素材,原版在谷歌浏览器的地址栏中输入chrome://dino/即可打开,图片使用内容元素检查即可查看。这段素材也一并上传至github仓库中了。