Canvas Snake Game Tutorial

Image of my avatar with a snake

Feeling nostalgic and looking to create your own phone game Snake?

This game tutorial will show you how to create it using just a simple text editor and web browser.

If you have been following the series of posts linked below, then this will add to what you have done so far and you will see some things being done in a slightly different way.

If you haven’t been following this series, then it is expected that you have at least a basic understanding of HTML and Canvas/JavaScript.

If you just want to see the code for the Snake game, then skip to the bottom where I give you the code for the .html and .js files (which should be saved to the same location).

Otherwise, I hope you find this post helpful.

To get a preview of the finished game, have a look at http://games.thejaytray.com/snake.html.

When I first tried to write the Snake code, I found it to be one of the tougher 2D games to work out, this was mostly due to the movement of the snake.

As well as explaining the code, I will also go through the thought process for creating the game.  This is one that I mapped out on paper before touching the keyboard.

 

The Snake Game

The basics of the game are:

  1. You control a snake using the up, down, left and right arrow keys
  2. The snake is trying to eat food that randomly appears
  3. When the snake eats the food:
    1. The food randomly appears somewhere else
    2. The snake gets a bit longer
    3. The score increases
  4. The game is over if:
    1. The snake ‘head’ touches part of its own body
    2. The snake touches the boundaries of the game area
  5. To make it a bit more interesting, as the player’s level increases, so too does the speed of the game.

Image of my avatar with a snake

The Snake Movement

The first time I tried recreating Snake, the movement of the snake was the part I found hardest.

To demonstrate the movement, in the image below the snake is moving right and then down.

This movement (from right to down and continuing down) looks like the following 4 movements…

image demonstrating how snake movement looks animated

The snake is one object to the person playing the game, but to you, it is an array of objects.

In the snake above, you are controlling the movement of 4 squares.

 

Originally when I was trying to code the movement, I was trying to move the head piece to its new location, then move the second piece to where the head was previously, then move the third piece to where the second piece was and so on.

The image below demonstrates how I thought the movement worked.

As you can see, the first object in an array is 0.

I was moving square 3 to a new spot, then moving 2 to where 3 was and then moving 1 to where 2 was and so on.

image demonstrating how i thought the snake movement worked

This wasn’t working out!

Take a minute to see if you can spot how the movement should be coded.

 

It was a long time ago and I wish I could remember where I read about how the movement works so that I could give full credit here, but when I read about it I had one of those head-slapping “d’oh” moments!

When the game is playing, not all the squares making up the snake move.

Only 1 square is moving.

Does this make sense yet?

To demonstrate, I’ll apply different colors to my 4 squares and show you the movement…

 

image showing how snake movement works using colored squares

The last piece(or tail piece) is removed and a piece is added to the new head location.

Only this piece needs to be moved and the rest of the body stays the same until the next time the function runs and the new tail piece is moved to the new head location and so on.

In the case above, blue becomes the new head and yellow becomes the new tail.  Then yellow becomes the new head and green becomes the new tail, and so on.

 

The Plan

We will set the game area as 300 by 300 pixels.

This time though, the canvas itself will be longer than that because we will show the score and game name outside the game area.

Each square, whether it is food or a segment of the snake will by 10 pixels wide and high.

 

The game area will be split into 30×30 10 pixel cells.

When we place the food, the cell will be assigned a letter F.

When we place and move the snake, each cell the snake covers will be assigned a letter S.

A function that ‘paints’ the canvas will run through each cell and paint the f cells red and the S cells blue witha  light grey border.

 

The game area will be an array of 30.

The X and Y values of the snake and food will be between 1 and 30 (or 0 and 29 in array terms).

When they are being painted on screen though, the X and Y values will be multiplied by 10.

So, if the X value of the head was 10, the new head will be 11 if moving right.

This does not show as 10 on the X axis and then 11 on the X axis.

When it comes to painting the snake onscreen, each X and Y is multiplied by 10, so the old head will appear at 100 on the X axis of the screen and then the new head position will be 110.

The snake is actually moving 10 pixels at a time, rather than just 1 at a time.

 

Given how we are controlling the snake movement, by removing the last piece and adding a head, handling the X and Y values this way will make it easier to manage.

 

The Thinking Process

As I said already, before putting my hands on the keyboard, I roughly mapped out how I thought the game would play out on paper first.  Sometimes this is a good thing to do, before you get bogged down in coding without knowing what the end product should look like, or what your code is supposed to do.

In the image below I recreated the process I had scribbled down – I was originally going to scan my actual notes, but between my shorthand and handwriting they wouldn’t have been very easy to read!

