Snake Game in HTML5

from tuts/html/Snake Game in HTML5


I can't understand Snake Games. How do I make one?

Asked by a fictitious person as an excuse to post this

Hold up!

I've already written about this, haven't I?

Oh well, I guess it would be good to move that over here, no?

Note: This was written in 2012. Very old code.

Assumptions

This tutorial makes the following assumptions:

Snake Game in HTML5

The main idea behind this is that there is a master X and Y of the snake. Individual pieces do not have their own X's or Y's

The master X and Y coordinates represent where the snake's head is (or will be when the frame gets rendered)

This is my initial implementation:


(function(canvas){
  var mem = {}, ctx = canvas.getContext('2d'), W, H;
  canvas.width = W = 300; canvas.height = H = 300;
  function add(o){var i;o.id = Math.floor(Math.random()*10000).toString(36);for(i in mem){if(i === o.id){add(o);return false;}}mem[o.id] = o;};
  function remove(o){delete mem[o.id];};
  function render(o){var info = o.getRenderInfo();ctx.fillStyle = info.c;ctx.fillRect(info.x,info.y,info.w,info.h);};
  (function loop(){ctx.clearRect(0,0,W,H);for(var a in mem){if(mem.hasOwnProperty(a) && mem[a].run){mem[a].run()}}setTimeout(loop,1000/12);})();
  
  function Snake(){
    var x = 0, y = H/2, w = 10, h = 10, c = 'rgb('+0+','+0+','+255+')';
    this.run = function(){
      x+=w;
      if(x > W)x = 0;
      render(this);
    };
    this.getRenderInfo = function(){
      return {x:x,y:y,w:w,h:h,c:c};
    };
    add(this);
  };
  new Snake();
})(c1);

The way it works is that a chunk, the "tail", is removed from the end of an array of pieces, let's pretend it looks like this: [1,2,3,4,5]. [5] gets taken from the right, X gets incremented, applied to [5], and then [5] gets put at the head of the array, which is now [5,1,2,3,4]. This process continues, making it look like it's moving. That's right: it's an illusion! Everything you know is a LIE! DOWN WITH THE SYSTEM! DOW--ahem got a little...carried away there, heheh...heh...

Anyways, here's the new Snake class, adapted to use an array of pieces:


function Snake(){
    var x = 0, y = H/2, w = 10, h = 10, pieces = [];
    function SnakePiece(){
      function r(){return Math.floor(Math.random()*0xFF)};
      this.x = -10;
      this.y = -10;
      this.w = w;
      this.h = h;
      this.c = 'rgb('+r()+','+r()+','+r()+')';
    };
    for(var i = 0; i < 10; i ++){
      pieces.push(new SnakePiece());
    }
    this.run = function(){
      var piece, i;
      x+=w;
      y+=0;
      piece = pieces.pop();
      piece.x = x;
      piece.y = y;
      pieces.unshift(piece);
      for(i = 0; i < pieces.length; i ++){
        render(pieces[i]);
      }
      if(x > W)x = 0;
    };
    add(this);
  };

And the illusion is illustrated below, with random colors for each cell so you can see what's going on

Pretty fascinating, huh?

So, now for the controls. The way this works is the direction is stored in a number. Of course, it can be done in any number of convoluted ways, but being a minimalist, I like using a small integer.

   ↑
   0
←3   1→
   2
   ↓

0 = up

1 = right

2 = down

3 = left

Simple, elegant, and it could work in an 8-bit processor, like the one used to run pacman, or the 6502 that powers the NES.

So I can implement that by checking the direction during each loop before rendering, changing x and y based on that, then binding the change of direction to the keyboard.


var key = [];
...
canvas.tabIndex = 0;
...
if(key[38]){
  if(dir !== 2){
    dir = 0;
  }
}else if(key[39]){
  if(dir !== 3){
    dir = 1;
  }
}else if(key[40]){
  if(dir !== 0){
    dir = 2;
  }
}else if(key[37]){
  if(dir !== 1){
    dir = 3;
  }
}
...
if(dir == 0)y-=h;
else if(dir == 2)y+=h;
else if(dir == 1)x+=w;
else if(dir == 3)x-=w;
...
canvas.onkeydown = canvas.onkeyup = function(e){
  key[e.keyCode] = e.type == 'keydown';
  return false;
};

As you can see, implementing keyboard interactivity isn't too easy. There's a lot of convoluted stuff you have to use to get a well-working implementation. One of those is knowing the ASCII standard, how it works, and what the ASCII keycodes for the keyboard's keys are. In this case, I'm using left, up, right, and down, or to the CPU, 37, 38, 39, and 40

Go ahead, control the little snakey down below: click the canvas to focus it (so it won't scroll when you press up and down), then use the arrow keys to change the direction.

That's pretty much it. The rest is just hit detection (checking the head's coordinates against the coordinates of other objects), fine tuning, and eye candy. A Food Class can easily be implemented to appear on a random part of the field through something like floor( random float between 0 & 1 * Width of Field ) % Width of Snake Piece. New snake pieces can be added by appending objects with the needed information to the end of the Snake's pieces[] array.

