Урок 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. Игра окончена
Скачать
-
Видео с демонстрацией работы (YouTube): https://youtu.be/9gyiFfciUU4
-
Исходные коды (GitHub): https://github.com/dsiberia9s/SpaceDefense-m5stack
-
Конверторы изображений (Yandex Disk):https://yadi.sk/d/m2zvebPf3T5Zrc
-
Изображения (Yandex Disk): https://yadi.sk/d/JMMyPHOq3T5Zmf