Урок 7. MPU9250. Игра "SPACE DEFENSE" (бета-версия)



  • Цель урока

    Привет! Сегодня мы научимся работать со встроенным датчиком MPU9250 в серебристой модели M5STACK.
    Датчик MPU9250 включает в себя гироскоп, акселерометр и компас. В данном уроке мы напишем игру "SPACE DEFENSE" (рис. 1), управлять кораблём будет возможно только при помощи наклона устройства. По оси X - движение корабля, а по оси Y - открытие огня. Гироскоп и компас оставим на самостоятельное изучение.

    Рисунок 1. Экран начала игры

    Краткая теория

    Рисунок 2. Устройство механического акселерометра

    Что же такое акселерометр? Акселерометр – прибор (рис. 2), измеряющий проекцию кажущегося ускорения (разности между истинным ускорением объекта и гравитационным ускорением). Как правило, акселерометр представляет собой чувствительную массу, закреплённую в упругом подвесе. Отклонение массы от её первоначального положения при наличии кажущегося ускорения несёт информацию о величине этого ускорения.

    Более подробная информация на Wiki: https://en.wikipedia.org/wiki/Accelerometer

    Перечень компонентов для урока

    • M5STACK серебристая модель со встроенным MPU9250.

    Начнём!

    Шаг 1. Нарисуем картинки

    Нарисуем космический корабль, взрыв корабля и логотип игры. Используйте любой удобный для Вас графический редактор. Мы будем использовать как всегда Paint (рис. 3 – 3.2).

    Рисунок 3. Рисуем космический корабль

    Рисунок 3.1. Рисуем логотип игры

    Рисунок 3.2. Коллекция рисунков для проекта

    В дальнейшем подключим изображения к нашему новому проекту:

    extern unsigned char craft[];
    extern unsigned char craft_logo[];
    extern unsigned char explode[];
    extern unsigned char logo[];
    

    Шаг 2. Пули и космический мусор

    Рассмотрим на примере пуль. Сделаем структуру bulletsObject, которая включает в себя координаты размеры и состояние пули (если пуля в полёте, то busy = true).

    struct bulletsObject
    {
    	int x;
    	int y;
    	int width = 3;
    	int height = 6;
    	bool busy;
    };
    

    Сделаем буфер, содержащий информацию о трёх пулях:

    const int bullets_max = 3;
    bulletsObject bulletsObjectArray[bullets_max];
    

    Космический мусор (если hidden = true, то объект нейтрализован):

    struct spaceDebrisObject
    {
      int x;
      int y;
      int width = 15;
      int height = 15;
      bool hidden;
    };
    

    Сделаем буфер, содержащий информацию о космическом мусоре:

    const int spaceDebris_max = 100;
    spaceDebrisObject spaceDebrisArray[spaceDebris_max];
    

    Шаг 3. Двигаем то, чего много

    Многозадачность? Нет – всё проще. Помните в одной из статей на сайте Arduino было рассказано, как реализовать мигание светодиодом без функции delay, которая заставляет микроконтроллер бездействовать? Если нет, то айда почитать https://www.arduino.cc/en/Tutorial/BlinkWithoutDelay.
    Аналогичным образом поступим и мы (рис. 5). Когда придёт время, тогда выполним необходимые действия и обновим значение предыдущего времени millis.

    Рисунок 5.

    Функция fire вызывается из бесконечного цикла gameLoop() и принимает в качестве аргумента целочисленное значение вектора наклона по оси Y. Если значение вектора менее, чем -250, то будет открыт огонь. О том, как получать значения векторов наклона поговорим в следующем шаге.
    Движение происходит на экране по оси Y всего массива с графическими объектами (рис. 5.1).

    Рисунок 5.1. Принцип движения графических объектов

    void gameLoop() {
      while (true)
      {
        if (!moveCraft(getAccel('X'))) break;
        if (!moveSpaceDebris()) break;
        fire(getAccel('Y'));
        if (health < 0) break;
      }
      craftExplode();  
    }
    
    void fire(int vector) {
      if (vector < -250)
      {
        if (!openFire)
        {
          for (int i = 0; i < bullets_max; i++)
          {
            if (!bulletsObjectArray[i].busy)
            {
              openFire = true;
              bulletsObjectArray[i].x = ((craft_x + (craft_width / 2)) - 1);
              bulletsObjectArray[i].y = craft_y;
              bulletsObjectArray[i].busy = true;
              break;
            }
          }
        }
      }
      else
      {
        openFire = false;
      }
    
      unsigned long millis_ = millis();
      if (((millis_ - moveBullets_pre_millis) >= moveBullets_time))
      {
        moveBullets_pre_millis = millis_; 
        for (int i = 0; i < bullets_max; i++)
        {
          M5.Lcd.fillRect(bulletsObjectArray[i].x, bulletsObjectArray[i].y, bulletsObjectArray[i].width, bulletsObjectArray[i].height, 0x0000);
          if (bulletsObjectArray[i].busy)
          {
            bulletsObjectArray[i].y--;
            M5.Lcd.fillRect(bulletsObjectArray[i].x, bulletsObjectArray[i].y, bulletsObjectArray[i].width, bulletsObjectArray[i].height, 0xff80);
            if (bulletsObjectArray[i].y <= statusBar_height)
            {
              bulletsObjectArray[i].busy = false;
            }
            for (int j = 0; j < spaceDebris_max; j++)
            {
              if (((((spaceDebrisArray[j].x + spaceDebrisArray[j].width) >= bulletsObjectArray[i].x) && (spaceDebrisArray[j].x <= (bulletsObjectArray[i].x + bulletsObjectArray[i].width))) && ((spaceDebrisArray[j].y + spaceDebrisArray[j].height) >= (bulletsObjectArray[i].y + (bulletsObjectArray[i].height / 2))) && (spaceDebrisArray[j].y <= (bulletsObjectArray[i].y + bulletsObjectArray[i].height)) && (!spaceDebrisArray[j].hidden)))
              {
                bulletsObjectArray[i].busy = false;
                spaceDebrisArray[j].hidden = true;
                M5.Lcd.fillRect(bulletsObjectArray[i].x, bulletsObjectArray[i].y, bulletsObjectArray[i].width, bulletsObjectArray[i].height, 0x0000);
                M5.Lcd.fillRect(spaceDebrisArray[j].x, spaceDebrisArray[j].y, spaceDebrisArray[j].width, spaceDebrisArray[j].height, 0x0000);
                score++;  
                drawHealthAndScore();
              }
            }
          }
        }
      }
    }
    

    Если пуля соприкасается с космическим мусором, то счёт игры увеличиваем на единицу, состояние пули принимает значение busy = false, космический мусор становится скрытым hidden = true.

    Шаг 4. Получение данных с акселерометра

    Как было сказано раньше – акселерометр возвращает значения углов отклонения от осей "векторы наклона" (рис. 6).

    Рисунок 6. Векторы наклона акселерометра

    Прежде всего необходимо подключить библиотеку для работы с MPU9250:

    #include "utility/MPU9250.h"
    

    Объявим экземпляр класса:

    MPU9250 IMU;
    

    Произведён инициализацию и выполним калибровку датчика. Помните, что MPU9250 подключён к ESP32 через I2C интерфейс:

    void setup(){
    	M5.begin();
    	Wire.begin();
      	IMU.initMPU9250();
      	IMU.calibrateMPU9250(IMU.gyroBias, IMU.accelBias);
    	// any actions	
    }
    

    Напишем функцию, которая принимает в качестве аргумента символ, соответствующий одной из осей 'X', 'Y' или 'Z'. Возвращает функция вектор наклона по оси. Если никаких данных не получено, то функция возвращает 0:

    int getAccel(char axis) {
      if (IMU.readByte(MPU9250_ADDRESS, INT_STATUS) & 0x01)
      {
        IMU.readAccelData(IMU.accelCount);
        IMU.getAres();
        switch(axis)
        {
          case 'X':
            IMU.ax = (float)IMU.accelCount[0] * IMU.aRes * 1000;
            return IMU.ax;
          case 'Y':
            IMU.ax = (float)IMU.accelCount[1] * IMU.aRes * 1000;
            return IMU.ax;
          case 'Z':
            IMU.az = (float)IMU.accelCount[2] * IMU.aRes * 1000;
            return IMU.az;
        }
      }
      return 0;
    }
    

    Шаг 5. Двигаем космический корабль

    Функция чрезмерно простая, принцип работы мы рассмотрели на предыдущем шаге:

    bool moveCraft(int vector) {
      unsigned long millis_ = millis();
      if (((millis_ - moveCraft_pre_millis) >= moveCraft_time))
      {
        moveCraft_pre_millis = millis_;  
        int craft_x_pre = craft_x;
    
        if (abs(vector) > 70)
        {
          if (vector > 0)
            craft_x -= craft_step;
          else if (vector < 0)
            craft_x += craft_step;
          M5.Lcd.fillRect(craft_x_pre, craft_y, craft_width, craft_height, 0x0000);
        }
    
        if ((craft_x < (screen_width - craft_width - craft_step)) && (craft_x > craft_step))
        { 
          M5.Lcd.drawBitmap(craft_x, craft_y, craft_width, craft_height, (uint16_t *)craft);
        }
        else
        {
          return false;                                                                               
        }
      }
      return true;
    }
    

    Шаг 6. Игра окончена

    void gameOver() {
      M5.Lcd.fillScreen(0x0000);
      M5.Lcd.setTextSize(2);
      M5.Lcd.setTextColor(0x7bef);
      M5.Lcd.setCursor(35, 80);
      M5.Lcd.print("GAME OVER");
      M5.Lcd.setCursor(35, 110);
      M5.Lcd.print("score: ");
      M5.Lcd.print(score);
      M5.Lcd.setCursor(35, 140);
      M5.Lcd.print("please, press any key");  
      gameReset();
      while(true)
      {
        M5.update();
        if (M5.BtnA.wasPressed() || M5.BtnB.wasPressed() || M5.BtnC.wasPressed()) break;
      }
    }
    

    Шаг 7. Запуск!

    В целом это был очень интересный проект, посвященный работе с акселерометром на борту MPU9250. Давайте запустим и посмотрим, как это работает (рис. 7, 7.1.):

    Рисунок 7. Игровой процесс

    Рисунок. 7.1. Игра окончена

    Скачать