Skip to contentSkip to main navigation Skip to footer

Игра “1D Pong” на Ардуино

Задача


  • Сделать одномерную версию классической игры “Понг” для двух игроков

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


Подключение


  • Вторую кнопку удобнее расположить на отдельной макетке
  • Остальное как в базовых уроках

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

Библиотеки


  • FastLED – можно установить через менеджер библиотек

Для работы с кнопкой используется “мини библиотека” кнопки, которая просто обрабатывает клики с гашением дребезга:

Кнопка
// мини класс кнопки
#define BTN_DEB 50    // дебаунс, мс
struct Button {
  public:
    Button (byte pin) {
      _pin = pin;
      pinMode(_pin, INPUT_PULLUP);
    }
    bool click() {
      bool btnState = digitalRead(_pin);
      if (!btnState && !_flag && millis() - _tmr >= BTN_DEB) {
        _flag = true;
        _tmr = millis();
        return true;
      }
      if (btnState && _flag && millis() - _tmr >= BTN_DEB) {
        _flag = false;
        _tmr = millis();
      }
      return false;
    }
    uint32_t _tmr;
    byte _pin;
    bool _flag;
};

Программа


Зададим константами все настройки и пины проекта:

#define LED_PIN 2     // пин ленты
#define LED_NUM 90   // кол-во светодиодов
#define LED_BR 250     // яркость ленты

#define B1_PIN 3      // пин кнопки 1
#define B2_PIN 4      // пин кнопки 2
#define BUZZ_PIN 5    // пин пищалки

#define ZONE_SIZE 10  // размер зоны
#define MIN_SPEED 5   // минимальная скорость
#define MAX_SPEED 20  // максимальная скорость
#define WIN_SCORE 5   // победный счёт

Подключим библиотеку ленты и создадим ленту:

#include "FastLED.h"
CRGB leds[LED_NUM];

Также создадим две кнопки (структура описана выше в главе Библиотеки):

Button b1(B1_PIN);
Button b2(B2_PIN);

В блоке setup() настроим ленту, пин пищалки, а также запустим новую игру:

void setup() {
  FastLED.addLeds<WS2812, LED_PIN, GRB>(leds, LED_NUM);
  FastLED.setBrightness(LED_BR);
  pinMode(BUZZ_PIN, OUTPUT);
  newGame();
}

Функция новой игры мигает зонами игроков тремя цветами и пищит, после чего задаёт начальные условия:

void newGame() {
  blinkTone(CRGB::Red, CRGB::Red, 300, 300);
  blinkTone(CRGB::Yellow, CRGB::Yellow, 300, 300);
  blinkTone(CRGB::Green, CRGB::Green, 600, 300);
  fillZones(CRGB::Green, CRGB::Green);
  FastLED.show();
  randomSeed(millis()); // делаем случайные числа более случайными
  newRound();
}

Где newRound() и задаёт стартовые значения скорости и позиции шарика:

void newRound() {
  spd = random(0, 2) ? MIN_SPEED : -MIN_SPEED;  // случайное направление
  pos = (LED_NUM * 10) / 2;     // в центр ленты
}

Выше использовалась функция blinkTone(), давайте посмотрим что у неё внутри:

// (цвет1, цвет2, частота, время задержки)
void blinkTone(CRGB color1, CRGB color2, int freq, int del) {
  fillZones(color1, color2);    // залить зоны
  FastLED.show();               // показать
  tone(BUZZ_PIN, freq);         // пищать
  delay(del);                   // ждём
  noTone(BUZZ_PIN);             // не пищать
  fillZones(0, 0);              // выключить зоны
  FastLED.show();               // показать
  delay(del);                   // ждать
}

Данная функция моргает и пищит с заданной частотой и временем. Моргает здесь функция fillZones(), которая просто красит начальный и конечный отрезки ленты указанными цветами. Она выглядит так:

void fillZones(CRGB color1, CRGB color2) {
  // заливаем концы ленты переданными цветами
  for (int i = 0; i < ZONE_SIZE; i++) {
    leds[i] = color1;
    leds[LED_NUM - i - 1] = color2;
  }
}

В основном цикле программы у нас вызываются две функции: опрос кнопок и процесс самой игры:

void loop() {
  poolButtons();
  gameRoutine();
}

Опрос кнопок – проверяем клик. Если игрок кликнул вне своей зоны – проиграл раунд. Кликнул внутри зоны – мяч отбивается и происходит пересчёт скорости: чем ближе к краю ленты, тем сильнее отскок.

void poolButtons() {
  if (b1.click()) {                             // произошёл клик игрока 1
    if (pos >= ZONE_SIZE * 10) gameOver(0);     // мячик вне зоны 1 игрока - проиграл
    else {                                      // мячик в зоне - отбил
      tone(BUZZ_PIN, 1200, 60);
      spd = map(pos, ZONE_SIZE * 10, 0, MIN_SPEED, MAX_SPEED); // меняем скорость
    }
  }

  // аналогично для игрока 2
  if (b2.click()) {
    if (pos < (LED_NUM - ZONE_SIZE) * 10) gameOver(1);
    else {
      tone(BUZZ_PIN, 1200, 60);
      spd = map(pos, (LED_NUM - ZONE_SIZE) * 10, LED_NUM * 10, -MIN_SPEED, -MAX_SPEED);
    }
  }
}

Тут же используется функция gameOver(), которая сигнализирует о проигрыше раунда у конкретного игрока:

// (номер игрока, 0 или 1)
void gameOver(byte player) {
  newRound();    // новый раунд

  if (player == 0) {
    score2++;
    if (score2 == WIN_SCORE) {  // победил игрок 2
      score1 = score2 = 0;      // обнуляем счёт
      // победный бип бип бип игрока 2
      blinkTone(CRGB::Black, CRGB::Green, 600, 200);
      blinkTone(CRGB::Black, CRGB::Green, 600, 200);
      blinkTone(CRGB::Black, CRGB::Green, 600, 200);
      delay(500);
      newGame();  // новая игра
    } else blinkTone(CRGB::Red, CRGB::Green, 200, 500);   // красный бииип игрока 1
  } else {
    score1++;
    if (score1 == WIN_SCORE) {  // победил игрок 1
      score1 = score2 = 0;
      blinkTone(CRGB::Green, CRGB::Black, 600, 200);
      blinkTone(CRGB::Green, CRGB::Black, 600, 200);
      blinkTone(CRGB::Green, CRGB::Black, 600, 200);
      delay(500);
      newGame();  // новая игра
    } else blinkTone(CRGB::Green, CRGB::Red, 200, 500);
  }
}

Функция реализована не очень красиво, можно сократить её вдвое. Это ваше домашнее задание =)

Процесс игры – тут нас встречает таймер на 10мс, в котором происходит движение “шарика” и его отрисовка:

void gameRoutine() {
  if (millis() - tmr >= 10) {   // каждые 10 мс
    tmr = millis();
    // ...
  }
}

Внутри таймера двигаем шарик и проверяем, не вылетел ли он за ленту:

pos += spd;     // двигаем мячик

if (pos < 0) {  // вылетел слева
  gameOver(0);  // игрок 1 проиграл
  return;       // выходим
}

if (pos >= LED_NUM * 10) {  // вылетел справа
  gameOver(1);              // игрок 2 проиграл
  return;                   // выходим
}

Если вылетел – выходим из функции, чтобы не отрисовывать шарик в несуществующем месте ленты!

Дальше очищаем ленту, заново рисуем зоны и поверх – текущею позицию шарика. Координаты у нас десятикратные, поэтому делим на 10:

FastLED.clear();
fillZones(CRGB::Green, CRGB::Green);  // показываем зоны
leds[pos / 10] = CRGB::Cyan;          // рисуем мячик
FastLED.show();
Полный код программы
/*
  Игра на адресной светодиодной ленте "Понг"
  - Нажимай кнопку, когда шарик попал в твою зону
  - Чем ближе к краю ленты, тем сильнее будет отскок
  - Нажал не в своей зоне - проиграл
  - Пропустил - проиграл
*/

#define LED_PIN 2     // пин ленты
#define LED_NUM 90   // кол-во светодиодов
#define LED_BR 250     // яркость ленты

#define B1_PIN 3      // пин кнопки 1
#define B2_PIN 4      // пин кнопки 2
#define BUZZ_PIN 5    // пин пищалки

#define ZONE_SIZE 10  // размер зоны
#define MIN_SPEED 5   // минимальная скорость
#define MAX_SPEED 20  // максимальная скорость
#define WIN_SCORE 5   // победный счёт

// =============================================
#include "FastLED.h"
CRGB leds[LED_NUM];

// ========== КНОПКА ==========
// мини класс кнопки
#define BTN_DEB 50    // дебаунс, мс
struct Button {
  public:
    Button (byte pin) {
      _pin = pin;
      pinMode(_pin, INPUT_PULLUP);
    }
    bool click() {
      bool btnState = digitalRead(_pin);
      if (!btnState && !_flag && millis() - _tmr >= BTN_DEB) {
        _flag = true;
        _tmr = millis();
        return true;
      }
      if (btnState && _flag && millis() - _tmr >= BTN_DEB) {
        _flag = false;
        _tmr = millis();
      }
      return false;
    }
    uint32_t _tmr;
    byte _pin;
    bool _flag;
};

Button b1(B1_PIN);
Button b2(B2_PIN);

// ============ SETUP =============
void setup() {
  FastLED.addLeds<WS2812, LED_PIN, GRB>(leds, LED_NUM);
  FastLED.setBrightness(LED_BR);
  pinMode(BUZZ_PIN, OUTPUT);
  newGame();
}

// ========== ПЕРЕМЕННЫЕ ==========
int pos = 0;
int spd;
byte score1 = 0, score2 = 0;
uint32_t tmr;

// ============= LOOP =============
void loop() {
  poolButtons();
  gameRoutine();
}

// ========= ОПРОС КНОПОК =========
void poolButtons() {
  if (b1.click()) {                             // произошёл клик игрока 1
    if (pos >= ZONE_SIZE * 10) gameOver(0);     // мячик вне зоны 1 игрока - проиграл
    else {                                      // мячик в зоне - отбил
      tone(BUZZ_PIN, 1200, 60);
      spd = map(pos, ZONE_SIZE * 10, 0, MIN_SPEED, MAX_SPEED); // меняем скорость
    }
  }

  // аналогично для игрока 2
  if (b2.click()) {
    if (pos < (LED_NUM - ZONE_SIZE) * 10) gameOver(1);
    else {
      tone(BUZZ_PIN, 1200, 60);
      spd = map(pos, (LED_NUM - ZONE_SIZE) * 10, LED_NUM * 10, -MIN_SPEED, -MAX_SPEED);
    }
  }
}

// ========= ЗАЛИВКА ЗОН =========
void fillZones(CRGB color1, CRGB color2) {
  // заливаем концы ленты переданными цветами
  for (int i = 0; i < ZONE_SIZE; i++) {
    leds[i] = color1;
    leds[LED_NUM - i - 1] = color2;
  }
}

// ========= МИГАТЬ И ПИЩАТЬ =========
// (цвет1, цвет2, частота, время задержки)
void blinkTone(CRGB color1, CRGB color2, int freq, int del) {
  fillZones(color1, color2);    // залить зоны
  FastLED.show();               // показать
  tone(BUZZ_PIN, freq);         // пищать
  delay(del);                   // ждём
  noTone(BUZZ_PIN);             // не пищать
  fillZones(0, 0);              // выключить зоны
  FastLED.show();               // показать
  delay(del);                   // ждать
}

// =========== ПРОИГРЫШ ===========
// (номер игрока, 0 или 1)
void gameOver(byte player) {
  newRound();    // новый раунд

  if (player == 0) {
    score2++;
    if (score2 == WIN_SCORE) {  // победил игрок 2
      score1 = score2 = 0;      // обнуляем счёт
      // победный бип бип бип игрока 2
      blinkTone(CRGB::Black, CRGB::Green, 600, 200);
      blinkTone(CRGB::Black, CRGB::Green, 600, 200);
      blinkTone(CRGB::Black, CRGB::Green, 600, 200);
      delay(500);
      newGame();  // новая игра
    } else blinkTone(CRGB::Red, CRGB::Green, 200, 500);   // красный бииип игрока 1
  } else {
    score1++;
    if (score1 == WIN_SCORE) {  // победил игрок 1
      score1 = score2 = 0;
      blinkTone(CRGB::Green, CRGB::Black, 600, 200);
      blinkTone(CRGB::Green, CRGB::Black, 600, 200);
      blinkTone(CRGB::Green, CRGB::Black, 600, 200);
      delay(500);
      newGame();  // новая игра
    } else blinkTone(CRGB::Green, CRGB::Red, 200, 500);
  }
}

// ============== НОВАЯ ИГРА ==============
void newGame() {
  blinkTone(CRGB::Red, CRGB::Red, 300, 300);
  blinkTone(CRGB::Yellow, CRGB::Yellow, 300, 300);
  blinkTone(CRGB::Green, CRGB::Green, 600, 300);
  fillZones(CRGB::Green, CRGB::Green);
  FastLED.show();
  randomSeed(millis()); // делаем случайные числа более случайными
  newRound();
}

// ============== НОВЫЙ РАУНД ==============
void newRound() {
  spd = random(0, 2) ? MIN_SPEED : -MIN_SPEED;  // случайное направление
  pos = (LED_NUM * 10) / 2;     // в центр ленты
}

// ============== ИГРА ==============
void gameRoutine() {
  if (millis() - tmr >= 10) {   // каждые 10 мс
    tmr = millis();
    pos += spd;     // двигаем мячик

    if (pos < 0) {  // вылетел слева
      gameOver(0);  // игрок 1 проиграл
      return;       // выходим
    }

    if (pos >= LED_NUM * 10) {  // вылетел справа
      gameOver(1);              // игрок 2 проиграл
      return;                   // выходим
    }

    FastLED.clear();
    fillZones(CRGB::Green, CRGB::Green);  // показываем зоны
    leds[pos / 10] = CRGB::Cyan;          // рисуем мячик
    FastLED.show();
  }
}

Видео


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

Похожие примеры
14 Комментариев
  • А можно получить схему “не на макетке”? Упрощённую, чтобы было легче понять, что к чему паять)

  • А есть понимание надолго ли хватит аккумуляторов 3.7V если их туда поставить? Это желание избавиться от проводов.

    • У меня от лития работает уже пятые сутки( примерно час игры в день) 18650 3000махов

  • Кнопка автоматически нажимается даже если я подниму голый контакт к цифровому входу, подскажите почему так происходит?проблема точно не в альдруино,я на обеих попробовал

  • Спасибо за проект, детей теперь можно выманить из-за телефона🔥😅👍

  • добрый день. собрал, залил программу, за исключением ленту использовал 12V, ардуино нано подключил VIN и GND? Ардуинка сгорела, можете подсказать почему?

  • добрый день.
    как зделат штобе мячик минал цвет (на рандомный) после каждого отбития

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

Ваш адрес email не будет опубликован.