This post was originally published on the New Bamboo blog, before New Bamboo joined thoughtbot in London.
Introduction
The HTML5 spec is currently in draft, but is already being supported by web browsers like Apple’s Safari, Mozilla Firefox, and Opera. One of the wonderful new features of the spec is the canvas tag, a html element which combined with the javascript api allows you to draw graphics and animate them. I’m going to demonstrate one of the many cool things you can do with the canvas tag by remaking the Snake mobile phone game with it. You can view the source code here, and play the finished game here.
Getting Started
To start with, you’ll want to create a html file with the following markup:
<html> <head> <title>Snake</title> <link href='/reset.css' rel='stylesheet'> <link href='/master.css' rel='stylesheet'> <script src='snake.js' type='text/javascript'></script> </head> <body> <canvas id="canvas" width="400" height="300"></canvas> </body> </html>
For the reset.css stylesheet:
html, body div, span, applet, object, iframe, h1, h2, h3, h4, h5, h6, p, blockquote, pre, a abbr, acronym, address, big, cite, code, del, dfn, em, font, img, ins, kdb, q, s, samp, small, strike, strong, sub, sup, tt, var, b, u, i, center, dl, dt, dd, ol, ul, li fieldset, form, label, legend, table, caption, tbody, tfoot, thead, tr, th, td { margin: 0; padding: 0; font-size: 100%; vertical-align: baseline; border: 0; outline: 0; background: transparent; } ol, ul { list-style: none; } blockquote, q { quotes: none; } a, a:hover { text-decoration: none; } table { border-collapse: collapse; border-spacing: none; }
For the master.css stylesheet:
body { background: #111; } canvas { border: solid 1px red; }
This sets up a black web page, with a red border around the canvas tag.
Detecting Canvas support
Unfortunately, not all web browsers support canvas yet (cough IE), so the first thing we should do is check if the browser supports it. The snake.js file is where we will put the code to check for canvas support.
function checkSupported() { canvas = document.getElementById('canvas'); if (canvas.getContext){ ctx = canvas.getContext('2d'); // Canvas is supported } else { // Canvas is not supported alert("We're sorry, but your browser does not support the canvas tag. Please use any web browser other than Internet Explorer."); } }
And to make this code execute when the web page loads, adjust the body tag so it reads like this:
<body onload="checkSupported();">
Drawing
Now, I’m interested in drawing objects on the canvas, namely the snake. From what I remember from my first mobile phone (a Nokia 3210), the snake was a chain of squares which you could move in four directions, so let’s start by drawing a square:
// This sets the fill color to red ctx.fillStyle = "rgb(200,0,0)"; // This sets some variables for demonstration purposes var x = 50; var y = 50; var width = 10; var height = 10; // This draws a square with the parameters from the variables set above ctx.fillRect(x, y, width, height);
If you put this javascript code in the canvas-supported section of the IF block, then you should see the following result below:
A nice little red square somewhere in the top left corner of the canvas. This shows you how easy it is to draw rectangular objects on the canvas. There are also options to draw the stroke of a rectangle, as well as clear a rectangle:
strokeRect(x,y,w,h); clearRect(x,y,w,h);
Movement
Now that I know how to draw squares, I want to move that square, or at least give the impression of a moving square. The snake can move up, down, left and right. In javascript, we want to capture keyboard keys being hit, and use those keys to interact with the canvas.
To capture the keystrokes, I use the following javascript code:
document.onkeydown = function(event) { var keyCode; if(event == null) { keyCode = window.event.keyCode; } else { keyCode = event.keyCode; } switch(keyCode) { // left case 37: // action when pressing left key break; // up case 38: // action when pressing up key break; // right case 39: // action when pressing right key break; // down case 40: // action when pressing down key break; default: break; } }
This code sets up the ability to capture the user pressing the up, down, left, and right keys on the keyboard, providing the controls needed to move the Snake around the canvas.
When we press a key, we want to draw a new square going in that direction. To match the direction, we need to know the following:
- What is the current position of the snake’s head
- What direction do we want to go, which is given by the key press.
So, we need a variable to record the position of the snake’s head, and when we press a key, we draw a new square, whose position is the offset of the direction from the current position of the snake’s head.
In the canvas-supported part of the IF block, fill in the following code:
// The current position of the Snake's head, as xy coordinates this.currentPosition = [50, 50]; // Sets the grid dimensions as one value this.gridSize = 10;
We’re going to use these global variables to set the starting point for the snake, as well as the grid size, which defines both how far they move in a direction, as well as how large each square will be.
Now, we want to adjust the keydown switch statement to generate new xy coordinates, which we will then provide to the fillRect argument to create a new square:
switch(keyCode) { // left case 37: // set new position, and draw square at that position. currentPosition['x'] = currentPosition['x'] - gridSize; ctx.fillRect(currentPosition['x'], currentPosition['y'], gridSize, gridSize); break; // up case 38: currentPosition['y'] = currentPosition['y'] - gridSize; ctx.fillRect(currentPosition['x'], currentPosition['y'], gridSize, gridSize); break; // right case 39: currentPosition['x'] = currentPosition['x'] + gridSize; ctx.fillRect(currentPosition['x'], currentPosition['y'], gridSize, gridSize); break; // down case 40: currentPosition['y'] = currentPosition['y'] + gridSize; ctx.fillRect(currentPosition['x'], currentPosition['y'], gridSize, gridSize); break; default: break; }
You should see something like this below:
If you were successful, you should be able to draw a line that can go in all four directions. You can also change the code that drew the initial square in the IF block so that it looks like this:
ctx.fillRect(currentPosition['x'], currentPosition['y'], gridSize, gridSize);
You’ll notice that this line is commonly repeated in several places within the code. I’m going to create a function for convenience, which does seem a bit much for a 1 line method, but will make the code easier to read:
function drawSnake() { ctx.fillRect(currentPosition['x'], currentPosition['y'], gridSize, gridSize); }
You can go through the code and replace those lines as appropriate.
Snake keeps moving
When you play the Snake mobile phone game, you can control the direction of the snake, but not it’s speed, that remains a constant, and means that you have to focus on guiding the snake around the area so as to not bump into itself. It’s a bit like the guy in Australia who had to steer his car as the cruise control locked.
To provide this functionality, we need to do two things:
- Have a new variable record what direction the snake is going in
- Make a timed loop that will execute a function to move the snake in that direction.
So, first things first, we create a new global variable, called direction:
direction = 'right';
Now, we need to execute an animation that moves the Snake in that direction, put this in the canvas-supported IF block:
setInterval(moveSnake,100);
now, add this function somewhere towards the bottom of the code:
function moveSnake(){ switch(direction){ case 'up': currentPosition['y'] = currentPosition['y'] - gridSize; drawSnake(); break; case 'down': currentPosition['y'] = currentPosition['y'] + gridSize; drawSnake(); break; case 'left': currentPosition['x'] = currentPosition['x'] - gridSize; drawSnake(); break; case 'right': currentPosition['x'] = currentPosition['x'] + gridSize; drawSnake(); break; } }
You should see the Snake moving right, which is good, but it seems that despite our insisting, the Snake is determined to go right, right out of the canvas in fact. This highlights that a) we haven’t associated keystrokes with a change in direction, and b) The snake is free to escape the canvas, which it shouldn’t be. The latter issue we will deal with later, but first, let’s associate the keystrokes with the direction.
Simply set the direction variable with the keystoke:
switch(keyCode) { // left case 37: // set new position, and draw square at that position. direction = 'left'; currentPosition['x'] = currentPosition['x'] - gridSize; drawSnake(); break;
Now, let’s set the boundaries for the Snake to move within.
Setting boundary on movement
We want our Snake’s movements to be restricted to within the boundaries of the canvas tag, therefore we need to do the following:
- Find out what the canvas boundaries are
- Detect if the snake’s position will go out of the boundary
- and if it will, redirect it to an alternative direction.
The first step is simple:
canvas.width; canvas.height;
The html attributes we defined for the canvas tag are available in javascript, so we will refer to them in that way. When the snake moves down or right, we then want to check whether the new position’s x or y value is greater than the canvas’ width or height. If it is, then we don’t draw the new square. Instead, we choose to move in a different direction, so as to keep the momentum going. The same principle takes effect if the current position’s x or y value is less than 0 when the snake moves up or left.
I have to admit, at this point I took the opportunity to refactor my code, so that I kept the logic in one place. I started by turning the calculated positions for up, down, left, and right into functions:
function leftPosition(){ return currentPosition['x'] - gridSize; } function rightPosition(){ return currentPosition['x'] + gridSize; } function upPosition(){ return currentPosition['y'] - gridSize; } function downPosition(){ return currentPosition['y'] + gridSize; }
The calculated position functions would then be used for the logic to determine if the snake could move in a certain direction, as well as for setting the current position for which to move to.
The next step would be to wrap the boundary logic, direction setter, current position setter and drawShape function call within named functions:
function moveUp(){ if (upPosition() >= 0) { executeMove('up', 'y', upPosition()); } } function moveDown(){ if (downPosition() < canvas.height) { executeMove('down', 'y', downPosition()); } } function moveLeft(){ if (leftPosition() >= 0) { executeMove('left', 'x', leftPosition()); } } function moveRight(){ if (rightPosition() < canvas.width) { executeMove('right', 'x', rightPosition()); } } function executeMove(dirValue, axisType, axisValue) { direction = dirValue; currentPosition[axisType] = axisValue; drawSnake(); }
The first four methods are similar in setup. The first line calls an IF statement to check if the movement in that direction will set the current position inside of the canvas area. If it does, then the second line is executed, passe 3 parameters to the executeMove function.
The executeMove function is a refactoring of the same code that was encapsulated in the moveUp/moveDown/moveLeft/moveRight functions. It sets the direction, then it sets the currentPosition according to the axis type and the axis value passed in, then it draws the sqaure representing the head of the snake.
Finally, we can now clean up a lot of common code, so now our keystroke calls and automated snake movements look like this:
document.onkeydown = function(event) { var keyCode; if(event == null) { keyCode = window.event.keyCode; } else { keyCode = event.keyCode; } switch(keyCode) { // left case 37: moveLeft(); break; // up case 38: moveUp(); break; // right case 39: moveRight(); break; // down case 40: moveDown(); break; default: break; } } function moveSnake(){ switch(direction){ case 'up': moveUp(); break; case 'down': moveDown(); break; case 'left': moveLeft(); break; case 'right': moveRight(); break; } }
Nice and clean. What you should see now is that the Snake moves by itself, and when it gets to the edge of the canvas, it actually stops at that point. You can test this by waiting a few seconds after the snake has reaches the edge, then press a key going to either side of the snake, and you will notice that the snake then traverses immediately from that point in the given direction, it has not escaped from the canvas.
This is good so far, but what we want to happen is for the snake to automatically move to either side once it hits an edge and cannot go further. We do this by using the following code:
function whichWayToGo(axisType){ if (axisType=='x') { a = (currentPosition['x'] > canvas.width / 2) ? moveLeft() : moveRight(); } else { a = (currentPosition['y'] > canvas.height / 2) ? moveUp() : moveDown(); } }
This code takes an axis type, and then works out which way to go based on the current position. So, for example if the snake has hit the right edge of the canvas, then the function is passed a ‘y’ parameter, telling the function it needs to move either up or down. This function will then check if the current position is above or below the middle of the canvas. If it’s above, it will move down, and vice versa. The sample principles apply for the horizontal case.
So, with that function, adjust the moveUp/moveDown/moveLeft/moveRight functions as shown below:
function moveUp(){ if (upPosition() >= 0) { executeMove('up', 'y', upPosition()); } else { whichWayToGo('x'); } } function moveDown(){ if (downPosition() < canvas.height) { executeMove('down', 'y', downPosition()); } else { whichWayToGo('x'); } } function moveLeft(){ if (leftPosition() >= 0) { executeMove('left', 'x', leftPosition()); } else { whichWayToGo('y'); } } function moveRight(){ if (rightPosition() < canvas.width) { executeMove('right', 'x', rightPosition()); } else { whichWayToGo('y'); } }
If you now reload the page, you will see that the Snake keeps moving when it hits an edge, and cleverly opts to head in the direction furthest from an edge.
Pulling the Snake’s body along
It’s starting to get there, the Snake looks great, even a bit intelligent as he, or she, sees an edge. That’s all good, but right now the Snake has a backside that doesn’t move, or to quote Homer Simpson (and I kid you not, this is in one of the early episodes), “Marge, you got a butt that won’t quit”. It needs to drag it’s body along, not draw an infinite line, so that’s what we’re going to do.
You’ll notice that I thought about this earlier on when I came up with the drawSnake() function name. I wanted to have a placeholder where I would draw the Snake’s body. Remember when I referred to the Snake as looking like ‘a chain of squares’? That’s exactly what the snake will be, a chain of squares.
The idea is to have an array of xy coordinates that will represent all the square points which the Snake body occupies:
snakeBody = [];
and adjust the drawSnake function so it looks like this:
function drawSnake() { snakeBody.push([currentPosition['x'], currentPosition['y']]); ctx.fillRect(currentPosition['x'], currentPosition['y'], gridSize, gridSize); if (snakeBody.length > 3) { var itemToRemove = snakeBody.shift(); ctx.clearRect(itemToRemove[0], itemToRemove[1], gridSize, gridSize); } }
What happens here is that every time there is a call to drawSnake, the function adds the new position to the snakeBody array, keeping it in memory. Then, when the size of the SnakeBody array grows above 3, the SnakeBody array’s first element is removed, and the rectangle is removed. What you should see now is a little snake moving around, with a tiny body rather than an infinitely-long line.
Eat food, grow longer
Now we need to add 1 element to the game which is currently not implemented in any way; we need the food. In the Snake game, little squares of food pop up around the area, and when the Snake passes over it, he grows longer, hence the label ‘food’. To add this element to the game, we need to do the following:
- Create a method to draw a food square on the canvas, inserted in a random location that is not occupied by the Snake’s body.
- 2 - When the Snake passes over that food item, increase the Snake’s body length, and place a food square somewhere else.:
function makeFoodItem(){ suggestedPoint = [Math.floor(Math.random()*(canvas.width/gridSize))*gridSize, Math.floor(Math.random()*(canvas.height/gridSize))*gridSize]; if (snakeBody.some(hasPoint)) { makeFoodItem(); } else { ctx.fillStyle = "rgb(10,100,0)"; ctx.fillRect(suggestedPoint[0], suggestedPoint[1], gridSize, gridSize); }; } function hasPoint(element, index, array) { return (element[0] == suggestedPoint[0] && element[1] == suggestedPoint[1]); }
It looks ugly, it probably is ugly, so I must confess, I’m not a Javascript guru (that’s why I pester Ismael now and then), but I’ll explain the logic. First, before we can do anything, we need to generate a random point in the canvas, an array of two numbers well within the range of the canvas’ width and height, but also rounded so that the random numbers are divisble by the gridSize, which happens to be 10.
Secondly, we need to check if the suggestedPoint is already occupied by the Snake’s body. Unfortunately comparing an array of arrays is not a straightforward procedure in javascript, so I had to create a function called hasPoint, which using Array.some, will return true if any of the snakeBody array’s arrays directly match with the suggestedPoint array.
If snakeBody already has the suggestedPoint in its collection, then we run the whole method all over again, and this repeats until the other case occurs, in which case set the fill color to green, and then draw a square of that color, with the suggestedPoint’s coordinates.
A special note, make sure to move the red fillcolor line to the drawSnake function, as demonstrated below:
function drawSnake() { snakeBody.push([currentPosition['x'], currentPosition['y']]); ctx.fillStyle = "rgb(200,0,0)"; }
If you don’t, you’ll be scratching your head wondering why your beloved snake just turned green. Put ‘makeFoodItem();’ right in the IF block for the canvas-supported initial startup code, and what you should see is a little green square on the map, awesome. For an added bonus, move your Snake so he eats the food…
It’s gone! but wait, we didn’t write any code to remove the green square after we ate it?
Intentionally, no, unintentionally, yes. The little code which helps to give the impression of the Snake moving by removing the trailing squares also clears the area where the Snake has been, including green squares. How nice is that?
We’re close, now we need to make the food reappear in a different place once it has been snacked on, and we need the Snake to grow in length. First, start by putting this code in the canvas-supported IF statement block, precisely in this order:
snakeLength = 3; makeFoodItem(); drawSnake();
The ‘makeFoodItem()’ function must come before the drawSnake function, otherwise the code will break because of the suggestedPoint variable being missing. You’ll also notice that we’ve defined a snakeLength variable, with the value of 3, the same value used to determine when to start bumping the last points from the snakeBody array? You can guess where this is heading, and here is the answer:
function drawSnake() {; } }
You’ll notice 2 changes to the drawSnake method..
Game Over
We’re very close to finishing the game, except for one small thing, we haven’t catered for when the Snake bumps into itself, and thus the end game case. Right now, if the Snake moves over itself, it simply continues its path. What we need to do here is the following:
- Track when the snake traverses over itself
- End the game once that happens.
Thankfully, to deal with No. 1, we’ve already got a solution at hand:
function drawSnake() { if (snakeBody.some(hasEatenItself)) { gameOver(); return false; } function hasEatenItself(element, index, array) { return (element[0] == currentPosition['x'] && element[1] == currentPosition['y']); }
The hasEatenItself function checks if the currentPosition point is already occupied by the Snake’s body (i.e. the snakeBody array contains the currentPosition), and if it is, it will call a new function called gameOver:
function gameOver(){ var score = (snakeLength - 3)*10; clearInterval(interval); snakeBody = []; snakeLength = 3; allowPressKeys = false; alert("Game Over. Your score was "+ score); ctx.clearRect(0,0, canvas.width, canvas.height); }
The gameOver function does a couple of things:
- Calculates the score, and displays it.
- Tells the Snake to stop animating (moving)
- Resets the Snake
- flicks a allowPressKeys variable to false (usage explained later)
- Clears the canvas
The allowPressKeys variable is defined in the IF statement canvas-supported block like this:
allowPressKeys = true;
and used to disable key press functionality like this:
document.onkeydown = function(event) { if (!allowPressKeys){ return null; }
That way, the user cannot move the Snake once the game is over. At this point, we have effectively replicated the Snake game. That’s it. Finito.
A little spit and polish
Not quite. Yes we could ship it and it would work, but how does the user start up a new game? or pause it? and what about displaying the score in real time? We’re going to add a few more features before we call this game finished.
Before we add restart and pause functionality, we’ve got a small usability flaw in the game. If you press a key that is going in the opposite direction to the Snake, the game ends since it’s effectively trying to go over itself. Logically this is the correct outcome for the Snake, but when you accidentally go backwards or hit the keys in the wrong order when trying to execute a sharp u-turn, then it becomes annoying.
The solution here is to ignore any keystorke going in the opposite direction to the Snake, like this:
switch(keyCode) { case 37: if (direction != "right"){ moveLeft(); } break; case 38: if (direction != "down"){ moveUp(); } break; case 39: if (direction != "left"){ moveRight(); } break; case 40: if (direction != "up"){ moveDown(); } break;
You simply don’t execute the move if the Snake is going in the opposite direction. Try it now, you’ll notice that the Snake continues in its current direction if you attempt to make it go backwards.
Next, we want to be able to pause the game.
function pause(){ clearInterval(interval); allowPressKeys = false; }
This stops the animation of the moving snake, and disables keypress movements, effectively pausing the game for the user. To resume the game, the reverse happens:
function play(){ interval = setInterval(moveSnake,100); allowPressKeys = true; }
With this functionality isolated, you can now DRY up some of the code in other places:
function gameOver(){ var score = (snakeLength - 3)*10; pause(); alert("Game Over. Your score was "+ score); ctx.clearRect(0,0, canvas.width, canvas.height); }
And we can now create a start function that can be used to initialize a new game.
if (canvas.getContext){ ctx = canvas.getContext('2d'); this.gridSize = 10; start(); function start(){ ctx.clearRect(0,0, canvas.width, canvas.height); this.currentPosition = {'x':50, 'y':50}; snakeBody = []; snakeLength = 3; makeFoodItem(); drawSnake(); direction = 'right'; play(); }
The start function encapsulates all the methods needs to setup a new game. However, executing a game whilst another game is in progress will cause an error; the game will restart, but the snake will start to travel faster and faster as a result. What we need is a way to remove the existing interval variable, and then start the game:
function restart(){ pause(); start(); }
You can test this methods using the error console in Safari, or by using Firebug’s javascript console for Firefox.
Now would be a good time to add some buttons to the web page to provide the user with access to these functions. In terms of context, it would make sense to have the restart and resume buttons hidden by default, and the pause button displayed by default. Once the pause button is pressed, it is hidden, and the other 2 buttons are shown.
<body onload='checkSupported();'> <span id='logo'>SNAKE</span> <div id='play_menu'> <button onclick="pause();document.getElementById('play_menu').style. <button onclick="restart();document.getElementById('play_menu').style.display='block';document.getElementById('pause_menu').style.display='none';">Restart</button> <button onclick="play();document.getElementById('play_menu').style.display='block';document.getElementById('pause_menu').style.display='none';">Resume</button> </div>
Also add this css rule for displaying the logo and hiding the pause menu buttons by default:
#logo { font-family: helvetica; font-size: 1.4em; } #pause_menu { display: none; }
We’re now able to display context-relevant buttons to the user as they play the game, oh, and when the game finishes:
<div id='restart_menu'> <button onclick="restart();document.getElementById('play_menu').style.display='block';document.getElementById('restart_menu').style.display='none';">Restart</button> </div>
and the css:
#pause_menu, #restart_menu { display: none; }
and the Snake js:
function gameOver(){ var score = (snakeLength - 3)*10; pause(); alert("Game Over. Your score was "+ score); ctx.clearRect(0,0, canvas.width, canvas.height); document.getElementById('play_menu').style.display='none'; document.getElementById('restart_menu').style.display='block'; }
Now when a game ends, you can restart the game with ease. I won’t say that this is the most efficient way of doing this, but it does the job. The final thing to do is to display the score in real time.
<div id="score_container"> Score: <span id="score">0</span> </div>
The css:
#score_container { float: right; display: inline; }
The snake js code:
function updateScore(){ var score = (snakeLength - 3)*10 document.getElementById('score').innerText = score; }
We have a new method for updating the score on the page, and we use this in two places, the first being the drawSnake function, when the Snake eats the food:
function drawSnake() { if (snakeBody.some(hasEatenItself)) { gameOver(); return false; }; updateScore(); } }
and when the game is started/restarted:
function start(){ ctx.clearRect(0,0, canvas.width, canvas.height); this.currentPosition = {'x':50, 'y':50}; snakeBody = []; snakeLength = 3; updateScore(); makeFoodItem(); drawSnake(); direction = 'right'; play(); }
Now, when you play the game, you should see the score updated when you eat food, and reset whenever you press the restart button:
That is it. We have a fully-functioning game of Snake, written purely using HTML5’s canvas tag, some CSS, and some javascript. Want to see what it should play like? Try the finished game here.