DIY аркадная игра Whac-A-Mole (кнопочная версия) — проект на Arduino
Компоненты и материалы
| Arduino Mega 2560 | ×1 |
| Adafruit 1.4" 4-разрядный 7-сегментный дисплей с I2C-рюкзаком | ×2 |
| Adafruit Большая аркадная кнопка с LED (60 мм) | ×5 |
| Adafruit Audio FX Board 16 МБ | ×1 |
| Adafruit 2.5W усилитель | ×2 |
| Динамик 3 Вт, 4 Ом | ×2 |
| Adafruit Вкл/Выкл кнопка с LED | ×1 |
| Adafruit Потенциометр 10 кОм | ×2 |
| Adafruit Ручка потенциометра | ×2 |
| Провода | ×1 |
| Контрубиты M3 + гайки | ×1 |
| Контрубиты M4 + гайки | ×1 |
| Все 3D-печатные детали | ×1 |
| Все лазерно-вырезанные MDF-детали | ×1 |
| Кастомный шилд для Arduino Mega | ×1 |
| Stackable headers для Arduino Mega (для зазора) | ×1 |
| Adafruit DC-DC понижающий преобразователь 5В 3А | ×1 |
| БП 12В | ×1 |
| Разъём питания (female) | ×1 |
| USB-B панельный разъём | ×1 |
Необходимые инструменты и станки
- 3D-принтер
- Отвёртка
- Паяльник
Приложения и онлайн-сервисы
- Arduino IDE
- Сервис лазерной резки MDF
О проекте
Это кастомная игра Whac-A-Mole. Пока запрограммирована базовая игра, но возможно создание обновлений в будущем.
Ключевые особенности
- 5 кнопок для игры. У каждой — свой яркий LED. LED управляются ШИМ (используется в последовательности game over).
- 10 выбираемых уровней. Уровень 1 (лёгкий) — 10 (безумный). В игре нужно нажать правильную кнопку за определённое время. Длительность варьируется внутри уровня.
- (Псевдо)случайный выбор кнопки. Следующая кнопка всегда другая.
- 2 LED-дисплея. Один — текущий счёт / обратный отсчёт / выбор уровня, второй — рекорд для выбранного уровня.
- Цифровая громкость.
- Хранение рекорда.
- Звуковые эффекты. В разных состояниях игры звуковые циклы проигрываются через встроенные динамики.
- Лазерно-вырезанный корпус.
- Много 3D-печатных деталей.
- Кастомный шилд для Arduino Mega.
Корпус
Начните со сборки корпуса.


Установите 3D-печатные крепежные кронштейны на верхнюю крышку.

При желании покрасьте корпус.
Далее установите крепления LED-дисплеев. Не забудьте положить болты под крепления, иначе вертикальные рейки не зафиксируются.

Соберите Arduino-шилд. Использовал дополнительный комплект stackable headers для зазора между шилдом и Arduino.

Соберите заднюю панель управления.

Соберите верхнюю крышку (кнопки и LED-панели).




Задняя панель включает два потенциометра: громкость и выбор уровня.


Заключение
Готово!
На левом дисплее — выбранный уровень (1 — легко, 10 — безумно). На правом — рекорд для этого уровня. Во время игры левый показывает текущий счёт. Нажмите зелёную кнопку для старта. После обратного отсчёта игра начнётся.

