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