Of course, it does help to see an implementation. So here's the code for the full game


/**********************************************************************************
Simple Snake by Braden Best

As long as you can execute the script in a modern html5 web browser, this will work
***********************************************************************************/
(function(){
  var canvas, ctx, mem = {}, key = [], W, H, Snake, Food, GameOver;
  canvas = document.createElement('canvas');
  canvas.innerText = 'Your browser does not support HTML5 :(\n\nIf this is IE9, make sure "" is at the top of the source';
  canvas.width = W = 500;
  canvas.height = H = 500;
  canvas.tabIndex = 0; // allows canvas to be focused
  canvas.style.background = '#000';
  (function appendCanvas(){
    if(document.body){
      document.body.appendChild(canvas);
    }else{
      setTimeout(appendCanvas,100);
    }
  })();
  ctx = canvas.getContext('2d');
  ctx.fillStyle = '#fff';
  function add(o){//adds instance to memory
    o.id = Math.floor(Math.random()*10000).toString(36);
    for(var i in mem){
      if(o.id == i){
        add(o);
        return false;
      }
    }
    mem[o.id] = o;
  };
  function remove(o){ // removes instance from memory
    delete mem[o.id];
  };
  function Snake(x,y,w,h){ // Snake Class
    var x = x,
        y = y,
        w = w,
        h = h,
        body = [],
        dir = Math.floor(Math.random()*4), // 0 = up, 1 = right, 2 = down, 3 = left
        snakePiece = function(x,y,w,h){return {x:x,y:y,w:w,h:h}};
    for(var i = 0; i < 10; i++){
      body.push(snakePiece(-w,-h,w,h));
    }
    new Food(); // Make first food
    this.run = function(){
      var i;
      //Controls
      if(key[37] && dir != 1){ // Left
        dir = 3;
      }else if(key[39] && dir != 3){ // Right
        dir = 1;
      }else if(key[38] && dir != 2){ // Up
        dir = 0;
      }else if(key[40] && dir != 0){ // Down
        dir = 2;
      }
      //Movement vector
      if(dir == 0)y-=h;
      else if(dir == 1)x+=w;
      else if(dir == 2)y+=h;
      else if(dir == 3)x-=w;
      //Walls
      if(x<0)x=W-w;
      if(x>W-w)x=0;
      if(y<0)y=H-h;
      if(y>H-h)y=0;
      //Actual movement
      var curPiece = body.pop();
      curPiece.x = x;
      curPiece.y = y;
      body.unshift(curPiece);
      //Collision check...
      for(i in mem){
        //...with food
        if(mem[i].constructor.name === 'Food'){
          if(curPiece.x === mem[i].x && curPiece.y === mem[i].y){
            remove(mem[i]);
            new Food();
            for(var j = 0; j < 5; j++){
              body.push(snakePiece(-w,-h,w,h));
            }
          }
        }
      }
      for(i = 0; i < body.length; i++){
        if(i !== 0){
          //...with self
          if(curPiece.x === body[i].x && curPiece.y === body[i].y){
            remove(this);
            new GameOver();
          }
        }
        ctx.fillRect(body[i].x,body[i].y,body[i].w,body[i].h); // Render
      }
    };
    add(this); // add to memory
  };
  function Food(){ // Food Class
    var r1 = Math.floor(Math.random()*W),
        r2 = Math.floor(Math.random()*H);
    this.x = r1-r1%10;
    this.y = r2-r2%10;
    this.run = function(){
      var snake_exists = false;
      for(var i in mem){
        if(mem[i].constructor.name === 'Snake'){
          snake_exists = true;
          break;
        }
      }
      if(!snake_exists){
        remove(this);
      }
      ctx.fillRect(this.x,this.y,10,10); // Render
    };
    add(this); // add to memory
  };
  function GameOver(){ // Game Over Class
    var text = ['Game Over', 'Press \'R\' to Restart'];
    ctx.textBaseline = 'middle';
    ctx.textAlign = 'center';
    ctx.font = 'bold 42px "courier new" monospace';
    this.run = function(){
      ctx.fillText(text[0],W/2,H/2-20);
      ctx.fillText(text[1],W/2,H/2+20);
      if(key[0x52]){ // R
        remove(this);
        new Snake(W/2,H/2,10,10);
      }
    };
    add(this);
  };
  (function init(){
    new Snake(W/2,H/2,10,10);
  })();
  (function loop(){
    ctx.clearRect(0,0,W,H);
    for(var a in mem)if(mem.hasOwnProperty(a))mem[a].run(); // run every object in memory
    setTimeout(loop,1000/24); // call next frame at 24 fps
  })();
  canvas.onkeydown = canvas.onkeyup = function(e){
    key[e.keyCode] = e.type == 'keydown';
    return false;
  };
})();

And the game itself

Note: there is no wall in this game. The goal is simple: you must eat food and avoid this:

Permanent Link to this page: http://bradenbest.com/tutorials/get_page.php?path=tuts%2Fhtml%2F&name=Snake+Game+in+HTML5

Have an idea for a tutorial? Go to the Suggestion Box


Back to main