Midterm Project – Musical Brick Breaker Game

Concept: Recreating one of my favorite childhood game – The Musical Brick Breaker Game

So I chose to have the concept for this project, is to create a musical soothing classical Brick Breaker game using the p5.js library. The game involves controlling a paddle to bounce a ball and break bricks at the top of the screen. The user interacts with the game by moving the paddle horizontally using the left and right arrow keys. The goal is to break all the bricks without letting the ball fall below the paddle. The game provides feedback through visual cues such as the ball bouncing off objects, disappearing bricks, and a scoring system. Moreover, sound effects further enhance the user experience.

Designing the Code: Elaborating important areas

1) Ball Behavior: Within the Ball class, I define the behavior of the ball. This includes its movement across the screen, detection of collisions with other objects (such as the paddle and bricks), and rendering on the canvas. This encapsulation allows for clear organization and modularization of ball-related functionality.

2) Paddle Control: The Paddle class covers the movement and display of the paddle. It handles user input from the keyboard to move the paddle horizontally across the screen, ensuring precise control for the player.

3) Brick Management: Each brick in the game is represented by the Brick class. This class manages the display of individual bricks on the canvas and provides methods for their creation, rendering, and removal during gameplay.

4) User Interaction: The mousePressed function responds to user input by triggering specific game actions, such as starting or resetting the game. This function enhances the interactivity of the game and provides a seamless user experience.

Additional functions, such as createBricks and resetGame, are responsible for initializing game elements (such as bricks) and resetting the game state, respectively. These functions streamline the codebase and improve readability by encapsulating repetitive tasks.

By breaking down the code into these specific components, I ensured a clear and organized structure, facilitating easier maintenance.

Minimizing Risk: Code I’m proud of,

display() {
    fill(255, 0, 0);
    ellipse(this.x, this.y, this.radius * 2);
  }
  
  checkCollision() {
    if (this.x > paddle.x && this.x < paddle.x + paddle.width && this.y + this.radius > paddle.y) {
      this.speedY *= -1;
      paddleHitSound.play();
    }
  }
  
  bounce() {
    this.speedY *= -1;
    ballHitSound.play();
  }
  
  hits(brick) {
    let closestX = constrain(this.x, brick.x, brick.x + brick.width);
    let closestY = constrain(this.y, brick.y, brick.y + brick.height);
    let distance = dist(this.x, this.y, closestX, closestY);
    return distance < this.radius;
  }
}

One major complex aspect of the project is implementing collision detection between the ball and other game objects (paddle, bricks, walls). Ensuring accurate collision detection is crucial for the game’s mechanics and overall user experience. To minimize the risk of errors in this area, I employed two  strategies:

1) Collision Detection Algorithm: Implementing this collision detection algorithms is essential because, for example in the Ball class, I used a method called hits(brick) to check if the ball collided with a brick. This method calculates the distance between the ball and the brick’s edges to determine if a collision occurred. Moreover, By using the dist() function in favor with appropriate ball coordinates, I ensured this accurate collision detection is perfectly executed.

2) Testing with Edge Cases: To validate the accuracy of this collision detection algorithm, I conducted repeated testing with various edge cases. This includes scenarios where the ball collides with the corners of bricks or with multiple objects simultaneously. By systematically testing these cases and analyzing the results, I came to conclusion that the collision detection behaves as expected under different conditions.

Prototypes (wire-frame notes) :

Here’s the Game:

Features & Game Mechanics:
– Game initializes with a start screen displaying “Musical Brick Breaker Game” and along with three themes to choose.
– The player controls the paddle using the left and right arrow keys.
– The ball bounces off the paddle, walls, and bricks.
– When the ball hits a brick, it disappears, and the player earns points.
– If the ball falls below the paddle, the game ends.
– Once game ends, it displays either “Game Over” or “Congrats” message along with the score and a feature for click to return Home.
– Clicking on the canvas after the game ends resets the game, allowing the player to replay.

Additional Features:
– Sound effects are played when the ball hits the paddle and when it hits a brick.
– The player earns points for each brick broken, and the score is displayed on the screen.
– Background music plays throughout the game to enhance the gaming experience, moreover different sound tracks were added to different themes selected.

Here’s a snapshot taken during the game-play of all the themes:


Complete Code Snippet (With Comments):

// Variables to hold background images for different themes
let backgroundImage;
let marvelBackgroundImage;
let dcBackgroundImage;

