# Paddle Game Template (2D Breakout) A complete step-by-step guide for building a 2D Breakout game with pure JavaScript and the HTML5 Canvas API. This template walks through every stage of development, from setting up the canvas to implementing a lives system and polished game loop. **What you will build:** A classic breakout/paddle game where the player controls a paddle to bounce a ball and destroy a field of bricks, with score tracking, win/lose conditions, keyboard and mouse controls, and a lives system. **Prerequisites:** Basic to intermediate JavaScript knowledge and familiarity with HTML. **Source:** Based on the [MDN 2D Breakout Game Tutorial](https://developer.mozilla.org/en-US/docs/Games/Tutorials/2D_Breakout_game_pure_JavaScript). --- ## Step 1: Create the Canvas and Draw on It The first step is setting up the HTML document with a `` element and learning to draw basic shapes using the 2D rendering context. ### HTML Structure Create your base HTML file with an embedded canvas element: ```html Gamedev Canvas Workshop ``` ### Getting the Canvas Reference and 2D Context The canvas element provides a drawing surface. You access it through a 2D rendering context: ```javascript const canvas = document.getElementById("myCanvas"); const ctx = canvas.getContext("2d"); ``` - `canvas` is a reference to the HTML `` element. - `ctx` is the 2D rendering context object, which provides all drawing methods. ### Drawing a Filled Rectangle Use `rect()` to define a rectangle and `fill()` to render it: ```javascript ctx.beginPath(); ctx.rect(20, 40, 50, 50); ctx.fillStyle = "red"; ctx.fill(); ctx.closePath(); ``` - The first two parameters (`20, 40`) set the top-left corner coordinates. - The second two parameters (`50, 50`) set the width and height. - `fillStyle` sets the fill color. - `fill()` renders the shape as a solid fill. ### Drawing a Circle Use `arc()` to define a circle: ```javascript ctx.beginPath(); ctx.arc(240, 160, 20, 0, Math.PI * 2, false); ctx.fillStyle = "green"; ctx.fill(); ctx.closePath(); ``` - `240, 160` -- center x, y coordinates. - `20` -- radius. - `0` -- start angle (radians). - `Math.PI * 2` -- end angle (full circle). - `false` -- draw clockwise. ### Drawing a Stroked Rectangle (Outline Only) Use `stroke()` instead of `fill()` for outlines, and `strokeStyle` for outline color: ```javascript ctx.beginPath(); ctx.rect(160, 10, 100, 40); ctx.strokeStyle = "rgb(0 0 255 / 50%)"; ctx.stroke(); ctx.closePath(); ``` - Uses an RGB color with 50% alpha transparency. - `stroke()` draws only the outline, not a solid fill. ### Key Methods Reference | Method | Purpose | |--------|---------| | `beginPath()` | Start a new drawing path | | `closePath()` | Close the current path | | `rect(x, y, width, height)` | Define a rectangle | | `arc(x, y, radius, startAngle, endAngle, counterclockwise)` | Define a circle or arc | | `fillStyle` | Set the fill color | | `fill()` | Fill the shape with the fill color | | `strokeStyle` | Set the stroke (outline) color | | `stroke()` | Draw an outline of the shape | ### Complete Code for Step 1 ```html ``` --- ## Step 2: Move the Ball Now we animate the ball by creating a game loop that redraws the canvas on each frame and updates the ball position using velocity variables. ### Creating the Draw Loop Define a `draw()` function that executes repeatedly using `setInterval`: ```javascript function draw() { // drawing code } setInterval(draw, 10); ``` `setInterval(draw, 10)` calls the `draw` function every 10 milliseconds, creating approximately 100 frames per second. ### Drawing the Ball Inside the `draw()` function, draw a ball (circle) at a fixed position: ```javascript ctx.beginPath(); ctx.arc(50, 50, 10, 0, Math.PI * 2); ctx.fillStyle = "#0095DD"; ctx.fill(); ctx.closePath(); ``` ### Adding Position Variables Instead of hardcoded positions, use variables so we can update them each frame. Place these above the `draw()` function: ```javascript let x = canvas.width / 2; let y = canvas.height - 30; ``` This starts the ball at the horizontal center, near the bottom of the canvas. ### Adding Velocity Variables Define speed and direction for horizontal (`dx`) and vertical (`dy`) movement: ```javascript let dx = 2; let dy = -2; ``` - `dx = 2` moves the ball 2 pixels right per frame. - `dy = -2` moves the ball 2 pixels up per frame (negative y is upward on canvas). ### Updating Position Each Frame Add position updates at the end of the `draw()` function: ```javascript x += dx; y += dy; ``` ### Clearing the Canvas Without clearing, the ball leaves a trail. Add `clearRect()` at the start of each frame: ```javascript ctx.clearRect(0, 0, canvas.width, canvas.height); ``` ### Refactoring Into a Separate drawBall() Function For clean, maintainable code, separate the ball-drawing logic: ```javascript function drawBall() { ctx.beginPath(); ctx.arc(x, y, 10, 0, Math.PI * 2); ctx.fillStyle = "#0095DD"; ctx.fill(); ctx.closePath(); } ``` ### Complete Code for Step 2 ```javascript const canvas = document.getElementById("myCanvas"); const ctx = canvas.getContext("2d"); let x = canvas.width / 2; let y = canvas.height - 30; let dx = 2; let dy = -2; function drawBall() { ctx.beginPath(); ctx.arc(x, y, 10, 0, Math.PI * 2); ctx.fillStyle = "#0095DD"; ctx.fill(); ctx.closePath(); } function draw() { ctx.clearRect(0, 0, canvas.width, canvas.height); drawBall(); x += dx; y += dy; } setInterval(draw, 10); ``` **Key concepts:** - **Animation loop**: `setInterval(draw, 10)` continuously redraws the scene. - **Position variables**: `x` and `y` track the ball's current location. - **Velocity variables**: `dx` and `dy` determine movement per frame. - **Canvas clearing**: `clearRect()` removes the previous frame before drawing the new one. --- ## Step 3: Bounce Off the Walls We add collision detection so the ball bounces off the canvas edges instead of disappearing. ### Defining the Ball Radius Extract the ball radius into a named constant for reuse in collision calculations: ```javascript const ballRadius = 10; ``` Update `drawBall()` to use this variable: ```javascript function drawBall() { ctx.beginPath(); ctx.arc(x, y, ballRadius, 0, Math.PI * 2); ctx.fillStyle = "#0095DD"; ctx.fill(); ctx.closePath(); } ``` ### Basic Wall Collision (Without Radius Adjustment) The simplest approach checks if the next ball position goes beyond the canvas boundaries: ```javascript // Left and right walls if (x + dx > canvas.width || x + dx < 0) { dx = -dx; } // Top and bottom walls if (y + dy > canvas.height || y + dy < 0) { dy = -dy; } ``` Reversing `dx` or `dy` (multiplying by -1) changes the ball's direction. ### Improved Collision (Accounting for Ball Radius) The basic version lets the ball sink halfway into the wall before bouncing. To fix this, account for the ball's radius: ```javascript // Left and right walls if (x + dx > canvas.width - ballRadius || x + dx < ballRadius) { dx = -dx; } // Top and bottom walls if (y + dy > canvas.height - ballRadius || y + dy < ballRadius) { dy = -dy; } ``` ### Collision Detection Conditions | Wall | Condition | Action | |------|-----------|--------| | **Left** | `x + dx < ballRadius` | `dx = -dx` | | **Right** | `x + dx > canvas.width - ballRadius` | `dx = -dx` | | **Top** | `y + dy < ballRadius` | `dy = -dy` | | **Bottom** | `y + dy > canvas.height - ballRadius` | `dy = -dy` | ### Complete Code for Step 3 ```javascript const canvas = document.getElementById("myCanvas"); const ctx = canvas.getContext("2d"); const ballRadius = 10; let x = canvas.width / 2; let y = canvas.height - 30; let dx = 2; let dy = -2; function drawBall() { ctx.beginPath(); ctx.arc(x, y, ballRadius, 0, Math.PI * 2); ctx.fillStyle = "#0095DD"; ctx.fill(); ctx.closePath(); } function draw() { ctx.clearRect(0, 0, canvas.width, canvas.height); drawBall(); // Collision detection - left and right walls if (x + dx > canvas.width - ballRadius || x + dx < ballRadius) { dx = -dx; } // Collision detection - top and bottom walls if (y + dy > canvas.height - ballRadius || y + dy < ballRadius) { dy = -dy; } x += dx; y += dy; } setInterval(draw, 10); ``` --- ## Step 4: Paddle and Keyboard Controls Now we add a player-controlled paddle at the bottom of the screen and wire up keyboard input (left/right arrow keys). ### Defining Paddle Variables ```javascript const paddleHeight = 10; const paddleWidth = 75; let paddleX = (canvas.width - paddleWidth) / 2; ``` - `paddleHeight` and `paddleWidth` define the paddle dimensions. - `paddleX` starts the paddle centered horizontally. It is a `let` because it will change as the player moves it. ### Drawing the Paddle Create a `drawPaddle()` function. The paddle sits at the very bottom of the canvas: ```javascript function drawPaddle() { ctx.beginPath(); ctx.rect(paddleX, canvas.height - paddleHeight, paddleWidth, paddleHeight); ctx.fillStyle = "#0095DD"; ctx.fill(); ctx.closePath(); } ``` - The y-position is `canvas.height - paddleHeight`, placing it flush with the bottom edge. ### Keyboard State Variables Track whether arrow keys are currently pressed: ```javascript let rightPressed = false; let leftPressed = false; ``` ### Event Listeners for Key Presses Register handlers for `keydown` (key pressed) and `keyup` (key released): ```javascript document.addEventListener("keydown", keyDownHandler); document.addEventListener("keyup", keyUpHandler); ``` ### Key Handler Functions Set the boolean flags based on which key is pressed or released: ```javascript function keyDownHandler(e) { if (e.key === "Right" || e.key === "ArrowRight") { rightPressed = true; } else if (e.key === "Left" || e.key === "ArrowLeft") { leftPressed = true; } } function keyUpHandler(e) { if (e.key === "Right" || e.key === "ArrowRight") { rightPressed = false; } else if (e.key === "Left" || e.key === "ArrowLeft") { leftPressed = false; } } ``` Both `"ArrowRight"` (modern browsers) and `"Right"` (legacy IE/Edge) are checked for compatibility. ### Paddle Movement Logic (With Boundary Checking) Add this inside the `draw()` function to move the paddle based on key state, while keeping it within canvas bounds: ```javascript if (rightPressed) { paddleX = Math.min(paddleX + 7, canvas.width - paddleWidth); } else if (leftPressed) { paddleX = Math.max(paddleX - 7, 0); } ``` - The paddle moves 7 pixels per frame. - `Math.min` prevents the paddle from going past the right edge. - `Math.max` prevents it from going past the left edge. ### Complete Code for Step 4 ```javascript const canvas = document.getElementById("myCanvas"); const ctx = canvas.getContext("2d"); const ballRadius = 10; let x = canvas.width / 2; let y = canvas.height - 30; let dx = 2; let dy = -2; const paddleHeight = 10; const paddleWidth = 75; let paddleX = (canvas.width - paddleWidth) / 2; let rightPressed = false; let leftPressed = false; document.addEventListener("keydown", keyDownHandler); document.addEventListener("keyup", keyUpHandler); function keyDownHandler(e) { if (e.key === "Right" || e.key === "ArrowRight") { rightPressed = true; } else if (e.key === "Left" || e.key === "ArrowLeft") { leftPressed = true; } } function keyUpHandler(e) { if (e.key === "Right" || e.key === "ArrowRight") { rightPressed = false; } else if (e.key === "Left" || e.key === "ArrowLeft") { leftPressed = false; } } function drawBall() { ctx.beginPath(); ctx.arc(x, y, ballRadius, 0, Math.PI * 2); ctx.fillStyle = "#0095DD"; ctx.fill(); ctx.closePath(); } function drawPaddle() { ctx.beginPath(); ctx.rect(paddleX, canvas.height - paddleHeight, paddleWidth, paddleHeight); ctx.fillStyle = "#0095DD"; ctx.fill(); ctx.closePath(); } function draw() { ctx.clearRect(0, 0, canvas.width, canvas.height); drawBall(); drawPaddle(); if (x + dx > canvas.width - ballRadius || x + dx < ballRadius) { dx = -dx; } if (y + dy > canvas.height - ballRadius || y + dy < ballRadius) { dy = -dy; } if (rightPressed) { paddleX = Math.min(paddleX + 7, canvas.width - paddleWidth); } else if (leftPressed) { paddleX = Math.max(paddleX - 7, 0); } x += dx; y += dy; } setInterval(draw, 10); ``` --- ## Step 5: Game Over We replace the bottom-wall bounce with actual game logic: the ball should bounce off the paddle, but if it misses, it is game over. ### Storing the Interval Reference To stop the game loop on game over, store the interval ID: ```javascript let interval = 0; ``` Then assign the return value of `setInterval`: ```javascript interval = setInterval(draw, 10); ``` ### Implementing Game Over and Paddle Collision Replace the bottom-wall collision check. Instead of bouncing off the bottom edge, we now check whether the ball hits the paddle or misses it: ```javascript if (y + dy < ballRadius) { // Ball hits top wall -- bounce dy = -dy; } else if (y + dy > canvas.height - ballRadius) { // Ball reaches bottom edge if (x > paddleX && x < paddleX + paddleWidth) { // Ball hits paddle -- bounce dy = -dy; } else { // Ball missed the paddle -- game over alert("GAME OVER"); document.location.reload(); clearInterval(interval); } } ``` **How paddle collision works:** - `x > paddleX` -- the ball is past the paddle's left edge. - `x < paddleX + paddleWidth` -- the ball is before the paddle's right edge. - If both are true, the ball is above the paddle, so it bounces. - If the ball reaches the bottom without hitting the paddle, the game ends. ### Complete Code for Step 5 ```javascript const canvas = document.getElementById("myCanvas"); const ctx = canvas.getContext("2d"); const ballRadius = 10; let x = canvas.width / 2; let y = canvas.height - 30; let dx = 2; let dy = -2; const paddleHeight = 10; const paddleWidth = 75; let paddleX = (canvas.width - paddleWidth) / 2; let rightPressed = false; let leftPressed = false; let interval = 0; document.addEventListener("keydown", keyDownHandler); document.addEventListener("keyup", keyUpHandler); function keyDownHandler(e) { if (e.key === "Right" || e.key === "ArrowRight") { rightPressed = true; } else if (e.key === "Left" || e.key === "ArrowLeft") { leftPressed = true; } } function keyUpHandler(e) { if (e.key === "Right" || e.key === "ArrowRight") { rightPressed = false; } else if (e.key === "Left" || e.key === "ArrowLeft") { leftPressed = false; } } function drawBall() { ctx.beginPath(); ctx.arc(x, y, ballRadius, 0, Math.PI * 2); ctx.fillStyle = "#0095DD"; ctx.fill(); ctx.closePath(); } function drawPaddle() { ctx.beginPath(); ctx.rect(paddleX, canvas.height - paddleHeight, paddleWidth, paddleHeight); ctx.fillStyle = "#0095DD"; ctx.fill(); ctx.closePath(); } function draw() { ctx.clearRect(0, 0, canvas.width, canvas.height); drawBall(); drawPaddle(); // Left and right wall collision if (x + dx > canvas.width - ballRadius || x + dx < ballRadius) { dx = -dx; } // Top wall collision if (y + dy < ballRadius) { dy = -dy; } else if (y + dy > canvas.height - ballRadius) { // Bottom edge: paddle collision or game over if (x > paddleX && x < paddleX + paddleWidth) { dy = -dy; } else { alert("GAME OVER"); document.location.reload(); clearInterval(interval); } } // Paddle movement if (rightPressed) { paddleX = Math.min(paddleX + 7, canvas.width - paddleWidth); } else if (leftPressed) { paddleX = Math.max(paddleX - 7, 0); } x += dx; y += dy; } interval = setInterval(draw, 10); ``` --- ## Step 6: Build the Brick Field Now we create the grid of bricks that the ball will destroy. The bricks are stored in a 2D array and drawn in rows and columns. ### Brick Configuration Variables Define constants that control the layout of the brick field: ```javascript const brickRowCount = 3; const brickColumnCount = 5; const brickWidth = 75; const brickHeight = 20; const brickPadding = 10; const brickOffsetTop = 30; const brickOffsetLeft = 30; ``` - `brickRowCount` / `brickColumnCount` -- how many rows and columns of bricks. - `brickWidth` / `brickHeight` -- dimensions of each individual brick. - `brickPadding` -- space between bricks. - `brickOffsetTop` / `brickOffsetLeft` -- distance from the top and left canvas edges to the first brick. ### Creating the Bricks 2D Array Use nested loops to create a 2D array. Each brick stores its `x` and `y` position (initially `0`, calculated during drawing): ```javascript const bricks = []; for (let c = 0; c < brickColumnCount; c++) { bricks[c] = []; for (let r = 0; r < brickRowCount; r++) { bricks[c][r] = { x: 0, y: 0 }; } } ``` ### The drawBricks() Function Loop through every brick, calculate its position, store it, and draw it: ```javascript function drawBricks() { for (let c = 0; c < brickColumnCount; c++) { for (let r = 0; r < brickRowCount; r++) { const brickX = c * (brickWidth + brickPadding) + brickOffsetLeft; const brickY = r * (brickHeight + brickPadding) + brickOffsetTop; bricks[c][r].x = brickX; bricks[c][r].y = brickY; ctx.beginPath(); ctx.rect(brickX, brickY, brickWidth, brickHeight); ctx.fillStyle = "#0095DD"; ctx.fill(); ctx.closePath(); } } } ``` **Position calculation formula:** - `brickX = column * (brickWidth + brickPadding) + brickOffsetLeft` - `brickY = row * (brickHeight + brickPadding) + brickOffsetTop` This creates an evenly-spaced grid with consistent padding and margins. ### Calling drawBricks() in the Game Loop Add the call at the beginning of your `draw()` function, after clearing the canvas: ```javascript function draw() { ctx.clearRect(0, 0, canvas.width, canvas.height); drawBricks(); drawBall(); drawPaddle(); // ... rest of draw function } ``` ### Complete Code for Step 6 ```javascript const canvas = document.getElementById("myCanvas"); const ctx = canvas.getContext("2d"); const ballRadius = 10; let x = canvas.width / 2; let y = canvas.height - 30; let dx = 2; let dy = -2; const paddleHeight = 10; const paddleWidth = 75; let paddleX = (canvas.width - paddleWidth) / 2; let rightPressed = false; let leftPressed = false; let interval = 0; const brickRowCount = 3; const brickColumnCount = 5; const brickWidth = 75; const brickHeight = 20; const brickPadding = 10; const brickOffsetTop = 30; const brickOffsetLeft = 30; const bricks = []; for (let c = 0; c < brickColumnCount; c++) { bricks[c] = []; for (let r = 0; r < brickRowCount; r++) { bricks[c][r] = { x: 0, y: 0 }; } } document.addEventListener("keydown", keyDownHandler); document.addEventListener("keyup", keyUpHandler); function keyDownHandler(e) { if (e.key === "Right" || e.key === "ArrowRight") { rightPressed = true; } else if (e.key === "Left" || e.key === "ArrowLeft") { leftPressed = true; } } function keyUpHandler(e) { if (e.key === "Right" || e.key === "ArrowRight") { rightPressed = false; } else if (e.key === "Left" || e.key === "ArrowLeft") { leftPressed = false; } } function drawBall() { ctx.beginPath(); ctx.arc(x, y, ballRadius, 0, Math.PI * 2); ctx.fillStyle = "#0095DD"; ctx.fill(); ctx.closePath(); } function drawPaddle() { ctx.beginPath(); ctx.rect(paddleX, canvas.height - paddleHeight, paddleWidth, paddleHeight); ctx.fillStyle = "#0095DD"; ctx.fill(); ctx.closePath(); } function drawBricks() { for (let c = 0; c < brickColumnCount; c++) { for (let r = 0; r < brickRowCount; r++) { const brickX = c * (brickWidth + brickPadding) + brickOffsetLeft; const brickY = r * (brickHeight + brickPadding) + brickOffsetTop; bricks[c][r].x = brickX; bricks[c][r].y = brickY; ctx.beginPath(); ctx.rect(brickX, brickY, brickWidth, brickHeight); ctx.fillStyle = "#0095DD"; ctx.fill(); ctx.closePath(); } } } function draw() { ctx.clearRect(0, 0, canvas.width, canvas.height); drawBricks(); drawBall(); drawPaddle(); if (x + dx > canvas.width - ballRadius || x + dx < ballRadius) { dx = -dx; } if (y + dy < ballRadius) { dy = -dy; } else if (y + dy > canvas.height - ballRadius) { if (x > paddleX && x < paddleX + paddleWidth) { dy = -dy; } else { alert("GAME OVER"); document.location.reload(); clearInterval(interval); } } if (rightPressed) { paddleX = Math.min(paddleX + 7, canvas.width - paddleWidth); } else if (leftPressed) { paddleX = Math.max(paddleX - 7, 0); } x += dx; y += dy; } interval = setInterval(draw, 10); ``` --- ## Step 7: Collision Detection With bricks on screen, we need to detect when the ball hits one and make it disappear. Each brick gets a `status` property: `1` means visible, `0` means destroyed. ### Adding the Status Property to Bricks Update the brick initialization to include a `status` flag: ```javascript const bricks = []; for (let c = 0; c < brickColumnCount; c++) { bricks[c] = []; for (let r = 0; r < brickRowCount; r++) { bricks[c][r] = { x: 0, y: 0, status: 1 }; } } ``` ### The collisionDetection() Function Loop through every brick and check if the ball's center is within the brick's bounding box: ```javascript function collisionDetection() { for (let c = 0; c < brickColumnCount; c++) { for (let r = 0; r < brickRowCount; r++) { const b = bricks[c][r]; if (b.status === 1) { if ( x > b.x && x < b.x + brickWidth && y > b.y && y < b.y + brickHeight ) { dy = -dy; b.status = 0; } } } } } ``` **Collision conditions (all four must be true simultaneously):** - `x > b.x` -- ball center is to the right of the brick's left edge. - `x < b.x + brickWidth` -- ball center is to the left of the brick's right edge. - `y > b.y` -- ball center is below the brick's top edge. - `y < b.y + brickHeight` -- ball center is above the brick's bottom edge. When a collision is detected: - `dy = -dy` reverses the ball's vertical direction (bounce). - `b.status = 0` marks the brick as destroyed. ### Updating drawBricks() to Respect Status Only draw bricks that are still active (`status === 1`): ```javascript function drawBricks() { for (let c = 0; c < brickColumnCount; c++) { for (let r = 0; r < brickRowCount; r++) { if (bricks[c][r].status === 1) { const brickX = c * (brickWidth + brickPadding) + brickOffsetLeft; const brickY = r * (brickHeight + brickPadding) + brickOffsetTop; bricks[c][r].x = brickX; bricks[c][r].y = brickY; ctx.beginPath(); ctx.rect(brickX, brickY, brickWidth, brickHeight); ctx.fillStyle = "#0095DD"; ctx.fill(); ctx.closePath(); } } } } ``` ### Calling collisionDetection() in the Game Loop Add the call in your `draw()` function, after drawing all elements: ```javascript function draw() { ctx.clearRect(0, 0, canvas.width, canvas.height); drawBricks(); drawBall(); drawPaddle(); collisionDetection(); // ... rest of draw function } ``` --- ## Step 8: Track the Score and Win We add a score counter that increments each time a brick is destroyed, and a win condition that triggers when all bricks are gone. ### Initializing the Score ```javascript let score = 0; ``` ### The drawScore() Function Display the current score on the canvas using text rendering: ```javascript function drawScore() { ctx.font = "16px Arial"; ctx.fillStyle = "#0095DD"; ctx.fillText(`Score: ${score}`, 8, 20); } ``` - `ctx.font` sets the font size and family (like CSS). - `ctx.fillText(text, x, y)` renders text at the given coordinates. - Position `(8, 20)` places the score in the top-left corner. ### Incrementing the Score In the `collisionDetection()` function, increment the score when a brick is hit: ```javascript dy = -dy; b.status = 0; score++; ``` ### Adding the Win Condition After incrementing the score, check if the player has destroyed all bricks: ```javascript score++; if (score === brickRowCount * brickColumnCount) { alert("YOU WIN, CONGRATULATIONS!"); document.location.reload(); clearInterval(interval); } ``` The total number of bricks is `brickRowCount * brickColumnCount`. When the score reaches that number, every brick has been destroyed. ### Complete collisionDetection() with Score and Win ```javascript function collisionDetection() { for (let c = 0; c < brickColumnCount; c++) { for (let r = 0; r < brickRowCount; r++) { const b = bricks[c][r]; if (b.status === 1) { if ( x > b.x && x < b.x + brickWidth && y > b.y && y < b.y + brickHeight ) { dy = -dy; b.status = 0; score++; if (score === brickRowCount * brickColumnCount) { alert("YOU WIN, CONGRATULATIONS!"); document.location.reload(); clearInterval(interval); } } } } } } ``` ### Calling drawScore() in the Game Loop Add the call in your `draw()` function: ```javascript function draw() { ctx.clearRect(0, 0, canvas.width, canvas.height); drawBricks(); drawBall(); drawPaddle(); drawScore(); collisionDetection(); // ... rest of draw function } ``` ### Canvas Text Methods Reference | Method/Property | Purpose | |-----------------|---------| | `ctx.font` | Set font size and family | | `ctx.fillStyle` | Set text color | | `ctx.fillText(text, x, y)` | Draw filled text at coordinates | --- ## Step 9: Mouse Controls In addition to keyboard controls, we add mouse support so the player can move the paddle by moving the mouse. ### Adding the mousemove Event Listener Register the handler alongside the existing keyboard listeners: ```javascript document.addEventListener("mousemove", mouseMoveHandler); ``` ### The mouseMoveHandler Function Calculate the mouse's horizontal position relative to the canvas and update the paddle position: ```javascript function mouseMoveHandler(e) { const relativeX = e.clientX - canvas.offsetLeft; if (relativeX > 0 && relativeX < canvas.width) { paddleX = relativeX - paddleWidth / 2; } } ``` **How it works:** - `e.clientX` -- the mouse's horizontal position in the browser viewport. - `canvas.offsetLeft` -- the distance from the canvas's left edge to the viewport's left edge. - `relativeX` -- the mouse position relative to the canvas (not the viewport). - The boundary check (`relativeX > 0 && relativeX < canvas.width`) ensures the paddle only moves when the mouse is over the canvas. - `paddleX = relativeX - paddleWidth / 2` centers the paddle under the mouse cursor by subtracting half the paddle width. ### Complete Event Listener Setup (Keyboard + Mouse) ```javascript document.addEventListener("keydown", keyDownHandler); document.addEventListener("keyup", keyUpHandler); document.addEventListener("mousemove", mouseMoveHandler); ``` Both control methods work simultaneously. The player can use arrow keys or mouse -- or switch between them at any time. --- ## Step 10: Finishing Up The final step adds a lives system (so the player gets multiple chances) and upgrades the game loop from `setInterval` to `requestAnimationFrame` for smoother rendering. ### Adding the Lives Variable ```javascript let lives = 3; ``` ### The drawLives() Function Display the remaining lives in the top-right corner: ```javascript function drawLives() { ctx.font = "16px Arial"; ctx.fillStyle = "#0095DD"; ctx.fillText(`Lives: ${lives}`, canvas.width - 65, 20); } ``` ### Implementing the Lives System Replace the immediate game-over logic with a lives-based system. When the ball misses the paddle: ```javascript if (y + dy < ballRadius) { dy = -dy; } else if (y + dy > canvas.height - ballRadius) { if (x > paddleX && x < paddleX + paddleWidth) { dy = -dy; } else { lives--; if (!lives) { alert("GAME OVER"); document.location.reload(); } else { // Reset ball and paddle positions x = canvas.width / 2; y = canvas.height - 30; dx = 2; dy = -2; paddleX = (canvas.width - paddleWidth) / 2; } } } ``` **What happens when a life is lost:** - `lives--` decrements the lives counter. - If `lives` reaches `0`, the game ends with an alert and page reload. - Otherwise, the ball resets to center-bottom, velocity resets, and the paddle resets to center. ### Upgrading to requestAnimationFrame Replace `setInterval` with `requestAnimationFrame` for a smoother, browser-optimized game loop: **Old approach (remove):** ```javascript interval = setInterval(draw, 10); ``` **New approach:** Add `requestAnimationFrame(draw)` at the end of the `draw()` function: ```javascript function draw() { // ... all drawing and logic ... requestAnimationFrame(draw); } // Start the game by calling draw() once: draw(); ``` `requestAnimationFrame` lets the browser schedule rendering at the optimal frame rate (typically 60fps), which is more efficient than a fixed 10ms interval. ### Calling drawLives() in the Game Loop ```javascript function draw() { ctx.clearRect(0, 0, canvas.width, canvas.height); drawBricks(); drawBall(); drawPaddle(); drawScore(); drawLives(); collisionDetection(); // ... rest of logic ... requestAnimationFrame(draw); } ``` --- ## Complete Final Game Code Below is the entire game in a single, self-contained HTML file. This is the final product of all 10 steps combined. ```html 2D Breakout Game ``` --- ## Quick Reference: All Game Variables | Variable | Type | Purpose | |----------|------|---------| | `canvas` | const | Reference to the HTML canvas element | | `ctx` | const | 2D rendering context | | `ballRadius` | const | Radius of the ball (10) | | `x`, `y` | let | Current ball position | | `dx`, `dy` | let | Ball velocity (pixels per frame) | | `paddleHeight` | const | Height of the paddle (10) | | `paddleWidth` | const | Width of the paddle (75) | | `paddleX` | let | Current horizontal position of the paddle | | `rightPressed` | let | Whether the right arrow key is held down | | `leftPressed` | let | Whether the left arrow key is held down | | `brickRowCount` | const | Number of brick rows (3) | | `brickColumnCount` | const | Number of brick columns (5) | | `brickWidth` | const | Width of each brick (75) | | `brickHeight` | const | Height of each brick (20) | | `brickPadding` | const | Space between bricks (10) | | `brickOffsetTop` | const | Distance from top of canvas to first brick row (30) | | `brickOffsetLeft` | const | Distance from left of canvas to first brick column (30) | | `bricks` | const | 2D array holding all brick objects | | `score` | let | Current player score | | `lives` | let | Remaining lives (starts at 3) | ## Quick Reference: All Functions | Function | Purpose | |----------|---------| | `keyDownHandler(e)` | Sets `rightPressed` or `leftPressed` to `true` on key press | | `keyUpHandler(e)` | Sets `rightPressed` or `leftPressed` to `false` on key release | | `mouseMoveHandler(e)` | Moves paddle to follow mouse horizontal position | | `collisionDetection()` | Checks ball against all active bricks; destroys hit bricks, increments score, checks win | | `drawBall()` | Renders the ball at current `(x, y)` position | | `drawPaddle()` | Renders the paddle at current `paddleX` position | | `drawBricks()` | Renders all bricks with `status === 1` | | `drawScore()` | Renders the score text in the top-left corner | | `drawLives()` | Renders the lives text in the top-right corner | | `draw()` | Main game loop: clears canvas, draws everything, handles collisions, updates positions |