Building a Snake Game App with Next.js
Day 26: Snake Game — 30 Days of 30 Projects Challenge
Hey everyone, I hope you’re all doing well and enjoying your coding journey, Am I right? By the way, today marks the 26th day of the 30-day of 30-projects challenge. Our 26th project will be creating a mini Next.js application — a snake game. Please make sure to clap and comment on this blog post. It motivates me to create more amazing content like this, Let’s get started straight away.
Overview of the Mini Next.js Application
Our Snake Game application allows users to:
- Start, stop, and pause the game
- Interactive gaming theme
- Display the final results, and try again
Tech-Stack Used:
- Next.js: A React framework for building full-stack web applications.
- React: A JavaScript library for building user interfaces.
- Tailwind CSS: A utility-first CSS framework for styling.
- Shadcn UI: Beautifully designed tailwindcss components that you can copy and paste into your application.
- Vercel: For deploying the Nextjs web application.
Initialize a Nextjs project
Start by following this guide to set up the new project.
- Go to the “components” folder.
- Create a new file named “
snake-game.tsx
”. - This file will manage the entire functionality of the project.
We will go through the code step-by-step to make it easy to understand.
Component Breakdown
Import Statements
"use client";
import { useState, useEffect, useCallback, useRef } from "react";
import { Button } from "@/components/ui/button";
import { PauseIcon, PlayIcon, RefreshCcwIcon } from "lucide-react";
- Enables client-side rendering.
- Imports necessary hooks and custom components for building the UI.
Define Game States and Directions
enum GameState {
START,
PAUSE,
RUNNING,
GAME_OVER,
}
enum Direction {
UP,
DOWN,
LEFT,
RIGHT,
}
interface Position {
x: number;
y: number;
}
const initialSnake: Position[] = [{ x: 0, y: 0 }];
const initialFood: Position = { x: 5, y: 5 };
- Defines the game states and directions for the snake movement.
- Defines the
Position
interface and initial state for the snake and food.
Component Definition and Initial State
export default function SnakeGameComponent() {
const [gameState, setGameState] = useState<GameState>(GameState.START);
const [snake, setSnake] = useState<Position[]>(initialSnake);
const [food, setFood] = useState<Position>(initialFood);
const [direction, setDirection] = useState<Direction>(Direction.RIGHT);
const [score, setScore] = useState<number>(0);
const [highScore, setHighScore] = useState<number>(0);
const gameInterval = useRef<NodeJS.Timeout | null>(null);
- Defines the
SnakeGameComponent
function. - Manages state for the game, snake, food, direction, score, and high score.
Move Snake Function
const moveSnake = useCallback(() => {
setSnake((prevSnake) => {
const newSnake = [...prevSnake];
const head = newSnake[0];
let newHead: Position;
switch (direction) {
case Direction.UP:
newHead = { x: head.x, y: head.y - 1 };
break;
case Direction.DOWN:
newHead = { x: head.x, y: head.y + 1 };
break;
case Direction.LEFT:
newHead = { x: head.x - 1, y: head.y };
break;
case Direction.RIGHT:
newHead = { x: head.x + 1, y: head.y };
break;
default:
return newSnake;
}
newSnake.unshift(newHead);
if (newHead.x === food.x && newHead.y === food.y) {
setFood({
x: Math.floor(Math.random() * 10),
y: Math.floor(Math.random() * 10),
});
setScore((prevScore) => prevScore + 1);
} else {
newSnake.pop();
}
return newSnake;
});
}, [direction, food]);
- Moves the snake in the current direction.
- Increases the snake’s length when it eats the food and spawns new food at a random position.
Handle Key Press Events
const handleKeyPress = useCallback(
(event: KeyboardEvent) => {
switch (event.key) {
case "ArrowUp":
if (direction !== Direction.DOWN) setDirection(Direction.UP);
break;
case "ArrowDown":
if (direction !== Direction.UP) setDirection(Direction.DOWN);
break;
case "ArrowLeft":
if (direction !== Direction.RIGHT) setDirection(Direction.LEFT);
break;
case "ArrowRight":
if (direction !== Direction.LEFT) setDirection(Direction.RIGHT);
break;
}
},
[direction]
);
- Handles key press events to change the snake’s direction.
useEffect for Game Interval and Key Press Events
useEffect(() => {
if (gameState === GameState.RUNNING) {
gameInterval.current = setInterval(moveSnake, 200);
document.addEventListener("keydown", handleKeyPress);
} else {
if (gameInterval.current) clearInterval(gameInterval.current);
document.removeEventListener("keydown", handleKeyPress);
}
return () => {
if (gameInterval.current) clearInterval(gameInterval.current);
document.removeEventListener("keydown", handleKeyPress);
};
}, [gameState, moveSnake, handleKeyPress]);
- Manages the game interval and key press events based on the game state.
Start, Pause, Reset Game Functions
const startGame = () => {
setSnake(initialSnake);
setFood(initialFood);
setScore(0);
setDirection(Direction.RIGHT);
setGameState(GameState.RUNNING);
};
const pauseGame = () => {
setGameState(
gameState === GameState.RUNNING ? GameState.PAUSE : GameState.RUNNING
);
};
const resetGame = () => {
setGameState(GameState.START);
setSnake(initialSnake);
setFood(initialFood);
setScore(0);
};
- Functions to start, pause, and reset the game.
Update High Score
useEffect(() => {
if (score > highScore) {
setHighScore(score);
}
}, [score, highScore]);
- Updates the high score if the current score exceeds the high score.
Render Snake Game UI
return (
<div className="flex flex-col items-center justify-center h-screen bg-gradient-to-br from-[#0F0F0F] to-[#1E1E1E]">
<div className="bg-[#1E1E1E] rounded-lg shadow-lg p-8 w-full max-w-md">
<div className="flex items-center justify-between mb-6">
<div className="text-3xl font-bold text-[#FF00FF]">Snake Game</div>
<div className="flex gap-4">
<Button
variant="ghost"
size="icon"
className="text-[#00FFFF]"
onClick={startGame}
>
<PlayIcon className="w-6 h-6" />
<span className="sr-only">Start</span>
</Button>
<Button
variant="ghost"
size="icon"
className="text-[#00FFFF]"
onClick={pauseGame}
>
<PauseIcon className="w-6 h-6" />
<span className="sr-only">Pause/Resume</span>
</Button>
<Button
variant="ghost"
size="icon"
className="text-[#00FFFF]"
onClick={resetGame}
>
<RefreshCcwIcon className="w-6 h-6" />
<span className="sr-only">Reset</span>
</Button>
</div>
</div>
<div className="bg-[#0F0F0F] rounded-lg p-4 grid grid-cols-10 gap-1">
{Array.from({ length: 100 }).map((_, i) => {
const x = i % 10;
const y = Math.floor(i / 10);
const isSnakePart = snake.some(
(part) => part.x === x && part.y === y
);
const isFood = food.x === x && food.y === y;
return (
<div
key={i}
className={`w-5 h-5 rounded-sm ${
isSnakePart
? "bg-[#FF00FF]"
: isFood
? "bg-[#00FFFF]"
: "bg-[#1E1E1E]"
}`}
/>
);
})}
</div>
<div className="flex items-center justify-between mt-6 text-[#00FFFF]">
<div>Score: {score}</div>
<div>High Score: {highScore}</div>
</div>
</div>
</div>
);
}
- Renders the Snake Game UI, including the game board, control buttons, and score display.
(Bonus just for you): Full Code with Comments
"use client"; // Enables client-side rendering for this component
import { useState, useEffect, useCallback, useRef } from "react"; // Import React hooks
import { Button } from "@/components/ui/button"; // Import custom Button component
import { PauseIcon, PlayIcon, RefreshCcwIcon } from "lucide-react"; // Import icons from lucide-react
// Define the possible game states
enum GameState {
START,
PAUSE,
RUNNING,
GAME_OVER,
}
// Define the directions for the snake movement
enum Direction {
UP,
DOWN,
LEFT,
RIGHT,
}
// Define the Position interface
interface Position {
x: number;
y: number;
}
// Initial state for the snake and food
const initialSnake: Position[] = [{ x: 0, y: 0 }];
const initialFood: Position = { x: 5, y: 5 };
export default function SnakeGame() {
// State to manage the game
const [gameState, setGameState] = useState<GameState>(GameState.START);
const [snake, setSnake] = useState<Position[]>(initialSnake);
const [food, setFood] = useState<Position>(initialFood);
const [direction, setDirection] = useState<Direction>(Direction.RIGHT);
const [score, setScore] = useState<number>(0);
const [highScore, setHighScore] = useState<number>(0);
const gameInterval = useRef<NodeJS.Timeout | null>(null);
// Function to move the snake
const moveSnake = useCallback(() => {
setSnake((prevSnake) => {
const newSnake = [...prevSnake];
const head = newSnake[0];
let newHead: Position;
switch (direction) {
case Direction.UP:
newHead = { x: head.x, y: head.y - 1 };
break;
case Direction.DOWN:
newHead = { x: head.x, y: head.y + 1 };
break;
case Direction.LEFT:
newHead = { x: head.x - 1, y: head.y };
break;
case Direction.RIGHT:
newHead = { x: head.x + 1, y: head.y };
break;
default:
return newSnake;
}
newSnake.unshift(newHead);
if (newHead.x === food.x && newHead.y === food.y) {
// Snake eats the food
setFood({
x: Math.floor(Math.random() * 10),
y: Math.floor(Math.random() * 10),
});
setScore((prevScore) => prevScore + 1);
} else {
newSnake.pop(); // Remove the last part of the snake's body
}
return newSnake;
});
}, [direction, food]);
// Function to handle key press events
const handleKeyPress = useCallback(
(event: KeyboardEvent) => {
switch (event.key) {
case "ArrowUp":
if (direction !== Direction.DOWN) setDirection(Direction.UP);
break;
case "ArrowDown":
if (direction !== Direction.UP) setDirection(Direction.DOWN);
break;
case "ArrowLeft":
if (direction !== Direction.RIGHT) setDirection(Direction.LEFT);
break;
case "ArrowRight":
if (direction !== Direction.LEFT) setDirection(Direction.RIGHT);
break;
}
},
[direction]
);
// useEffect to handle the game interval and key press events
useEffect(() => {
if (gameState === GameState.RUNNING) {
gameInterval.current = setInterval(moveSnake, 200);
document.addEventListener("keydown", handleKeyPress);
} else {
if (gameInterval.current) clearInterval(gameInterval.current);
document.removeEventListener("keydown", handleKeyPress);
}
return () => {
if (gameInterval.current) clearInterval(gameInterval.current);
document.removeEventListener("keydown", handleKeyPress);
};
}, [gameState, moveSnake, handleKeyPress]);
// Function to start the game
const startGame = () => {
setSnake(initialSnake);
setFood(initialFood);
setScore(0);
setDirection(Direction.RIGHT);
setGameState(GameState.RUNNING);
};
// Function to pause or resume the game
const pauseGame = () => {
setGameState(
gameState === GameState.RUNNING ? GameState.PAUSE : GameState.RUNNING
);
};
// Function to reset the game
const resetGame = () => {
setGameState(GameState.START);
setSnake(initialSnake);
setFood(initialFood);
setScore(0);
};
// useEffect to update the high score
useEffect(() => {
if (score > highScore) {
setHighScore(score);
}
}, [score, highScore]);
// JSX return statement rendering the Snake Game UI
return (
<div className="flex flex-col items-center justify-center h-screen bg-gradient-to-br from-[#0F0F0F] to-[#1E1E1E]">
<div className="bg-[#1E1E1E] rounded-lg shadow-lg p-8 w-full max-w-md">
<div className="flex items-center justify-between mb-6">
<div className="text-3xl font-bold text-[#FF00FF]">Snake Game</div>
<div className="flex gap-4">
<Button
variant="ghost"
size="icon"
className="text-[#00FFFF]"
onClick={startGame}
>
<PlayIcon className="w-6 h-6" />
<span className="sr-only">Start</span>
</Button>
<Button
variant="ghost"
size="icon"
className="text-[#00FFFF]"
onClick={pauseGame}
>
<PauseIcon className="w-6 h-6" />
<span className="sr-only">Pause/Resume</span>
</Button>
<Button
variant="ghost"
size="icon"
className="text-[#00FFFF]"
onClick={resetGame}
>
<RefreshCcwIcon className="w-6 h-6" />
<span className="sr-only">Reset</span>
</Button>
</div>
</div>
<div className="bg-[#0F0F0F] rounded-lg p-4 grid grid-cols-10 gap-1">
{Array.from({ length: 100 }).map((_, i) => {
const x = i % 10;
const y = Math.floor(i / 10);
const isSnakePart = snake.some(
(part) => part.x === x && part.y === y
);
const isFood = food.x === x && food.y === y;
return (
<div
key={i}
className={`w-5 h-5 rounded-sm ${
isSnakePart
? "bg-[#FF00FF]"
: isFood
? "bg-[#00FFFF]"
: "bg-[#1E1E1E]"
}`}
/>
);
})}
</div>
<div className="flex items-center justify-between mt-6 text-[#00FFFF]">
<div>Score: {score}</div>
<div>High Score: {highScore}</div>
</div>
</div>
</div>
);
}
Okay, you’ve completed the main component with functional UI. Now, you need to import this component into the app directory to use it in the app/page.tsx
file. Your final code should look like this:
import SnakeGame from "@/components/snake-game";
export default function Home() {
return (
<div>
<SnakeGame />
</div>
);
}
Running the Project
To see the snake game in action, follow these steps:
- Start the Development Server: Run
npm run dev
to start the development server. - Open in Browser: Open
http://localhost:3000
in your browser to view the application.
Make sure to test it properly (each and everything) so that we don’t have any errors in the production mode aka when we host on the internet.
Now, we want people to see our application on the internet. All you have to do is create a repository on GitHub and then push your code to it. After that, deploy the Snake Game application using Vercel.
Once you’re done with the deployment, please share the application link with me by commenting on this blog post, on Linkedin, and (most importantly, on X a.k.a. Twitter. Tag me there, and I’ll reply and appreciate your efforts) 👀
(Optional): One thing you can do on your own is to add new functionalities, enhance the styling, and improve the overall application. This way, you’ll learn something new by making modifications.
✨ Star Github Repository of the project 👈
Conclusion
In this blog post, we built a Snake Game application using Next.js. We covered:
- Setting up the project and using client-side rendering.
- Managing state and game logic in a React application.
- Implementing user input handling for game control.
See you tomorrow with the latest project. Happy coding!
Stay updated with the latest in cutting-edge technology! Follow me:
- Twitter: @0xAsharib
- LinkedIn: Asharib Ali
- GitHub: AsharibAli
- Website: asharib.xyz
Thanks for reading!