Create a Next.js Breakout Game

Eugene Musebe

Introduction

This article how Nextjs can be used to create a simple breakout game.

Codesandbox

The final version of this project can be viewed on Codesandbox.

You can find the full source code on my Github repo.

Prerequisites

Basic/entry-level knowledge and understanding of javascript and React/Nextjs.

Setting Up the Sample Project

Create your project root directory: npx create-next-app breakoutgame

Enter the directory: cd breakoutgame

In our game, we involve Cloudinary for the game's online storage feature. The site is where we store the final score.

Include Cloudinary in your project dependencies: npm install cloudinary

Use this link to create or log into your Cloudinary account. You will be provided with a dashboard containing the necessary environment variables for integration.

In your root directory, create a new file named .env.local and use the following guide to fill your dashboard's variables.

1"pages/api/upload.js"
2
3
4CLOUDINARY_CLOUD_NAME =
5
6CLOUDINARY_API_KEY =
7
8CLOUDINARY_API_SECRET=

Restart your project: npm run dev.

Create another directory pages/api/upload.js.

Configure the environment keys and libraries.

1"pages/api/upload.js"
2
3
4var cloudinary = require("cloudinary").v2;
5
6cloudinary.config({
7 cloud_name: process.env.CLOUDINARY_NAME,
8 api_key: process.env.CLOUDINARY_API_KEY,
9 api_secret: process.env.CLOUDINARY_API_SECRET,
10});

Finally, add a handler function to execute Nextjs post request:

1"pages/api/upload.js"
2
3
4export default async function handler(req, res) {
5 if (req.method === "POST") {
6 let url = ""
7 try {
8 let fileStr = req.body.data;
9 const uploadedResponse = await cloudinary.uploader.upload(
10 fileStr,
11 {
12 resource_type: "video",
13 chunk_size: 6000000,
14 }
15 );
16 } catch (error) {
17 res.status(500).json({ error: "Something wrong" });
18 }
19
20 res.status(200).json("backend complete");
21 }
22}

The above function will upload the request body containing media files to Cloudinary and return the file's Cloudinary link as a response

We can now work on our front end.

Start by importing the necessary hooks in your pages/index directory:

1"pages/index"
2
3import React, { useRef, useState, useEffect } from 'react';

Declare the following variables inside the Home function. We will use them as we move on

1let rulesBtn, closeBtn, rules, canvas, ctx, ball, paddle, brickInfo, bricks, animationID;
2
3 let score = 0;
4
5 const brickRowCount = 9;
6 const brickColumnCount = 5;
7 const delay = 500; //delay to reset the game

Paste the following code in the Home function return statement. Don't worry about the undefined functions. We add them as we move on. Trace the css file from the Github repo.

1return (
2 <div className="container">
3 <div id="rules" className="rules">
4 <h2>How To Play:</h2>
5 <p>
6 Use your right and left keys to move the paddle to bounce the ball up
7 and break the blocks.
8 </p>
9 <p>If you miss the ball, your score and the blocks will reset.</p>
10 </div>
11 <div className="row">
12 <div className="column">
13 <canvas id="canvas" width="800" height="600"></canvas>
14 <button onClick={startGame}>Start Game</button>
15 </div>
16 </div>
17 </div>
18 )

Create a useEffect hook, start by refferencing the necessary DOM element. We will also assign the canvas context to the variable ctx. useEffect(() => { rules = document.getElementById('rules'); canvas = document.getElementById('canvas'); ctx = canvas.getContext('2d');

},[])

Everything about the game will be drawn inside the canvas. We however must first create the game components. We need to draw a ball, a paddle, a single brick as well as several brick blocks. Add the mentioned component's props to the useEffect hook.

1// Create ball props
2 ball = {
3 x: canvas.width / 2,
4 y: canvas.height / 2,
5 size: 10,
6 speed: 4,
7 dx: 4,
8 dy: -4,
9 visible: true
10 };
11
12 // Create paddle props
13 paddle = {
14 x: canvas.width / 2 - 40,
15 y: canvas.height - 20,
16 w: 80,
17 h: 10,
18 speed: 8,
19 dx: 0,
20 visible: true
21 };
22
23 // Create brick props
24 brickInfo = {
25 w: 70,
26 h: 20,
27 padding: 10,
28 offsetX: 45,
29 offsetY: 60,
30 visible: true
31 };
32
33 // Create bricks
34 bricks = [];
35 for (let i = 0; i < brickRowCount; i++) {
36 bricks[i] = [];
37 for (let j = 0; j < brickColumnCount; j++) {
38 const x = i * (brickInfo.w + brickInfo.padding) + brickInfo.offsetX;
39 const y = j * (brickInfo.h + brickInfo.padding) + brickInfo.offsetY;
40 bricks[i][j] = { x, y, ...brickInfo };
41 }
42 }

Create the functions to draw the components.

1// Draw ball
2 const drawBall = () => {
3 ctx.beginPath();
4 ctx.arc(ball.x, ball.y, ball.size, 0, Math.PI * 2);
5 ctx.fillStyle = ball.visible ? '#0095dd' : 'transparent';
6 ctx.fill();
7 ctx.closePath();
8 }
9
10 // Draw Paddle
11 const drawPaddle = () => {
12 ctx.beginPath();
13 ctx.rect(paddle.x, paddle.y, paddle.w, paddle.h);
14 ctx.fillStyle = paddle.visible ? '#0095dd' : 'transparent';
15 ctx.fill();
16 ctx.closePath();
17 }
18
19 // Draw Bricks
20 const drawBricks = () => {
21 bricks.forEach(column => {
22 column.forEach(brick => {
23 ctx.beginPath();
24 ctx.rect(brick.x, brick.y, brick.w, brick.h);
25 ctx.fillStyle = brick.visible ? '#0095dd' : 'transparent';
26 ctx.fill();
27 ctx.closePath();
28 });
29 });
30 }
31
32 // Draw Score
33 function drawScore() {
34 ctx.font = "20px Arial";
35 ctx.fillText(`Score: ${score}`, canvas.width - 100, 30);
36 }