// Game objects
let ball;
let paddle;
let bricks = [];

// Brick layout parameters
let brickRowCount = 3;
let brickColumnCount = 5;
let brickWidth = 80;
let brickHeight = 20;
let brickPadding = 10;
let brickOffsetTop = 50;
let brickOffsetLeft = 30;

// Game score
let score = 0;

// Sound effects and background music variables
let ballHitSound;
let paddleHitSound;
let backgroundMusic;
let marvelSoundtrack;
let dcSoundtrack;

// Game state management variables
let gameState = 'home'; // Possible states: 'home', 'playing', 'gameOver'
let theme = 'default'; // Current theme: 'default', 'marvel', 'dc'
let gameStarted = false; // Flag to indicate if the game has started

// Preload function to load images and sounds before the game starts
function preload() {
  backgroundImage = loadImage('background_image.jpg');
  marvelBackgroundImage = loadImage('marvel_background.jpg');
  dcBackgroundImage = loadImage('dc_background.jpg');
  ballHitSound = loadSound('ball_hit.mp3');
  paddleHitSound = loadSound('paddle_hit.mp3');
  backgroundMusic = loadSound('background_music.mp3');
  marvelSoundtrack = loadSound('marvel_soundtrack.mp3');
  dcSoundtrack = loadSound('dc_soundtrack.mp3');
}

// Setup function to initialize game elements
function setup() {
  createCanvas(500, 400); // Set canvas size
  paddle = new Paddle(); // Initialize paddle
  ball = new Ball(); // Initialize ball
  createBricks(); // Create brick layout
}

// Main draw loop to render the game frame by frame
function draw() {
  background(255); // Clear the canvas with a white background
  // Apply background image based on the current theme with opacity
  if (theme === 'default') {
    tint(255, 127); // Half opacity
    image(backgroundImage, 0, 0, width, height);
  } else if (theme === 'marvel') {
    tint(255, 127); // Half opacity
    image(marvelBackgroundImage, 0, 0, width, height);
  } else if (theme === 'dc') {
    tint(255, 127); // Half opacity
    image(dcBackgroundImage, 0, 0, width, height);
  }
  noTint(); // Reset tint effect for drawing other elements without opacity

  // Display the appropriate screen based on the game state
  if (gameState === 'home') {
    displayHomeScreen(); // Display home screen with theme options
  } else if (gameState === 'playing') {
    playGame(); // Main game logic
  } else if (gameState === 'gameOver') {
    displayGameOver(); // Display game over screen
  }
}

// Handler for mouse press events to interact with the game
function mousePressed() {
  if (gameState === 'home') {
    checkButtonPressed(); // Check if any theme button was pressed
  } else if (gameState === 'gameOver') {
    resetGame(); // Reset game to initial state
    gameState = 'home'; // Return to home screen
  } else {
    if (!gameStarted) {
      gameStarted = true; // Start the game
    }
  }
}

// Function to check if a theme button was pressed and change the game state accordingly
function checkButtonPressed() {
  const buttonWidth = 200;
  const buttonHeight = 50;
  const startY = height / 2 - 75;
  const gap = 60;

  // Detect button press based on mouse coordinates
  if (mouseX >= width / 2 - buttonWidth / 2 && mouseX <= width / 2 + buttonWidth / 2) {
    stopAllMusic(); // Stop any currently playing music
    // Check which button was pressed and update the theme and game state
    if (mouseY >= startY && mouseY <= startY + buttonHeight) {
      theme = 'default';
      gameState = 'playing';
      backgroundMusic.loop(); // Start playing default background music
    } else if (mouseY >= startY + gap && mouseY <= startY + gap + buttonHeight) {
      theme = 'marvel';
      gameState = 'playing';
      marvelSoundtrack.loop(); // Start playing Marvel soundtrack
    } else if (mouseY >= startY + 2 * gap && mouseY <= startY + 2 * gap + buttonHeight) {
      theme = 'dc';
      gameState = 'playing';
      dcSoundtrack.loop(); // Start playing DC soundtrack
    }
  }
}

