DIY 두더지 잡기 아케이드 게임 (버튼 에디션) - 아두이노 프로젝트
부품 및 소모품
| 부품 |
수량 |
비고 |
| Arduino Mega 2560 |
1 |
|
| Adafruit 1.4" 4자리 7세그먼트 디스플레이 (I2C 백팩) |
2 |
원하는 색상 선택 |
| Adafruit 대형 아케이드 버튼 (LED 내장, 60mm) |
5 |
5가지 색상 선택 |
| Adafruit Audio FX 보드 16MB |
1 |
|
| Adafruit 2.5W 앰프 |
2 |
|
| 스피커: 3W, 4Ω |
2 |
|
| Adafruit 온/오프 버튼 (LED 내장) |
1 |
|
| Adafruit 포텐셔미터 10k |
2 |
|
| Adafruit 포텐셔미터 노브 |
2 |
|
| 와이어 |
1 |
|
| 카운터싱크 나사 M3 + 너트 |
1세트 |
|
| 카운터싱크 나사 M4 + 너트 |
1세트 |
|
| 모든 3D 프린트 파트 |
1세트 |
|
| 모든 레이저컷 MDF 파트 |
1세트 |
|
| 커스텀 아두이노 메가 실드 |
1 |
|
| 아두이노 메가 스택커블 헤더 (간격용) |
1 |
|
| Adafruit DC 스텝다운 컨버터 5V 3A |
1 |
|
| 12V PSU |
1 |
|
| 암 PSU 버스 |
1 |
|
| USB-B 패널 마운트 |
1 |
|
필요 도구 및 기계
- 3D 프린터 (범용)
- 드라이버
- 납땜 인두 (범용)
앱 및 온라인 서비스
프로젝트 소개
커스텀 두더지 잡기 게임입니다. 현재 기본 게임으로 프로그래밍되어 있지만, 향후 업데이트로 확장 가능합니다.
주요 특징
- 게임용 5개 버튼. 각 버튼에 고휘도 LED 내장. LED는 PWM 제어(게임 오버 시퀀스에 사용).
- 선택 가능한 10개 레벨. 레벨 1(쉬움) ~ 10(광기). 게임 중 정해진 시간 내 올바른 버튼을 눌러야 함. 이 시간은 레벨 내에서도 가변적(때론 더 빨리 눌러야 함).
- (의사) 랜덤 버튼 선택. 다음 단계(눌러야 할 버튼)는 항상 다른 버튼.
- LED 디지트 디스플레이 2개. 하나는 현재 점수/카운트다운/레벨 선택, 다른 하나는 해당 레벨 하이스코어 표시.
- 디지털 볼륨 제어
- 하이스코어 저장 (EEPROM)
- 사운드 이펙트. 게임 다양한 상태에서 내장 스피커로 사운드 루프 재생.
- 레이저컷 인클로저
- 다수 3D 프린트 파트
- 커스텀 아두이노 메가 실드
인클로저 조립
먼저 인클로저 조립부터 시작합니다.
상판 커버에 3D 프린트 마운팅 브래킷을 장착합니다.
원하면 인클로저에 도색합니다.
LED 디스플레이 마운트 장착. 마운트 아래 볼트를 먼저 놓지 않으면 수직 바를 고정할 수 없으니 주의.
아두이노 실드 조립. 스택커블 헤더를 추가해 실드와 아두이노 사이 간격을 벌렸습니다.
후면 컨트롤 패널 조립.
상판 커버 조립 (버튼 & LED 패널).
후면 컨트롤 패널엔 포텐셔미터 2개: 볼륨 제어용, 레벨 선택용.
결론
완성!
왼쪽 LED 디지트 패널에 선택된 레벨 표시(1 쉬움 ~ 10 광기). 오른쪽 패널에 해당 레벨 하이스코어 표시. 게임 중엔 왼쪽 패널에 현재 점수 표시. 녹색 버튼 누르면 게임 시작. 카운트다운 후 게임 개시.
코드
/* 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 valPotmeterVolume = 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 counter = 0; counter < 5; counter++) analogWrite(btnLedArray[counter], 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 6mm: OC9CvHjuTzsYL9POZCJt.dxf
- 레이저컷 MDF 9mm: 1irkeqvO4ABzhKqhmyG7.dxf
- 레이저컷 아크릴 3mm: j4Tnmni6dQ0KXJtiHcy1.dxf
- 커스텀 아두이노 메가 실드: 5yiRX1RdQuPX0gNXjYkm.zip
- 3D 프린트: 앰프 마운트(2개), 아두이노 메가 마운트, 후면 연결 패널 마운트, 후면 연결 패널, 케이블 클립(필요 수량), LED 디스플레이 마운트(2개), LED 디스플레이 마운트 락(4개), LED 디스플레이 탑 프레임(2개), 상판 커버 마운트 백/프론트(2개)/사이드(2개)
회로도
회로도 이미지 참조