The paddle should be able to move only left and right within the canvas.

1// Move Paddle
2 const movePaddle = () => {
3
4 paddle.x += paddle.dx;
5
6 // Wall detection
7 if (paddle.x + paddle.w > canvas.width) {
8 paddle.x = canvas.width - paddle.w;
9 }
10
11 if (paddle.x < 0) {
12 paddle.x = 0;
13 }
14 }

Use the code below for ball movement. We set up the conditions for ball movement limits both horizontally and vertically and also its contact with the paddle. The player loses when the ball hits the bottom of the canvas. We also need to constantly update the canvas drawing on every frame using a function that runs over and over again. We will achieve this using the inbuilt javascript timing function setInterval. If the user loses, the will be an alert string message and the final canvas shall be sent to the uploadHandler function for download.

1// Move ball
2 const moveBall = () => {
3 ball.x += ball.dx;
4 ball.y += ball.dy;
5
6 // Wall collision (right/left)
7 if (ball.x + ball.size > canvas.width || ball.x - ball.size < 0) {
8 ball.dx *= -1; // ball.dx = ball.dx * -1
9 }
10
11 // Wall collision (top/bottom)
12 if (ball.y + ball.size > canvas.height || ball.y - ball.size < 0) {
13 ball.dy *= -1;
14 }
15
16 // console.log(ball.x, ball.y);
17
18 // Paddle collision
19 if (
20 ball.x - ball.size > paddle.x &&
21 ball.x + ball.size < paddle.x + paddle.w &&
22 ball.y + ball.size > paddle.y
23 ) {
24 ball.dy = -ball.speed;
25 }
26
27 // Brick collision
28 bricks.forEach(column => {
29 column.forEach(brick => {
30 if (brick.visible) {
31 if (
32 ball.x - ball.size > brick.x && // left brick side check
33 ball.x + ball.size < brick.x + brick.w && // right brick side check
34 ball.y + ball.size > brick.y && // top brick side check
35 ball.y - ball.size < brick.y + brick.h // bottom brick side check
36 ) {
37 ball.dy *= -1;
38 brick.visible = false;
39
40 increaseScore();
41 }
42 }
43 });
44 });
45
46 // Hit bottom wall - Lose
47 if (ball.y + ball.size > canvas.height) {
48 showAllBricks();
49 uploadHandler(canvas.toDataURL());
50 alert("GAME OVER! score recorded at Cloudinary")
51 score = 0;
52 }
53}

Add the following code to track and increase your scores:

1const increaseScore = () => {
2 score++;
3
4 if (score % (brickRowCount * brickColumnCount) === 0) {
5
6 ball.visible = false;
7 paddle.visible = false;
8
9 //After 0.5 sec restart the game
10 setTimeout(function () {
11 showAllBricks();
12 score = 0;
13 paddle.x = canvas.width / 2 - 40;
14 paddle.y = canvas.height - 20;
15 ball.x = canvas.width / 2;
16 ball.y = canvas.height / 2;
17 ball.visible = true;
18 paddle.visible = true;
19 }, 3000)
20 }
21}

Add the following function that ensures all the brick column blocks are visible

1const showAllBricks = () => {
2 bricks.forEach(column => {
3 column.forEach(brick => (brick.visible = true));
4 });
5}

The following code draws all the components inside the canvas

1const draw = () => {
2 // clear canvas
3 ctx.clearRect(0, 0, canvas.width, canvas.height);
4
5 drawBall();
6 drawPaddle();
7 drawScore();
8 drawBricks();
9 }

You can now use the code below to run the game

1const startGame = () => {
2 movePaddle();
3 moveBall();
4
5 // Draw everything
6 draw();
7
8 animationID = requestAnimationFrame(startGame);
9 }

However, we haven't configured the game keyboard instructions yet.

Use the following functions for your paddle movement

1const keyDown = (e) => {
2 if (e.key === 'Right' || e.key === 'ArrowRight') {
3 paddle.dx = paddle.speed;
4 } else if (e.key === 'Left' || e.key === 'ArrowLeft') {
5 paddle.dx = -paddle.speed;
6 }
7 }
8
9 const keyUp = (e) => {
10 if (
11 e.key === 'Right' ||
12 e.key === 'ArrowRight' ||
13 e.key === 'Left' ||
14 e.key === 'ArrowLeft'
15 ) {
16 paddle.dx = 0;
17 }
18 }

Remember to include the functions above's event listeners to your useEffect hook.

1// Keyboard event handlers
2 document.addEventListener('keydown', keyDown);
3 document.addEventListener('keyup', keyUp);

Finally, use the code below to handle your backend upload

1const uploadHandler = (base64) => {
2 try {
3 fetch('/api/upload', {
4 method: 'POST',
5 body: JSON.stringify({ data: base64 }),
6 headers: { 'Content-Type': 'application/json' },
7 })
8 .then((response) => response.json())
9 .then((data) => {
10 console.log(data.data);
11 });
12 } catch (error) {
13 console.error(error);
14 }
15}

Your game should look like shown below at this point:

.

That completes the game build. Ensure to go through the article to enjoy the experience.

Eugene Musebe

Software Developer

I’m a full-stack software developer, content creator, and tech community builder based in Nairobi, Kenya. I am addicted to learning new technologies and loves working with like-minded people.