Skip to contentSkip to main navigation Skip to footer

Игра “Автотрек” на Arduino

Задача


  • Сделать аналог игры “Автотрек” на Arduino и адресной светодиодной ленте
  • Поддержка нескольких игроков
  • Управление кнопкой
  • Механика “поворотов”, в которых нельзя разгоняться
  • Повороты генерируются случайным образом

Базовые уроки


Подключение


  • Arduino и лента питаются от сетевого адаптера на 5V (есть в наборе)
  • Кнопки подключаются к GND и любым цифровым пинам
    • Можно подключить несколько кнопок
    • Можно вынести кнопку на отдельную макетку для удобства

Примечание: не подключайте Arduino к компьютеру без внешнего питания 5V, так как лента потребляет большой ток!

Библиотеки


  • 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();

Полный код программы выглядит так:

Код программы
/*
  Игра на адресной светодиодной ленте "Гонки"
  - Подключи сколько угодно кнопок
  - Удержание кнопки - разгон
  - Отпустил - торможение
  - Разгон в зоне поворота приводит к ограничению скорости до конца поворота
*/

#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);  // размер массива == колво игроков

// ============ SETUP =============
void 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);
  }
  newGame();    // начать новую игру
}

// ========== ПЕРЕМЕННЫЕ ==========
int pos[players];       // позиции машинок
int spd[players];       // скорость машинок
int score[players];     // счёт
bool drag[players];     // флаг торможения

// ============= LOOP =============
void loop() {
  static uint32_t tmr, tmr2;
  // пересчёт скорости
  // таймер на 200 мс
  if (millis() - tmr2 >= 200) {
    tmr2 = millis();
    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);
    }
  }

  // движение машинок
  // таймер на 10 мс
  if (millis() - tmr >= 10) {
    tmr = millis();
    FastLED.clear();  // очищаем ленту

    // выводим зоны поворотов
    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;   // выходим на следующий цикл
        }
      }

      // если не выиграл - рисуем текущую точку цветом игрока
      leds[pos[p] / 10].setHue(p * 255 / players);

    }   // конец for players
    FastLED.show();
  } // конец таймера
}

void newGame() {
  // обнуляем счёт и скорости игроков
  for (int np = 0; np < players; np++) {
    pos[np] = spd[np] = score[np] = 0;
    drag[np] = 0;
  }
}

Возможные доработки


  • Игроков можно описать классами для лучшей читаемости кода

Видео


Полезный пример?

0 Комментариев

Нет комментариев

Оставить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *