Общие сведения
"Реактор" - это игра на скорость реакции сделанная на основе Arduino UNO/Piranha UNO с использованием библиотеки Arduino FreeRTOS. Цель игры - набрать 9 очков быстрее противника. Очки можно заработать нажав на кнопку, соответствующую цвету горящему на Trema-модуле NeoPixel.
Видео
Редактируется...
Нам понадобится
Модули
- 1x Arduino/Piranha UNO
- 1x Trema Shield
- 3x Trema модуль - Матрица 8x8 Flash-I2C
- 3x Кнопка аркадная 30мм, красная
- 3x Кнопка аркадная 30мм, синяя
- 3x Кнопка аркадная 30мм, белая
- 3x Плоский громкоговоритель
- 1x i2c hub
- 1x Trema-модуль Зуммер
- 1x Trema-модуль NeoPixel
Библиотеки
- iarduino_I2C_Matrix_8x8
- iarduino_NeoPixel
- Arduino_FreeRTOS - устанавливается из менеджера библиотек Arduino IDE. Подробнее...
Корпус
Подключение
Для удобства подключения мы воспользуемся Trema Shield для Arduino.
Устанавливаем Trema Shield на Piranha UNO
Подключаем модули
NeoPixel
Вывод модуля | Вывод Arduino |
---|---|
IN S | 10 |
IN V | Vcc |
IN G | GND |
Зуммер
Вывод модуля | Вывод Arduino |
---|---|
S | 11 |
V | Vcc |
G | GND |
Подключаем кнопки
рекомендуем припаять конденсатор номиналом 1uF на на выводы каждой кнопки
Игрок | Цвет | Вывод Arduino |
---|---|---|
1 | Синий | 2 |
1 | Белый | 3 |
1 | Красный | 4 |
2 | Синий | 5 |
2 | Белый | 6 |
2 | Красный | 7 |
3 | Синий | A0 |
3 | Белый | A1 |
3 | Красный | A2 |
Скетч проекта
На заметку: Скетч написан с использованием макросов PINC
и PIND
, которые переопределены как P_3_PORT_READ
и P_1_2_PORT_READ
с побитовым сдвигом. Для работы с другими микроконтроллерами (не UNO) их необходимо переопределить в соответствии с подключением кнопок и портами этих микроконтроллеров
/*☢*/ #include <iarduino_NeoPixel.h> #include <Arduino_FreeRTOS.h> #include <Wire.h> #include <iarduino_I2C_Matrix_8x8.h> // Определяем макросы чтения состояния выводов #define P_3_PORT_READ (PINC << 6); #define P_1_2_PORT_READ (PIND >> 2); // Определяем количество игроков #define PLAYERS 3 // Определяем цвета #define RED 0xFF0000 #define GREEN 0x00FF00 #define BLUE 0x0000FF #define WHITE 0xFFFFFF #define BLACK 0x000000 // Определяем макрос ожидания #define sleep(A) vTaskDelay(A/portTICK_PERIOD_MS) // Определяем временные интервалы в миллисекундах #define SECOND 1000 #define HALF 500 #define DEBOUNCE 50 #define FADE_DELAY 25 #define DISPLAY_DELAY 50 #define TIMEOUT 2000UL #define WIN_TIMER 1000UL #define GAMEOVER_TIMER 10000UL // Определяем битовые маски кнопок игроков #define BLUE_BUT_BITMASK 0b001 #define WHITE_BUT_BITMASK 0b010 #define RED_BUT_BITMASK 0b100 #define BITMASK 0b111 // Определяем побитовый сдвиг для второго и третьего игроков #define SHIFT_P2 3 #define SHIFT_P3 6 // Определяем частоты нот #define NOTE_A3 220 #define NOTE_C4 262 #define NOTE_G4 392 // Изображение для неправильного ответа uint8_t image_x[] { 0b10000001, 0b01000010, 0b00100100, 0b00011000, 0b00011000, 0b00100100, 0b01000010, 0b10000001 }; /* Константы */ // Количество кнопок constexpr uint8_t BUTTONS = 9; // Адреса матриц на шине I2C constexpr uint8_t P1_MATR_ADDR = 9; constexpr uint8_t P2_MATR_ADDR = 10; constexpr uint8_t P3_MATR_ADDR = 11; // Выводы NeoPixel и Зуммера constexpr uint8_t NEOPIN = 10; constexpr uint8_t NEONUM = 4; constexpr uint8_t SPKPIN = 11; /* Выводы кнопок игроков Эти константы используются только для функции begin() объекта кнопок. Функция чтения кнопок осуществляется через макросы P_3_PORT_READ и P_1_2_PORT_READ. При смене модели микропроцессора или подключения кнопок к другим портам их необходимо переопределить */ constexpr uint8_t P1_R_BUT = 2; constexpr uint8_t P1_W_BUT = 3; constexpr uint8_t P1_B_BUT = 4; constexpr uint8_t P2_R_BUT = 5; constexpr uint8_t P2_W_BUT = 6; constexpr uint8_t P2_B_BUT = 7; constexpr uint8_t P3_R_BUT = A0; constexpr uint8_t P3_W_BUT = A1; constexpr uint8_t P3_B_BUT = A2; // Переменные состояния игры и счёта uint8_t gamestate, p1_score, p2_score, p3_score; // Переменные для функции перелива цвета NeoPixel uint8_t fade; uint8_t spec; // Переменные блокировки кнопок игроков bool p1_stop, p2_stop, p3_stop; // Переменные первого нажатия кнопок bool p1_first_press, p2_first_press, p3_first_press; // Переменная случайного цвета uint8_t r_color; // Определения нового типа для цвета typedef uint32_t color; // Константы для идентификации цвета enum { red, blue, white }; // Константы идентификации состояния игры enum { // На старт start, // Ожидания выбора цвета running, // Цвет выбран stop, // Сброс нажатых кнопок reset_input, // Оценка правильности нажатых кнопок evaluate, // Нажата правильная кнопка win, // Игра окончена gameover }; // Константы идентификации игроков enum { player1, player2, player3 }; // Переменные таймеров unsigned long callMillis = 0; unsigned long startMillis = 0; // Объекты NeoPixel и матриц 8x8 iarduino_NeoPixel leds(NEOPIN, NEONUM); iarduino_I2C_Matrix_8x8 disp[PLAYERS] {P1_MATR_ADDR, P2_MATR_ADDR, P3_MATR_ADDR}; // Функция вычисления бОльшего счёта uint8_t getHighest() { uint8_t m = (uint8_t)max(max(p1_score, p2_score), p3_score); return m; } // Функция сброса состояния игры void reset() { gamestate = start; p1_score = p2_score = p3_score = 0; p1_stop = p2_stop = p3_stop = false; p1_first_press = p2_first_press = p3_first_press = false; fade = 0; spec = 0; r_color = 0; } // Определяем класс кнопок class reactor_buttons{ public: // Функция включения внутрисхемной подтяжки на всех кнопках void begin() { pinMode(P1_R_BUT, INPUT_PULLUP); pinMode(P1_W_BUT, INPUT_PULLUP); pinMode(P1_B_BUT, INPUT_PULLUP); pinMode(P2_W_BUT, INPUT_PULLUP); pinMode(P2_R_BUT, INPUT_PULLUP); pinMode(P2_B_BUT, INPUT_PULLUP); pinMode(P3_R_BUT, INPUT_PULLUP); pinMode(P3_W_BUT, INPUT_PULLUP); pinMode(P3_B_BUT, INPUT_PULLUP); } // Функция обновления состояния кнопок uint16_t state() { // Читаем кнопки третьего игрока, _b_state = P_3_PORT_READ; // читаем состояние кнопок первого и второго игрока, _b_state ^= P_1_2_PORT_READ; // выключаем ненужные биты и инвертируем состояние // (1 - нажата, 0 - не нажата) _b_state = ~_b_state & 0b0000000111111111; // возвращаем состояние всех кнопок в одной // шестнадцатибитной переменной. return _b_state; } // Возвращаем биты кнопок первого игрока uint8_t p1_state() { return _b_state & BITMASK; } // Возвращаем биты кнопок второго игрока uint8_t p2_state() { return (_b_state >> SHIFT_P2) & BITMASK; } // Возвращаем биты кнопок третьего игрока uint8_t p3_state() { return (_b_state >> SHIFT_P3) & BITMASK; } private: // Переменная состояния всех кнопок uint16_t _b_state; }; // Создаём объект кнопок reactor_buttons buttons; // Функция проверки кнопок void buttonTest() { uint8_t i = 0; // Пока не прошлись по всем кнопкам... while(i < BUTTONS) { // Пока не нажата ни одна кнопка, остановиться. while(!buttons.state()); // Если нажата кнопка соответствующая биту i... if (buttons.state() & 1 << i) i++; // вывести её номер на экран disp[0].print(i); } } void setup() { // Инициируем кнопки buttons.begin(); // Ждём одну секунду до завершения переходных процессов // связанных с подачей питания delay(1000); // Вызываем функцию сброса параметров игры reset(); // Инициируем матрицы 8x8 for (auto& i:disp) { i.begin(); i.bright(255); i.angle(0); } /* Если хотя бы одна кнопка нажата при старте скетча - входим в режим тестирования кнопок. */ if (buttons.state()) { buttonTest(); } // Инициируем модуль NeoPixel и устанавливаем отсутствие цвета leds.begin(); leds.setColor(NeoPixelAll, BLACK); leds.write(); // Создаём задачу для звука xTaskCreate( TaskSound, // Указатель на функцию "Sound", // Строка названия 64, // Размер стэка NULL, 0, // Приоритет 0 - 3 (0 - самый низкий, 3 - самый высокий) NULL ); // Создаём задачу для NeoPixel xTaskCreate(TaskLight, "Light", 64, NULL, 0, NULL); // Создаём задачу игры xTaskCreate(TaskGame, "Game", 128, NULL, 3, NULL); // Создаём задачу обработки матриц 8x8 xTaskCreate(TaskDisplays, "Displays", 256, NULL, 2, NULL); } void loop() { // При использовании FreeRTOS тело функции loop остаётся пустым } // Функция ожидания void wait() { // Если перелив цветов остановлен if (gamestate == stop) { // выбираем случайный цвет из трёх возможных r_color = random(3); // Устанавливаем это цвет на NeoPixel if (r_color == red) { setLed(RED); } else if (r_color == blue) { setLed(BLUE); } else if (r_color == white) { setLed(WHITE); } gamestate = evaluate; } // Если кто-то нажал правильную кнопку else if (gamestate == win) { // Ждём немного if (millis() - callMillis > WIN_TIMER) // и сбрасываем состояние игровых кнопок gamestate = reset_input; } // Если игра окончена else if (gamestate == gameover) { if (millis() - callMillis > GAMEOVER_TIMER) { // сбрасываем переменные игры, reset(); // устанавливаем состояния игры на старт gamestate == start; } } } // Функция зачисления очков void playerScore(uint8_t player) { /* Функция принимает идентификатор игрока и начисляет этому игроку очки. Если у одного из игроков 9 очков - функция завершает игру. */ uint8_t score; if (player1 == player) score = ++p1_score; else if (player2 == player) score = ++p2_score; else if (player3 == player) score = ++p3_score; gamestate = win; callMillis = millis(); if (getHighest() >= 9) { gamestate = gameover; } } // Функция обновления кнопок void readButtons(uint16_t& b_state, uint8_t& p1, uint8_t& p2, uint8_t& p3) { b_state = buttons.state(); p1 = buttons.p1_state(); p2 = buttons.p2_state(); p3 = buttons.p3_state(); if (gamestate != start && millis() - startMillis > SECOND) { if (p1 && p1_first_press) p1_stop = true; if (p1 && !p1_first_press) p1_first_press = true; if (p2 && p2_first_press) p2_stop = true; if (p2 && !p2_first_press) p2_first_press = true; if (p3 && p3_first_press) p3_stop = true; if (p3 && !p3_first_press) p3_first_press = true; } } // Функция проверки кнопок //(эту функцию хотелось бы оптимизировать, но c'est la vie) void checkButtons(uint16_t b_state, uint8_t p1, uint8_t p2, uint8_t p3) { // Если прошло немного времени - сбросить состояние кнопок if (millis() - callMillis > TIMEOUT) { gamestate = reset_input; } // Если нажата хоть одна кнопка if (b_state) { // Переборка случайного цвета switch (r_color) { case red: // Если зажата правильная кнопка if (p1 & RED_BUT_BITMASK // и только она && !(p1 ^ RED_BUT_BITMASK) // и игрок не проштрафился && !p1_stop) { // Увеличиваем счёт этого игрока на одно очко. playerScore(player1); return; } else if (p2 & RED_BUT_BITMASK && !(p2 ^ RED_BUT_BITMASK) && !p2_stop) { playerScore(player2); return; } else if (p3 & RED_BUT_BITMASK && !(p3 ^ RED_BUT_BITMASK) && !p3_stop) { playerScore(player3); return; } case white: if (p1 & WHITE_BUT_BITMASK && !(p1 ^ WHITE_BUT_BITMASK) && !p1_stop) { playerScore(player1); return; } else if (p2 & WHITE_BUT_BITMASK && !(p2 ^ WHITE_BUT_BITMASK) && !p2_stop) { playerScore(player2); return; } else if (p3 & WHITE_BUT_BITMASK && !(p3 ^ WHITE_BUT_BITMASK) && !p3_stop) { playerScore(player3); return; } break; case blue: if (p1 & BLUE_BUT_BITMASK && !(p1 ^ BLUE_BUT_BITMASK) && !p1_stop) { playerScore(player1); return; } else if (p2 & BLUE_BUT_BITMASK && !(p2 ^ BLUE_BUT_BITMASK) && !p2_stop) { playerScore(player2); return; } else if (p3 & BLUE_BUT_BITMASK && !(p3 ^ BLUE_BUT_BITMASK) && !p3_stop) { playerScore(player3); return; } break; default: break; } } } // Функция ожидания выборки цвета void run(uint16_t timer, unsigned long callMillis) { if (millis() - callMillis > timer * 1000) gamestate = stop; } // Функция-задача игры void TaskGame() { // Временной интервал uint16_t timer = 0; // Кнопки uint16_t b_state; uint8_t p1, p2, p3; for (;;) { // Если мы не ждём выборки цвета if (gamestate != running) // устанавливаем временной интервал для следующего раунда timer = random(2, 8); // Перебираем состояния игры switch (gamestate) { // Раунд выигран case win: // ждём wait(); break; // Если цвет определён case stop: // запоминаем время выбранного цвета callMillis = millis(); // ждём wait(); break; // Если игра в режиме определения цвета нажатой кнопки case evaluate: // Читаем состояние кнопок readButtons(b_state, p1, p2, p3); // Проверяем кнопки checkButtons(b_state, p1, p2, p3); break; // Если происходит сброс состояния кнопок case reset_input: // Сбрасываем штрафные флаги p1_stop = p2_stop = p3_stop = false; // Сбрасываем флаги первого нажатия за раунд p1_first_press = p2_first_press = p3_first_press = false; // Переключаем игру в режим ожидания выбора цвета gamestate = running; break; // Если игра ожидает старта case start: // Если нажата хоть одна кнопка if (buttons.state()) { // Переключаем игру в режим ожидания выбора цвета gamestate = running; // Записываем время переключения callMillis = millis(); // Записываем время старта startMillis = millis(); } break; // Если игра ожидает выборки цвета case running: // Читаем кнопки (для штрафов) readButtons(b_state, p1, p2, p3); // Вызываем функцию ожидания run(timer, callMillis); break; case gameover: // Если игра закончена - ждём wait(); break; } // Спим DEBOUNCE мс sleep(DEBOUNCE); } } // Функция-задача для матриц 8x8 void TaskDisplays() { for (;;) { // Если игра не ожидает старта if (gamestate != start) { // Если игрок не проштрафился if (!p1_stop) // Выводим его счёт disp[player1].print(p1_score); else // иначе выводим изображение штрафа disp[player1].drawImage(image_x); if (!p2_stop) disp[player2].print(p2_score); else disp[player2].drawImage(image_x); if (!p3_stop) disp[player3].print(p3_score); else disp[player3].drawImage(image_x); } // Если игра ожидает старта - выводим случайные изображения else { uint8_t r_image[8]; for (auto& i:r_image) i = random(256); for (auto& i:disp) { i.drawImage(r_image); } } // Спим DISPLAY_DELAY мс sleep(DISPLAY_DELAY); } } // Функция-задача для звука void TaskSound() { bool once = false; for (;;) { // Если происходит ожидание выбора цвета if (gamestate == running) { // Звук - мало ионизирующих частиц playSound(); // Сбрасываем флаг однократного проигрывания once = false; } // Если происходит оценка нажатых кнопок else if (gamestate == evaluate) // Звук - много ионизирующих частиц playEvalSound(); // Если раунд выигран else if (gamestate == win) // Звук - нажата правильная кнопка playWinSound(once); // Если игра окончена else if (gamestate == gameover) // Звук - игра окончена playGamerOverSound(once); // Иначе else // Выключаем звук stopSound(); } } // Функция имитации счётчика Гейгера: мало ионизирующих частиц void playSound() { tone(SPKPIN, 100, 10); sleep(random(30, 500)); } // Функция имитации счётчика Гейгера: много ионизирующих частиц void playEvalSound() { tone(SPKPIN, 100, 10); sleep(random(10, 70)); } // Функция проигрывания звука правильного ответа void playWinSound(bool& once) { if (!once) { for (int i = 0; i < 2; i++) { tone(SPKPIN, NOTE_A3 * (i + 1)); sleep(100); noTone(SPKPIN); sleep(50); } once = true; } } // Функция проигрывания звука окончания игры void playGamerOverSound(bool& once) { if (!once) { tone(SPKPIN, NOTE_C4); sleep(100); noTone(SPKPIN); sleep(100); tone(SPKPIN, NOTE_C4); sleep(50); noTone(SPKPIN); sleep(50); tone(SPKPIN, NOTE_C4); sleep(50); noTone(SPKPIN); sleep(50); tone(SPKPIN, NOTE_G4); sleep(200); noTone(SPKPIN); sleep(200); once = true; } } // Функция остановки проигрывания звука void stopSound() { noTone(SPKPIN); } // Функция-задача для NeoPixel void TaskLight() { for (;;) { // Если ожидание выборки цвета if (gamestate == running) // переливаем цвета runLed(); // Установка цвета при чтении кнопок (gamestate == evaluate) // происходит в функции wait() TODO: исправить это // Если игра ожидает старта else if (gamestate == start) // устанавливаем зелёный цвет setLed(GREEN); } } // Функция переливания цветов на NeoPixel void runLed() { fade++; uint8_t r, g, b; // Проходим по всем светодиодам for (uint16_t i=0; i<leds.count(); i++) { // Определяем положение очередного светодиода на смещённом спектре цветов spec = ((uint16_t)(i*256/leds.count()) + fade); if (spec<85) { b=0; r=spec*3; g=255-r; } else if (spec<170) { spec-=85; g=0; b=spec*3; r=255-b; } else { spec-=170; r=0; g=spec*3; b=255-g; } // Устанавливаем выбранный цвет для очередного светодиода leds.setColor(i, r,g,b); } leds.write(); sleep(FADE_DELAY); } // Функция установки цвета на NeoPixel void setLed(color c) { leds.setColor(NeoPixelAll, c); leds.write(); }
Обсуждение