Собираем «Змейку»‎ на Arduino

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

Змейка, питон, удав, червяк, snake — как только не называли легендарную игру. Впервые она появилась на экране игрового автомата ещё в 1977 году. С тех пор она была портирована на все мыслимые платформы. Мы перенесли Змейку на Arduino.

Игровым полем станут четыре RGB матрицы. Вместе они соберутся в экран из дадут экран 64 цветных пикселей. Мозг проекта — оригинальная Arduino Uno. Движением змейки управляет 3D-джойстик. Голосом игры станет простая пьезопищалка.

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

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

Как собрать

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

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

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

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

Подключите оставшуюся пару матриц к пинам 6 и 9 слотов B и E. Вставьте в слот D пьезопищалку. Нога S модуля должна встать в пин 8.

Скетч

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

snake.ino
// библиотека для работы с RGB-матрицей
#include <Adafruit_NeoPixel.h>
 
// пин пищалки
#define BUZZER_PIN  8
 
// номер пина, к которому подключена RGB-матрица
#define MATRIX_B_PIN    6
#define MATRIX_E_PIN    9
#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_B_PIN, NEO_GRB + NEO_KHZ800),
                                 Adafruit_NeoPixel(LED_COUNT, MATRIX_E_PIN, NEO_GRB + NEO_KHZ800),
                                 Adafruit_NeoPixel(LED_COUNT, MATRIX_C_PIN, NEO_GRB + NEO_KHZ800),
                                 Adafruit_NeoPixel(LED_COUNT, MATRIX_F_PIN, NEO_GRB + NEO_KHZ800)
                              };
// перечисляем имена направлений, в которых будет двигаться змейка
enum {
  NOTOUCH,  // имя для состояния ожидания
  RIGHT,    // имя для направления "направо"
  LEFT,     // имя для направления "налево"
  DOWN,     // имя для направления "вниз"
  UP        // имя для направления "вверх"
};
 
// создаем структуру для размещения в ней цветов
struct Light
{
  uint32_t R = 0xff0000;
  uint32_t G = 0x00ff00;
  uint32_t B = 0x0000ff;
};
// создаем структуру для размещения в ней параметров счета
struct Store
{
  int value;
  bool flag;
};
// создаем структуру для размещения в ней параметров змеи
struct Snakes
{
  int x;
  int y;
  bool flagX;
  bool flagY;
};
// создаем структуру для размещения в ней параметров мыши
struct Mouses
{
  int x;
  int y;
};
// создаем объект структуры Light
Light light;
// создаем объект структуры Store
Store store;
// создаем объект структуры Snakes
Snakes snake;
// создаем объект структуры Mouses
Mouses mouse;
 
// создаем объект класса long для хранения времени
unsigned long timeOfMovement = 0;
// создаем счетчик, по которому будет перемещаться змея
int movement = 500;
 
// создаем фишку, в которой будем хранить направление движения змейки
int player, playerOld ;
// создаем две матрицы в которых будем хранить координаты всех точек хвоста змеи
int tailX[64], tailY[64];
 
void setup() {
  //инициализируем выход пищалки
  pinMode(BUZZER_PIN, OUTPUT);
  // инициализируем последовательность случайных чисел
  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) {
    // считываем движения джойстика
    player_move ('x');
    player_move ('y');
    // проверяем, съела ли змея мышь
    if (snake.x == mouse.x && snake.y == mouse.y) {
      for (int i = 0; i < 64; i++) {
        // создаем новую мышь не на змее
        if (mouse.x == tailX[i] && mouse.y == tailY[i]) {
          mouse.x = random(0, 8);
          mouse.y = random(0, 8);
          i = 0;
        }
      }
      // раз мышь съели, то увеличиваем счет
      store.value++;
      // и воспроизводим звук
      tone(BUZZER_PIN, 440, 100);
    }
    // производим перемещение змеи по таймеру
    if (millis() -  timeOfMovement > movement) {
      // очищаем матрицы
      draw_clear ();
      // рисуем мышь
      draw_point (mouse.x, mouse.y, light.R);
      // рисуем змею
      snake_drow ();
      // проверяем, не вышел ли игрок за пределы поля
      if (snake.x < 0 || snake.x >= 8 || snake.y < 0 || snake.y >= 8) {
        // если вышел, то воспроизводим проигрыш
        game_over();
      }
      // проверяем не "съела" ли змея сама себя
      for (int i = 1; i <= store.value; i++) {
        if (snake.x == tailX[i] && snake.y == tailY[i]) {
          // если "съела", то воспроизводим проигрыш
          game_over();
        }
      }
      // обнуляем таймер
      timeOfMovement =  millis();
    }
  }
}
 
