산업 제조
산업용 사물 인터넷 | 산업자재 | 장비 유지 보수 및 수리 | 산업 프로그래밍 |
home  MfgRobots >> 산업 제조 >  >> Manufacturing Technology >> 제조공정

아두이노 메가와 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

라이브러리

전체 소스 코드


/*********************************************************
 * 아스테로이드 게임 - 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 매트릭스라는 극도로 제약된 디스플레이로 고전 아케이드 게임의 재미를 구현한 인상적인 프로젝트입니다. 인터럽트 기반 입력, 비동기 게임 루프, 절차적 난이도 상승, 비트맵 폰트 렌더링 등 임베디드 게임 프로그래밍의 핵심 기법들을 컴팩트한 코드에 담았습니다. 초보자도 부품 몇 개로 바로 만들어볼 수 있으며, 확장 아이디어를 통해 더 풍부한 게임으로 발전시킬 여지가 충분합니다.

제조공정

  1. 체스: 고대 기원에서 현대 제조까지 전문가 개요
  2. 핀볼: 클래식 아케이드 게임의 역사, 디자인, 제조
  3. 미식축구공의 진화와 장인정신: 고대 경기에서 현대 NFL 표준까지
  4. 아두이노 우노와 OLED 디스플레이로 클래식 퐁(Pong) 게임 만들기: 단계별 튜토리얼
  5. 아두이노 나노로 37개 LED 룰렛 게임 만들기
  6. 아두이노 나노와 MAX7219로 만드는 24x16 LED 퐁 게임 콘솔
  7. 아두이노 나노로 테트리스 게임 만들기: DIY 16×8 LED 매트릭스
  8. 아두이노 조이스틱 인터랙티브 게임 - LED 피드백으로 방향 감지
  9. PIR 모션 센서 연동 인터랙티브 LED 매트릭스 도어 디스플레이 - 아두이노 홀리데이 인사
  10. 소리 감지 8-LED 라이트 디스플레이 - 아두이노 초보자 프로젝트