Hire a web Developer and Designer to upgrade and boost your online presence with cutting edge Technologies

Wednesday, January 18, 2012

Create a mobile version of Snake with HTML5 canvas and JavaScript

Create a mobile version of Snake using HTML5 canvas and JavaScript
  • Knowledge needed: Basic - intermediate JavaScript
  • Requires: Browser and your favourite text editor (or IDE if that floats your boat)
  • Project time: 1 hour

This article 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:
  1. Check for user input.
  2. Move players (In this case the snake, its various segments and the tasty red apple).
  3. 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.
  4. 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.
  1. (function () {
  2.     var canvas = document.getElementById('snakeCanvas'),
  3.         ctx = canvas.getContext('2d'),
  4.         score = 0,
  5.         hiScore = 20,
  6.         leftButton = document.getElementById('leftButton'),
  7.         rightButton = document.getElementById('rightButton'),
  8.         input = { left: false, right: false };
  9.     canvas.width = 320;
  10.     canvas.height = 350;
  11.     // check for keypress and set input properties
  12.     window.addEventListener('keyup', function(e) {
  13.        switch (e.keyCode) {
  14.             case 37: input.left = true; break;                            
  15.             case 39: input.right = true; break;                            
  16.        }
  17.     }, false);
  18.     // the rest of the code goes here
  19. }());
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.
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.
  1.    // a collection of methods for making our mark on the canvas
  2.     var draw = {
  3.         clear: function () {
  4.             ctx.clearRect(0, 0, canvas.width, canvas.height);
  5.         },    
  6.         rect: function (x, y, w, h, col) {
  7.             ctx.fillStyle = col;
  8.             ctx.fillRect(x, y, w, h);
  9.         },
  10.        
  11.       circle: function (x, y, radius, col) {
  12.           ctx.fillStyle = col;
  13.           ctx.beginPath();
  14.           ctx.arc(x, y, radius, 0, Math.PI*2, true);
  15.           ctx.closePath();
  16.           ctx.fill();
  17.       },
  18.         text: function (str, x, y, size, col) {
  19.             ctx.font = 'bold ' + size + 'px monospace';
  20.             ctx.fillStyle = col;
  21.             ctx.fillText(str, x, y);
  22.         }
  23.     };
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.
    1.    // let's see if it works by drawing some shapes
    2.     draw.rect(50,20,100,100,'green');
    3.     draw.rect(70,40,20,20,'white');
    4.     draw.rect(80,50,10,10,'black');
    5.     draw.rect(120,40,20,20,'white');
    6.     draw.rect(130,50,10,10,'black');
    7.     draw.rect(60,90,80,10, 'darkgreen');
    8.     draw.circle(200,80,30,'red');
    9.     // have you guessed what it is yet?
    10.     draw.text('Snake, meet apple.', 70, 180, 14);
    11.     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.
    1. // main snake class
    2.     var Snake = function() {
    3.         this.init = function() {
    4.             this.dead = false;
    5.             this.len = 0; // length of the snake (number of segments)
    6.             this.speed = 4; // amount of pixels moved per frame
    7.             this.history = []; // we'll need to keep track of where we've been
    8.             this.dir = [    // the four compass points in which the snake moves
    9.                 [0, -1],    // up
    10.                 [1, 0],     // right
    11.                 [0, 1],     // down
    12.                 [-1, 0]     // left
    13.             ];
    14.             this.x = 100;
    15.             this.y = 100;
    16.             this.w = this.h = 16;
    17.             this.currentDir = 2;    // i.e. this.dir[2] = down
    18.             this.col = 'darkgreen';
    19.         };
    20.  
    21.         this.move = function() {
    22.             if (this.dead) {
    23.                 return;
    24.             }
    25.             // check if a button has been pressed
    26.             if (input.left) {
    27.                 this.currentDir += 1;
    28.                 if (this.currentDir > 3) {
    29.                     this.currentDir = 0;
    30.                 }
    31.             } else if (input.right) {
    32.                 this.currentDir -= 1;
    33.                 if (this.currentDir < 0) {
    34.                     this.currentDir = 3;
    35.                 }
    36.             }
    37.             // check if out of bounds
    38.             if (this.x < 0 || this.x > (canvas.width - this.w)
    39.                 || this.y < 0 || this.y > (canvas.height - this.h)) {
    40.                 this.dead = true;    
    41.             }
    42.             // update position
    43.             this.x += (this.dir[this.currentDir][0] * this.speed);
    44.             this.y += (this.dir[this.currentDir][1] * this.speed);
    45.             // store this position in the history array
    46.             this.history.push({x: this.x, y: this.y, dir: this.currentDir});
    47.         };
    48.         this.draw = function () {
    49.               draw.rect(this.x, this.y, this.w, this.h, this.col); // draw head
    50.               draw.rect(this.x + 4, this.y + 1, 3, 3, 'white');    // draw eyes
    51.               draw.rect(this.x + 12, this.y + 1, 3, 3, 'white');
    52.         };
    53.         this.collides: function () {
    54.             // we'll come back to this in a bit
    55.         },
    56.     };
    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:
    1. var p1 = new Snake();
    2. p1.init();
    3. function loop() {
    4.   draw.clear(); // clear our canvas. the previous loop's drawings are still there
    5.   p1.move();
    6.   p1.draw();
    7.   if (p1.dead) {
    8.     draw.text('Game Over', 100, 100, 12, 'black');
    9.   }
    10.   // we need to reset right and left or else the snake keeps on turning
    11.   input.right = input.left = false;
    12. };
    13. setInterval(loop, 30); // call the loop function every 30 milliseconds
    See it in action.


    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:
    1.   var Apple = function() {
    2.    
    3.         this.x = 0;
    4.         this.y = 0;
    5.         this.w = 16;
    6.         this.h = 16;
    7.         this.col = 'red';
    8.         this.replace = 0;   // game turns until we move the apple elsewhere
    9.         this.draw = function() {
    10.             if (this.replace === 0) { // time to move the apple elsewhere
    11.                 this.relocate();
    12.             }
    13.             draw.rect(this.x, this.y, this.w, this.h, this.col);
    14.             this.replace -= 1;
    15.         };
    16.         this.relocate = function() {
    17.             this.x = Math.floor(Math.random() * (canvas.width - this.w));
    18.             this.y = Math.floor(Math.random() * (canvas.height -this.h));
    19.             this.replace = Math.floor(Math.random() * 200) + 200;
    20.         };
    21.     };
    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.
    1.    // add this into the Snake class
    2.         this.collides = function(obj) {
    3.             // this sprite's rectangle
    4.             this.left = this.x;
    5.             this.right = this.x + this.w;
    6.             this.top = this.y;
    7.             this.bottom = this.y + this.h;
    8.             // other object's rectangle
    9.             // note: we assume that obj has w, h, w & y properties
    10.             obj.left = obj.x;
    11.             obj.right = obj.x + obj.w;
    12.             obj.top = obj.y;
    13.             obj.bottom = obj.y + obj.h;
    14.             // determine if not intersecting
    15.             if (this.bottom < obj.top) { return false; }
    16.             if (this.top > obj.bottom) { return false; }
    17.             if (this.right < obj.left) { return false; }
    18.             if (this.left > obj.right) { return false; }
    19.             // otherwise, it's a hit
    20.             return true;
    21.         };
    We need to update the loop function to handle drawing the apple and checking for collision.
    1.     function loop() {
    2.       draw.clear();
    3.       p1.move();
    4.       p1.draw();
    5.       if (p1.collides(apple)) {
    6.         score += 1;
    7.         p1.len += 1;
    8.         apple.relocate();
    9.       }
    10.       if (score > hiScore) {
    11.         hiScore = score;
    12.       }
    13.       apple.draw();
    14.       draw.text('Score: '+score, 20, 20, 12, 'black');
    15.       draw.text('Hi: '+hiScore, 260, 20, 12, 'black');
    16.       if (p1.dead === true) {
    17.         draw.text('Game Over', 100, 200, 20, 'black');
    18.         if (input.right || input.left) {
    19.           p1.init();
    20.           score = 0;
    21.         }
    22.       }
    23.       input.right = input.left = false;
    24.     }
    Check our progress so far.


    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:
    1. For each segment of the snake grab its position from the history array.
    2. 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).
    3. Draw a rectangle at that position.
    4. 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:
    1.   this.draw = function () {
    2.     var i, offset, segPos, col;
    3.     // loop through each segment of the snake,
    4.     // drawing & checking for collisions
    5.     for (i = 1; i <= this.len; i += 1) {
    6.       // offset calculates the location in the history array
    7.       offset = i * Math.floor(this.w / this.speed);
    8.       offset = this.history.length - offset;
    9.       segPos = this.history[offset];
    10.       col = this.col;
    11.       // reduce the area we check for collision, to be a bit
    12.       // more forgiving with small overlaps
    13.       segPos.w = segPos.h = (this.w - this.speed);
    14.       if (i > 2 && i !== this.len && this.collides(segPos)) {
    15.         this.dead = true;
    16.         col = 'darkred'; // highlight hit segments
    17.       }
    18.       draw.rect(segPos.x, segPos.y, this.w, this.h, col);
    19.     }
    20.     draw.rect(this.x, this.y, this.w, this.h, this.col); // draw head
    21.     draw.rect(this.x + 4, this.y + 1, 3, 3, 'white');    // draw eyes  
    22.     draw.rect(this.x + 12, this.y + 1, 3, 3, 'white');
    23.   };
    Check out your work so far.

    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.
    1.   //let's assume we're not using a touch capable device
    2.     var clickEvent = 'click';
    3.     // now we try a simple test to see if we have touch
    4.     try {
    5.         document.createEvent('TouchEvent');
    6.         // it seems we do, so we should check for it rather than click
    7.         clickEvent = 'touchend';
    8.     } catch(e) { }
    9.     leftButton.addEventListener(clickEvent, function(e) {
    10.         e.preventDefault();
    11.         input.left = true;
    12.     }, false);
    13.     rightButton.addEventListener(clickEvent, function(e) {
    14.         e.preventDefault();
    15.         input.right = true;
    16.     }, 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:
    1. <meta name="viewport" content="width=320, height=440,
    2.    user-scalable=no, initial-scale=1.0; maximum-scale=1.0; user-scalable=0;" />
    3. <meta name="apple-mobile-web-app-capable" content="yes" />
    4. <meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
    5. <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.
    1.  window.scrollTo(0,0);
    2.   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.

    No comments:

    Post a Comment