«Flappy Bird»‎ на Arduino Uno

Проекты на Arduino Uno и Slot Shield

«Flappy Bird» — звезда вьетнамского игростроя. Она произвела революцию на рынке мобильных игр. За неказистой графикой и простым геймплеем скрывалась невероятно азартная и увлекательная игра. Говорят, она стоила геймерам не одного разбитого телефона. Мы портировали игру на Ардуино.

Игровое поле — это пара RGB матриц. Они превратятся в экран разрешением 4 на 8 пикселей. Мозг проекта — оригинальная Arduino Uno. Полётом жёлтой птички управляет 3D-джойстик. Нажмёте джойстик вверх, птичка взмахнёт крыльями и взлетит. Оставите в покое, и она начнёт снижаться. Всё просто. Главное — не врезайтесь в зелёные трубы.

Что потребуется

Полный сет компонентов проекта. В сет входят:

Видеоинструкция

Как собрать

Установите Troyka Slot Shield на Arduino Uno

Вставьте RGB матрицу в разъём С. Ножка S должна подключится к пину 4.

3D-джойстик подключите в слот A. Сигнал с X придёт на пин A4, сигнал с Y на A4. Нажатие на джойстик будет передаваться на пин 2.

Вставьте вторую RGB матрицу в разъём F. Сигнальный пин S модуля должен подключится к пину "10" платы.

Скетч

Прошейте контроллер скетчем через Arduino IDE.

flappy-bird.ino
// библиотека для работы с RGB-матрицей
#include <Adafruit_NeoPixel.h>
 
// номер пина, к которому подключена RGB-матрица
#define MATRIX_C_PIN    4
#define MATRIX_F_PIN    10
// количество светодиодов в матрице
#define LED_COUNT 16
// зерно для генератора случайных чисел
#define ANALOG_PIN_FOR_RND  A3
// максимальная яркость матрицы от 0 до 255
#define BRIGHT 10
 
// даём разумные имена пинам, к которым подключён джойстик
#define X  A5
#define Y  A4
#define Z  2
 
// создаём объект класса Adafruit_NeoPixel
Adafruit_NeoPixel matrix [] = {Adafruit_NeoPixel(LED_COUNT, MATRIX_C_PIN, NEO_GRB + NEO_KHZ800),
                               Adafruit_NeoPixel(LED_COUNT, MATRIX_F_PIN, NEO_GRB + NEO_KHZ800)
                              };
 
// создаем структуру для размещения в ней цветов
struct Light
{
  uint32_t Y = 0xffff00;
  uint32_t G = 0x00ff00;
  uint32_t B = 0x0000ff;
};
// создаем структуру для размещения в ней параметров счета
struct Store
{
  int value;
  bool flag;
};
// создаем структуру для размещения в ней параметров игровых объектов
struct GameObjects
{
  int x;
  int y;
  bool flag;
};
 
// создаем объект структуры Light
Light light;
// создаем объект структуры Store
Store store;
// создаем объекты структуры GameObjects: игрока и стену
GameObjects player, wall;
 
// создаем объект класса long для хранения времени
unsigned long timeOfGravity = 0;
// создаем счетчик, по которому действует гравитация
int gravity = 500;
 
// создаем объект класса long для хранения времени
unsigned long timeOfSide = 0;
// создаем счетчик, который определяет "скорость" стены
int side;
 
void setup() {
  // инициализируем последовательность случайных чисел
  randomSeed(analogRead(ANALOG_PIN_FOR_RND));
  // инициализируем все матрицы
  for (int i = 0; i < sizeof(matrix) / sizeof(Adafruit_NeoPixel); i++) {
    matrix[i].begin();
    matrix[i].setBrightness(10);
  }
  // открываем последовательный порт
  Serial.begin(9600);
  // очищаем матрицы
  draw_clear ();
}
 
void loop() {
  // запускаем функцию предстартовой подготовки
  at_the_start();
  // пока флаг счета опущен, игра не начнется
  // когда же он поднят, то не игра закончится
  while (store.flag) {
    // очищаем матрицы
    draw_clear ();
    // отслеживаем перемещение стены
    wall_move ();
    // рисуем стену на матрицах
    wall_draw();
    // считываем движения джойстика
    player_move ();
    // рисуем на матрице положение игрока
    draw_point(player.x, player.y, light.Y);
    // проверяем, не вышел ли игрок за пределы поля
    if ( player.y < 0 || player.y >= 8) {
      // если вышел, то воспроизводим проигрыш
      game_over();
    }
    // когда стена и игрок сравняются по x...
    if (player.x == wall.x ) {
      // ...проверяем, не врезался ли игрок в стену
      if (player.y != wall.y && player.y != wall.y + 1) {
        // если врезался, то воспроизводим проигрыш
        game_over();
      }
      // ... если игра все же не проиграна то однократно выполняем условие победы
      if (wall.flag) {
        // увеличиваем счет
        store.value ++;
        // увеличиваем "скорость"
        side -= side / 10;
        // опускаем флаг однократного выполнения
        wall.flag = false;
      }
    } else {
      // держим флаг поднятым до тех пор, пока не наступит победа
      wall.flag = true;
    }
  }
}
 