// Function to display the home screen with game title and theme selection buttons
function displayHomeScreen() {
  fill('black');
  textSize(32);
  textAlign(CENTER, CENTER);
  textFont('Georgia');
  text("Musical Brick Breaker Game", width / 2, 100); // Game title
  
  // Display theme selection buttons
  textSize(20);
  const buttonWidth = 200;
  const buttonHeight = 50;
  const startY = height / 2 - 75;
  const gap = 60;

  // Default theme button
  stroke(0);
  strokeWeight(2);
  fill(255, 0, 0, 200); // Semi-transparent red
  rect(width / 2 - buttonWidth / 2, startY, buttonWidth, buttonHeight, 20); // Rounded corners
  fill(0); // Black text
  noStroke();
  text("Default", width / 2, startY + 28);

  // Marvel theme button
  fill(0, 0, 255, 200); // Semi-transparent blue
  stroke(0);
  strokeWeight(2);
  rect(width / 2 - buttonWidth / 2, startY + gap, buttonWidth, buttonHeight, 20); // Rounded corners
  fill(255); // White text
  noStroke();
  text("Marvel Universe", width / 2, startY + gap + 25);

  // DC theme button
  fill(255, 255, 0, 200); // Semi-transparent yellow
  stroke(0);
  strokeWeight(2);
  rect(width / 2 - buttonWidth / 2, startY + 2 * gap, buttonWidth, buttonHeight, 20); // Rounded corners
  fill(0); // Black text
  noStroke();
  text("DC Universe", width / 2, startY + 2 * gap + 28);
}

// Function to handle gameplay logic
function playGame() {
  ball.update();
  ball.checkCollision();
  ball.display();
  
  paddle.display();
  paddle.update();
  
  // Loop through and display all bricks, check for collisions
  for (let i = bricks.length - 1; i >= 0; i--) {
    bricks[i].display();
    if (ball.hits(bricks[i])) {
      ball.bounce();
      bricks.splice(i, 1); // Remove hit brick from array
      score += 10; // Increase score
    }
  }
  
  // Check if game is over (no bricks left)
  if (bricks.length === 0) {
    gameState = 'gameOver';
  }
  
  // Display current score
  fill('rgb(2,46,82)');
  textSize(20);
  textAlign(LEFT);
  text("Score: " + score, 20, 30);
}

// Function to display game over screen
function displayGameOver() {
  fill('rgb(24,21,21)');
  textSize(32);
  textAlign(CENTER, CENTER);
  if (score >= 150) {
   text("Congratss! Score: " + score, width / 2, height / 2+50);
   textSize(18);
   text("You mastered this universe, click to win others", width / 2, height / 2 + 90);
  }
  else{
  text("Game Over!! Score: " + score, width / 2, height / 2+50);
  textSize(20);
  text("Click to return Home", width / 2, height / 2 + 90);
  }
}

// Function to reset the game to initial state
function resetGame() {
  gameStarted = false;
  score = 0;
  ball.reset();
  createBricks(); // Re-initialize bricks
}

// Function to stop all music tracks
function stopAllMusic() {
  backgroundMusic.stop();
  marvelSoundtrack.stop();
  dcSoundtrack.stop();
}

// Function to initialize bricks based on row and column counts
function createBricks() {
  bricks = [];
  for (let c = 0; c < brickColumnCount; c++) {
    for (let r = 0; r < brickRowCount; r++) {
      let brickX = c * (brickWidth + brickPadding) + brickOffsetLeft;
      let brickY = r * (brickHeight + brickPadding) + brickOffsetTop;
      bricks.push(new Brick(brickX, brickY));
    }
  }
}
// Class representing the ball object
class Ball {
  constructor() {
    this.radius = 10; // Radius of the ball
    this.reset(); // Reset ball position and speed
  }

  // Reset ball position and speed
  reset() {
    this.x = width / 2; // Initial x position at the center of the canvas
    this.y = paddle.y - 10; // Initial y position above the paddle
    this.speedX = 5; // Initial speed along the x-axis
    this.speedY = -5; // Initial speed along the y-axis
  }

  // Update ball position
  update() {
    this.x += this.speedX; // Update x position
    this.y += this.speedY; // Update y position
    // Bounce off the sides of the canvas
    if (this.x < this.radius || this.x > width - this.radius) {
      this.speedX *= -1; // Reverse speed along the x-axis
    }
    // Bounce off the top of the canvas
    if (this.y < this.radius) {
      this.speedY *= -1; // Reverse speed along the y-axis
    } 
    // Check if ball falls below the canvas
    else if (this.y > height - this.radius) {
      gameState = 'gameOver'; // Set game state to 'gameOver'
    }
  }

