Create a mobile version of Snake with HTML5 canvas and JavaScript
Eoin McGrath, co-founder and lead developer at boutique web studio Starfish, explains how to bring mobile game classic Snake into the brave new world of smartphones using HTML5 canvas and JavaScript
Online games were once the sole domain of Flash but the last year or so has seen an explosion in games written in JavaScript using the canvas API. Dust off your JavaScript skills and join me on a quick tour of game creation.
In this tutorial we're going to bring that old chunky phone fav of yesteryear, Snake, kicking and screaming into the brave new world of smartphones.
Typically most games are comprised of the following loop:
- Check for user input.
- Move players (In this case the snake, its various segments and the tasty red apple).
- Check for collisions and take appropriate action. This will be either the snake bites the apple, the snake bites its own tail or collides with the boundaries of the screen.
- Repeat.
Step 1: A blank canvas
First we have a basic HTML file, demo.html with a dash of CSS, a canvas tag and a left and right button. Just before the closing body tag we load our JavaScript, this ensures that the JavaScript will be loaded when the page is ready, eliminating the need for checking if the DOM is ready in JavaScript.
The pithy part is contained in the demo.js that handles all aspects of the game from drawing to the screen, checking user input etc.
Fire up you're favourite text editor and we'll build the game from scratch.
(function () { var canvas = document.getElementById('snakeCanvas'), ctx = canvas.getContext('2d'), score = 0, hiScore = 20, leftButton = document.getElementById('leftButton'), rightButton = document.getElementById('rightButton'), input = { left: false, right: false }; canvas.width = 320; canvas.height = 350; // check for keypress and set input properties window.addEventListener('keyup', function(e) { switch (e.keyCode) { case 37: input.left = true; break; case 39: input.right = true; break; } }, false); // the rest of the code goes here}());
At the top of the script we've declared some basic variables. Hopefully it's obvious what each of them means. In keeping with best practice it is a good idea to declare these at the beginning of your program.
Get top Black Friday deals sent straight to your inbox: Sign up now!
We curate the best offers on creative kit and give our expert recommendations to save you time this Black Friday. Upgrade your setup for less with Creative Bloq.
The last few lines tell the browser to check for keyboard input. Each key in has its own keyCode. We only need to check for the right and left cursor, and if they've been pressed we set the input to true for that key.
Note the syntax: this is a self executing anonymous function and a rather handy way of encapsulating all your variables and functions without polluting the global namespace. The parentheses after the closing curly bracket means: run this function now.
Now let's add in a simple object that'll take care of drawing for us. This will allow us to easily draw the building blocks of our game.
// a collection of methods for making our mark on the canvas var draw = { clear: function () { ctx.clearRect(0, 0, canvas.width, canvas.height); }, rect: function (x, y, w, h, col) { ctx.fillStyle = col; ctx.fillRect(x, y, w, h); }, circle: function (x, y, radius, col) { ctx.fillStyle = col; ctx.beginPath(); ctx.arc(x, y, radius, 0, Math.PI*2, true); ctx.closePath(); ctx.fill(); }, text: function (str, x, y, size, col) { ctx.font = 'bold ' + size + 'px monospace'; ctx.fillStyle = col; ctx.fillText(str, x, y); } };
We've created a draw method that will allow us to clear the canvas, draw rectangles and circles. ctx is our reference to the canvas and allows us to use the canvas API. If you have time on your hands, read the full canvas API spec.
Note: this uses the object literal notation which is a simple way to create a single instance of an object.
And now test drive the draw object.
// let's see if it works by drawing some shapes draw.rect(50,20,100,100,'green'); draw.rect(70,40,20,20,'white'); draw.rect(80,50,10,10,'black'); draw.rect(120,40,20,20,'white'); draw.rect(130,50,10,10,'black'); draw.rect(60,90,80,10, 'darkgreen'); draw.circle(200,80,30,'red'); // have you guessed what it is yet? draw.text('Snake, meet apple.', 70, 180, 14); draw.text('Apple, meet snake.', 70, 200, 14);
You can see the draw class in action here.
Step 2: A snake in the grass
Next up is our snake class. We're going to need to track the coordinates of the beast as well as its length. Let's also throw in some values such as width, height, colour etc so we can easily change them later when the need arises.
What actions will our snake need to perform? Well, it's going to need to move itself and each segment of it's slithery body, to draw itself, check for collisions with either the game borders, the apple or its tail.
Note: unlike the draw object we declare this as a function (functions are first class objects in JavaScript). In the next step we'll look at adding methods to the Snake.
// main snake class var Snake = function() { this.init = function() { this.dead = false; this.len = 0; // length of the snake (number of segments) this.speed = 4; // amount of pixels moved per frame this.history = []; // we'll need to keep track of where we've been this.dir = [ // the four compass points in which the snake moves [0, -1], // up [1, 0], // right [0, 1], // down [-1, 0] // left ]; this.x = 100; this.y = 100; this.w = this.h = 16; this.currentDir = 2; // i.e. this.dir[2] = down this.col = 'darkgreen'; }; this.move = function() { if (this.dead) { return; } // check if a button has been pressed if (input.left) { this.currentDir += 1; if (this.currentDir > 3) { this.currentDir = 0; } } else if (input.right) { this.currentDir -= 1; if (this.currentDir < 0) { this.currentDir = 3; } } // check if out of bounds if (this.x < 0 || this.x > (canvas.width - this.w) || this.y < 0 || this.y > (canvas.height - this.h)) { this.dead = true; } // update position this.x += (this.dir[this.currentDir][0] * this.speed); this.y += (this.dir[this.currentDir][1] * this.speed); // store this position in the history array this.history.push({x: this.x, y: this.y, dir: this.currentDir}); }; this.draw = function () { draw.rect(this.x, this.y, this.w, this.h, this.col); // draw head draw.rect(this.x + 4, this.y + 1, 3, 3, 'white'); // draw eyes draw.rect(this.x + 12, this.y + 1, 3, 3, 'white'); }; this.collides: function () { // we'll come back to this in a bit }, };
Our snake now has four methods that we'll need to manipulate it. Let's take a look at each of them:
init
This sets up all variables (or properties) needed for the snake; its x and y coordinates, colour, length, direction, speed, a history array of all previous positions etc. We'll need to call this after creating our snake, or on starting a new game.
You maybe wondering what's with the this.dir array? What we have here is an array which contains four arrays, corresponding to up, left, right, down. this.currentDir points to an entry in this.dir. In the following method you can see how pressing left or right cycles through the directions in a clockwise or anti-clockwise manner.
move
This is where most of the snakey action takes place, so there's a few things to digest. Here, we check for user input and adjust our direction accordingly, check if we are still on the screen and if not flag the snake as dead.
Next we update the snake's position by checking our input.left and input.right. For example, if this.currentDir is 0, our direction is this.dir[0], which contains an array of 0, 1. We add the first value to our x coordinate and multiply by speed. Since it is 0, this means we won't change our position on the x axis. The second value, -1, gets added to the y coordinate and multiplied by speed again. In this case we move -4 pixels up.
TLDR; we moved up!
The final line pushes a record of our coordinates into the history array. This will come in handy when we place each segment of the snake's tail later on.
draw
This is very straightforward. We first make a call to draw.rect, based on the position of the beast's head. Subsequently we draw two more rectangles to depict the eyes.
collide
Here we need to check if the snake's head is touching another object. We'll dissect this bit of code in the following step.
Enough with the set up and theory already, let's put our code to the test!
We just need to add a loop function that will call itself periodically, so without further ado:
var p1 = new Snake();p1.init();function loop() { draw.clear(); // clear our canvas. the previous loop's drawings are still there p1.move(); p1.draw(); if (p1.dead) { draw.text('Game Over', 100, 100, 12, 'black'); } // we need to reset right and left or else the snake keeps on turning input.right = input.left = false;};setInterval(loop, 30); // call the loop function every 30 milliseconds
Step 3: Add in the apple
The apple class is going to much simpler than it's snakey counterpart. Basically, we just need to place it randomly on the screen. If it gets eaten then we should place it elsewhere. Here's the basic class:
var Apple = function() { this.x = 0; this.y = 0; this.w = 16; this.h = 16; this.col = 'red'; this.replace = 0; // game turns until we move the apple elsewhere this.draw = function() { if (this.replace === 0) { // time to move the apple elsewhere this.relocate(); } draw.rect(this.x, this.y, this.w, this.h, this.col); this.replace -= 1; }; this.relocate = function() { this.x = Math.floor(Math.random() * (canvas.width - this.w)); this.y = Math.floor(Math.random() * (canvas.height -this.h)); this.replace = Math.floor(Math.random() * 200) + 200; }; };
Now let's put it in the mix and use the snake's collision method to see it we've gobbled it. The collision method may look a bit daunting but basically what it does is check if two rectangles overlap and if so returns true.
// add this into the Snake class this.collides = function(obj) { // this sprite's rectangle this.left = this.x; this.right = this.x + this.w; this.top = this.y; this.bottom = this.y + this.h; // other object's rectangle // note: we assume that obj has w, h, w & y properties obj.left = obj.x; obj.right = obj.x + obj.w; obj.top = obj.y; obj.bottom = obj.y + obj.h; // determine if not intersecting if (this.bottom < obj.top) { return false; } if (this.top > obj.bottom) { return false; } if (this.right < obj.left) { return false; } if (this.left > obj.right) { return false; } // otherwise, it's a hit return true; };
We need to update the loop function to handle drawing the apple and checking for collision.
function loop() { draw.clear(); p1.move(); p1.draw(); if (p1.collides(apple)) { score += 1; p1.len += 1; apple.relocate(); } if (score > hiScore) { hiScore = score; } apple.draw(); draw.text('Score: '+score, 20, 20, 12, 'black'); draw.text('Hi: '+hiScore, 260, 20, 12, 'black'); if (p1.dead === true) { draw.text('Game Over', 100, 200, 20, 'black'); if (input.right || input.left) { p1.init(); score = 0; } } input.right = input.left = false; }
Step 4: Watch me grow
Each segment of the tail will always be n moves behind its closest front neighbour. From this we can come up with the following very simple algorithm:
- For each segment of the snake grab its position from the history array.
- More precisely, the position will be a function of i (the segment) minus width divided by speed. This means, for the first segment we need to move back 1 * (16 / 4).
- Draw a rectangle at that position.
- Check to see if the reptile's head is in collision with this segment and if so set the snake's status to dead.
Now to translate this into JavaScript, adding it to our Snake.move method:
this.draw = function () { var i, offset, segPos, col; // loop through each segment of the snake, // drawing & checking for collisions for (i = 1; i <= this.len; i += 1) { // offset calculates the location in the history array offset = i * Math.floor(this.w / this.speed); offset = this.history.length - offset; segPos = this.history[offset]; col = this.col; // reduce the area we check for collision, to be a bit // more forgiving with small overlaps segPos.w = segPos.h = (this.w - this.speed); if (i > 2 && i !== this.len && this.collides(segPos)) { this.dead = true; col = 'darkred'; // highlight hit segments } draw.rect(segPos.x, segPos.y, this.w, this.h, col); } draw.rect(this.x, this.y, this.w, this.h, this.col); // draw head draw.rect(this.x + 4, this.y + 1, 3, 3, 'white'); // draw eyes draw.rect(this.x + 12, this.y + 1, 3, 3, 'white'); };
Step 5: Going mobile
You're probably thinking whatever happened to the mobile part in the title.
Currently, we have just check the cursor keys for input. Our html file does stick a couple of control buttons at the bottom of the canvas. All we need to do is check if they've been tapped.
//let's assume we're not using a touch capable device var clickEvent = 'click'; // now we try a simple test to see if we have touch try { document.createEvent('TouchEvent'); // it seems we do, so we should check for it rather than click clickEvent = 'touchend'; } catch(e) { } leftButton.addEventListener(clickEvent, function(e) { e.preventDefault(); input.left = true; }, false); rightButton.addEventListener(clickEvent, function(e) { e.preventDefault(); input.right = true; }, false);
Firstly, we declare our clickEvent variable, defaulting to click. Then we use a try/ catch block to see if we have touch capability by creating a TouchEvent. The benefit of using try/ catch is that if the browser is not touch capable it won't exit with a unrecoverable error. Essentially, we suppress any possible error.
Now we know whether the clickEvent is either the default click or touchstart we can add event listeners to the right and left buttons to update the input status.
There are a few more tricks we can use to improve the experience for iPhone users.
Add the following meta tags to the top of your HTML file:
<meta name="viewport" content="width=320, height=440, user-scalable=no, initial-scale=1.0; maximum-scale=1.0; user-scalable=0;" /><meta name="apple-mobile-web-app-capable" content="yes" /><meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" /><link rel="apple-touch-startup-image" href="iphonestartup.png" />
The first meta tag importantly disables scaling. The next two are, obviously, iPhone specific. The first of which removes the button bars and the URL, the second adjusts the status bar.
The link tag, allows us to set a nice homepage icon.
One final thing we can is try and hide the URL bar, by scrolling to the first pixel on the screen. This is easily achieved with window.scrollTo(x, y), which is called just before we invoke the setInterval.
window.scrollTo(0,0); setInterval(loop, 30);
At this stage we have a passably playable snake clone. Not exactly ground breaking stuff, but I hope it has given you some idea of what is involved in creating games with JavaScript and the canvas API.
One thing you learn fast in this industry is not to reinvent the wheel and chances are that some bright spark already has come up with a solution. In this spirit, I suggest you take a look at some JavaScript game libraries:
- ImpactJS: Will set you back $100 but really is worth it. Boasts excellent performance, a level editor, sprites and great sound support.
- Mibbu: Open source, lightweight and supports the DOM as well as canvas.
- Akihabra: Aimed at creating retro style games.
- More: A nicely compiled, comprehensive list of JavaScript game engines.
Possible improvements
That concludes this tutorial, though there is plenty of scope for improvement. Why not try your hand at some of the suggestions listed below?
- Easy: Use HTML5's localstorage API to save hiscores across sessions.
- Easy: Use HTML5's cache manifest for offline gaming.
- Easy: Use the circle draw method to draw a more realistic apple.
- Medium: Add a snakey pattern to the serpent's body.
- Medium: Add some sound effects.
- Hard: Add a pointy tail to the snake.Hint: You will need to add a triangle method to the draw class and rotate it depending on direction. Read more about rotating the canvas here.
You can see some of these features implemented here.
Thank you for reading 5 articles this month* Join now for unlimited access
Enjoy your first month for just £1 / $1 / €1
*Read 5 free articles per month without a subscription
Join now for unlimited access
Try first month for just £1 / $1 / €1
The Creative Bloq team is made up of a group of design fans, and has changed and evolved since Creative Bloq began back in 2012. The current website team consists of eight full-time members of staff: Editor Georgia Coggan, Deputy Editor Rosie Hilder, Ecommerce Editor Beren Neale, Senior News Editor Daniel Piper, Editor, Digital Art and 3D Ian Dean, Tech Reviews Editor Erlingur Einarsson and Ecommerce Writer Beth Nicholls and Staff Writer Natalie Fear, as well as a roster of freelancers from around the world. The 3D World and ImagineFX magazine teams also pitch in, ensuring that content from 3D World and ImagineFX is represented on Creative Bloq.