// функция приготовления к игре
void at_the_start() {
  // очищаем матрицы
  draw_clear ();
  // задаем начальное значение счетчика "скорости" стены
  side = 1500;
  // обнуляем значение счета и держим флаг счетчика опущенным
  store = { 0, false};
  // задаем начальное значение координат игрока
  player = {3, 4, true};
  // задаем начальное значение координат стены
  wall = { -1, random(0, 6), true};
  // считываем движения джойстика
  player_move ();
  // рисуем на матрице положение игрока
  draw_point(player.x, player.y, light.Y);
  delay (250);
}
 
// функция окончания игры
void game_over() {
  // очищаем матрицы
  draw_clear ();
  // заливаем матрицы синим цветом
  draw_pouring (light.B);
  // выводим счет в Serial port
  Serial.print ("Your store: ");
  Serial.println (store.value);
  // опускаем флаг счета
  store.flag = false;
  // инициализируем последовательность случайных чисел
  randomSeed(analogRead(ANALOG_PIN_FOR_RND));
  delay (250);
}
 
// функция движения игрока
void player_move () {
  int y, z;
  // считываем текущее значение джойстика по Y
  y = analogRead(Y);
  if (y < 100 && player.flag) {
    player.y ++;
    player.flag = false;
  }
  if (y > 924 && player.flag) {
    player.y --;
    player.flag = false;
  }
  if (100 < y && y < 924) {
    player.flag = true;
  }
  // создаем постоянную простую гравитацию
  if (millis() - timeOfGravity > gravity) {
    player.y --;
    timeOfGravity = millis();
  }
  // считываем текущее значение джойстика по Z
  z = digitalRead(Z);
  if (z == 1 && store.flag == false) {
    store.flag = true;
  }
}
 
// функция движения стены
void wall_move () {
  // запускаем таймер, по которому стена будет перемещаться
  if (millis() - timeOfSide > side) {
    // если стена за пределами видимости игрока
    if (wall.x >= 4) {
      // то изменяем ее и перемещаем в начало
      wall.x = 0;
      wall.y = random(0, 7);
    } else {
      // если нет, то двигаем ее на игрока
      wall.x++;
    }
    timeOfSide = millis();
  }
}
 
// функция рисования препятствия
void wall_draw () {
  // проверяем координату стены, в зависимости от этого рисуем одну или 2 линии
  switch (wall.y) {
    case 0: {
        draw_line(wall.x, 2, wall.x, 7, light.G);
        break;
      }
    case 6: {
        draw_line(wall.x, 0, wall.x, 5, light.G);
        break;
      }
    default : {
        draw_line(wall.x, 0, wall.x, wall.y - 1, light.G);
        draw_line(wall.x, wall.y + 2, wall.x, 7, light.G);
        break;
      }
  }
}
 
// функция для перевода координат в № матрицы
int nHelper (int x, int y) {
  return y / 4 + ((x / 4) * 2);
}
 
// функция для перевода координат в № светодиода
int mHelper(int x, int y) {
  switch (nHelper (x, y)) {
    case 0: {
        return {y * 4 + x};
        break;
      }
    case 1: {
        return {abs (y - 8) * 4 - x - 1};
        break;
      }
  }
}
 
// функция для рисования линий по алгоритму Брезенхэма
void draw_line(int x1, int y1, int x2, int y2, uint32_t RGB) {
  const int deltaX = abs(x2 - x1);
  const int deltaY = abs(y2 - y1);
  const int signX = x1 < x2 ? 1 : -1;
  const int signY = y1 < y2 ? 1 : -1;
  int error = deltaX - deltaY;
  matrix[nHelper(x2, y2)].setPixelColor(mHelper(x2, y2), RGB);
  while (x1 != x2 || y1 != y2)
  {
    matrix[nHelper(x1, y1)].setPixelColor(mHelper(x1, y1), RGB);
    const int error2 = error * 2;
    if (error2 > -deltaY)
    {
      error -= deltaY;
      x1 += signX;
    }
    if (error2 < deltaX)
    {
      error += deltaX;
      y1 += signY;
    }
  }
  for (int i = 0; i < sizeof(matrix) / sizeof(Adafruit_NeoPixel); i++) {
    matrix[i].show();
  }
}
 
// функция для рисования сплошной заливкой
void draw_pouring (uint32_t RGB) {
  for (int i = 0; i < sizeof(matrix) / sizeof(Adafruit_NeoPixel); i++) {
    for (int j = 0; j < LED_COUNT; j++) {
      matrix[i].setPixelColor(j, RGB);
      matrix[i].show();
    }
  }
}
 
// функция для рисования точки
void draw_point (int x, int y, uint32_t RGB) {
  matrix[nHelper(x, y)].setPixelColor(mHelper(x, y), RGB);
  matrix[nHelper(x, y)].show();
}
 
// функция для очистки матриц
void draw_clear () {
  for (int i = 0; i < sizeof(matrix) / sizeof(Adafruit_NeoPixel); i++) {
    matrix[i].clear();
    matrix[i].show();
  }
}

Часто задаваемые вопросы

Где скачать необходимые библиотеки и как их установить?

Что дальше

  • Замените 3D-джойстик на датчик шума (Troyka-модуль) и сможете управлять полётом птички крича на неё.
  • Добавьте Power Shield — игру можно будет взять с собой в дорогу.
  • Вставьте устройство в корпус из #cтруктора. Slot Box придаст игре законченный вид.