一、项目介绍
本项目利用M5StickC Plus开发板制作一个体感游戏手柄,移植了经典游戏2048,利用陀螺仪控制滑动方向。
二、设计思路
硬禾学堂针对此平台设计了五个任务,假期中我完成的是任务三,使用姿态传感器完成体感游戏手柄,并设计完成一个游戏。
选择的游戏是「2048」,该游戏的基本玩法是每次可以选择上下左右其中一个方向去移动,每移动一次,所有的数字方块都会往滑动的方向靠拢外,在空白处会随机生成一个数字方块,另外,相同数字的方块在靠拢、相撞时会相加,经过不断的叠加最终拼出2048这个数字就算完成游戏,否则游戏结束,由于单片机屏幕面积限制,我将数字换成了字母,如数字2对应字母A,数字4对应字母B,数字8对应字母C,以此类推,2048对应字母K,下面来看下实际的演示。
选择2048的原因是游戏操作方法简单,仅需4个方向的移动功能即可进行游戏,采集移动主要使用了MPU6886检测开发板倾斜方向,这是一个6轴的IMU姿态传感器,具有3轴重力加速度计和3轴陀螺仪,能够实时计算欧拉角和加速度,具有16位ADC,内置可编程数字滤波器和片上温度传感器,采用I2C接口与上位机通信,支持低功耗模式。
经过陀螺仪得到玩家想要的移动方向后,程序根据现有状态进行计算,生成新的方块并刷新界面,等待玩家的下一次移动,假如所有方块在玩家移动后没有可用的解法或无法在空白处生成新的方块,游戏结束。
当玩家成功移动一次方块的同时,蜂鸣器会滴一声,作为玩家成功移动的反馈,辅助玩家确认当前移动已触发,此时需要将开发板放平归位,执行下一次移动操作。此外,蜂鸣器还用于游戏开始和结束时的特殊音效。
当游戏结束后,玩家可通过,屏幕下方的游戏复位按键重置游戏,回到最初始的状态,当然,玩家也可以提前重置游戏,在每次游戏开始时方块会随机生成新的位置来保障游戏可玩性。
三、M5StickC Plus开发板介绍
M5StickC PLUS 是M5StickC的大屏幕版本,主控采用ESP32-PICO-D4模组,具有蓝牙4.2与WIFI的功能,机身内部集成了丰富的硬件资源,如红外、RTC、麦克风、LED、IMU、按键、蜂鸣器、PMU等,屏幕为1.14寸TFT、分辨率135*240像素,电池容量为120mAh。
主控采用ESP32-PICO-D4,这是一款基于ESP32的系统级封装 (SiP) 模组,可提供完整的Wi-Fi和蓝牙 功能。该模组的外观尺寸仅为 (7.000±0.100) mm × (7.000±0.100) mm × (0.940±0.100) mm,整体占用的 PCB 面积最小,已集成1个4 MB SPI flash。
ESP32-PICO-D4 的核心是ESP32芯片。ESP32 是集成 2.4 GHz Wi-Fi 和蓝牙双模的单芯片方案,采用台积电(TSMC)超低功耗的40纳米工艺。ESP32-PICO-D4模组已将晶振、flash、滤波电容、RF匹配链路等所有外围器件无缝集成进封装内,不再需要外围元器件即可工作。此时,由于无需外围器件,模组焊接和测试过程也可以避免,因此 ESP32-PICO-D4 可以大大降低供应链的复杂程度并提升管控效率。ESP32-PICO-D4具备体积紧凑、性能强劲及功耗低等特点,适用于任何空间有限或电池供电的设备,比如可穿戴设备、医疗设备、传感器及其他 IoT 设备。
四、开发环境的部署
使用的Arduino IDE 1.8.19作为编译工具,VSCode作为代码编辑器,参考相应API进行开发。
- 集成开发环境 Arduino IDE 下载地址:
- 代码编辑器 VSCode 下载地址:
- 开发板 M5StickC PLUS 针对 Arduino 开发设计了相应的 API,参考地址:
五、程序流程图与代码解析
// 陀螺仪相关参数
#define DIRECTION_JUDGE_VALUE 30
#define IMU_AHRS_MID_PITCH 13
#define IMU_AHRS_MID_ROLL -15
// 方向
#define DIRECTION_MID 0
#define DIRECTION_UP 1
#define DIRECTION_DOWN 2
#define DIRECTION_LEFT 3
#define DIRECTION_RIGHT 4
// 颜色
#define COLOR_BACKGROUND 0xffde
#define COLOR_MAIN_SQUARE 0xbd74
#define COLOR_TEXT 0x738c
// 方块颜色
#define COLOR_NUMB_NONE 0xd678
#define COLOR_NUMB_2 0xef3b
#define COLOR_NUMB_4 0xef18
#define COLOR_NUMB_8 0xf58f
#define COLOR_NUMB_16 0xf4ac
#define COLOR_NUMB_32 0xf3eb
#define COLOR_NUMB_64 0xf2e7
#define COLOR_NUMB_128 0xee6e
#define COLOR_NUMB_256 0xee6c
#define COLOR_NUMB_512 0xee4a
#define COLOR_NUMB_1024 0xee26
#define COLOR_NUMB_2048 0xee05
// 游戏运行状态
#define STATE_OVER 0
#define STATE_NORMAL 1
#define STATE_WAIT 2
// 块变化状态
#define STATE_HOLD 0
#define STATE_CHANGE 1
uint8_t Block_State = STATE_CHANGE; // 块状态
uint8_t Game_State = STATE_NORMAL; // 游戏状态
uint8_t Game_Map[4][4] = {0}; // 游戏地图
uint8_t step = 0; // 总步数
uint8_t score = 0; // 总分数
float Imu_Gyro_X, Imu_Gyro_Y, Imu_Gyro_Z; // IMU三轴陀螺仪数据
float Imu_Accel_X, Imu_Accel_Y, Imu_Accel_Z; // IMU三轴加速度计数据
float Imu_pitch, Imu_roll, Imu_yaw; // IMU三轴姿态数据
float Imu_Temperature; // IMU温度数据
int direction_old = 0, direction = 0;
void setup() {
M5.begin(); // 初始化
M5.Imu.Init(); // 陀螺仪初始化
M5.Lcd.setRotation(0); // 显示方向
M5.Lcd.fillScreen(COLOR_BACKGROUND);
M5.Lcd.fillRect(0, 40, 135, 135, COLOR_MAIN_SQUARE);
M5.Lcd.setTextSize(3);
M5.Lcd.setTextColor(COLOR_TEXT, COLOR_BACKGROUND);
M5.Lcd.setCursor(34, 10);
M5.Lcd.println("2048");
M5.Lcd.setTextSize(2);
randIntNum();
Fill_Block_Color();
Show_Msg();
}
void loop() {
Imu_Get();
M5.update();
if(M5.BtnA.wasReleased())
{
Reset_Game();
}
// direction = Judge_Direction_Ahrs(Imu_pitch, Imu_roll);
// direction = Judge_Direction_Accel(Imu_Accel_X, Imu_Accel_Y);
direction = Judge_Direction_Gryo(Imu_Gyro_X, Imu_Gyro_Y);
if((direction != direction_old) && (direction != 0))
{
direction_old = direction;
// 游戏正常进行
if(Game_State == STATE_NORMAL)
{
M5.Beep.tone(500, 10);
delay(15);
M5.Beep.update();
move();
randIntNum();
Fill_Block_Color();
over();
Show_Msg();
delay(200);
}
// 游戏结束
else if(Game_State == STATE_OVER)
{
M5.Lcd.fillRect(5, 100, 125, 30, BLACK);
M5.Lcd.fillRect(7, 102, 121, 26, COLOR_BACKGROUND);
M5.Lcd.setTextSize(2);
M5.Lcd.setTextColor(COLOR_TEXT);
M5.Lcd.setCursor(13, 108);
M5.Lcd.printf("GAME OVER");
M5.Beep.tone(1500, 200);
delay(200);
M5.update();
M5.Beep.tone(150, 250);
delay(250);
M5.update();
M5.Beep.tone(1500, 200);
delay(200);
M5.update();
M5.Beep.tone(150, 250);
delay(250);
M5.update();
Game_State = STATE_WAIT;
}
// 等待按键重新开始
else if(Game_State == STATE_WAIT)
{
if(M5.BtnA.wasReleased())
{
Reset_Game();
}
}
}
else if(direction == 0)
{
direction_old = direction;
}
}
void randIntNum(void)
{
int i, j, n;
if (Block_State == STATE_CHANGE)
{
do{
i=((unsigned)rand())%4;
j=((unsigned)rand())%4;
}while(Game_Map[i][j]!=0);
n=((unsigned)rand())%2;
if (n == 0)
Game_Map[i][j] = 2;
else if(n == 1)
Game_Map[i][j] = 4;
}
}
填充颜色区块
void Fill_Block_Color()
{
uint8_t i, j;
M5.Lcd.setTextSize(2);
M5.Lcd.setTextColor(0x738c);
for(i = 0; i < 4; i++)
{
for(j = 0; j < 4; j++)
{
if(Game_Map[i][j] == 0)
{
M5.Lcd.fillRect(6+27*i+5*i, 46+27*j+5*j, 27, 27, COLOR_NUMB_NONE);
}
else if(Game_Map[i][j] == 2)
{
M5.Lcd.fillRect(6+27*i+5*i, 46+27*j+5*j, 27, 27, COLOR_NUMB_2);
M5.Lcd.setCursor(6+27*i+5*i+9, 46+27*j+5*j+7);
M5.Lcd.println("A");
}
else if(Game_Map[i][j] == 4)
{
M5.Lcd.fillRect(6+27*i+5*i, 46+27*j+5*j, 27, 27, COLOR_NUMB_4);
M5.Lcd.setCursor(6+27*i+5*i+9, 46+27*j+5*j+7);
M5.Lcd.println("B");
}
else if(Game_Map[i][j] == 8)
{
M5.Lcd.fillRect(6+27*i+5*i, 46+27*j+5*j, 27, 27, COLOR_NUMB_8);
M5.Lcd.setCursor(6+27*i+5*i+9, 46+27*j+5*j+7);
M5.Lcd.println("C");
}
else if(Game_Map[i][j] == 16)
{
M5.Lcd.fillRect(6+27*i+5*i, 46+27*j+5*j, 27, 27, COLOR_NUMB_16);
M5.Lcd.setCursor(6+27*i+5*i+9, 46+27*j+5*j+7);
M5.Lcd.println("D");
}
else if(Game_Map[i][j] == 32)
{
M5.Lcd.fillRect(6+27*i+5*i, 46+27*j+5*j, 27, 27, COLOR_NUMB_32);
M5.Lcd.setCursor(6+27*i+5*i+9, 46+27*j+5*j+7);
M5.Lcd.println("E");
}
else if(Game_Map[i][j] == 64)
{
M5.Lcd.fillRect(6+27*i+5*i, 46+27*j+5*j, 27, 27, COLOR_NUMB_64);
M5.Lcd.setCursor(6+27*i+5*i+9, 46+27*j+5*j+7);
M5.Lcd.println("F");
}
else if(Game_Map[i][j] == 128)
{
M5.Lcd.fillRect(6+27*i+5*i, 46+27*j+5*j, 27, 27, COLOR_NUMB_128);
M5.Lcd.setCursor(6+27*i+5*i+9, 46+27*j+5*j+7);
M5.Lcd.println("G");
}
else if(Game_Map[i][j] == 256)
{
M5.Lcd.fillRect(6+27*i+5*i, 46+27*j+5*j, 27, 27, COLOR_NUMB_256);
M5.Lcd.setCursor(6+27*i+5*i+9, 46+27*j+5*j+7);
M5.Lcd.println("H");
}
else if(Game_Map[i][j] == 512)
{
M5.Lcd.fillRect(6+27*i+5*i, 46+27*j+5*j, 27, 27, COLOR_NUMB_512);
M5.Lcd.setCursor(6+27*i+5*i+9, 46+27*j+5*j+7);
M5.Lcd.println("I");
}
else if(Game_Map[i][j] == 1024)
{
M5.Lcd.fillRect(6+27*i+5*i, 46+27*j+5*j, 27, 27, COLOR_NUMB_1024);
M5.Lcd.setCursor(6+27*i+5*i+9, 46+27*j+5*j+7);
M5.Lcd.println("J");
}
else if(Game_Map[i][j] == 2048)
{
M5.Lcd.fillRect(6+27*i+5*i, 46+27*j+5*j, 27, 27, COLOR_NUMB_2048);
M5.Lcd.setCursor(6+27*i+5*i+9, 46+27*j+5*j+7);
M5.Lcd.println("K");
}
}
}
}
void Imu_show()
{
M5.Lcd.setCursor(0, 30);
M5.Lcd.printf("accX:%3.0f \naccY:%3.0f \naccZ:%3.0f ", Imu_Accel_X, Imu_Accel_Y, Imu_Accel_Z);
M5.Lcd.setCursor(0, 110);
M5.Lcd.printf("pit:%3.0f \nrow:%3.0f \nyaw:%3.0f ", Imu_pitch, Imu_roll, Imu_yaw);
}
void Imu_Get()
{
M5.Imu.getAccelData(&Imu_Gyro_X, &Imu_Gyro_Y, &Imu_Gyro_Z);
M5.Imu.getAccelData(&Imu_Accel_X, &Imu_Accel_Y, &Imu_Accel_Z);
M5.Imu.getAhrsData(&Imu_pitch, &Imu_roll, &Imu_yaw);
M5.Imu.getTempData(&Imu_Temperature);
Imu_Accel_X *= 90;
Imu_Accel_Y *= 90;
Imu_Accel_Z *= 90;
Imu_Gyro_X *= 90;
Imu_Gyro_Y *= 90;
Imu_Gyro_Z *= 90;
}
int8_t Judge_Direction_Gryo(float X, float Y)
{
if(Y > DIRECTION_JUDGE_VALUE)
return DIRECTION_DOWN;
else if(Y < -DIRECTION_JUDGE_VALUE)
return DIRECTION_UP;
else if(X > DIRECTION_JUDGE_VALUE)
return DIRECTION_LEFT;
else if(X < -DIRECTION_JUDGE_VALUE)
return DIRECTION_RIGHT;
else if((abs(X) <15) && (abs(Y) <15))
return DIRECTION_MID;
}
以向左移动为例
void left(void)
{
int now, next;
int i, j, k;
for (i = 0; i < 4; i++)
{
for (j = 0; j < 4; j++)
{
now=Game_Map[i][j];
if (now!=0)
{
k=i+1;
while(k<4)
{
next=Game_Map[k][j];
if (next!=0)
{
if (now==next)
{
Block_State = STATE_CHANGE;
score+=Game_Map[k][j];
Game_Map[i][j]=Game_Map[k][j] * 2;
Game_Map[k][j]=0;
}
k=4;
}
k++;
}
}
}
}
for (i = 0; i < 4; i++)
{
for (j = 0; j < 4; j++)
{
now=Game_Map[i][j];
if (now==0)
{
k=1+i;
while(k<4)
{
next=Game_Map[k][j];
if (next!=0)
{
Block_State = STATE_CHANGE;
Game_Map[i][j]=next;
Game_Map[k][j]=0;
k=4;
}
k++;
}
}
}
}
}
移动控制
void move(void)
{
Block_State = STATE_HOLD;
switch(direction){
case DIRECTION_UP:
up();
step++;
break;
case DIRECTION_LEFT:
left();
step++;
break;
case DIRECTION_DOWN:
down();
step++;
break;
case DIRECTION_RIGHT:
right();
step++;
break;
}
}
uint8_t over(void)
{
int i,j;
Game_State = STATE_OVER;
for ( i = 0; i < 4; i++)
{
for ( j = 0; j < 4; j++)
{
// 还有空位
if(Game_Map[i][j]==0)
{
Game_State = STATE_NORMAL;
}
// 还有相邻的字母
if(i > 1)
{
if (Game_Map[i][j]==Game_Map[i-1][j])
Game_State = STATE_NORMAL;
}
if(j > 1)
{
if (Game_Map[i][j]==Game_Map[i][j-1])
Game_State = STATE_NORMAL;
}
}
}
return Game_State;
}
void Show_Dire()
{
M5.Lcd.setTextSize(3);
M5.Lcd.setTextColor(0x738c, 0xffde);
M5.Lcd.setCursor(60, 200);
if(direction == DIRECTION_MID)
M5.Lcd.printf(" ");
else if(direction == DIRECTION_UP)
M5.Lcd.printf("U");
else if(direction == DIRECTION_DOWN)
M5.Lcd.printf("D");
else if(direction == DIRECTION_LEFT)
M5.Lcd.printf("L");
else if(direction == DIRECTION_RIGHT)
M5.Lcd.printf("R");
}
void Show_Msg()
{
M5.Lcd.setTextSize(2);
M5.Lcd.setTextColor(0x738c, 0xffde);
M5.Lcd.setCursor(8, 180);
M5.Lcd.print(" Dire:");
if(direction == DIRECTION_MID)
M5.Lcd.printf("None ");
else if(direction == DIRECTION_UP)
M5.Lcd.printf("Up ");
else if(direction == DIRECTION_DOWN)
M5.Lcd.printf("Down ");
else if(direction == DIRECTION_LEFT)
M5.Lcd.printf("Left ");
else if(direction == DIRECTION_RIGHT)
M5.Lcd.printf("Righ");
M5.Lcd.setCursor(8, 200);
M5.Lcd.print(" Step:");
M5.Lcd.print(step);
M5.Lcd.print(" ");
M5.Lcd.setCursor(8, 220);
M5.Lcd.print("Score:");
M5.Lcd.print(score);
M5.Lcd.print(" ");
}
void Reset_Game(void)
{
int i,j;
for(i = 0; i < 4; i++)
{
for(j = 0; j < 4; j++)
{
Game_Map[i][j] = 0;
}
}
M5.Lcd.fillRect(0, 40, 135, 135, COLOR_MAIN_SQUARE);
step = 0;
score = 0;
Block_State = STATE_CHANGE;
Game_State = STATE_NORMAL;
randIntNum();
Fill_Block_Color();
Show_Msg();
M5.Beep.tone(100, 30);
delay(30);
M5.update();
M5.Beep.tone(400, 30);
delay(30);
M5.update();
M5.Beep.tone(700, 30);
delay(30);
M5.update();
M5.Beep.tone(1000, 30);
delay(30);
M5.update();
M5.Beep.tone(1300, 30);
delay(30);
M5.update();
}
六、难点及注意要点
1.难点
- 2048的逻辑部分,相同的块相撞需要增加合并,不相同的块相撞需要依次排列
- 2048的UI部分,需要模仿经典版的配色以及界面
2.注意要点
- 向相同方向的移动不能触发多次,需要归中后再倾斜触发移动,否则会造成误操作
- 本次开发环境使用Arduino,在Arduino IDE中添加M5StickCPlus开发板需要较好的网络环境,一直刷新不出来的话或者安装进行不下去的话可以尝试使用手机热点。
- 若计算机无法识别设备,可尝试安装FTDI驱动,下载地址:
- 对于刚上手的开发者可以尝试官方例程,下载地址:
七、未来计划
- 学习使用IDF环境进行开发,编译速度显著高于Arduino,提高开发效率。
- 将开发板的全部功能用上,如麦克风、扬声器、红外、WiFi、蓝牙等。