> Tiny Game

Build the OG Minified Snake Game with JS and Canvas.
Written by Mark VincentUpdated: September 2024

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:

  1. Set up the game grid using the Canvas API.
  2. Control the snake’s movement with keyboard inputs.
  3. Implement the snake’s growing body and how it interacts with the apple.
  4. Create a simple scoring system.
  5. Add difficulty levels that adjust the game speed.

Let’s get started!

Table of Contents

Overview

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.

Setup

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.

HTML Structure

<div id="display"></div> <div id="options"></div>

CSS (Basic Styling)

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; }

Drawing the Game

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.

JavaScript Setup

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.

Handling Snake Movement

Next, we’ll add keyboard controls to move the snake and continuously update the game state using a game loop.

Handling Key Presses

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).

Game Loop

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!):

1. Update Snake’s Position

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.
  • Each time the game loop runs, the snake’s position is updated by adding the movement values to posX and posY.

2. Clear the Canvas

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.

3. Handle Edge Wrapping

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
  • This is known as “edge wrapping,” which creates a seamless playing field by allowing the snake to reappear on the opposite side of the screen when it crosses an edge.

4. Draw the Snake

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); }
  • The fillRect method draws each segment of the snake based on its x and y coordinates.
  • The gridSize - 2 ensures a slight gap between the segments, giving the snake its “blocky” appearance.

5. Check for Self-Collision

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 }
  • If the head’s coordinates match any of the body segment’s coordinates, it means the snake has collided with itself, and the game resets by reducing the snake back to its starting length and resetting the score.

6. Move the Snake Forward

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) }
  • The snake grows when it eats an apple (handled later), so we only remove the tail if the snake’s length exceeds the number of segments.

7. Check if the Snake Eats the Apple

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); }
  • If the snake eats an apple, we increase the score and snake length by 1, and then move the apple to a new random location on the grid.

8. Draw the Apple

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);

9. Display Score and Controls

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

10. Run the Game Loop Continuously

Finally, the loop repeats itself using setTimeout to control the game speed:

setTimeout(game, level);
  • The 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.

Growing the Snake and Eating Apples

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); }

Adding Difficulty Levels

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.

Setting Game Speed

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.

Conclusion

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:

  • Obstacles on the grid
  • A high-score tracking system
  • Different game modes

Happy Hacking!