  // Check collision with the paddle
  checkCollision() {
    // Check if ball collides with paddle
    if (this.x > paddle.x && this.x < paddle.x + paddle.width && this.y + this.radius > paddle.y) {
      this.speedY *= -1; // Reverse speed along the y-axis
      paddleHitSound.play(); // Play paddle hit sound
    }
  }

  // Display the ball
  display() {
    fill(255, 0, 0); // Red fill color
    stroke(0); // Black thin outline
    strokeWeight(1); // Thin outline weight
    ellipse(this.x, this.y, this.radius * 2); // Draw ball as circle
  }

  // Bounce the ball (reverse speed along y-axis)
  bounce() {
    this.speedY *= -1; // Reverse speed along the y-axis
    ballHitSound.play(); // Play ball hit sound
  }

  // Check if ball hits a brick
  hits(brick) {
    // Calculate distance between ball center and brick center
    let distX = Math.abs(this.x - brick.x - brick.width / 2);
    let distY = Math.abs(this.y - brick.y - brick.height / 2);
    // Check if distance is less than combined radii of ball and brick
    if (distX > (brick.width / 2 + this.radius) || distY > (brick.height / 2 + this.radius)) {
      return false; // No collision
    }
    return true; // Collision detected
  }
}

// Class representing the paddle object
class Paddle {
  constructor() {
    this.width = 100; // Width of the paddle
    this.height = 20; // Height of the paddle
    this.x = (width - this.width) / 2; // Initial x position at the center of the canvas
    this.y = height - 35; // Initial y position near the bottom of the canvas
    this.speed = 10; // Speed of the paddle
  }

  // Display the paddle
  display() {
    fill(0, 0, 255); // Blue fill color
    stroke(0); // Black thin outline
    strokeWeight(1); // Thin outline weight
    rect(this.x, this.y, this.width, this.height); // Draw paddle as rectangle
  }

  // Update paddle position based on keyboard input
  update() {
    // Move paddle left
    if (keyIsDown(LEFT_ARROW) && this.x > 0) {
      this.x -= this.speed;
    } 
    // Move paddle right
    else if (keyIsDown(RIGHT_ARROW) && this.x < width - this.width) {
      this.x += this.speed;
    }
  }
}

// Class representing a brick object
class Brick {
  constructor(x, y) {
    this.x = x; // x position of the brick
    this.y = y; // y position of the brick
    this.width = brickWidth; // Width of the brick
    this.height = brickHeight; // Height of the brick
  }

  // Display the brick
  display() {
    fill(200, 50, 50); // Red fill color
    stroke(0); // Black thin outline
    strokeWeight(1); // Thin outline weight
    rect(this.x, this.y, this.width, this.height); // Draw brick as rectangle
  }
}


 


Reflection: Ideas added to this Final Midterm

1) User Experience: I’ve incorporated different themes so that the user won’t be limited to play in one environment. In addition, all the three themes have their separate backgrounds and soundtracks.

2) End Card Message:  I’ve implemented a conditional statement to display “Congrats” message if the user wins and “Game Over” message if the player losses along with the displaying scores on a side.

3) Immersive Audio Design: To ensure that the soundtrack and the paddle hit sound doesn’t overlap, I first reduced the volume (decibels) of the soundtrack using Audacity software before importing it into P5.js.

4) Areas I can Improve Further: Firstly, I aim to introduce difficulty levels to enhance the game’s complexity. Currently, players have three themes to choose from. By adding difficulty levels, players would face nine different variations (3 themes x 3 levels), offering a certain complex gaming experience.

Secondly, I plan to implement full-screen gameplay. However, attempting to switch to full-screen mode has caused disruptions in the game’s initial phase. It has affected the ball’s movement, needed adjustments in the number of bricks, required an increase in paddle size, and led to a decline in background quality. Additionally, P5.js has been slow in loading and executing the code, posing further challenges.

Conclusion: I’m very much satisfied with the output of my game, I never imagined I can implement complex algorithms and great features of P5.js programming: OOPS and Functions to recreate my favorite childhood game. For further learning experience, I’ll try to incorporate the changes I mentioned earlier.

Reference: Used Tools/ Software
1) Dall-E for background images : https://openai.com/dall-e-3
2) Audacity Software for sound editing. https://www.audacityteam.org
3) Coding Garder (Youtube Channel): https://www.youtube.com/watch?v=3GLirU3SkDM

Leave a Reply