Creating A Snake Game Tutorial With HTML5
This tutorial shows you how to create a Snake Game using JavaScript and HTML5. A Snake Game is an action game that consists of a snake that is constantly moving inside a level. The player controls the direction of the snake, but the snake always keeps moving. When the snake eats something, it grows in size. If the snake touches the boundaries of the level, or if it tries to eat its own tail, the game is over. The goal of the game is to keep the snake alive as long as possible.
We are creating a Snake Game step by step using JavaScript and the HTML5 Canvas element. We implement the game logic and add some sprite graphics. At the end of this tutorial, you can play the finished game.
Click here to go directly to the end of this article and play the game.
Defining The Level
The snake in our game needs a place to live, a level. We define a two-dimensional grid on which the snake is allowed to move. The grid has certain dimensions defined by columns and rows properties. Each cell of the grid is occupied by a certain tile. Two basic tiles are the empty tile and the wall tile. The snake can move on empty tiles without any problem, but when it tries to move into a wall tile, the game is over.
We define a Level class below. The level has a two-dimensional grid, an array, of a certain size. The tiles, the cells of the grid, have a certain tilewidth and tileheight. We initialize the array of tiles to 0, which means that all of the tiles are the empty tile.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | // Level properties var Level = function (columns, rows, tilewidth, tileheight) { this.columns = columns; this.rows = rows; this.tilewidth = tilewidth; this.tileheight = tileheight; // Initialize tiles array this.tiles = []; for (var i=0; i<this.columns; i++) { this.tiles[i] = []; for (var j=0; j<this.rows; j++) { this.tiles[i][j] = 0; } } }; |
We need to generate an actual level by adding the generate() function to the Level class. The level we generate below will simply have an open area of empty tiles, while the borders of the level are wall tiles. A wall tile is defined by the number 1, while the empty spaces are defined by the number 0. To create a more interesting level, you could hand-craft the level array and load it when the game starts.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | // Generate a default level with walls Level.prototype.generate = function() { for (var i=0; i<this.columns; i++) { for (var j=0; j<this.rows; j++) { if (i == 0 || i == this.columns-1 || j == 0 || j == this.rows-1) { // Add walls at the edges of the level this.tiles[i][j] = 1; } else { // Add empty space this.tiles[i][j] = 0; } } } }; |
Creating The Snake
The snake needs to be able to move in a certain direction. When the snake eats something, the snake needs to grow. We create an init function that initializes the properties of the snake. The body of the snake is represented by an array of positions. Check out the comments in the code below.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 | // Snake var Snake = function() { this.init(0, 0, 1, 10, 1); } // Direction table: Up, Right, Down, Left Snake.prototype.directions = [[0, -1], [1, 0], [0, 1], [-1, 0]]; // Initialize the snake at a location Snake.prototype.init = function(x, y, direction, speed, numsegments) { this.x = x; this.y = y; this.direction = direction; // Up, Right, Down, Left this.speed = speed; // Movement speed in blocks per second this.movedelay = 0; // Reset the segments and add new ones this.segments = []; this.growsegments = 0; for (var i=0; i<numsegments; i++) { this.segments.push({x:this.x - i*this.directions[direction][0], y:this.y - i*this.directions[direction][1]}); } } // Increase the segment count Snake.prototype.grow = function() { this.growsegments++; }; // Check we are allowed to move Snake.prototype.tryMove = function(dt) { this.movedelay += dt; var maxmovedelay = 1 / this.speed; if (this.movedelay > maxmovedelay) { return true; } return false; }; // Get the position of the next move Snake.prototype.nextMove = function() { var nextx = this.x + this.directions[this.direction][0]; var nexty = this.y + this.directions[this.direction][1]; return {x:nextx, y:nexty}; } |
Moving the snake is the most interesting part. When the snake makes a move, all of the segments of the snake body need to be updated. The basic concept is that every tail segment is moved to the position of the previous tail segment. Finally, the head is moved to the position of the next move. The order of the segments is important here: segments[0] represents the head of the snake while the next segments represent the body and the tail.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 | // Move the snake in the direction Snake.prototype.move = function() { // Get the next move and modify the position var nextmove = this.nextMove(); this.x = nextmove.x; this.y = nextmove.y; // Get the position of the last segment var lastseg = this.segments[this.segments.length-1]; var growx = lastseg.x; var growy = lastseg.y; // Move segments to the position of the previous segment for (var i=this.segments.length-1; i>=1; i--) { this.segments[i].x = this.segments[i-1].x; this.segments[i].y = this.segments[i-1].y; } // Grow a segment if needed if (this.growsegments > 0) { this.segments.push({x:growx, y:growy}); this.growsegments--; } // Move the first segment this.segments[0].x = this.x; this.segments[0].y = this.y; // Reset movedelay this.movedelay = 0; } |
Drawing The Snake Using A Texture Atlas
We draw the snake by first creating and loading a sprite sheet, also called a texture atlas. The sprite sheet is a single image that consists of multiple smaller images, that represent parts of the snake that get composited later on. In my article How To Load And Draw Images With HTML5 Canvas you can see how you can load the image. The sprite sheet that you can see below shows all of the sprites that we will be using. You can see The different snake parts in all of the different directions. Finally, you can see the sprite for the apple. All of these sprite images have a resolution of 64 by 64 pixels and have a transparent background. When we draw them to the screen, we will scale them to fit our game.
To draw the snake, we have to connect the right sprites to each other. We want to draw a head, than parts of the body and finally a tail. They have to be drawn in the right direction as well. What we have to do, is create a big conditional statement and check all of the cases. The code below calculates the snake parts that need to be drawn to the screen. The tx and ty variables that are determined, are column and row indices in the sprite sheet. We use drawImage to select a part of the sprite sheet and draw it to the correct position on the screen. The result of the drawSnake function is a seamless snake on the screen that is composited of multiple sub-images from the sprite sheet.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 | // Draw the snake function drawSnake() { // Loop over every snake segment for (var i=0; i<snake.segments.length; i++) { var segment = snake.segments[i]; var segx = segment.x; var segy = segment.y; var tilex = segx*level.tilewidth; var tiley = segy*level.tileheight; // Sprite column and row that gets calculated var tx = 0; var ty = 0; if (i == 0) { // Head; Determine the correct image var nseg = snake.segments[i+1]; // Next segment if (segy < nseg.y) { // Up tx = 3; ty = 0; } else if (segx > nseg.x) { // Right tx = 4; ty = 0; } else if (segy > nseg.y) { // Down tx = 4; ty = 1; } else if (segx < nseg.x) { // Left tx = 3; ty = 1; } } else if (i == snake.segments.length-1) { // Tail; Determine the correct image var pseg = snake.segments[i-1]; // Prev segment if (pseg.y < segy) { // Up tx = 3; ty = 2; } else if (pseg.x > segx) { // Right tx = 4; ty = 2; } else if (pseg.y > segy) { // Down tx = 4; ty = 3; } else if (pseg.x < segx) { // Left tx = 3; ty = 3; } } else { // Body; Determine the correct image var pseg = snake.segments[i-1]; // Previous segment var nseg = snake.segments[i+1]; // Next segment if (pseg.x < segx && nseg.x > segx || nseg.x < segx && pseg.x > segx) { // Horizontal Left-Right tx = 1; ty = 0; } else if (pseg.x < segx && nseg.y > segy || nseg.x < segx && pseg.y > segy) { // Angle Left-Down tx = 2; ty = 0; } else if (pseg.y < segy && nseg.y > segy || nseg.y < segy && pseg.y > segy) { // Vertical Up-Down tx = 2; ty = 1; } else if (pseg.y < segy && nseg.x < segx || nseg.y < segy && pseg.x < segx) { // Angle Top-Left tx = 2; ty = 2; } else if (pseg.x > segx && nseg.y < segy || nseg.x > segx && pseg.y < segy) { // Angle Right-Up tx = 0; ty = 1; } else if (pseg.y > segy && nseg.x > segx || nseg.y > segy && pseg.x > segx) { // Angle Down-Right tx = 0; ty = 0; } } // Draw the image of the snake part context.drawImage(tileimage, tx*64, ty*64, 64, 64, tilex, tiley, level.tilewidth, level.tileheight); } } |
Snake Game Logic
Creating the basic game loop is already explained in my tutorial How To Make An HTML5 Canvas Game. Our game logic builds upon this basic game structure. Let’s start by creating the snake and level objects and starting a new game. We initialize the snake at position 10, 10, facing to the right, at 10 blocks per second with a length of 4 body segments. The level is generated and an apple is added to the level. The apple is a food item that the snake can eat to grow.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | // Create objects var snake = new Snake(); var level = new Level(20, 15, 32, 32); // Variables var score = 0; // Score var gameover = true; // Game is over var gameovertime = 1; // How long we have been game over var gameoverdelay = 0.5; // Waiting time after game over // (...) function newGame() { // Initialize the snake snake.init(10, 10, 1, 10, 4); // Generate the default level level.generate(); // Add an apple addApple(); // Initialize the score score = 0; // Initialize variables gameover = false; } |
We have to create the addApple function. We need to add an apple to the level, but we only want to add the apple at an empty position. We don’t want to add the apple at a position where the body of the snake is or where there is a wall in the level. The code below finds a random valid position. If it can’t find a position right away, it keeps trying. For the code to work efficiently, there should be enough valid empty spaces left.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 | // Add an apple to the level at an empty position function addApple() { // Loop until we have a valid apple var valid = false; while (!valid) { // Get a random position var ax = randRange(0, level.columns-1); var ay = randRange(0, level.rows-1); // Make sure the snake doesn't overlap the new apple var overlap = false; for (var i=0; i<snake.segments.length; i++) { // Get the position of the current snake segment var sx = snake.segments[i].x; var sy = snake.segments[i].y; // Check overlap if (ax == sx && ay == sy) { overlap = true; break; } } // Tile must be empty if (!overlap && level.tiles[ax][ay] == 0) { // Add an apple at the tile position level.tiles[ax][ay] = 2; valid = true; } } } |
Moving the snake inside the level and checking collisions is done in the code below. When the snake collides with an apple, the snake grows and a new apple is added to the level. Check out the comments below for more information.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 | function updateGame(dt) { // Move the snake if (snake.tryMove(dt)) { // Check snake collisions // Get the coordinates of the next move var nextmove = snake.nextMove(); var nx = nextmove.x; var ny = nextmove.y; if (nx >= 0 && nx < level.columns && ny >= 0 && ny < level.rows) { if (level.tiles[nx][ny] == 1) { // Collision with a wall gameover = true; } // Collisions with the snake itself for (var i=0; i<snake.segments.length; i++) { var sx = snake.segments[i].x; var sy = snake.segments[i].y; if (nx == sx && ny == sy) { // Found a snake part gameover = true; break; } } if (!gameover) { // The snake is allowed to move // Move the snake snake.move(); // Check collision with an apple if (level.tiles[nx][ny] == 2) { // Remove the apple level.tiles[nx][ny] = 0; // Add a new apple addApple(); // Grow the snake snake.grow(); // Add a point to the score score++; } } } else { // Out of bounds gameover = true; } if (gameover) { gameovertime = 0; } } } |
Snake Game Example
Here you can play the finished Snake Game that was created in this tutorial. The full source code is available on GitHub, licensed under GPLv3. Move the snake by pressing the arrow keys, wasd keys or by using the mouse button. As a bonus debug feature, you can grow the snake by pressing the spacebar key.