image of snake game planned out

Despite how it may look, the above is not meant to be a process flow-chart, it is just to show how I try to plan ahead of attempting to code.

Especially in a game like Snake, which has a bit more going on that previous games we looked at in this series.

 

When it came to coding I didn’t try to create the whole game in one run.

I created the base elements first, then added the border, scoreboard and random positions.

Then I moved onto moving the snake, then collision detections and ending the game.

 

What you are going to see below is the finished code.

Once you have gone through it, could you recreate the code and follow the steps I actually went through (just the border and scoreboard, then the food, then the snake, then animation and so on)?

 

Snake Game Tutorial

Before jumping straight into the Snake game tutorial, let’s create the .html file which you are probably very familiar with by now.

<!DOCTYPE html>
<head>
<title>My Snake Game</title>
</head>
<body>
<canvas id="canvas">
Your browser doesn't support Canvas, try another or update
</canvas>
<script src="snake.js"></script>
</body>
</html>

And now we can finally start to look at the Snake game code!

 

The Snake game code should be saved in a file called snake.js and saved to the same location as the above .html file.

 

In one of the earlier versions of Snake I created, I had problems running the game; to work around this, I am putting the game inside a function that runs when the page has loaded.

window.onload = function () {

The whole script for the game will be contained between the opening { and the very last }.

When the window loads then the function will run.

 

var canvas = document.createElement('canvas');
var ctx = canvas.getContext('2d');

Instead of getting the canvas element (as we did in earlier posts) this time we are creating an element called canvas.

in a few lines we will be adding this to the document body (appending).

 

var score = 0;
var segSize = 10;
var level = 1;
var dir = 'right';
var snake = new Array(4);
var inPlay = true;

Our starting variables will set the score to zero; the direction (dir) to right and the game level to 1.

The other variables here are:

  • segSize – Which will be the size of each segment or cell in the game.  The food will be one segment and the snake will be made up of several segments.
  • Snake – This will be a new array and its initial length will be 4.  This will translate as 4 segments to start the game.
  • inPlay – This will be used as a status to determine if the game is playing or over.  If inPlay is true, then the game is running or if it is false, then the game is over or not running.

 

var gameArea = new Array(30);
for (i = 0; i < gameArea.length; i++) {
gameArea[i] = new Array(30);
}

The game area will be an array of 30 objects.

This will be where our game plays out.

The 30 objects will effectively be 30 segments (30 x 10 = 300 which is our game area height and width).

 

canvas.width = 300;
canvas.height = 380;

The canvas width and height will be 300 and 380 respectively.

But didn’t I just say that the game area is 300 x 300?

Yes I did, but above the game area we will have 80 pixels of height that will show the score and game name.

 

var body = document.getElementsByTagName('body')[0];
body.appendChild(canvas);

Earlier we created a new canvas element; here we are adding it to the document body.

 

gameArea = createSnake(gameArea);
gameArea = createFood(gameArea);
runGame();

We add the initial snake and food objects to the game area by calling the functions as above.

Then we also call the runGame function.

You will see the 3 functions and others in later code.

 

window.addEventListener('keydown', function (e) {
if (e.keyCode == 37 && dir != 'right') {
dir = 'left';
} else if (e.keyCode == 38 && dir != 'down') {
dir = 'up';
} else if (e.keyCode == 39 && dir != 'left') {
dir = 'right';
} else if (e.keyCode == 40 && dir != 'up') {
dir = 'down';
}
});

This is our event listener which will ‘listen’ for when arrow keys are pressed.

This time we are not setting an array for keys pressed, because this game plays with only one direction at a time (other games may allow diagonal directions which are 2 keys at the same time).

There is additional code with each key press.

This code is to stop the player from turning the snake back on itself.

If we take keyCode 37 as an example first.

This is the left arrow.

What we are saying here is if keyCode 37 is pressed and the direction is not right (&& dir != ‘right’) then change the direction to left.

If the snake was already moving right, we do not want to change the direction to left or the snake hits its own body.

 

function runGame() {
ctx.clearRect(0, 0, canvas.width, canvas.height);

Our runGame function starts by clearing the canvas area.

 

for (i = snake.length - 1; i >= 0; i--) {
if (i == 0) {
switch (dir) {
case 'right':
if (dir != 'left') {
snake[0] = { x: snake[0].x + 1, y: snake[0].y }
break;
}
case 'left':
if (dir != 'right') {
snake[0] = { x: snake[0].x - 1, y: snake[0].y }
break;
}
case 'up':
if (dir != 'down') {
snake[0] = { x: snake[0].x, y: snake[0].y - 1 }
break;
}
case 'down':
if (dir != 'up') {
snake[0] = { x: snake[0].x, y: snake[0].y + 1 }
break;
}
}

We work through our snake array starting from the end and working through it.

What we are doing here is saying that if the direction is right, then the new 1st piece of the snake (head) should be placed one more than the current head piece on the X axis.

At this point it is worth reminding you that the X and Y values above aren’t the X and Y values of the screen.

If the head is at 10 on X and is moving right, the new head position will be 11.

However, onscreen the old head will be at 100 and the new head will be at 110 because all the X and Y values (of the gameArea) will be multiplied by the segSize (10) later.

When it comes to collision detection, we will be using the snake’s head position to check for collision because as the snake ‘moves’ this i the only part updating its position.

 

if(snake[0].x < 0 ||
snake[0].x >=30 ||
snake[0].y <0 ||
snake[0].y >=30) {
endGame();
return;
}

The above code is check if the head’s X and Y values have gone outside the game area (between 0 and 30).

If it has, then it calls the endGame function.

 

if(gameArea[snake[0].x][snake[0].y] == 'F') {
score++;
gameArea=createFood(gameArea);
snake.push({
x: snake[snake.length-1].x,
y: snake[snake.length-1].y
});
gameArea[snake[snake.length-1].x][snake[snake.length-1].y] = 'S';
if ((score % 10) == 0) {
level++;
}
}

The above code checks if the snake’s head is in an a cell of the gameArea designated F (food cell).

If it is, then the score increases and the createFood function is called to create another food segment randomly on the game area.

The snake array size increases (push).

The game area cell where the new snake segment is will be designated S (snake segment cell).

“if((score % 10) == 0)” after the score updates, we check if the new score is divisible by 10 (10, 20, 30, etc.) and if it is, then the player’s level should be increased.

 

else if (gameArea[snake[0].x][snake[0].y] == 'S') {
endGame();
return;
}

If the snake head doesn’t move to a cell containing food, then the code above checks if the new head position will be in a cell already designated S, that is to say, it checks if the head is colliding with part of the body.

If it does, then the endGame function is called.

The return line cleans up the code when the game ends.

 

gameArea[snake[0].x][snake[0].y] = 'S';
}
else {
if(i==(snake.length-1)){
gameArea[snake[i].x][snake[i].y]=null;
}
snake[i]={
x: snake[i-1].x,
y: snake[i-1].y
};
gameArea[snake[i].x][snake[i].y] = 'S';
}
}

If the snake’s new head position does not collide with the game area borders, the food or it’s own body then the new cell should be designated S.

The cell containing the tail piece of the snake should have the S removed (set to null) and the new tail piece’s X and Y locations are updated.

 

paintCanvas();

Here we call the paintCanvas function which you will see later ‘paints’ parts of the canvas.

 

for (x = 0; x < gameArea.length; x++) {
for (y = 0; y < gameArea[0].length; y++) {
if (gameArea[x][y] == 'F') {
ctx.fillStyle = 'red';
ctx.fillRect(x * segSize, y * segSize + 80, segSize, segSize);
}
else if (gameArea[x][y] == 'S') {
ctx.fillStyle = 'blue';
ctx.strokeSytle = 'lightgrey';
ctx.fillRect(x * segSize, y * segSize + 80, segSize, segSize);
ctx.strokeRect(x * segSize, y * segSize + 80, segSize, segSize);
}
}
}

We cycle through all cells in the gameArea array.

If a cell has been designated F, then we draw a red segment for food.

It is at this point that our segments’ X and Y values are converted to X and Y positions on screen when they are multiplied by 10, the segSize.

If a cell has been designated S, then it is filled in blue with a light grey border (stroke) – these segments will make up our snake.

 

if(inPlay){
setTimeout(runGame, 400-(level*50));
}
}

If our game is in play, then we want to call the runGame function every X  milliseconds.

In earlier posts we used a requestAnimation loop to animate movement.

What we are doing here is instructing the game to refresh at a specific speed.

We will run the code at 400 milliseconds minus the level value multiplied by 50.

If the level is 1, the game will refresh every 350 milliseconds (400-1×50).

If the level is 2, the game will refresh every 300 milliseconds (400-2×50).

As the level increases, so too does the speed or refreshes, which to the player looks like faster movement, making the game harder and harder.

 

function paintCanvas() {
ctx.strokeSytle = "black";
ctx.strokeRect(0, 80, canvas.width, canvas.height - 80);
ctx.font = 'bold 30px Arial';
ctx.fillStyle = 'blue';
ctx.fillText("Snake", (canvas.width / 2) - ctx.measureText("Snake").width / 2, 30);
ctx.font = '14px sans-serif';
ctx.fillStyle = 'black';
ctx.fillText("Score: " + score, (canvas.width/2)-ctx.measureText("Score: "+score).width/2, 60);
}

The paintCanvas function paints the border around the game area.

This is a stroke starting at 0 on the X axis and 80 on the Y axis and then covering the rest of the canvas below these points.

Remember that the top 80 pixels of our canvas will be used to display the score and game name, below this point is the game area.

Next we display the text “Snake”.

The font size will be 30 and it will be displayed at 30 on the Y axis, and centered on the X axis.

To center it on the X axis, we take half the width of the text and deduct that from half the canvas width.

Next at position 60, in smaller font we display “Score: ” and then the value of the score variable.

 

function createFood(gameArea) {
var foodX = Math.round(Math.random() * 29);
var foodY = Math.round(Math.random() * 29);
while (gameArea[foodX][foodY] == 'S') {
foodX = Math.round(Math.random() * 29);
foodY = Math.round(Math.random() * 29);
}
gameArea[foodX][foodY] = 'F';
return gameArea;
}

This function will randomly place food on our game area.

It assigns a random number for the X and Y values of food and then it checks if those values have already been designated as S’s.  If they have been then it picks new random values.

When random values have been selected, it assigns that cell in the game area as F for food cell.

 

function createSnake(gameArea) {
var snakeX = Math.round(Math.random() * 29);
var snakeY = Math.round(Math.random() * 29);
// Make sure the snake is in bounds
while ((snakeX - snake.length) < 0) {
snakeX = Math.round(Math.random() * 29);
}
for (i = 0; i < snake.length; i++) {
snake[i] = {
x: snakeX - i,
y: snakeY
};
gameArea[snakeX - i][snakeY] = "S";
}
return gameArea;
}

The createSnake function creates random values for the snake’s X and Y cell values.

It then checks to make sure that these values are within the gameArea for the whole length of the snake.

The for loop starts at 0 and works through the length of the snake assigning X and Y values for each segment.

It then designates these values on the game area as S’s.

 

function endGame() {
inPlay = false;
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.fillStyle = 'blue';
ctx.font = '24px sans-serif';
ctx.fillText('Game Over!!', ((canvas.width / 2) - (ctx.measureText('Game Over!!').width / 2)), 100);
ctx.font = '14px sans-serif';
ctx.fillText('Your Score Was ' + score, ((canvas.width / 2) - (ctx.measureText('Your Score Was ' + score).width / 2)), 200);
ctx.font = '14px sans-serif';
ctx.fillText('Better luck next time!!', ((canvas.width / 2) - (ctx.measureText('Better luck next time!!').width / 2)), 250);
}
};

