#include <U8g2lib.h>
#include <Wire.h>
// --- Pin Definitions ---
#define JOY_X_PIN A2 // Joystick X output (Use an analog pin) - Assuming Y moves paddle horizontally based on original code
#define JOY_Y_PIN A3 // Joystick Y output (Use an analog pin)
#define JOY_BTN_PIN 4 // Joystick Button Pin
#define BUZZER_PIN 11 // Buzzer connected to D11
// --- Sound Constants ---
const int NOTE_C5 = 523;
const int NOTE_D5 = 587;
const int NOTE_E5 = 659;
const int NOTE_F5 = 698;
const int NOTE_G5 = 784;
const int NOTE_A5 = 880;
const int NOTE_B5 = 988;
const int NOTE_C6 = 1047;
// --- OLED Display Setup ---
// Use Hardware I2C
U8G2_SSD1306_128X64_NONAME_F_HW_I2C u8g2(U8G2_R0, /* reset=*/U8X8_PIN_NONE);
// --- Screen Dimensions ---
constexpr uint8_t SCREEN_WIDTH = 128;
constexpr uint8_t SCREEN_HEIGHT = 64;
// --- Game States ---
enum GameState
{
START_SCREEN,
GAME_SCREEN,
GAME_OVER_SCREEN,
LEADERBOARD_SCREEN
};
GameState currentState = START_SCREEN;
// --- Timing ---
unsigned long lastStateChangeTime = 0;
// const unsigned long STATE_DURATION_MS = 3000; // 3 seconds - No longer needed for GAME_SCREEN transition
const unsigned long OTHER_STATE_DURATION_MS = 3000; // Duration for GameOver and Leaderboard
unsigned long buttonPressStartTime = 0;
const unsigned long LONG_PRESS_DURATION_MS = 3000; // 3 seconds for long press
// --- Start Screen Constants ---
const int16_t START_SCREEN_CENTER_X = 40;
const int16_t START_SCREEN_BRICK_Y = 20;
const uint8_t START_BRICK_WIDTH = 24;
const uint8_t START_BRICK_HEIGHT = 12;
const uint8_t START_BRICK_DEPTH_X = 6;
const uint8_t START_BRICK_DEPTH_Y = 4;
const uint8_t START_BRICK_SPACING = 4;
const uint8_t START_NUM_BRICKS = 3;
const uint8_t START_BALL_RADIUS = 4;
const uint8_t START_TEXT_Y_TITLE = 10;
const uint8_t START_TEXT_Y_PROMPT = 55;
// --- Game Screen Constants ---
constexpr uint8_t GAME_BRICK_ROWS = 2;
constexpr uint8_t GAME_BRICK_COLS = 10;
constexpr uint8_t GAME_BRICK_SPACING = 2;
constexpr uint8_t GAME_BRICK_HEIGHT = 4;
constexpr uint8_t GAME_BRICK_TOP_OFFSET = 0;
constexpr uint8_t GAME_BRICK_WIDTH = (SCREEN_WIDTH - (GAME_BRICK_COLS - 1) * GAME_BRICK_SPACING) / GAME_BRICK_COLS;
constexpr uint8_t GAME_PADDLE_WIDTH = 20; // Changed to match reference code's paddle width
constexpr uint8_t GAME_PADDLE_HEIGHT = 4;
constexpr uint8_t GAME_PADDLE_MARGIN = 1; // bottom margin
constexpr uint8_t GAME_BALL_RADIUS = 3;
constexpr uint8_t GAME_BALL_PADDLE_GAP = 6;
// Paddle movement constants matching 04_조이스틱_패들_잔상처리.ino
const int PADDLE_DEADZONE = 50;
const int PADDLE_SPEED = 5; // max speed (renamed to match reference)
float paddleVel = 0.0; // current velocity
const float ACCEL_RATE = 0.32; // acceleration per update (renamed to match reference)
// Motion Blur settings
const int MAX_BLUR_STEPS = 4; // Maximum number of trail steps (renamed to match reference)
const float TRAIL_SPACING_FACTOR = 0.8; // Spacing factor for trail segments (renamed to match reference)
// Paddle position variable
int paddleX = (SCREEN_WIDTH - GAME_PADDLE_WIDTH) / 2; // Initial position centered
// --- Game Over Screen Constants ---
const u8g2_uint_t GAMEOVER_TITLE_Y = 24;
const u8g2_uint_t GAMEOVER_SUBTITLE_Y = 55;
// --- Leaderboard Screen Constants ---
uint16_t highScores[4] = {0, 0, 0, 0}; // Initialize with zeros instead of example scores
// --- Ball Movement Variables ---
float ballX, ballY; // Ball position (using float for smooth movement)
float ballVelX = 1.5; // Ball X velocity
float ballVelY = -2.0; // Ball Y velocity (negative means moving up)
const float BALL_SPEED_INCREASE = 0.05; // Ball speed increase after each brick hit
const float MAX_BALL_SPEED = 1.7; // Maximum ball speed
// --- Game State Variables ---
bool ballLaunched = false; // Flag to indicate if the ball is in play
bool brickState[GAME_BRICK_ROWS][GAME_BRICK_COLS]; // Array to track brick state (true = active/visible)
uint16_t playerScore = 0; // Player's current score
const uint8_t BRICK_SCORE = 5; // Score earned per brick
const uint8_t LEVEL_SCORE = 50; // Score for completing a level
bool gameStarted = false; // Flag to indicate if game has started
uint8_t playerLives = 5; // Player starts with 5 lives
bool showLifeLostMessage = false; // Flag to show life lost message
unsigned long lifeLostMessageTime = 0; // Time when life was lost (for message display)
const unsigned long LIFE_MESSAGE_DURATION = 1500; // Show message for 1.5 seconds
bool startMusicPlayed = false; // Flag to track if the start music has been played
uint16_t highestScore = 0; // Track highest score ever achieved
// --- Function Prototypes ---
void drawStartScreen();
void drawStartBrick(int16_t x, bool filled);
void drawStartCracks(int16_t x);
void drawStartHatch(int16_t x);
void drawStartBallAtCenter();
void drawGameScreen();
void drawGameBricks();
void drawGamePaddle();
void drawGameBall();
void drawGameOverScreen();
void drawLeaderboardScreen();
void updateGamePaddle(); // Add prototype for paddle update logic
void updateGameBall(); // New function to update ball position and handle collisions
void checkBrickCollision(float x, float y); // Check and handle brick collisions
void resetGame(); // Reset game state for new game
bool checkPaddleCollision(float x, float y); // Check for paddle collision
void initBricks(); // Initialize all bricks
// Sound functions
void playStartMusic(); // Play music at start screen
void playBounceSound(); // Sound when ball bounces
void playBrickHitSound(); // Sound when brick is hit
void playLifeLostSound(); // Sound when player loses a life
void playGameOverSound(); // Sound when game is over
void playLevelClearedSound(); // Sound when level is cleared
void playButtonSound(); // Sound when button is pressed
void setup()
{
Serial.begin(115200); // Optional: for debugging
Wire.begin();
u8g2.begin();
u8g2.setFont(u8g2_font_ncenB08_tr); // Default font
pinMode(JOY_BTN_PIN, INPUT_PULLUP);
pinMode(BUZZER_PIN, OUTPUT); // Set buzzer pin as output
// Initialize Joystick pins - X pin is not used for paddle movement in this logic
// pinMode(JOY_X_PIN, INPUT); // Analog pins don't need pinMode for input
// pinMode(JOY_Y_PIN, INPUT);
// Initialize game state
initBricks();
resetGame(); // Don't initialize highest score to the array's first value since it's now 0
highestScore = 0; // Start with 0 as the highest score
lastStateChangeTime = millis(); // Initialize timer
paddleX = (SCREEN_WIDTH - GAME_PADDLE_WIDTH) / 2; // Reset paddle position on setup
paddleVel = 0.0; // Reset paddle velocity
startMusicPlayed = false; // Initialize start music flag
}
void loop()
{
unsigned long currentTime = millis();
bool stateChanged = false;
bool buttonPressed = (digitalRead(JOY_BTN_PIN) == LOW);
// --- State Logic and Transitions ---
switch (currentState)
{
case START_SCREEN:
// Play music immediately when entering the start screen
if (!startMusicPlayed)
{
playStartMusic();
startMusicPlayed = true;
lastStateChangeTime = currentTime;
}
if (buttonPressed)
{
playButtonSound();
currentState = GAME_SCREEN;
stateChanged = true;
paddleX = (SCREEN_WIDTH - GAME_PADDLE_WIDTH) / 2; // Reset paddle position
paddleVel = 0.0; // Reset paddle velocity
delay(50); // Simple debounce
}
// Play start music periodically
if (currentTime - lastStateChangeTime > 5000)
{ // Play every 5 seconds
playStartMusic();
lastStateChangeTime = currentTime;
}
break;
case GAME_SCREEN:
updateGamePaddle(); // Update paddle position based on joystick
// Ball movement and game logic
if (!gameStarted)
{
// First time entering game screen
initBricks();
resetGame();
gameStarted = true;
}
// Launch ball on button press if not already launched
if (!ballLaunched && buttonPressed)
{
ballLaunched = true;
delay(50); // Simple debounce
}
// Update ball position and check collisions if ball is in play
if (ballLaunched)
{
updateGameBall();
}
// Check for long press to transition
if (buttonPressed)
{
if (buttonPressStartTime == 0)
{ // Button just pressed
buttonPressStartTime = currentTime;
}
else if (currentTime - buttonPressStartTime >= LONG_PRESS_DURATION_MS)
{
currentState = GAME_OVER_SCREEN;
stateChanged = true;
buttonPressStartTime = 0; // Reset timer
}
}
else
{
buttonPressStartTime = 0; // Reset timer if button released
}
break;
case GAME_OVER_SCREEN:
if (currentTime - lastStateChangeTime >= OTHER_STATE_DURATION_MS)
{
currentState = LEADERBOARD_SCREEN;
stateChanged = true;
}
break;
case LEADERBOARD_SCREEN:
if (currentTime - lastStateChangeTime >= OTHER_STATE_DURATION_MS)
{
currentState = START_SCREEN;
stateChanged = true;
}
break;
}
if (stateChanged)
{
lastStateChangeTime = currentTime;
// Ensure button timer is reset on any state change away from GAME_SCREEN
if (currentState != GAME_SCREEN)
{
buttonPressStartTime = 0;
}
}
// --- Drawing based on State ---
u8g2.clearBuffer();
switch (currentState)
{
case START_SCREEN:
drawStartScreen();
break;
case GAME_SCREEN:
drawGameScreen();
break;
case GAME_OVER_SCREEN:
drawGameOverScreen();
break;
case LEADERBOARD_SCREEN:
drawLeaderboardScreen();
break;
}
u8g2.sendBuffer();
// Small delay to prevent excessive looping if needed,
// but state transitions are time-based or input-based.
// delay(10);
}
// --- Drawing Functions ---
// == Start Screen Functions ==
void drawStartScreen()
{
u8g2.setFont(u8g2_font_ncenB08_tr);
u8g2.drawStr(0, START_TEXT_Y_TITLE, "BRICK BREAKER");
u8g2.drawStr(0, START_TEXT_Y_PROMPT, "Press Button");
// Draw 3D Bricks
int16_t baseX = START_SCREEN_CENTER_X;
// Explicitly cast to int16_t to resolve narrowing conversion warnings
int16_t offsets[START_NUM_BRICKS] = {
static_cast<int16_t>(baseX - (START_BRICK_WIDTH + START_BRICK_DEPTH_X + START_BRICK_SPACING)),
static_cast<int16_t>(baseX),
static_cast<int16_t>(baseX + (START_BRICK_WIDTH + START_BRICK_DEPTH_X + START_BRICK_SPACING))};
for (uint8_t i = 0; i < START_NUM_BRICKS; i++)
{
bool isCenter = (i == 1);
drawStartBrick(offsets[i], isCenter);
if (isCenter)
drawStartCracks(offsets[i]);
else
drawStartHatch(offsets[i]);
}
drawStartBallAtCenter();
}
void drawStartBrick(int16_t x, bool filled)
{
// Front face
if (filled)
u8g2.drawBox(x, START_SCREEN_BRICK_Y, START_BRICK_WIDTH, START_BRICK_HEIGHT);
else
u8g2.drawFrame(x, START_SCREEN_BRICK_Y, START_BRICK_WIDTH, START_BRICK_HEIGHT);
// Top face
u8g2.drawLine(x, START_SCREEN_BRICK_Y, x + START_BRICK_DEPTH_X, START_SCREEN_BRICK_Y - START_BRICK_DEPTH_Y);
u8g2.drawLine(x + START_BRICK_WIDTH, START_SCREEN_BRICK_Y, x + START_BRICK_WIDTH + START_BRICK_DEPTH_X, START_SCREEN_BRICK_Y - START_BRICK_DEPTH_Y);
u8g2.drawLine(x + START_BRICK_DEPTH_X, START_SCREEN_BRICK_Y - START_BRICK_DEPTH_Y, x + START_BRICK_WIDTH + START_BRICK_DEPTH_X, START_SCREEN_BRICK_Y - START_BRICK_DEPTH_Y);
// Side face
u8g2.drawLine(x + START_BRICK_WIDTH, START_SCREEN_BRICK_Y, x + START_BRICK_WIDTH + START_BRICK_DEPTH_X, START_SCREEN_BRICK_Y - START_BRICK_DEPTH_Y); // Corrected line
u8g2.drawLine(x + START_BRICK_WIDTH, START_SCREEN_BRICK_Y + START_BRICK_HEIGHT, x + START_BRICK_WIDTH + START_BRICK_DEPTH_X, START_SCREEN_BRICK_Y + START_BRICK_HEIGHT - START_BRICK_DEPTH_Y); // Corrected line
u8g2.drawLine(x + START_BRICK_WIDTH + START_BRICK_DEPTH_X, START_SCREEN_BRICK_Y - START_BRICK_DEPTH_Y, x + START_BRICK_WIDTH + START_BRICK_DEPTH_X, START_SCREEN_BRICK_Y + START_BRICK_HEIGHT - START_BRICK_DEPTH_Y); // Corrected line
}
void drawStartCracks(int16_t x)
{
// Simplified crack drawing
u8g2.drawLine(x + START_BRICK_WIDTH / 2, START_SCREEN_BRICK_Y, x + START_BRICK_WIDTH / 2, START_SCREEN_BRICK_Y + START_BRICK_HEIGHT);
u8g2.drawLine(x, START_SCREEN_BRICK_Y + START_BRICK_HEIGHT / 2, x + START_BRICK_WIDTH, START_SCREEN_BRICK_Y + START_BRICK_HEIGHT / 2);
}
void drawStartHatch(int16_t x)
{
// Simplified hatch drawing on front face
for (int16_t i = 2; i < START_BRICK_HEIGHT; i += 4)
{
u8g2.drawLine(x, START_SCREEN_BRICK_Y + i, x + START_BRICK_WIDTH, START_SCREEN_BRICK_Y + i);
}
}
void drawStartBallAtCenter()
{
int16_t bx = START_SCREEN_CENTER_X + START_BRICK_WIDTH / 2; // Centered below middle brick
int16_t by = START_SCREEN_BRICK_Y + START_BRICK_HEIGHT + START_BALL_RADIUS + 3;
u8g2.drawDisc(bx, by, START_BALL_RADIUS, U8G2_DRAW_ALL);
}
// == Game Screen Functions ==
void updateGamePaddle()
{
// Non-blocking update logic adapted from 04_조이스틱_패들_잔상처리.ino
// Note: This assumes a loop interval similar to the original 10ms for smooth acceleration.
// If the main loop runs slower, acceleration might feel different.
int joyVal = analogRead(JOY_Y_PIN); // Read Y-axis for horizontal movement (matching reference code)
// Calculate target velocity from joystick deflection
float targetVel = 0;
int diff = joyVal - 512; // Assuming 512 is the center value
if (abs(diff) > PADDLE_DEADZONE)
{
// Map joystick deflection to target velocity
// Use Y-axis: Positive diff moves right (increase paddleX), negative moves left (decrease paddleX)
float norm = (abs(diff) - PADDLE_DEADZONE) / float(512 - PADDLE_DEADZONE);
targetVel = norm * PADDLE_SPEED * (diff > 0 ? 1 : -1);
}
// Accelerate/decelerate toward targetVel
if (paddleVel < targetVel)
paddleVel = min(paddleVel + ACCEL_RATE, targetVel);
else if (paddleVel > targetVel)
paddleVel = max(paddleVel - ACCEL_RATE, targetVel);
// Update position with smooth velocity
paddleX += paddleVel;
// Keep paddle within screen bounds
paddleX = constrain(paddleX, 0, SCREEN_WIDTH - GAME_PADDLE_WIDTH);
}
void drawGameScreen()
{
// 일반 플레이 중일 때는 Score와 Life를 표시하지 않음
// 생명 손실 메시지를 표시해야 할 때만 메시지 표시
if (showLifeLostMessage)
{
// Check if we should still show the message based on time
if (millis() - lifeLostMessageTime < LIFE_MESSAGE_DURATION)
{
// Draw a centered "Score" message
u8g2.setFont(u8g2_font_ncenB08_tr);
char scoreText[20];
sprintf(scoreText, "Score: %u", playerScore);
int scoreWidth = u8g2.getStrWidth(scoreText);
u8g2.drawStr((SCREEN_WIDTH - scoreWidth) / 2, 20, scoreText);
// Draw a centered "Remained Life" message
const char *message = "Remained Life:";
int msgWidth = u8g2.getStrWidth(message);
u8g2.drawStr((SCREEN_WIDTH - msgWidth) / 2, 32, message);
// Draw the number of remaining lives
char numText[3];
sprintf(numText, "%u", playerLives);
int numWidth = u8g2.getStrWidth(numText);
u8g2.drawStr((SCREEN_WIDTH - numWidth) / 2, 45, numText);
}
else
{
showLifeLostMessage = false; // Time's up, stop showing message
}
}
// 항상 게임 요소들을 그림
// 단, 메시지 표시 중에는 다른 게임 요소들만 표시
drawGameBricks();
drawGamePaddle();
drawGameBall();
}
void drawGameBricks()
{
for (uint8_t r = 0; r < GAME_BRICK_ROWS; ++r)
{
for (uint8_t c = 0; c < GAME_BRICK_COLS; ++c)
{
// Only draw active bricks (those that haven't been hit)
if (brickState[r][c])
{
uint8_t x = c * (GAME_BRICK_WIDTH + GAME_BRICK_SPACING);
uint8_t y = GAME_BRICK_TOP_OFFSET + r * (GAME_BRICK_HEIGHT + GAME_BRICK_SPACING);
// Draw bricks matching the reference code style:
// First draw the frame (outline)
u8g2.drawFrame(x, y, GAME_BRICK_WIDTH, GAME_BRICK_HEIGHT);
// Then draw a slightly smaller filled box inside
u8g2.drawBox(x + 1, y + 1, GAME_BRICK_WIDTH - 2, GAME_BRICK_HEIGHT - 2);
}
}
}
}
void drawGamePaddle()
{
// Draw paddle with motion blur, matching 04_조이스틱_패들_잔상처리.ino
// Calculate number of blur steps based on velocity magnitude
int blurSteps = map(abs(paddleVel), 0, PADDLE_SPEED, 0, MAX_BLUR_STEPS);
uint8_t paddleY = SCREEN_HEIGHT - GAME_PADDLE_MARGIN - GAME_PADDLE_HEIGHT;
// Draw trail segments (motion blur)
for (int i = 1; i <= blurSteps; ++i)
{
// Calculate position of the trail segment
// Offset is opposite to the direction of velocity
float trailX = paddleX - (paddleVel * i * TRAIL_SPACING_FACTOR);
// Ensure trail stays within bounds
trailX = constrain(trailX, 0, SCREEN_WIDTH - GAME_PADDLE_WIDTH);
// Draw trail segment (using drawFrame for a hollow effect)
u8g2.drawFrame(trailX, paddleY, GAME_PADDLE_WIDTH, GAME_PADDLE_HEIGHT);
}
// Draw the main paddle (solid) at the current paddleX position
u8g2.drawBox(paddleX, paddleY, GAME_PADDLE_WIDTH, GAME_PADDLE_HEIGHT);
}
void drawGameBall()
{
// Draw ball at its current position (using int cast for drawing)
if (ballLaunched)
{
// Draw moving ball
u8g2.drawDisc(ballX, ballY, GAME_BALL_RADIUS);
}
else
{
// Draw static ball waiting to be launched
uint8_t x = SCREEN_WIDTH / 2;
uint8_t y = SCREEN_HEIGHT - GAME_PADDLE_MARGIN - GAME_PADDLE_HEIGHT - GAME_BALL_PADDLE_GAP;
u8g2.drawDisc(x, y, GAME_BALL_RADIUS);
}
}
// == Game Over Screen Function ==
void drawGameOverScreen()
{
u8g2.setFont(u8g2_font_ncenB24_tr); // Larger font for title
u8g2_uint_t title_w = u8g2.getStrWidth("Ooops!");
u8g2.drawStr((SCREEN_WIDTH - title_w) / 2, GAMEOVER_TITLE_Y, "Ooops!");
u8g2.setFont(u8g2_font_ncenB14_tr); // Medium font for subtitle
u8g2_uint_t subtitle_w = u8g2.getStrWidth("Game Over !~");
u8g2.drawStr((SCREEN_WIDTH - subtitle_w) / 2, GAMEOVER_SUBTITLE_Y, "Game Over !~");
}
// == Leaderboard Screen Function ==
void drawLeaderboardScreen()
{
// Draw border
u8g2.drawFrame(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT);
// Top score title and value centered
char buf[20];
u8g2.setFont(u8g2_font_ncenB10_tr); // Slightly larger font
const char *title = "TOP SCORE";
int16_t w0 = u8g2.getStrWidth(title);
u8g2.drawStr((SCREEN_WIDTH - w0) / 2, 14, title); // Adjusted Y
u8g2.setFont(u8g2_font_ncenB14_tr); // Larger font for score
// Show 0 if no scores have been set yet
sprintf(buf, "%u", highestScore);
int16_t w1 = u8g2.getStrWidth(buf);
u8g2.drawStr((SCREEN_WIDTH - w1) / 2, 30, buf); // Adjusted Y
// Recent scores header
u8g2.setFont(u8g2_font_ncenB08_tr);
const char *recentTitle = "RECENT SCORES:";
u8g2.drawStr(10, 40, recentTitle);
// Count how many non-zero scores we have
int nonZeroScores = 0;
for (int i = 0; i < 4; i++)
{
if (highScores[i] > 0)
nonZeroScores++;
}
// Display message if no scores yet
if (nonZeroScores == 0)
{
const char *noScores = "No scores yet";
int16_t w2 = u8g2.getStrWidth(noScores);
u8g2.drawStr((SCREEN_WIDTH - w2) / 2, 55, noScores);
}
else
{
// Only show non-zero scores (up to 3)
int displayCount = 0;
for (uint8_t i = 0; i < 4 && displayCount < 3; i++)
{
if (highScores[i] > 0)
{
sprintf(buf, "%u.", displayCount + 1);
u8g2.drawStr(30, 50 + displayCount * 8, buf);
sprintf(buf, "%u", highScores[i]);
u8g2.drawStr(50, 50 + displayCount * 8, buf);
displayCount++;
}
}
}
}
// Initialize all bricks to active state
void initBricks()
{
for (uint8_t r = 0; r < GAME_BRICK_ROWS; ++r)
{
for (uint8_t c = 0; c < GAME_BRICK_COLS; ++c)
{
brickState[r][c] = true; // Set all bricks to active
}
}
}
// Reset game state for a new game
void resetGame()
{
// Reset ball position to starting position above paddle
ballX = SCREEN_WIDTH / 2.0;
ballY = SCREEN_HEIGHT - GAME_PADDLE_MARGIN - GAME_PADDLE_HEIGHT - GAME_BALL_PADDLE_GAP;
// Reset ball velocity with a slight randomness to make each game different
ballVelX = random(10) / 10.0 + 1.0; // Random X velocity between 1.0 and 2.0
if (random(2) == 0)
ballVelX = -ballVelX; // Randomly go left or right
ballVelY = -2.0; // Initial Y velocity (negative = upward)
// Reset game state
ballLaunched = false;
// Only reset player lives and score when starting a new game from scratch
if (!gameStarted)
{
playerScore = 0;
playerLives = 5;
}
}
// Update ball position and handle all collisions
void updateGameBall()
{
// Update ball position
ballX += ballVelX;
ballY += ballVelY;
// Check wall collisions
// Left and right walls
if (ballX <= GAME_BALL_RADIUS || ballX >= SCREEN_WIDTH - GAME_BALL_RADIUS)
{
ballVelX = -ballVelX; // Reverse X direction
// Adjust position to prevent sticking to wall
if (ballX < GAME_BALL_RADIUS)
ballX = GAME_BALL_RADIUS;
if (ballX > SCREEN_WIDTH - GAME_BALL_RADIUS)
ballX = SCREEN_WIDTH - GAME_BALL_RADIUS;
}
// Top wall
if (ballY <= GAME_BALL_RADIUS)
{
ballVelY = -ballVelY; // Reverse Y direction
ballY = GAME_BALL_RADIUS; // Adjust position to prevent sticking
} // Bottom (game over condition)
if (ballY >= SCREEN_HEIGHT)
{
// Ball went past the paddle - lose a life
playerLives--;
playLifeLostSound(); // Add this line
// Set flag to show life lost message
showLifeLostMessage = true;
lifeLostMessageTime = millis();
// Reset ball position and state
ballLaunched = false;
ballX = SCREEN_WIDTH / 2.0;
ballY = SCREEN_HEIGHT - GAME_PADDLE_MARGIN - GAME_PADDLE_HEIGHT - GAME_BALL_PADDLE_GAP;
// If all lives are lost, go to game over screen
if (playerLives <= 0)
{
// Update highest score if current score is higher
if (playerScore > highestScore)
{
highestScore = playerScore;
}
// Only add to leaderboard if score is greater than zero
if (playerScore > 0)
{
// Shift scores down
for (int i = 3; i > 0; i--)
{
highScores[i] = highScores[i - 1];
}
highScores[0] = playerScore; // Current score becomes the newest entry
// Sort the scores in descending order (bubble sort for simplicity)
for (int i = 0; i < 3; i++)
{
for (int j = 0; j < 3 - i; j++)
{
if (highScores[j] < highScores[j + 1])
{
// Swap scores
uint16_t temp = highScores[j];
highScores[j] = highScores[j + 1];
highScores[j + 1] = temp;
}
}
}
}
// Go to game over screen
currentState = GAME_OVER_SCREEN;
lastStateChangeTime = millis();
gameStarted = false; // Will cause full reset on next game
playGameOverSound(); // Add this line
}
}
// Check paddle collision
if (checkPaddleCollision(ballX, ballY))
{
// Ball hit the paddle, bounce upward with slight angle adjustment
ballVelY = -abs(ballVelY); // Ensure upward movement
// Calculate hit position relative to paddle center (value between -1.0 and 1.0)
float hitPos = (ballX - (paddleX + GAME_PADDLE_WIDTH / 2.0)) / (GAME_PADDLE_WIDTH / 2.0);
// Adjust X velocity based on where the ball hit the paddle
// This makes the ball bounce at different angles depending on where it hits
ballVelX = hitPos * 2.0; // Max horizontal velocity depends on hit position
// Ensure minimum X velocity to prevent ball moving straight up
if (abs(ballVelX) < 0.5)
{
ballVelX = (ballVelX < 0) ? -0.5 : 0.5;
}
}
// Check brick collisions and update score
checkBrickCollision(ballX, ballY);
}
// Check for collision between ball and bricks
void checkBrickCollision(float x, float y)
{
// Calculate which brick the ball might be hitting
// This uses the AABB (Axis-Aligned Bounding Box) collision detection
// First see if the ball is in the brick area at all
if (y < GAME_BRICK_ROWS * (GAME_BRICK_HEIGHT + GAME_BRICK_SPACING) + GAME_BRICK_TOP_OFFSET + GAME_BALL_RADIUS)
{
// Calculate the row and column of the potential brick hit
int8_t row = (y - GAME_BRICK_TOP_OFFSET) / (GAME_BRICK_HEIGHT + GAME_BRICK_SPACING);
int8_t col = x / (GAME_BRICK_WIDTH + GAME_BRICK_SPACING);
// Check if calculated row/col is within valid range and brick exists
if (row >= 0 && row < GAME_BRICK_ROWS &&
col >= 0 && col < GAME_BRICK_COLS &&
brickState[row][col])
{
// Calculate brick edges
float brickLeft = col * (GAME_BRICK_WIDTH + GAME_BRICK_SPACING);
float brickRight = brickLeft + GAME_BRICK_WIDTH;
float brickTop = GAME_BRICK_TOP_OFFSET + row * (GAME_BRICK_HEIGHT + GAME_BRICK_SPACING);
float brickBottom = brickTop + GAME_BRICK_HEIGHT;
// Determine if collision is more from sides or top/bottom
bool collideX = (ballVelX > 0 && ballX + GAME_BALL_RADIUS >= brickLeft && ballX < brickLeft) ||
(ballVelX < 0 && ballX - GAME_BALL_RADIUS <= brickRight && ballX > brickRight);
bool collideY = (ballVelY > 0 && ballY + GAME_BALL_RADIUS >= brickTop && ballY < brickTop) ||
(ballVelY < 0 && ballY - GAME_BALL_RADIUS <= brickBottom && ballY > brickBottom);
// Brick was hit, destroy it and add to score
brickState[row][col] = false;
playerScore += BRICK_SCORE;
playBrickHitSound();
// Reverse velocity based on collision direction
if (collideX)
ballVelX = -ballVelX;
if (collideY)
ballVelY = -ballVelY;
// If neither was detected, it's likely a corner collision
if (!collideX && !collideY)
{
ballVelX = -ballVelX;
ballVelY = -ballVelY;
}
// Increase ball speed slightly (with cap)
float currentSpeed = sqrt(ballVelX * ballVelX + ballVelY * ballVelY);
if (currentSpeed < MAX_BALL_SPEED)
{
float speedRatio = min((currentSpeed + BALL_SPEED_INCREASE) / currentSpeed, MAX_BALL_SPEED / currentSpeed);
ballVelX *= speedRatio;
ballVelY *= speedRatio;
}
// Check if all bricks are cleared
bool allCleared = true;
for (uint8_t r = 0; r < GAME_BRICK_ROWS; ++r)
{
for (uint8_t c = 0; c < GAME_BRICK_COLS; ++c)
{
if (brickState[r][c])
{
allCleared = false;
break;
}
}
if (!allCleared)
break;
}
// If all bricks cleared, add bonus and reset level
if (allCleared)
{
playerScore += LEVEL_SCORE; // Bonus for clearing level
initBricks(); // Reset bricks for new level
playLevelClearedSound(); // Add this line
// Could increase difficulty here for each level
}
}
}
}
// Check for collision between ball and paddle
bool checkPaddleCollision(float x, float y)
{
// Only check if ball is moving downward and in the right Y position
if (ballVelY > 0 &&
y + GAME_BALL_RADIUS >= SCREEN_HEIGHT - GAME_PADDLE_MARGIN - GAME_PADDLE_HEIGHT &&
y < SCREEN_HEIGHT - GAME_PADDLE_MARGIN)
{
// Check if ball is horizontally within paddle bounds
return (x >= paddleX && x <= paddleX + GAME_PADDLE_WIDTH);
}
return false;
}
// --- Sound Functions ---
void playStartMusic()
{
// Simple melody for start screen
int melody[] = {NOTE_C5, NOTE_E5, NOTE_G5, NOTE_C6};
int durations[] = {100, 100, 100, 200};
for (int i = 0; i < 4; i++)
{
tone(BUZZER_PIN, melody[i], durations[i]);
delay(durations[i] + 20); // Short delay between notes
}
noTone(BUZZER_PIN);
}
void playButtonSound()
{
tone(BUZZER_PIN, NOTE_C6, 50);
delay(50);
noTone(BUZZER_PIN);
}
void playBounceSound()
{
tone(BUZZER_PIN, NOTE_E5, 30);
delay(30);
noTone(BUZZER_PIN);
}
void playBrickHitSound()
{
tone(BUZZER_PIN, NOTE_A5, 40);
delay(40);
noTone(BUZZER_PIN);
}
void playLifeLostSound()
{
// Descending tone
for (int i = NOTE_C6; i > NOTE_C5; i -= 50)
{
tone(BUZZER_PIN, i, 20);
delay(20);
}
noTone(BUZZER_PIN);
}
void playGameOverSound()
{
// Game over sound
tone(BUZZER_PIN, NOTE_G5, 100);
delay(150);
tone(BUZZER_PIN, NOTE_E5, 100);
delay(150);
tone(BUZZER_PIN, NOTE_C5, 300);
delay(300);
noTone(BUZZER_PIN);
}
void playLevelClearedSound()
{
// Level clear sound (ascending)
for (int i = 0; i < 3; i++)
{
tone(BUZZER_PIN, NOTE_C5 + (i * 100), 100);
delay(120);
}
tone(BUZZER_PIN, NOTE_C6, 200);
delay(200);
noTone(BUZZER_PIN);
}