Snake Game using React Hooks
Posted Dec 24, 2022
Welcome to the #react10 Day 5 challenge.
In this challenge we will develop the snake game using react hooks like useState, useEffect, useRef along with the basic JS concepts.
Demo
Get Github Code
Pre-requisites
- Code Editor (like VS Code)
- React basic knowledge (as in this challenge we are not focusing on explaining about the react concepts)
- Basic knowledge of HTML & CSS
Lets Begin!
Open terminal in the vs code and run these commands,
npx create-react-app snake-game
cd snake-game
npm start
After doing all of this, your UI would look like,
Run these commands in a new terminal (let the other terminal run our project),
mkdir src\components\common
type NUL > src\components\Snake.js
type NUL > src\components\Food.js
Don't get confused with the above commands, you can also create files or folders using file explorer manually.
Folder Structure
Note - I have removed few files which 'create-react-app' util gives us by default.
You can also remove those but that is not mandatory step for this challenge.
Lets add some code
For better understanding, I have added few comments in the code itself.
Snake.js
import React from "react";
//snake prop is an array of small boxes having x and y value
// we will set App.css -> position: relative
// here we gave -> position: absolute and top, left values in % to fix each element
// in snake array in the UI
function Snake({ snake }) {
return (
<div>
{snake.map((box, i) => (
<div
style={{
width: "15px",
height: "15px",
backgroundColor: "#e7da3d",
margin: "5px",
position: "absolute",
left: `${box.x}%`,
top: `${box.y}%`,
zIndex: 1,
}}
/>
))}
</div>
);
}
export default Snake;
Food.js
import React from "react";
//food is just a 15x15px box.
//position is an object with x & y property
function Food({ position }) {
return (
<div
style={{
width: "15px",
height: "15px",
backgroundColor: "#3dd1e7",
margin: "3px",
position: "absolute",
left: `${position.x}%`,
top: `${position.y}%`,
zIndex: 0,
}}
/>
);
}
export default Food;
App.js
import { useEffect, useRef, useState } from "react";
import "./App.css";
import Food from "./components/Food";
import Snake from "./components/Snake";
//method to get random x & y number between 0-96 for random food position
const randomFoodPosition = () => {
const pos = { x: 0, y: 0 };
let x = Math.floor(Math.random() * 96);
let y = Math.floor(Math.random() * 96);
pos.x = x - (x % 4); // to get multiple of 4
pos.y = y - (y % 4); // to get multiple of 4
return pos;
};
const initialSnake = {
snake: [
{ x: 0, y: 0 },
{ x: 4, y: 0 },
{ x: 8, y: 0 },
],
direction: "ArrowRight",
speed: 100,
};
function App() {
//snake = [{x,y},{x,y},{x,y},{x,y},...]
const [snake, setSnake] = useState(initialSnake.snake);
const [lastDirection, setLastDirection] = useState(initialSnake.direction);
const [foodPosition, setFoodPosition] = useState(randomFoodPosition);
const [isStarted, setIsStarted] = useState(false);
const [gameOver, setGameOver] = useState(false);
//to set focus on big square html box on game start
const playgroundRef = useRef();
useEffect(() => {
if (!isStarted) return;
//if snake array last element touches the box boundary then game over
if (
snake[snake.length - 1].x === 100 ||
snake[snake.length - 1].x === 0 ||
snake[snake.length - 1].y === 100 ||
snake[snake.length - 1].y === -4
) {
setGameOver(true);
return;
}
//interval needed to continuously move the snake by manipulating snake array item's x & y value
//every 'speed' milliseconds
const interval = setInterval(move, initialSnake.speed);
return () => clearInterval(interval);
});
//method to update snake array's values on keyboard event
const move = () => {
const tmpSnake = [...snake];
let x = tmpSnake[tmpSnake.length - 1].x,
y = tmpSnake[tmpSnake.length - 1].y;
switch (lastDirection) {
case "ArrowUp":
y -= 4; //move by -4% top
break;
case "ArrowRight":
x += 4; //move by 4% right
break;
case "ArrowDown":
y += 4; //move by 4% down
break;
case "ArrowLeft":
x -= 4; //move by -4% left
break;
default:
break;
}
tmpSnake.push({
x,
y,
});
// if food position === snake newly added position, then do not remove item from an array
// otherwise remove everytime to get feel that snake is moving forward
// i.e - add at end and remove at start in snake array
if (x !== foodPosition.x || y !== foodPosition.y) tmpSnake.shift();
else setFoodPosition(randomFoodPosition());
setSnake(tmpSnake);
};
return (
<div
className="App"
onKeyDown={(e) => setLastDirection(e.key)}
ref={playgroundRef}
tabIndex={0}
>
{isStarted && <div className="count"> score: {snake.length - 3}</div>}
{!isStarted && (
<>
<button
onClick={() => {
setIsStarted(true);
playgroundRef.current.focus();
}}
type="submit"
>
Start
</button>
<div className="arrow-msg text">Press Arrows keys to play!</div>
</>
)}
{gameOver && (
<>
<div className="game-over text">Game Over!</div>
<button
onClick={() => {
setIsStarted(true);
setGameOver(false);
setSnake(initialSnake.snake);
setLastDirection(initialSnake.direction);
playgroundRef.current.focus();
}}
type="submit"
>
Restart
</button>
</>
)}
<Snake snake={snake} lastDirection={lastDirection} />
{!gameOver && (
<>
<Food position={foodPosition} />
</>
)}
</div>
);
}
export default App;
App.css
.App {
width: 500px;
height: 500px;
border: 1px solid white;
position: relative;
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
font-family: monospace;
border-radius: 10px;
}
button {
font-family: inherit;
font-size: 1.5rem;
border-radius: 10px;
color: black;
width: 50%;
max-width: 150px;
height: 50px;
border: none;
background-color: #e73d9f;
font-weight: 600;
z-index: 4;
cursor: pointer;
}
.text {
color: #eb2d2d;
z-index: 4;
opacity: 0.5;
}
.arrow-msg {
font-size: 1rem;
margin: 10px;
}
.game-over {
font-size: 3rem;
}
.count {
position: absolute;
font-size: 2rem;
bottom: 0px;
color: #eb2d2d;
opacity: 0.5;
}
index.css
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto",
"Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans",
"Helvetica Neue", sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
width: 100vw;
height: 100vh;
display: flex;
justify-content: center;
align-items: center;
background-color: black;
}
code {
font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New",
monospace;
}
And that's it! You have created the classic snake game using react hooks.
Final UI
Enjoy your Game :)
Your feedback is our favorite notification! Share your thoughts about this page and make us smile.