Код
- Whac_A_Mole_v1.0.0
/* Whac_A_Mole_v1.0.0 By WT040 */
#include "Wire.h"
#include "Adafruit_LEDBackpack.h"
#include "Adafruit_GFX.h"
Adafruit_7segment matrix1 = Adafruit_7segment();
Adafruit_7segment matrix2 = Adafruit_7segment();
#define debounceTime 10
#define potmeterVolume 0
#define potmeterGameLevel 1
#define ampPower 32
#define btn1Pin 22
#define btn2Pin 24
#define btn3Pin 26
#define btn4Pin 28
#define btn5Pin 30
byte btnArray[] = { btn1Pin, btn2Pin, btn3Pin, btn4Pin, btn5Pin };
#define btn1 0
#define btn2 1
#define btn3 2
#define btn4 3
#define btn5 4
#define btnLed1Pin 9
#define btnLed2Pin 10
#define btnLed3Pin 11
#define btnLed4Pin 12
#define btnLed5Pin 13
byte btnLedArray[] = { btnLed1Pin, btnLed2Pin, btnLed3Pin, btnLed4Pin, btnLed5Pin };
#define btnLed1 0
#define btnLed2 1
#define btnLed3 2
#define btnLed4 3
#define btnLed5 4
#define fx0TriggerPin 29
#define fx1TriggerPin 31
#define fx2TriggerPin 33
#define fx3TriggerPin 35
#define fx4TriggerPin 37
#define fx5TriggerPin 39
#define fx6TriggerPin 41
#define fx7TriggerPin 43
#define fx8TriggerPin 45
#define fx9TriggerPin 47
#define fx10TriggerPin 49
#define fxVolUpPin 51
#define fxVolDnPin 53
byte fxTriggerArray[] = { fx0TriggerPin, fx1TriggerPin, fx2TriggerPin, fx3TriggerPin, fx4TriggerPin, fx5TriggerPin, fx6TriggerPin, fx7TriggerPin, fx8TriggerPin, fx9TriggerPin, fx10TriggerPin };
byte fxVolumeArray[] = { fxVolUpPin, fxVolDnPin };
#define fx0Trigger 0
#define fx1Trigger 1
#define fx2Trigger 2
#define fx3Trigger 3
#define fx4Trigger 4
#define fx5Trigger 5
#define fx6Trigger 6
#define fx7Trigger 7
#define fx8Trigger 8
#define fx9Trigger 9
#define fx10Trigger 10
#define fxVolUp 0
#define fxVolDn 1
signed int currentPlayingSound = -1;
byte btnPressed[5], ledState[5];
unsigned long previousMillisLedBlink = 0;
#define intervalLedBlink 750
unsigned long previousMillisGameStarting = 0;
#define intervalCountDown 1000
byte gameStartCountDown;
unsigned long previousMillisGameStep = 0;
int gameStepLength = 0;
int gameStepBtn = 0;
int volume = 0;
int actualVolume = 35;
int valPotmeterGameLevel = 0;
int gameLevel;
int currentScore;
int highScore;
int highScoreAddr = 1;
byte activeBtn;
bool debug;
byte wackAMoleState;
bool SystemInitialized = false;
#define stateUnknown 0
#define waitForStart 1
#define starting 2
#define running 3
#define gameOver 4
#define stopped 5
void setup() {
Serial.begin(9600);
pinMode(ampPower, OUTPUT);
digitalWrite(ampPower, LOW);
for (int Pin = 0; Pin < 5; Pin++) pinMode(btnArray[Pin], INPUT);
for (int Pin = 0; Pin < 5; Pin++) pinMode(btnLedArray[Pin], OUTPUT);
for (int Pin = 0; Pin < 11; Pin++) { pinMode(fxTriggerArray[Pin], OUTPUT); digitalWrite(fxTriggerArray[Pin], HIGH); }
for (int Pin = 0; Pin < 2; Pin++) { pinMode(fxVolumeArray[Pin], OUTPUT); digitalWrite(fxVolumeArray[Pin], HIGH); }
matrix1.begin(0x70);
matrix2.begin(0x71);
while (!eeprom_is_ready());
cli();
highScore = eeprom_read_word((uint16_t*)(gameLevel * 2));
sei();
matrix1.print(0); matrix1.writeDisplay();
matrix2.print(0); matrix2.writeDisplay();
initializeSystem();
}
void loop() {
checkButtonInputs();
readVolumePotmeter();
if (wackAMoleState == waitForStart) {
playSound(fx0Trigger);
if (btnPressed[btn4]) wackAMoleState = starting;
readGameLevelPotmeter();
ledBlinkStart();
}
if (wackAMoleState == starting) startGame();
if (wackAMoleState == running) {
playSound(fx1Trigger);
checkNextGameStep();
}
}
void checkNextGameStep() {
unsigned long currentMillisGameStep = millis();
int amountOfButtonPresses = 0;
for (int btn = 0; btn < 5; btn++) if (btnPressed[btn]) amountOfButtonPresses++;
if (previousMillisGameStep == 0) {
previousMillisGameStep = currentMillisGameStep;
digitalWrite(btnLedArray[btnLed4], LOW);
randomSeed(analogRead(2));
gameStepBtn = random(0, 5);
gameStepLength = random(3000 / gameLevel, 5000 / gameLevel);
digitalWrite(btnLedArray[gameStepBtn], HIGH);
}
if ((currentMillisGameStep - previousMillisGameStep >= gameStepLength) || (amountOfButtonPresses > 1) || (amountOfButtonPresses == 1 && !btnPressed[gameStepBtn])) {
playSound(fx2Trigger);
wackAMoleState = gameOver;
previousMillisGameStep = 0;
for (int counter = 0; counter < 5; counter++) analogWrite(btnLedArray[counter], 255);
for (int brightness = 255; brightness > 0; brightness--) {
analogWrite(btnLedArray[btnLed1], brightness);
analogWrite(btnLedArray[btnLed2], brightness);
analogWrite(btnLedArray[btnLed3], brightness);
analogWrite(btnLedArray[btnLed4], brightness);
analogWrite(btnLedArray[btnLed5], brightness);
delay(10);
}
for (int i = 0; i < 5; i++) analogWrite(btnLedArray[i], 0);
Serial.println("Game Over");
if (currentScore > highScore) {
while (!eeprom_is_ready());
cli();
eeprom_write_word((uint16_t*)(gameLevel * 2), currentScore);
sei();
}
int counter = 0;
do { delay(1); checkButtonInputs(); counter++; } while ((counter < 1000) && !btnPressed[btn1] && !btnPressed[btn2] && !btnPressed[btn3] && !btnPressed[btn4] && !btnPressed[btn5]);
matrix1.print(0); matrix1.writeDisplay();
wackAMoleState = waitForStart;
} else if (btnPressed[gameStepBtn] && (amountOfButtonPresses == 1)) {
digitalWrite(btnLedArray[gameStepBtn], LOW);
int counter = 0;
do { delay(1); checkButtonInputs(); counter++; } while ((counter < 1000) && btnPressed[gameStepBtn]);
previousMillisGameStep = currentMillisGameStep;
int tempStepBtn = 0;
do { tempStepBtn = random(0, 5); gameStepLength = random(3000 / gameLevel, 5000 / gameLevel); } while (gameStepBtn == tempStepBtn);
gameStepBtn = tempStepBtn;
digitalWrite(btnLedArray[gameStepBtn], HIGH);
currentScore++;
matrix1.print(currentScore); matrix1.writeDisplay();
if (currentScore > highScore) { matrix2.print(currentScore); matrix2.writeDisplay(); }
}
}
void ledBlinkStart() {
unsigned long currentMillisLedBlink = millis();
if (currentMillisLedBlink - previousMillisLedBlink >= intervalLedBlink) {
previousMillisLedBlink = currentMillisLedBlink;
ledState[0] = (ledState[0] == LOW) ? HIGH : LOW;
digitalWrite(btnLedArray[btnLed4], ledState[0]);
}
}
void startGame() {
unsigned long currentMillisGameStarting = millis();
digitalWrite(btnLedArray[btnLed4], 0);
currentScore = 0;
if (gameStartCountDown == 0) {
playSound(fx3Trigger);
previousMillisGameStarting = millis();
gameStartCountDown = 4;
delay(300);
matrix1.print(gameStartCountDown, DEC); matrix1.writeDisplay();
}
if (currentMillisGameStarting - previousMillisGameStarting >= intervalCountDown) {
previousMillisGameStarting = currentMillisGameStarting;
if (gameStartCountDown > 0) {
gameStartCountDown--;
matrix1.print(gameStartCountDown, DEC); matrix1.writeDisplay();
if (gameStartCountDown == 0) wackAMoleState = running;
}
}
}
void initializeSystem() {
digitalWrite(btnLedArray[btnLed1], HIGH); delay(75);
digitalWrite(btnLedArray[btnLed2], HIGH); delay(75);
digitalWrite(btnLedArray[btnLed3], HIGH); delay(75);
digitalWrite(btnLedArray[btnLed4], HIGH); delay(75);
digitalWrite(btnLedArray[btnLed5], HIGH); delay(1000);
digitalWrite(btnLedArray[btnLed5], LOW); delay(75);
digitalWrite(btnLedArray[btnLed4], LOW); delay(75);
digitalWrite(btnLedArray[btnLed3], LOW); delay(75);
digitalWrite(btnLedArray[btnLed2], LOW); delay(75);
digitalWrite(btnLedArray[btnLed1], LOW); delay(1000);
debug = true;
wackAMoleState = waitForStart;
initVolume();
if (actualVolume > 0) enableAmpPower(true);
}
void checkButtonInputs() {
static byte previousstate[5], currentstate[5];
static long lasttime;
byte index;
if ((lasttime + debounceTime) > millis()) return;
lasttime = millis();
for (index = 0; index < 5; index++) {
currentstate[index] = digitalRead(btnArray[index]);
if (currentstate[index] != btnPressed[index]) {
if ((btnPressed[index] == HIGH) && (currentstate[index] == LOW)) {
if (debug) { Serial.print("button released: "); Serial.println(index + 1); }
btnPressed[index] = 0;
}
if ((btnPressed[index] == LOW) && (currentstate[index] == HIGH)) {
if (debug) { Serial.print("button pressed: "); Serial.println(index + 1); }
btnPressed[index] = 1;
}
}
}
}
void initVolume() {
valPotmeterVolume = (analogRead(potmeterVolume) / 30);
if (actualVolume > valPotmeterVolume) {
do { digitalWrite(fxVolumeArray[fxVolDn], LOW); actualVolume--; delay(20); digitalWrite(fxVolumeArray[fxVolDn], HIGH); delay(20); } while (actualVolume > valPotmeterVolume);
} else if (actualVolume < valPotmeterVolume) {
do { digitalWrite(fxVolumeArray[fxVolUp], LOW); actualVolume++; delay(20); digitalWrite(fxVolumeArray[fxVolUp], HIGH); delay(20); } while (actualVolume < valPotmeterVolume);
}
}
void readVolumePotmeter() {
valPotmeterVolume = (analogRead(potmeterVolume) / 30);
if (actualVolume > valPotmeterVolume) {
digitalWrite(fxVolumeArray[fxVolDn], LOW);
actualVolume--;
delay(40);
digitalWrite(fxVolumeArray[fxVolDn], HIGH);
if (actualVolume == 0) enableAmpPower(false);
} else if (actualVolume < valPotmeterVolume) {
digitalWrite(fxVolumeArray[fxVolUp], LOW);
if (actualVolume == 0) enableAmpPower(true);
actualVolume++;
delay(40);
digitalWrite(fxVolumeArray[fxVolUp], HIGH);
}
}
void readGameLevelPotmeter() {
valPotmeterGameLevel = analogRead(potmeterGameLevel);
if (valPotmeterGameLevel < 102) gameLevel = 1;
else if (valPotmeterGameLevel < 204) gameLevel = 2;
else if (valPotmeterGameLevel < 306) gameLevel = 3;
else if (valPotmeterGameLevel < 408) gameLevel = 4;
else if (valPotmeterGameLevel < 510) gameLevel = 5;
else if (valPotmeterGameLevel < 612) gameLevel = 6;
else if (valPotmeterGameLevel < 714) gameLevel = 7;
else if (valPotmeterGameLevel < 816) gameLevel = 8;
else if (valPotmeterGameLevel < 918) gameLevel = 9;
else gameLevel = 10;
matrix1.print(gameLevel); matrix1.writeDisplay();
while (!eeprom_is_ready());
cli();
highScore = eeprom_read_word((uint16_t*)(gameLevel * 2));
sei();
if (highScore > -1) matrix2.print(highScore);
else matrix2.print(0);
matrix2.writeDisplay();
}
void playSound(int sound) {
if (currentPlayingSound != sound) {
for (int counter = 0; counter < 11; counter++) digitalWrite(fxTriggerArray[counter], HIGH);
digitalWrite(fxTriggerArray[sound], LOW);
currentPlayingSound = sound;
}
}
void enableAmpPower(bool state) { digitalWrite(ampPower, state); }
Кастомные детали и корпуса
- Лазерная резка MDF 6 мм — OC9CvHjuTzsYL9POZCJt.dxf
- Лазерная резка MDF 9 мм — 1irkeqvO4ABzhKqhmyG7.dxf
- Лазерная резка Плексиглас 3 мм — j4Tnmni6dQ0KXJtiHcy1.dxf
- Кастомный шилд Arduino Mega — 5yiRX1RdQuPX0gNXjYkm.zip
- 3D-печатные: крепление усилителя (2 шт.), крепление Arduino Mega, крепление задней панели, задняя панель, зажим для кабелей, крепление LED-дисплея (2 шт.), защёлка дисплея (4 шт.), верхняя рамка дисплея (2 шт.), заднее крепление крышки, переднее крепление крышки (2 шт.), боковое крепление крышки (2 шт.)
Схемы

Производственный процесс
- Как выбрать правильные ручки приборов для вашего оборудования
- Выбор винтов с полукруглой головкой и шестигранником: практическое руководство
- Пуговица: от древней застежки к современной инновации
- Создание кнопки AWS IoT с управлением движением на Raspberry Pi, PIR-датчике и MQTT
- UnifiedWater v1: полный набор датчиков качества воды на Arduino MKR GSM
- Переносная система голосования с GSM-связью
- Как выбрать производителя прототипов печатных плат: полное руководство
- Выбор оптимальной скорости пиления для ленточных пил: практическое руководство
- Выбор правильного погружного насоса: экспертные советы и ключевые характеристики
- Почему стоит выбрать вертикально-токарные центры для точной обработки