The final part of this code is the endGame function which changes the inPlay value to be false (game is not running).

It clears the canvas area of all game elements and displays a game over and score message on screen.

 

And that is it!

You should have a working Snake game now (hopefully!!).

Does the code make sense?

Would you feel comfortable changing elements of the game? The text, colors, starting sizes, levels, speed, etc.?

 

Personally, I found this game to be one of the most challenging but rewarding games to recreate.

At this point a lot of 2D game elements have been covered and you should hopefully be able to attempt to recreate some other games on your own, or at least know where to start recreating them.

The next posts will be shorter and cover smaller game elements like changing the mouse pointers, animate the movement of a character (not just the position on screen) and other ways to use a game area array or map.

For now, top up the coffee and try it out yourself!

 


 

snake logo image

Here are the .html and .js full source codes:

SNAKE.HTML FILE

<!DOCTYPE html>
<head>
<title>My Snake Game</title>
</head>
<body>
<canvas id="canvas">
Your browser doesn't support Canvas, try another or update
</canvas>
<script src="snake.js"></script>
</body>
</html>

 


 

SNAKE.JS FILE

window.onload = function () {
var canvas = document.createElement('canvas');
var ctx = canvas.getContext('2d');
var score = 0;
var segSize = 10;
var level = 1;
var dir = 'right';
var snake = new Array(4);
var inPlay = true;
var gameArea = new Array(30);
for (i = 0; i < gameArea.length; i++) {
gameArea[i] = new Array(30);
}
canvas.width = 300;
canvas.height = 380;
var body = document.getElementsByTagName('body')[0];
body.appendChild(canvas);
gameArea = createSnake(gameArea);
gameArea = createFood(gameArea);
runGame();
window.addEventListener('keydown', function (e) {
if (e.keyCode == 37 && dir != 'right') {
dir = 'left';
} else if (e.keyCode == 38 && dir != 'down') {
dir = 'up';
} else if (e.keyCode == 39 && dir != 'left') {
dir = 'right';
} else if (e.keyCode == 40 && dir != 'up') {
dir = 'down';
}
});
function runGame() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
for (i = snake.length - 1; i >= 0; i--) {
if (i == 0) {
switch (dir) {
case 'right':
if (dir != 'left') {
snake[0] = { x: snake[0].x + 1, y: snake[0].y }
break;
}
case 'left':
if (dir != 'right') {
snake[0] = { x: snake[0].x - 1, y: snake[0].y }
break;
}
case 'up':
if (dir != 'down') {
snake[0] = { x: snake[0].x, y: snake[0].y - 1 }
break;
}
case 'down':
if (dir != 'up') {
snake[0] = { x: snake[0].x, y: snake[0].y + 1 }
break;
}
}
if(snake[0].x < 0 ||
snake[0].x >=30 ||
snake[0].y <0 ||
snake[0].y >=30) {
endGame();
return;
}
if(gameArea[snake[0].x][snake[0].y] == 'F') {
score++;
gameArea=createFood(gameArea);
snake.push({
x: snake[snake.length-1].x,
y: snake[snake.length-1].y
});
gameArea[snake[snake.length-1].x][snake[snake.length-1].y] = 'S';
if ((score % 10) == 0) {
level++;
}
}
else if (gameArea[snake[0].x][snake[0].y] == 'S') {
endGame();
return;
}
gameArea[snake[0].x][snake[0].y] = 'S';
}
else {
if(i==(snake.length-1)){
gameArea[snake[i].x][snake[i].y]=null;
}
snake[i]={
x: snake[i-1].x,
y: snake[i-1].y
};
gameArea[snake[i].x][snake[i].y] = 'S';
}
}
paintCanvas();
for (x = 0; x < gameArea.length; x++) {
for (y = 0; y < gameArea[0].length; y++) {
if (gameArea[x][y] == 'F') {
ctx.fillStyle = 'red';
ctx.fillRect(x * segSize, y * segSize + 80, segSize, segSize);
}
else if (gameArea[x][y] == 'S') {
ctx.fillStyle = 'blue';
ctx.strokeSytle = 'lightgrey';
ctx.fillRect(x * segSize, y * segSize + 80, segSize, segSize);
ctx.strokeRect(x * segSize, y * segSize + 80, segSize, segSize);
}
}
}
if(inPlay){
setTimeout(runGame, 400-(level*50));
}
}
function paintCanvas() {
ctx.strokeSytle = "black";
ctx.strokeRect(0, 80, canvas.width, canvas.height - 80);
ctx.font = 'bold 30px Arial';
ctx.fillStyle = 'blue';
ctx.fillText("Snake", (canvas.width / 2) - ctx.measureText("Snake").width / 2, 30);
ctx.font = '14px sans-serif';
ctx.fillStyle = 'black';
ctx.fillText("Score: " + score, (canvas.width/2)-ctx.measureText("Score: "+score).width/2, 60);
}
function createFood(gameArea) {
var foodX = Math.round(Math.random() * 29);
var foodY = Math.round(Math.random() * 29);
while (gameArea[foodX][foodY] == 'S') {
foodX = Math.round(Math.random() * 29);
foodY = Math.round(Math.random() * 29);
}
gameArea[foodX][foodY] = 'F';
return gameArea;
}
function createSnake(gameArea) {
var snakeX = Math.round(Math.random() * 29);
var snakeY = Math.round(Math.random() * 29);
while ((snakeX - snake.length) < 0) {
snakeX = Math.round(Math.random() * 29);
}
for (i = 0; i < snake.length; i++) {
snake[i] = {
x: snakeX - i,
y: snakeY
};
gameArea[snakeX - i][snakeY] = "S";
}
return gameArea;
}
function endGame() {
inPlay = false;
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.fillStyle = 'blue';
ctx.font = '24px sans-serif';
ctx.fillText('Game Over!!', ((canvas.width / 2) - (ctx.measureText('Game Over!!').width / 2)), 100);
ctx.font = '14px sans-serif';
ctx.fillText('Your Score Was ' + score, ((canvas.width / 2) - (ctx.measureText('Your Score Was ' + score).width / 2)), 200);
ctx.font = '14px sans-serif';
ctx.fillText('Better luck next time!!', ((canvas.width / 2) - (ctx.measureText('Better luck next time!!').width / 2)), 250);
}
};
email
Follow

Get every new post delivered to your Inbox

Join other followers:

%d bloggers like this: