КОРЗИНА
магазина
8 (499) 500-14-56 | ПН. - ПТ. 12:00-18:00
ЛЕСНОРЯДСКИЙ ПЕРЕУЛОК, 18С2, БЦ "ДМ-ПРЕСС"

Игра для 3-х игроков "Реактор" на Arduino UNO при помощи операционной системы FreeRTOS

Общие сведения

"Реактор" - это игра на скорость реакции сделанная на основе Arduino UNO/Piranha UNO с использованием библиотеки Arduino FreeRTOS. Цель игры - набрать 9 очков быстрее противника. Очки можно заработать нажав на кнопку, соответствующую цвету горящему на Trema-модуле NeoPixel.

Видео

Редактируется...

Нам понадобится

Модули

Библиотеки

Корпус

Подключение

Для удобства подключения мы воспользуемся 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) их необходимо переопределить в соответствии с подключением кнопок и портами этих микроконтроллеров

Для корректной работы I2C матриц 8x8 необходимо установить им адреса. Подробнее...
/*☢*/
#include <iarduino_NeoPixel.h>
#include <Arduino_FreeRTOS.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();
}

Ссылки




Обсуждение

Гарантии и возврат Используя сайт Вы соглашаетесь с условями