Собираем «Змейку» на Arduino
Проекты на Arduino Uno и Slot Shield
Змейка, питон, удав, червяк, snake — как только не называли легендарную игру. Впервые она появилась на экране игрового автомата ещё в 1977 году. С тех пор она была портирована на все мыслимые платформы. Мы перенесли Змейку на Arduino.
Игровым полем станут четыре RGB матрицы. Вместе они соберутся в экран из дадут экран 64 цветных пикселей. Мозг проекта — оригинальная Arduino Uno. Движением змейки управляет 3D-джойстик. Голосом игры станет простая пьезопищалка.
- Язык программирования: Arduino (C++)
Что потребуется
Полный сет компонентов проекта. В сет входят:
Видеоинструкция
Как собрать
Установите 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 придаст игре законченный вид.