基于M5StickC Plus平台的「2048」手柄游戏
本项目利用M5StickC Plus开发板制作一个体感游戏手柄,移植了经典游戏2048,利用陀螺仪控制滑动方向
标签
嵌入式系统
ESP32
SDeron
更新2022-09-02
金陵科技学院
712

一、项目介绍

本项目利用M5StickC Plus开发板制作一个体感游戏手柄,移植了经典游戏2048,利用陀螺仪控制滑动方向。

二、设计思路

硬禾学堂针对此平台设计了五个任务,假期中我完成的是任务三,使用姿态传感器完成体感游戏手柄,并设计完成一个游戏。

FvbO-538aJU6ZiWuQL1TNPuKV4oXFiv16wFkiDeR1QV42gqKRjwiBzhO

选择的游戏是「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进行开发。

FpLE5JUewuKAvL_ecNnWEJcZJfMt

五、程序流程图与代码解析

FiPDS9lmekq73ied2OtSV6eP5C3D

全局宏定义
// 陀螺仪相关参数
#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;
    }
}
随机生成一个2或4
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驱动,下载地址:
  • 对于刚上手的开发者可以尝试官方例程,下载地址:

七、未来计划

  1. 学习使用IDF环境进行开发,编译速度显著高于Arduino,提高开发效率。
  2. 将开发板的全部功能用上,如麦克风、扬声器、红外、WiFi、蓝牙等。
附件下载
2048.zip
程序
团队介绍
刚玩电子的小白
评论
0 / 100
查看更多
目录
硬禾服务号
关注最新动态
0512-67862536
info@eetree.cn
江苏省苏州市苏州工业园区新平街388号腾飞创新园A2幢815室
苏州硬禾信息科技有限公司
Copyright © 2023 苏州硬禾信息科技有限公司 All Rights Reserved 苏ICP备19040198号