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"234CLOUDINARY_CLOUD_NAME =56CLOUDINARY_API_KEY =78CLOUDINARY_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"234var cloudinary = require("cloudinary").v2;56cloudinary.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"234export 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 }1920 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"23import 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;23 let score = 0;45 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 up7 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 props2 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: true10 };1112 // Create paddle props13 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: true21 };2223 // Create brick props24 brickInfo = {25 w: 70,26 h: 20,27 padding: 10,28 offsetX: 45,29 offsetY: 60,30 visible: true31 };3233 // Create bricks34 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 ball2 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 }910 // Draw Paddle11 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 }1819 // Draw Bricks20 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 }3132 // Draw Score33 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 Paddle2 const movePaddle = () => {34 paddle.x += paddle.dx;56 // Wall detection7 if (paddle.x + paddle.w > canvas.width) {8 paddle.x = canvas.width - paddle.w;9 }1011 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 ball2 const moveBall = () => {3 ball.x += ball.dx;4 ball.y += ball.dy;56 // 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 * -19 }1011 // Wall collision (top/bottom)12 if (ball.y + ball.size > canvas.height || ball.y - ball.size < 0) {13 ball.dy *= -1;14 }1516 // console.log(ball.x, ball.y);1718 // Paddle collision19 if (20 ball.x - ball.size > paddle.x &&21 ball.x + ball.size < paddle.x + paddle.w &&22 ball.y + ball.size > paddle.y23 ) {24 ball.dy = -ball.speed;25 }2627 // Brick collision28 bricks.forEach(column => {29 column.forEach(brick => {30 if (brick.visible) {31 if (32 ball.x - ball.size > brick.x && // left brick side check33 ball.x + ball.size < brick.x + brick.w && // right brick side check34 ball.y + ball.size > brick.y && // top brick side check35 ball.y - ball.size < brick.y + brick.h // bottom brick side check36 ) {37 ball.dy *= -1;38 brick.visible = false;3940 increaseScore();41 }42 }43 });44 });4546 // Hit bottom wall - Lose47 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++;34 if (score % (brickRowCount * brickColumnCount) === 0) {56 ball.visible = false;7 paddle.visible = false;89 //After 0.5 sec restart the game10 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 canvas3 ctx.clearRect(0, 0, canvas.width, canvas.height);45 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();45 // Draw everything6 draw();78 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 }89 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 handlers2 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.