아두이노 메가와 MAX72XX LED 매트릭스로 만드는 클래식 아스테로이드 게임
필요한 부품 및 준비물
| 부품 |
수량 |
비고 |
| 아두이노 메가 2560 |
1 |
UNO도 가능하나 메가 권장 |
| MAX7219 8x8 LED 매트릭스 모듈 (또는 실드) |
1 |
MAXREFDES99# 등 1개 모듈 (8x8 단일) |
| 택트 스위치 12mm (푸시버튼) |
2 |
좌/우 이동용 |
| 액티브 부저 |
1 |
폭발음/효과음용 |
| 브레드보드 |
1 |
배선용 |
| 점퍼 와이어 |
1세트 |
연결용 |
게임 개요
전설적인 아케이드 게임 아스테로이드(Asteroids)를 8x8 LED 매트릭스로 구현한 미니 게임입니다. 플레이어는 우주선(하단 한 픽셀)을 좌우로 움직여 위에서 떨어지는 운석(픽셀)을 피해야 합니다. 시간이 지날수록 게임 속도가 빨라지며, 생존 시간이 점수가 됩니다. 충돌 시 폭발 애니메이션과 사운드 후 점수 표시, 버튼 누르면 재시작.
회로 연결
MAX7219 모듈 → 아두이노 메가
| 모듈 핀 |
아두이노 핀 |
| DIN (Data In) |
Digital 12 |
| CLK |
Digital 11 |
| CS (LOAD) |
Digital 10 |
| VCC |
5V |
| GND |
GND |
버튼 및 부저
| 부품 |
아두이노 핀 |
비고 |
| 좌측 버튼 |
Digital 2 (인터럽트 0) |
INPUT_PULLUP, 낙하 에지 트리거 |
| 우측 버튼 |
Digital 3 (인터럽트 1) |
INPUT_PULLUP, 낙하 에지 트리거 |
| 부저 (+) |
Digital 9 (PWM) |
tone() 함수용 |
| 부저 (-) |
GND |
|
라이브러리
- LedControl (Eberhard Fahle) - GitHub 또는 라이브러리 관리자에서 설치
전체 소스 코드
/*********************************************************
* 아스테로이드 게임 - MAX72XX LED 매트릭스 버전
* (c) 2018 Joshua Weßner
*********************************************************/
#include "LedControl.h"
// MAX7219 핀 설정: DIN=12, CLK=11, CS=10, 1개 모듈
LedControl lc = LedControl(12, 11, 10, 1);
// 전역 변수
unsigned long delayTime = 1000; // 초기 틱 간격 (ms)
int score = 0;
int tick = 300; // 현재 틱 간격 (ms)
int tickCounter = 1; // 틱 카운터 (4틱마다 운석 생성)
int ship = 3; // 우주선 위치 (0~7)
int columns[8] = {0}; // 각 열의 운석 행 위치 (0=없음, 1~10=행)
int randomInt;
volatile unsigned long buttonPressed = 0;
const int buttonDelay = 150; // 디바운싱 딜레이 (ms)
volatile bool gameOver = false;
void setup() {
// 게임 상태 초기화
gameOver = false;
score = 0;
tick = 300;
tickCounter = 1;
ship = 3;
for (int i = 0; i < 8; i++) columns[i] = 0;
randomSeed(analogRead(A15)); // 노이즈 핀으로 랜덤 시드
// MAX7219 초기화
lc.shutdown(0, false); // 절전 모드 해제
lc.setIntensity(0, 1); // 밝기 0~15 중 1 (낮음)
lc.clearDisplay(0);
// 버튼 핀 설정 및 인터럽트 등록
pinMode(2, INPUT_PULLUP);
pinMode(3, INPUT_PULLUP);
attachInterrupt(digitalPinToInterrupt(2), leftISR, FALLING);
attachInterrupt(digitalPinToInterrupt(3), rightISR, FALLING);
}
// 좌측 버튼 ISR
void leftISR() {
if (millis() - buttonPressed > buttonDelay) {
if (ship != 0) ship--; else ship = 7; // 화면 순환 이동
lc.clearDisplay(0);
buttonPressed = millis();
}
if (gameOver) { gameOver = false; setup(); } // 재시작
}
// 우측 버튼 ISR
void rightISR() {
if (millis() - buttonPressed > buttonDelay) {
if (ship != 7) ship++; else ship = 0;
lc.clearDisplay(0);
buttonPressed = millis();
}
if (gameOver) { gameOver = false; setup(); }
}
void loop() {
// 게임 틱 처리
if (millis() - delayTime > tick) {
delayTime = millis();
score++; // 생존 틱 = 점수
// 4틱마다 운석 생성 시도
if (tickCounter == 1) {
tick = tick / 1.02; // 점진적 난이도 상승 (약 2%씩 빨라짐)
randomInt = random(0, 8);
if (columns[randomInt] == 0) columns[randomInt] = 1; // 최상단에 생성
}
tickCounter = (tickCounter == 4) ? 1 : tickCounter + 1;
// 모든 운석 한 칸 아래로 이동
for (int i = 0; i < 8; i++) {
if (columns[i] == 10) columns[i] = 0; // 화면 벗어남
else if (columns[i] != 0) columns[i]++; // 아래로 이동
}
lc.clearDisplay(0);
}
// 디스플레이 그리기
// 우주선 (하단 행 7, 열 ship)
lc.setLed(0, 7, ship, true);
// 운석들 (각 열의 columns[i]-2, columns[i]-3 행에 2픽셀 길이로 표시)
for (int i = 0; i < 8; i++) {
if (columns[i] > 0) {
lc.setLed(0, columns[i] - 2, i, true);
lc.setLed(0, columns[i] - 3, i, true);
}
}
// 충돌 감지: 우주선 위치에 운석이 행 9 또는 10에 있으면 충돌
// (우주선은 행 7, 운석이 행 9~10이면 다음 틱에 충돌)
if (columns[ship] == 10 || columns[ship] == 9) {
lc.clearDisplay(0);
// 폭발 애니메이션 (4프레임)
for (int frame = 0; frame < 4; frame++) {
// 십자 모양 확장
lc.setLed(0, 7, ship + frame, true);
lc.setLed(0, 7, ship - frame, true);
lc.setLed(0, 7 - frame, ship + frame, true);
lc.setLed(0, 7 - frame, ship - frame, true);
lc.setLed(0, 7 - 1.5 * frame, ship, true);
// 폭발 사운드 (주파수 하강)
unsigned long t = millis();
int freq = 1000;
while (millis() - t <= 250) {
freq--;
tone(9, random(freq, 1000));
}
lc.clearDisplay(0);
noTone(9);
}
delay(500);
// 점수 표시 (최대 3자리, 매트릭스는 2자리만 표시 가능)
String scoreStr = String(score);
char d1 = scoreStr.length() > 0 ? scoreStr.charAt(0) : '0';
char d2 = scoreStr.length() > 1 ? scoreStr.charAt(1) : '0';
char d3 = scoreStr.length() > 2 ? scoreStr.charAt(2) : '0';
if (score < 100) {
drawDigit(d1, 1); // 십의 자리
drawDigit(d2, 5); // 일의 자리
} else {
drawDigit(d2, 1); // 백의 자리 제외, 하위 2자리만
drawDigit(d3, 5);
// 백의 자리는 좌측 상단에 점으로 표시 (100당 1개)
int hundreds = d1 - '0';
for (int i = 1; i <= hundreds; i++) {
lc.setLed(0, 0, i - 1, true);
lc.setLed(0, 1, i - 1, true);
delay(200);
}
}
gameOver = true;
while (gameOver) { /* 버튼 대기 */ }
}
}
// 숫자 그리기 함수들 (각 숫자 3열 x 8행 패턴)
void drawDigit(char digit, int col) {
switch (digit) {
case '0': draw0(col); break;
case '1': draw1(col); break;
case '2': draw2(col); break;
case '3': draw3(col); break;
case '4': draw4(col); break;
case '5': draw5(col); break;
case '6': draw6(col); break;
case '7': draw7(col); break;
case '8': draw8(col); break;
case '9': draw9(col); break;
}
}
void draw0(int p) { lc.setColumn(0, p, B00011111); lc.setColumn(0, p+1, B00010001); lc.setColumn(0, p+2, B00011111); }
void draw1(int p) { lc.setColumn(0, p, B00001000); lc.setColumn(0, p+1, B00011111); }
void draw2(int p) { lc.setColumn(0, p, B00010111); lc.setColumn(0, p+1, B00010101); lc.setColumn(0, p+2, B00011101); }
void draw3(int p) { lc.setColumn(0, p, B00010001); lc.setColumn(0, p+1, B00010101); lc.setColumn(0, p+2, B00011111); }
void draw4(int p) { lc.setColumn(0, p, B00011100); lc.setColumn(0, p+1, B00000100); lc.setColumn(0, p+2, B00011111); }
void draw5(int p) { lc.setColumn(0, p, B00011101); lc.setColumn(0, p+1, B00010101); lc.setColumn(0, p+2, B00010111); }
void draw6(int p) { lc.setColumn(0, p, B00011111); lc.setColumn(0, p+1, B00010101); lc.setColumn(0, p+2, B00010111); }
void draw7(int p) { lc.setColumn(0, p, B00010000); lc.setColumn(0, p+1, B00010011); lc.setColumn(0, p+2, B00011100); }
void draw8(int p) { lc.setColumn(0, p, B00011111); lc.setColumn(0, p+1, B00010101); lc.setColumn(0, p+2, B00011111); }
void draw9(int p) { lc.setColumn(0, p, B00011101); lc.setColumn(0, p+1, B00010101); lc.setColumn(0, p+2, B00011111); }
게임 메커니즘 상세
| 요소 |
구현 |
| 우주선 |
행 7, 열 0~7 단일 LED, 좌우 버튼으로 순환 이동 |
| 운석 |
각 열별 독립적, 행 1에서 시작하여 매 틱마다 행 증가 (최대 10) |
| 생성 주기 |
4틱마다 랜덤 열에 운석 생성 (빈 열만) |
| 난이도 조절 |
틱 간격 tick을 1.02로 나누어 점진적 가속 (300ms → 수 ms) |
| 충돌 판정 |
운석이 행 9 또는 10에 있고 우주선 열과 같으면 게임 오버 |
| 점수 |
생존 틱 수 = 점수, 최대 3자리 표시 (999까지) |
| 폭발 효과 |
십자 확장 애니메이션 + 부저 주파수 스윕 다운 |
확장 아이디어
- 다중 매트릭스: 2~4개 모듈 연결로 16x8, 32x8 등 확대
- 미사일 발사: 발사 버튼 추가로 운석 파괴 기능
- 하이스코어 저장: EEPROM에 최고 점수 기록
- 레벨/웨이브 시스템: 운석 패턴 다양화 (대각선, 분열 등)
- OLED/LCD 병행: 점수/메뉴를 별도 디스플레이에 표시
- 사운드 개선: 수동 파형 합성으로 레트로 사운드 구현
트러블슈팅
| 문제 |
해결 |
| LED 매트릭스 안 켜짐 |
배선 확인 (DIN/CLK/CS 순서), VCC/GND, LedControl 라이브러리 핀 번호 일치 |
| 버튼 반응 없음/튐 |
INPUT_PULLUP 확인, buttonDelay 증가 (200~300ms), 하드웨어 풀업/디바운스 회로 추가 |
| 게임 너무 빠름/느림 |
초기 tick 값(300) 및 감소 비율(1.02) 조정 |
| 점수 표시 이상 |
drawDigit 함수 열 오프셋(p) 확인, 매트릭스 8열 한계 고려 |
| 부저 소리 안 남 |
핀 9 PWM 지원 확인, tone()/noTone() 호출 순서, 부저 극성 확인 |
마치며
단일 8x8 LED 매트릭스라는 극도로 제약된 디스플레이로 고전 아케이드 게임의 재미를 구현한 인상적인 프로젝트입니다. 인터럽트 기반 입력, 비동기 게임 루프, 절차적 난이도 상승, 비트맵 폰트 렌더링 등 임베디드 게임 프로그래밍의 핵심 기법들을 컴팩트한 코드에 담았습니다. 초보자도 부품 몇 개로 바로 만들어볼 수 있으며, 확장 아이디어를 통해 더 풍부한 게임으로 발전시킬 여지가 충분합니다.