// функция приготовления к игре
void at_the_start() {
  // очищаем матрицы
  draw_clear ();
  // обнуляем значение счета и держим флаг счетчика опущенным
  store = { 0, false};
  // задаем направление движения змейки как состояние опущенным
  player = NOTOUCH;
  playerOld = NOTOUCH;
  // считываем движения джойстика
  player_move ('z');
  // задаем начальное значение координат головы змеи
  snake = {random(0, 8), random(0, 8), true, true};
  // задаем начальное значение координат головы мыши
  mouse = {random(0, 8), random(0, 8)};
  // рисуем на матрице положение мыши
  draw_point (mouse.x, mouse.y, light.R);
  delay (movement);
}
 
// функция окончания игры
void game_over() {
  // воспроизводим звук
  tone(BUZZER_PIN, 261, 200);
  // очищаем матрицы
  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 (movement);
}
 
// функция движения игрока
void player_move (char coordinate) {
  switch (coordinate) {
    case 'x': {
        int x;
        // считываем текущее значение джойстика по X
        x = analogRead(X);
        if (x < 350 && snake.flagX) {
          player = RIGHT;
          snake.flagX = false;
        }
        if (x > 850 && snake.flagX) {
          player = LEFT;
          snake.flagX = false;
        }
        if (350 < x && x < 850) {
          snake.flagX = true;
        }
        break;
      }
    case 'y': {
        int y;
        // считываем текущее значение джойстика по Y
        y = analogRead(Y);
        Serial.println(y);
        if (y < 350 && snake.flagY) {
          player = UP;
          snake.flagY = false;
        }
        if (y > 850 && snake.flagY) {
          player = DOWN;
          snake.flagY = false;
        }
        if (350 < y && y < 850) {
          snake.flagY = true;
        }
        break;
      }
    case 'z': {
        int z;
        // считываем текущее значение джойстика по Z
        z = digitalRead(Z);
        if (z == 1 && store.flag == false) {
          store.flag = true;
        }
        break;
      }
  }
}
 
// функция для отрисовки змеи
void snake_drow () {
  // отслеживаем куда переместилась голова змеи
  switch (player) {
    case RIGHT: {
        if (playerOld != LEFT) {
          snake.x++;
        }
        break;
      }
    case LEFT: {
        if (playerOld != RIGHT) {
          snake.x--;
        }
        break;
      }
    case UP: {
        if (playerOld != DOWN) {
          snake.y++;
        }
        break;
      }
    case DOWN: {
        if (playerOld != UP) {
          snake.y--;
        }
        break;
      }
  }
  playerOld = player;
  // создаем хвост
  tail_create ();
  // рисуем хвост
  for (int i = 0; i <= store.value; i++) {
    draw_point (tailX[i], tailY[i], light.G);
  }
}
 
// функция создания хвоста
void tail_create () {
  // создаем две пары переменных для промежуточного хранения значений массивов хвоста
  int bufferX = tailX[0];
  int bufferY = tailY[0];
  int bufferX2, bufferY2;
  // записываем значения "головы" в 0-вые элементы массивов хвоста
  tailX[0] = snake.x;
  tailY[0] = snake.y;
  // записываем хвост длинною в счет игрока, перемещая каждый элемент хвоста в следующий по счету элемент массива
  for (int i = 1; i <= store.value; i++) {
    bufferX2 = tailX[i];
    bufferY2 = tailY[i] ;
    tailX[i] = bufferX;
    tailY[i] = bufferY;
    bufferX = bufferX2;
    bufferY = bufferY2;
  }
}
 
// функция для перевода координаты в № матрици
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;
      }
    case 2: {
        return {y % 4 * 4 + x % 4};
        break;
      }
    case 3: {
        return {abs (y - 8) * 4 - x % 4 - 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-джойстик на IMU-сенсор и сможете управлять змейкой наклонами корпуса игры.
  • Добавьте Power Shield — игру можно будет взять в дорогу.
  • Вставьте устройство в корпус из #cтруктора. Slot Box придаст игре законченный вид.