Welcome to another lil' project! In this tutorial, we’ll be building a simple version of the classic Snake game using HTML Canvas and JavaScript. Snake is a great beginner app that helps improve your understanding of game loops, DOM manipulation, and handling user input.
We’ll go through how to:
Let’s get started!
In this version of Snake, the player controls a snake that moves around the grid, eating apples and growing longer. The game gets faster based on the selected difficulty level, and the player’s score increases with each apple eaten. If the snake collides with itself, the game resets, and the score is lost.
We’ll start by creating a canvas for the game board and generating some basic settings, such as the grid size, snake position, and apple placement.
<div id="display"></div> <div id="options"></div>
Here’s a simple style for the game:
canvas { border: 5px solid #C0D72D; /* Customize border color */ background-color: #000; } button { margin: 5px; padding: 10px; font-size: 16px; }
We will start by setting up the canvas and drawing the initial state of the game. The snake and apple will be drawn as squares on a 20x20 grid.
Here’s how we set up the canvas and the game variables:
const gameLevels = ['easy', 'medium', 'hard', 'beast']; const generateGame = () => { const canv = document.createElement('canvas'); canv.id = 'game'; canv.height = '500'; canv.width = '500'; canv.style.border = '5px solid #C0D72D'; // Customize as needed canv.style.background = '#000000'; posX = posY = 10; appleX = appleY = 15; gridSize = 20; tableSize = 25; moveX = moveY = 0; body = []; segments = 5; score = 0; level = 100; const ctx = canv.getContext('2d'); // Append canvas to display area display.append(canv); };
We start by creating a 500x500 canvas, initializing the snake’s position, the apple’s position, and the grid size. moveX
and moveY
represent the direction of the snake’s movement, while segments
tracks the snake's length.
Next, we’ll add keyboard controls to move the snake and continuously update the game state using a game loop.
const keyDown = e => { switch (e.keyCode) { case 65: // Left (A) moveX = -1; moveY = 0; break; case 87: // Up (W) moveX = 0; moveY = -1; break; case 68: // Right (D) moveX = 1; moveY = 0; break; case 83: // Down (S) moveX = 0; moveY = 1; break; } }; document.addEventListener('keydown', keyDown);
We use the keydown
event to detect arrow key presses and move the snake in the corresponding direction (A
for left, W
for up, D
for right, S
for down).
In any game, the game loop is essential. It’s responsible for updating the game state (in this case, the snake’s position), redrawing the game screen, and handling interactions like eating apples and collisions. The game loop runs continuously until the game ends or is reset.
Here’s the game loop code, which is executed repeatedly using setTimeout
to update the snake's movement, check for interactions, and render the next frame:
const game = () => { // Update snake's position based on the direction of movement posX += moveX; posY += moveY; // Clear the canvas ctx.fillStyle = '#000000'; ctx.fillRect(0, 0, canv.width, canv.height); // Draw the snake ctx.fillStyle = '#2ED9EB'; // Handle edge wrapping (snake reappears on the opposite side of the canvas) if (posX < 0) posX = tableSize - 1; if (posX > tableSize - 1) posX = 0; if (posY < 0) posY = tableSize - 1; if (posY > tableSize - 1) posY = 0; // Check for self-collision for (let i = 0; i < body.length; i++) { ctx.fillRect(body[i].x * gridSize, body[i].y * gridSize, gridSize - 2, gridSize - 2); // If the snake's head touches its body, reset the game if (body[i].x === posX && body[i].y === posY) { segments = 5; // Reset snake length score = 0; // Reset score } } // Move the snake forward (add new position to the body) body.push({ x: posX, y: posY }); // Remove the tail to keep the snake the right length while (body.length > segments) { body.shift(); // Remove the oldest segment (tail) } // Check if the snake has eaten the apple if (appleX === posX && appleY === posY) { score++; // Increase score segments++; // Make the snake longer // Move the apple to a new random position appleX = Math.floor(Math.random() * tableSize); appleY = Math.floor(Math.random() * tableSize); } // Draw the apple ctx.fillStyle = "#E91E63"; ctx.fillRect(appleX * gridSize, appleY * gridSize, gridSize - 2, gridSize - 2); // Display score ctx.font = "20px Courier New"; ctx.fillStyle = '#C0D72D'; ctx.fillText(`Score: ${score}`, 380, 20); // Display control instructions ctx.fillText(`ᐊA ᐃW Sᐁ Dᐅ`, 180, 495); // Continuously call the game function based on the selected level speed setTimeout(game, level); };
Game Loops can be difficult to wrap your head around, so let’s break this down in more detail (if everything above makese sense to you, feel free to move on to the next section!):
The first step in the game loop is to update the position of the snake’s head based on the direction of movement:
posX += moveX; posY += moveY;
posX
and posY
represent the current position of the snake’s head.moveX
and moveY
store the direction of movement. For example, if the snake is moving right, moveX
will be 1
, while moveY
will be 0
.posX
and posY
.Before redrawing anything, we clear the canvas to remove the previous frame’s drawings:
ctx.fillStyle = '#000000'; // Set the background color to black ctx.fillRect(0, 0, canv.width, canv.height); // Clear the entire canvas
This prevents old positions of the snake and apple from persisting on the canvas and ensures a smooth, clean visual update.
If the snake moves off the canvas (past its edges), it reappears on the opposite side:
if (posX < 0) posX = tableSize - 1; // Wrap around the left edge if (posX > tableSize - 1) posX = 0; // Wrap around the right edge if (posY < 0) posY = tableSize - 1; // Wrap around the top edge if (posY > tableSize - 1) posY = 0; // Wrap around the bottom edge
We then loop through the body
array (which stores the snake’s segments) and draw each one on the canvas:
for (let i = 0; i < body.length; i++) { ctx.fillRect(body[i].x * gridSize, body[i].y * gridSize, gridSize - 2, gridSize - 2); }
fillRect
method draws each segment of the snake based on its x
and y
coordinates.gridSize - 2
ensures a slight gap between the segments, giving the snake its “blocky” appearance.While drawing the snake, we also check if the snake’s head (posX
and posY
) collides with any part of its body:
if (body[i].x === posX && body[i].y === posY) { segments = 5; // Reset snake length score = 0; // Reset score }
After checking for collisions, we update the snake’s position by adding the new head position to the body
array:
body.push({ x: posX, y: posY });
Then, we ensure the snake doesn't exceed its intended length by removing the tail:
while (body.length > segments) { body.shift(); // Remove the oldest segment (tail) }
segments
.The snake eats an apple if its head moves onto the same coordinates as the apple:
if (appleX === posX && appleY === posY) { score++; // Increase score segments++; // Increase snake length appleX = Math.floor(Math.random() * tableSize); // Move apple to a new random position appleY = Math.floor(Math.random() * tableSize); }
We draw the apple as a red block on the canvas using its x
and y
coordinates:
ctx.fillStyle = "#E91E63"; // Apple color ctx.fillRect(appleX * gridSize, appleY * gridSize, gridSize - 2, gridSize - 2);
At the top of the canvas, we display the player’s score:
ctx.font = "20px Courier New"; ctx.fillStyle = '#C0D72D'; ctx.fillText(`Score: ${score}`, 380, 20); // Display score at the top right
We also provide control instructions at the bottom of the canvas:
ctx.fillText(`ᐊA ᐃW Sᐁ Dᐅ`, 180, 495); // Display movement controls
Finally, the loop repeats itself using setTimeout
to control the game speed:
setTimeout(game, level);
level
variable determines the speed of the game, with lower values making the game faster. You can modify the difficulty by adjusting this value, this functionality will be added later.When the snake reaches the apple, the score increases, and the snake grows longer. This is handled in the game loop by checking if the snake’s head is on the apple:
if (appleX === posX && appleY === posY) { score++; segments++; appleX = Math.floor(Math.random() * tableSize); // Move the apple to a new random position appleY = Math.floor(Math.random() * tableSize); }
You can adjust the difficulty of the game by changing the speed of the game loop. We’ll use buttons to let the player choose a difficulty level.
const setLevel = selection => { options.childNodes.forEach(option => { option.style.color = option.innerHTML === selection ? '#C0D72D' : '#000000'; }); switch (selection) { case 'easy': level = 180; break; case 'medium': level = 120; break; case 'hard': level = 85; break; case 'beast': level = 25; break; default: level = 150; break; } }; gameLevels.forEach(lev => { const button = document.createElement('button'); button.innerHTML = lev; button.onclick = () => setLevel(lev); options.append(button); });
The speed of the game loop (setTimeout
) is adjusted based on the selected difficulty level. The faster the loop, the more challenging the game becomes.
You’ve just built a fully functional Snake game using only JavaScript and HTML Canvas! This project not only strengthens your understanding of DOM manipulation and game loops but also teaches you how to handle user input and manage game states.
Feel free to extend this project with features like:
Happy Hacking!