Игра “Автотрек” на Arduino
Задача
- Сделать аналог игры “Автотрек” на Arduino и адресной светодиодной ленте
- Поддержка нескольких игроков
- Управление кнопкой
- Механика “поворотов”, в которых нельзя разгоняться
- Повороты генерируются случайным образом
Базовые уроки
Подключение
- Arduino и лента питаются от сетевого адаптера на 5V (есть в наборе)
- Кнопки подключаются к GND и любым цифровым пинам
- Можно подключить несколько кнопок
- Можно вынести кнопку на отдельную макетку для удобства
Библиотеки
- FastLED – можно установить через менеджер библиотек
Программа
Зададим константы настроек ленты (для удобства настройки):
#define LED_PIN 2 // пин ленты #define LED_NUM 120 // кол-во светодиодов #define LED_BR 250 // яркость ленты
Количество игроков соответствует количеству кнопок, поэтому просто сделаем массив пинов кнопок:
// пины кнопок по количеству игроков const byte pins[] = {3, 4};
Также вынесем некоторые игровые настройки, их комментарии указаны:
#define MAX_SPEED 15 // максимальная скорость #define MIN_SPEED 4 // макс. скорость в повороте #define TURN_ZONES 2 // количество поворотов #define TURN_MIN 10 // мин. длина поворота #define TURN_MAX 30 // макс. длина поворота #define WIN_SCORE 50 // победный счёт (кругов)
Подключим библиотеку ленты, объявим ленту:
#include "FastLED.h" CRGB leds[LED_NUM];
Нам нужно хранить начало и конец поворота, создадим двумерный массив:
int turns[TURN_ZONES][2];
Цвет секций поворота я выбрал красно-оранжевый, с пониженной яркостью. Запишем в отдельную переменную:
CRGB turnColor = CHSV(12, 255, 70);
Ну и для удобства сохраним в константу размер массива пинов кнопок, чтобы обращаться к ней как к количеству игроков:
const byte players = sizeof(pins);
Также нам понадобится набор переменных по числу игроков для хранения различных состояний в игре:
int pos[players]; // позиции машинок int spd[players]; // скорость машинок int score[players]; // счёт bool drag[players]; // флаг торможения
В блоке setup() настроим ленту, а также подтянем пины кнопок:
FastLED.addLeds<WS2812, LED_PIN, GRB>(leds, LED_NUM); FastLED.setBrightness(LED_BR); // все кнопки подтягиваем for (int p = 0; p < players; p++) pinMode(pins[p], INPUT_PULLUP);
Чтобы улучшить случайность генерации поворотов, зададим источник случайных чисел как сигнал с никуда не подключенного аналогового входа. Этот момент можно убрать, он необязательный:
randomSeed(analogRead(0));
Далее генерируем повороты. Чтобы они не пересекались, я создаю их внутри зон ленты, разделённой по количеству поворотов (назовём эти отрезки блоками). Вот картинка для лучшего понимания:
Сначала ищем начало поворота как случайное число от начала блока до (размер блока – макс. длина поворота). Затем ищем конец поворота как начало поворота + случайное число от мин. до макс. длины поворота из настроек:
int turnSize = LED_NUM / TURN_ZONES; // длина блока как кол-во ледов/кол-во поворотов for (int t = 0; t < TURN_ZONES; t++) { // для количества поворотов // ищем начало поворота внутри каждого блока turns[t][0] = random(t * turnSize, (t + 1) * turnSize - TURN_MAX); // ищем конец поворота, прибавив длину к началу turns[t][1] = turns[t][0] + random(TURN_MIN, TURN_MAX); }
После этого запускаем новую игру. Эта функция выглядит так:
void newGame() { // обнуляем счёт и скорости игроков for (int np = 0; np < players; np++) { pos[np] = spd[np] = score[np] = 0; drag[np] = 0; } }
В основном цикле loop()
у нас будет два таймера: один на 200мс (пересчёт скорости), второй на 10мс (обновление ленты):
void loop() { static uint32_t tmr, tmr2; // пересчёт скорости // таймер на 200 мс if (millis() - tmr2 >= 200) { tmr2 = millis(); // ... } // движение машинок // таймер на 10 мс if (millis() - tmr >= 10) { tmr = millis(); // ... } }
Пересчёт скорости: если кнопка нажата – скорость увеличивается, если не нажата – уменьшается. И ограничиваем снизу нулём, а сверху – текущим максимумом игрока: если игрок вошёл в поворот с нажатой кнопкой, он не сможет разогнаться выше MIN_SPEED:
for (int p = 0; p < players; p++) { // для всех игроков if (!digitalRead(pins[p])) spd[p] += 3; // если кнопка нажата - разгон else spd[p]--; // иначе - торможение // ограничиваем по макс. скорости или скорости в повороте spd[p] = constrain(spd[p], 0, drag[p] ? MIN_SPEED : MAX_SPEED); }
Движение машинок: для начала отрисуем зоны поворотов:
// выводим зоны поворотов for (int t = 0; t < TURN_ZONES; t++) { // для всех поворотов for (int s = turns[t][0]; s < turns[t][1]; s++) { // от начала до конца leds[ s] = turnColor; // красим пиксель } }
После этого идёт основное движение и проверки игры для всех игроков
for (int p = 0; p < players; p++) { // ... }
Двигаем по очереди машинку каждого игрока, прибавив скорость к позиции:
pos[p] += spd[p];
Если кнопка игрока нажата – проверяем, не находится ли игрок внутри поворота. Если находится – поднимаем для него флаг торможения. Если кнопка не нажата – сбрасываем флаг в любом случае.
if (!digitalRead(pins[p])) { // если кнопка нажата for (int t = 0; t < TURN_ZONES; t++) { // проверяем повороты // машинка внутри поворота if (pos[p] / 10 >= turns[t][0] && pos[p] / 10 < turns[t][1]) { drag[p] = 1; // флаг на ограничение скорости spd[p] = MIN_SPEED; // и ограничиваем скорость сразу } else drag[p] = 0; // кнопка не нажата - снимаем ограничение } } else drag[p] = 0; // кнопка не нажата - снимаем ограничение
Далее идёт проверка на окончание трассы и окончание раунда, если счётчик кругов превысил заданный победный счёт:
// проверяем конец трассы if (pos[p] >= (LED_NUM * 10)) { // лента кончилась, проехали круг pos[p] -= LED_NUM * 10; // вычитаем круг score[p]++; // счёт +1 // достигнут победный счёт if (score[p] >= WIN_SCORE) { // плавно закрашиваем ленту цветом победителя for (int led = 0; led < LED_NUM; led++) { leds[led].setHue(p * 255 / players); FastLED.show(); delay(30); } newGame(); return; // выходим на следующий цикл } }
Если раунд не закончился – выводим точку-“машинку” игрока его цветом. Цвет игрока считается следующим образом: номер игрока / количество игроков * 255, что позволяет автоматически раздать цвета по порядку “радуги” любому количеству игроков, чтобы они точно не повторялись:
leds[pos[p] / 10].setHue(p * 255 / players);
После цикла обновляем ленту:
FastLED.show();
Полный код программы выглядит так:
Возможные доработки
- Игроков можно описать классами для лучшей читаемости кода