/*
* 아두이노 조이스틱 스네이크 게임 (배경 음악 포함)
*
* 이 코드는 MIT 라이센스 하에 자유롭게 사용, 수정, 배포할 수 있습니다.
*
* 사용된 U8glib 라이브러리는 BSD 라이센스를 따릅니다.
* https://github.com/olikraus/u8glib
*
* 작성일: 2025년 4월 1일
*/
#include <U8glib.h>
// OLED Display configuration - using SSD1306 128x64 I2C
U8GLIB_SSD1306_128X64 u8g(U8G_I2C_OPT_NONE);
// Joystick pins
const int joystickXPin = A1; // Connect X output of joystick to A0
const int joystickYPin = A2; // Connect Y output of joystick to A1
// 부저 연결 핀 정의
const int buzzerPin = 8;
// Game constants
const int GRID_SIZE = 4; // Size of each grid cell in pixels
const int GRID_WIDTH = 128 / GRID_SIZE; // 32
const int GRID_HEIGHT = 64 / GRID_SIZE; // 16
const int MAX_SNAKE_LENGTH = 100;
// Game variables
int snakeX[MAX_SNAKE_LENGTH]; // X positions of snake body parts
int snakeY[MAX_SNAKE_LENGTH]; // Y positions of snake body parts
int snakeLength; // Current length of snake
int foodX; // X position of food
int foodY; // Y position of food
int direction; // 0=up, 1=right, 2=down, 3=left
unsigned long lastMoveTime; // Time when snake last moved
const int moveDelay = 80; // Delay between moves (milliseconds) - 속도 향상을 위해 150에서 80으로 변경
boolean gameOver; // Flag for game over state
int score; // Current score
int highestScore = 0; // Highest score achieved (persists while power is on)
boolean showStartScreen = true; // Flag to show start screen
// 음계 주파수 정의
#define NOTE_B0 31
#define NOTE_C1 33
#define NOTE_CS1 35
#define NOTE_D1 37
#define NOTE_DS1 39
#define NOTE_E1 41
#define NOTE_F1 44
#define NOTE_FS1 46
#define NOTE_G1 49
#define NOTE_GS1 52
#define NOTE_A1 55
#define NOTE_AS1 58
#define NOTE_B1 62
#define NOTE_C2 65
#define NOTE_CS2 69
#define NOTE_D2 73
#define NOTE_DS2 78
#define NOTE_E2 82
#define NOTE_F2 87
#define NOTE_FS2 93
#define NOTE_G2 98
#define NOTE_GS2 104
#define NOTE_A2 110
#define NOTE_AS2 117
#define NOTE_B2 123
#define NOTE_C3 131
#define NOTE_CS3 139
#define NOTE_D3 147
#define NOTE_DS3 156
#define NOTE_E3 165
#define NOTE_F3 175
#define NOTE_FS3 185
#define NOTE_G3 196
#define NOTE_GS3 208
#define NOTE_A3 220
#define NOTE_AS3 233
#define NOTE_B3 247
#define NOTE_C4 262
#define NOTE_CS4 277
#define NOTE_D4 294
#define NOTE_DS4 311
#define NOTE_E4 330
#define NOTE_F4 349
#define NOTE_FS4 370
#define NOTE_G4 392
#define NOTE_GS4 415
#define NOTE_A4 440
#define NOTE_AS4 466
#define NOTE_B4 494
#define NOTE_C5 523
#define NOTE_CS5 554
#define NOTE_D5 587
#define NOTE_DS5 622
#define NOTE_E5 659
#define NOTE_F5 698
#define NOTE_FS5 740
#define NOTE_G5 784
#define NOTE_GS5 831
#define NOTE_A5 880
#define NOTE_AS5 932
#define NOTE_B5 988
#define NOTE_C6 1047
#define NOTE_CS6 1109
#define NOTE_D6 1175
#define NOTE_DS6 1245
#define NOTE_E6 1319
#define NOTE_F6 1397
#define NOTE_FS6 1480
#define NOTE_G6 1568
#define NOTE_GS6 1661
#define NOTE_A6 1760
#define NOTE_AS6 1865
#define NOTE_B6 1976
#define NOTE_C7 2093
#define NOTE_CS7 2217
#define NOTE_D7 2349
#define NOTE_DS7 2489
#define NOTE_E7 2637
#define NOTE_F7 2794
#define NOTE_FS7 2960
#define NOTE_G7 3136
#define NOTE_GS7 3322
#define NOTE_A7 3520
#define NOTE_AS7 3729
#define NOTE_B7 3951
#define NOTE_C8 4186
#define NOTE_CS8 4435
#define NOTE_D8 4699
#define NOTE_DS8 4978
#define REST 0
// 음악 배열 - 음표와 지속 시간을 정의합니다
int melodyNotes[] = {
// 메인 테마 (8비트 스타일)
NOTE_E4, NOTE_G4, NOTE_E5, NOTE_C5, NOTE_D5, NOTE_G4,
NOTE_E4, NOTE_G4, NOTE_E5, NOTE_C5, NOTE_D5, NOTE_E4,
NOTE_E4, NOTE_G4, NOTE_E5, NOTE_C5, NOTE_D5, NOTE_G4,
NOTE_C4, NOTE_E4, NOTE_G4, NOTE_C5, NOTE_G4, NOTE_E4,
// 게임 오버 시 부분
NOTE_E3, NOTE_C3, NOTE_G2, NOTE_C2, NOTE_G1, NOTE_E1};
// 음표 지속 시간 (4 = 1/4 음표, 8 = 1/8 음표 등)
int noteDurations[] = {
8, 8, 4, 8, 8, 4,
8, 8, 4, 8, 8, 4,
8, 8, 4, 8, 8, 4,
8, 8, 4, 8, 8, 4,
4, 4, 4, 4, 4, 2};
// 음악 속도 관련 변수
int tempo = 120; // BPM (분당 비트 수)
unsigned long previousMillis = 0;
int currentNote = 0;
int totalNotes = sizeof(melodyNotes) / sizeof(melodyNotes[0]);
// 게임 상태 플래그
boolean isPlaying = false;
void setup()
{
Serial.begin(9600);
// Initialize joystick pins
pinMode(joystickXPin, INPUT);
pinMode(joystickYPin, INPUT);
// 부저 핀을 출력으로 설정
pinMode(buzzerPin, OUTPUT);
// Initialize display
if (u8g.getMode() == U8G_MODE_R3G3B2)
{
u8g.setColorIndex(255); // White on RGB displays
}
else if (u8g.getMode() == U8G_MODE_GRAY2BIT)
{
u8g.setColorIndex(3); // Max brightness for grayscale displays
}
else if (u8g.getMode() == U8G_MODE_BW)
{
u8g.setColorIndex(1); // Full pixel on for monochrome displays
}
// Initialize game
resetGame();
Serial.println("스네이크 게임 준비 완료!");
}
void loop()
{
// Process joystick input
readJoystick();
// 게임 시작 화면 처리
if (showStartScreen)
{
// 시작 화면 출력
u8g.firstPage();
do
{
drawStartScreen();
} while (u8g.nextPage());
// 조이스틱 움직임이 감지되면 게임 시작
if (readJoystickButton())
{
showStartScreen = false;
resetGame();
playStartMusic(); // 게임 시작 음악 재생
}
return; // 시작 화면에서는 게임 로직 실행하지 않음
}
// 게임 오버 상태일 때
if (gameOver)
{
// 게임 종료시 최고 점수 갱신 확인
if (score > highestScore)
{
highestScore = score;
}
// 게임 오버 화면 표시
u8g.firstPage();
do
{
drawGameOver();
} while (u8g.nextPage());
// 게임 오버시 음악 재생 (최초 한 번만)
static boolean gameOverMusicPlayed = false;
if (!gameOverMusicPlayed)
{
playGameOverMusic();
gameOverMusicPlayed = true;
}
// 조이스틱 움직임으로 게임 재시작
if (readJoystickButton())
{
resetGame();
gameOverMusicPlayed = false;
}
return; // 게임 오버 상태에서는 여기서 종료
}
// 음악 업데이트 - 게임 진행 중일 때 배경 음악 계속 재생
updateMusic();
// 게임 진행 중일 때 - 정해진 시간 간격으로 뱀 움직임
if (millis() - lastMoveTime >= moveDelay)
{
moveSnake();
lastMoveTime = millis();
// 충돌 확인
checkCollisions();
// 먹이를 먹었는지 확인
if (snakeX[0] == foodX && snakeY[0] == foodY)
{
// 뱀 길이 증가
snakeLength++;
// 새로운 먹이 생성
generateFood();
// 점수 증가
score++;
}
}
// 게임 화면 그리기
u8g.firstPage();
do
{
drawGame();
} while (u8g.nextPage());
}
void resetGame()
{
// Start with a snake of length 3 in the middle of the screen
snakeLength = 3;
direction = 1; // Start moving right
// Place snake in the middle
for (int i = 0; i < snakeLength; i++)
{
snakeX[i] = GRID_WIDTH / 2 - i;
snakeY[i] = GRID_HEIGHT / 2;
}
// Generate initial food
generateFood();
// Reset game variables
gameOver = false;
score = 0;
lastMoveTime = millis();
// 음악 초기화
pauseMusic();
}
void readJoystick()
{
// Read joystick values
int xValue = analogRead(joystickXPin);
int yValue = analogRead(joystickYPin);
// Print joystick values to Serial Monitor (디버깅용)
/*
Serial.print("Joystick X: ");
Serial.print(xValue);
Serial.print(", Y: ");
Serial.print(yValue);
Serial.print(", Direction: ");
// Display current direction
switch (direction)
{
case 0:
Serial.println("Up");
break;
case 1:
Serial.println("Right");
break;
case 2:
Serial.println("Down");
break;
case 3:
Serial.println("Left");
break;
}
*/
// X축 방향이 우선순위가 높도록 설정 (조이스틱이 대각선으로 움직일 때)
// 좌/우 방향 인식 먼저 처리 - X축 방향 수정
if (xValue > 700)
{ // Left - X값이 높을 때(조이스틱 물리적으로 왼쪽으로 기울임)
if (direction != 1)
{ // 현재 Right가 아닐 때만 Left로 바꿈 (180도 회전 방지)
direction = 3;
return; // 방향이 설정되면 함수를 종료하여 다른 방향 변경 방지
}
}
else if (xValue < 300)
{ // Right - X값이 낮을 때(조이스틱 물리적으로 오른쪽으로 기울임)
if (direction != 3)
{ // 현재 Left가 아닐 때만 Right로 바꿈 (180도 회전 방지)
direction = 1;
return; // 방향이 설정되면 함수를 종료하여 다른 방향 변경 방지
}
}
// 좌/우 방향이 인식되지 않았을 때만 위/아래 방향 처리
if (yValue < 300)
{ // Up - Y값이 낮을 때(조이스틱 물리적으로 위로 기울임)
if (direction != 0)
{ // 현재 Down이 아닐 때만 Up으로 바꿈 (180도 회전 방지)
direction = 2;
}
}
else if (yValue > 700)
{ // Down - Y값이 높을 때(조이스틱 물리적으로 아래로 기울임)
if (direction != 2)
{ // 현재 Up이 아닐 때만 Down으로 바꿈 (180도 회전 방지)
direction = 0;
}
}
}
// Function to simulate a button press with joystick (for game restart)
boolean readJoystickButton()
{
// Read values to detect a significant movement of joystick
int xValue = analogRead(joystickXPin);
int yValue = analogRead(joystickYPin);
// If joystick moved significantly in any direction
if (xValue < 200 || xValue > 800 || yValue < 200 || yValue > 800)
{
return true;
}
return false;
}
void moveSnake()
{
// Move body first (follow the leader)
for (int i = snakeLength - 1; i > 0; i--)
{
snakeX[i] = snakeX[i - 1];
snakeY[i] = snakeY[i - 1];
}
// Move head based on direction
switch (direction)
{
case 0: // Up
snakeY[0]--;
break;
case 1: // Right
snakeX[0]++;
break;
case 2: // Down
snakeY[0]++;
break;
case 3: // Left
snakeX[0]--;
break;
}
}
void checkCollisions()
{
// Check wall collisions
if (snakeX[0] < 0 || snakeX[0] >= GRID_WIDTH ||
snakeY[0] < 0 || snakeY[0] >= GRID_HEIGHT)
{
gameOver = true;
return;
}
// Check if snake has hit itself
for (int i = 1; i < snakeLength; i++)
{
if (snakeX[0] == snakeX[i] && snakeY[0] == snakeY[i])
{
gameOver = true;
return;
}
}
}
void generateFood()
{
boolean validPosition;
do
{
validPosition = true;
// Generate random position
foodX = random(GRID_WIDTH);
foodY = random(GRID_HEIGHT);
// Check if food is on snake
for (int i = 0; i < snakeLength; i++)
{
if (foodX == snakeX[i] && foodY == snakeY[i])
{
validPosition = false;
break;
}
}
} while (!validPosition);
}
// 시작 화면 그리기
void drawStartScreen()
{
// 게임 제목 표시
u8g.setFont(u8g_font_6x13);
u8g.drawStr(35, 20, "SNAKE GAME");
// 최고 점수 표시
u8g.setFont(u8g_font_6x13);
u8g.setPrintPos(25, 35);
u8g.print("Highest Score: ");
u8g.print(highestScore);
// 게임 시작 안내
u8g.drawStr(15, 53, "Move joystick ");
u8g.drawStr(15, 59, "to restart");
}
// 게임 오버 화면 그리기
void drawGameOver()
{
// 게임 오버 메시지
u8g.setFont(u8g_font_6x10);
u8g.drawStr(30, 15, "GAME OVER");
// 현재 점수 표시
u8g.setFont(u8g_font_6x13);
u8g.setPrintPos(25, 30);
u8g.print("Your Score: ");
u8g.print(score);
// 최고 점수 표시
u8g.setPrintPos(15, 40);
u8g.print("Highest Score: ");
u8g.print(highestScore);
// 재시작 안내
u8g.drawStr(15, 53, "Move joystick ");
u8g.drawStr(15, 59, "to restart");
}
void drawGame()
{
// 현재 게임 화면 그리기
// 뱀 그리기
for (int i = 0; i < snakeLength; i++)
{
// 뱀 몸통을 채워진 사각형으로 그림
u8g.drawBox(snakeX[i] * GRID_SIZE, snakeY[i] * GRID_SIZE, GRID_SIZE, GRID_SIZE);
}
// 먹이를 빈 사각형으로 그림
u8g.drawFrame(foodX * GRID_SIZE, foodY * GRID_SIZE, GRID_SIZE, GRID_SIZE);
// 현재 점수 표시
u8g.setFont(u8g_font_5x8);
u8g.setPrintPos(0, 6);
u8g.print("Score: ");
u8g.print(score);
// 최고 점수 표시
u8g.setPrintPos(70, 6);
u8g.print("High: ");
u8g.print(highestScore);
}
// ===== 음악 관련 함수 =====
// 배경 음악 재생 시작
void startBackgroundMusic()
{
currentNote = 0;
isPlaying = true;
previousMillis = millis();
Serial.println("배경 음악 재생을 시작합니다.");
}
// 게임 오버 사운드 재생
void playGameOverSound()
{
isPlaying = false;
noTone(buzzerPin);
Serial.println("게임 오버!");
// 게임 오버 음악 부분 재생 (마지막 6개 음표)
for (int i = totalNotes - 6; i < totalNotes; i++)
{
int noteDuration = 60000 / tempo / noteDurations[i];
tone(buzzerPin, melodyNotes[i], noteDuration * 0.9);
delay(noteDuration);
noTone(buzzerPin);
delay(noteDuration * 0.1); // 음표 사이의 짧은 간격
}
}
// 게임 시작 시 호출
void playStartMusic()
{
startBackgroundMusic();
}
// 게임 오버 시 호출
void playGameOverMusic()
{
playGameOverSound();
}
// 음악 일시 정지
void pauseMusic()
{
isPlaying = false;
noTone(buzzerPin);
}
// 음악 재개
void resumeMusic()
{
isPlaying = true;
}
// 주기적으로 호출해야 함 (메인 loop() 내에서)
void updateMusic()
{
if (isPlaying)
{
unsigned long currentMillis = millis();
int noteDuration = 60000 / tempo / noteDurations[currentNote];
if (currentMillis - previousMillis >= noteDuration)
{
previousMillis = currentMillis;
if (melodyNotes[currentNote] != REST)
{
tone(buzzerPin, melodyNotes[currentNote], noteDuration * 0.9);
}
else
{
noTone(buzzerPin);
}
currentNote++;
if (currentNote >= totalNotes - 6)
{
currentNote = 0;
}
}
}
}