UltraMega Blog
12Aug/117

Create Snake in JavaScript with HTML5 Canvas

Yesterday I had some spare time, so I decided to write Snake in JavaScript using the HTML5 canvas. If anything, this is a good simple example demonstrating a use of the canvas. So, here's a tutorial walking through the creation of the game.

If, for some reason, you are not familiar with the game Snake, Wikipedia explains it:

The player controls a long, thin creature, resembling a snake, which roams around on a bordered plane, picking up food (or some other item), trying to avoid hitting its own tail or the "walls" that surround the playing area. Each time the snake eats a piece of food, its tail grows longer, making the game increasingly difficult. The user controls the direction of the snake's head (up, down, left, or right), and the snake's body follows.

Here is the final product on jsFiddle (click Result to play):

This tutorial will cover:

  • Drawing on the canvas
  • Handling events in jQuery
  • Handling arrays in JavaScript
  • JavaScript math functions

HTML

We'll start with the HTML, which simply needs to include jQuery and a canvas element with ID of c. Something like this will work:

<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <title>Snake</title>
  <script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/1.6.2/jquery.min.js"></script>
  <script type='text/javascript'>// JS code here</script>
</head>
<body>
  <canvas id="c"></canvas>
</body>
</html>

Variables

Now we can start our code with some variables:

(function ($) {
    var canvas = document.getElementById('c'),
        c = canvas.getContext('2d'),
        height, width, pixelsize, rate,
        dir, newdir, snake = [], food = [], score,
        gstarted = false, gpaused = false;
}(jQuery));

So what are all these variables about?

We store a reference to our canvas element in canvas, which we get using document.getElementById.

The c variable is a reference to a 2d drawing context that allows us to actually draw on the canvas. We get this using the getContext method of the canvas element.

We have height and width to store the dimensions of the playing board, and pixelsize stores how big each "pixel" in our game is in actual pixels. The delay in ms between each frame of our animation is stored in rate.

For game-play data, dir is the direction the snake is moving, newdir is the direction the snake is turning, snake is an array of segments of our snake, food is the location of our food, and score tracks how many food balls have been eaten. Finally, gstarted is true if the game has started and gpaused is true if the game is paused.

Setup

We need a function to set some variables and set up everything. We'll call it setup and it will accept arguments for our customizable variable:

    function setup(h, w, ps, r) {
        height = h;
        width = w;
        pixelsize = ps;
        rate = r;
        canvas.height = h*ps;
        canvas.width = w*ps;

In addition to setting variables, we also set the real dimension of our canvas which is the product of our board size and the pixel size. Our setup function also needs to create the event handler for our keypresses:

        $(document).keydown(function (e) {
            switch(e.which) {
                case 38:
                    if(dir != 2) {
                        newdir = 0;
                    }
                    break;
                case 39:
                    if(dir != 3) {
                        newdir = 1;
                    }
                    break;
                case 40:
                    if(dir != 0) {
                        newdir = 2;
                    }
                    break;
                case 37:
                    if(dir != 1) {
                        newdir = 3;
                    }
                    break;
                case 32:
                    if(!gstarted) {
                        startGame();
                    }
                    else {
                        togglePause();
                    }
                    break;
            }
        });

We're using jQuery to bind a keydown event handler to the document, which is triggered when any key is pressed. We can find out which key was pressed by checking the which property of the event (e) object. We used a switch to perform an action based on the key pressed (38 = up, 39 = right, 40 = down, 37 = left, 32 = space).

For arrow keys, we set the value of newdir as long as it isn't the opposite direction of the current dir (the snake can't go in reverse). Our direction variables represent direction as a number incrementing clockwise (0 = up, 1 = right, 2 = down, 3 = left).

Space is used to either start the game or pause/unpause the game. We'll add the startGame and togglePause functions later.

We'll also call one more function at the end of setup to show the initial screen:

        showIntro();
    }

Intro Screen

Our showIntro function will generate a screen with instruction and it starts like this:

    function showIntro() {
        c.fillStyle = '#000';
        c.fillRect(0, 0, width*pixelsize, height*pixelsize);

So far, it sets the fill color black and draws a filled rectangle the same size as the canvas (our black background). The next part sets the fill color to white, sets the font and alignment, then writes our title:

        c.fillStyle = '#fff';
        c.font = '30px sans-serif';
        c.textAlign = 'center';
        c.fillText('Snake', width/2*pixelsize, height/4*pixelsize, width*pixelsize);

The fillText method accepts arguments for the text, x coordinate, y coordinate, and max width. Above, we're writing "Snake" in the center of the upper quarter of the canvas (remember everything is multiplied by pixelsize). Next, we'll lower the font size and write the instructions:

        c.font = '12px sans-serif';
        c.fillText('Arrows = change direction.', width/2*pixelsize, height/2*pixelsize);
        c.fillText('Space = start/pause.', width/2*pixelsize, height/1.5*pixelsize);
    }

Starting

Here's the function to start the game:

    function startGame() {
        var x = Math.floor(width/2), y = Math.floor(height/2);
        genFood();
        snake = [
            [x, y],
            [--x, y],
            [--x, y],
            [--x, y]
        ];
        dir = 1;
        newdir = 1;
        score = 0;
        gstarted = true;
        gpaused = false;
        frame();
    }

This function creates the initial snake with head in the center and three segments to the left. Note that each element in the snake array is an array of coordinates. The function also sets the initial direction to right, and resets our other game-play variables. Also note that this function calls two function yet to be created, genFood (will randomly generate food coordinates) and frame (processes each animation frame).

Ending and Pausing

Our function to end the game simple sets gstarted to false and draws "Game Over" with the score over a semi-transparent background (dims the game).

    function endGame() {
        gstarted = false;
        c.fillStyle = 'rgba(0,0,0,0.8)';
        c.fillRect(0, 0, width*pixelsize, height*pixelsize);
        c.fillStyle = '#f00';
        c.font = '20px sans-serif';
        c.textAlign = 'center';
        c.fillText('Game Over', width/2*pixelsize, height/2*pixelsize);
        c.fillStyle = '#fff';
        c.font = '12px sans-serif';
        c.fillText('Score: ' + score, width/2*pixelsize, height/1.5*pixelsize);
    }

The next function toggles the gpaused variable. If paused, it writes "Paused" in the center, otherwise it triggers the next frame to restart the paused animation.

    function togglePause() {
        if(!gpaused) {
            gpaused = true;
            c.fillStyle = '#fff';
            c.font = '20px sans-serif';
            c.textAlign = 'center';
            c.fillText('Paused', width/2*pixelsize, height/2*pixelsize);
        }
        else {
            gpaused = false;
            frame();
        }
    }

Checking for Collisions

The game ends when the snake runs into itself or the walls, so we need a function to test for that. This function returns true if the given coordinates are in the walls or snake.

    function testCollision(x, y) {
        var i, l;
        if(x < 0 || x > width-1) {
            return true;
        }
        if(y < 0 || y > height-1) {
            return true;
        }
        for(i = 0, l = snake.length; i < l; i++) {
            if(x == snake[i][0] && y == snake[i][1]) {
                return true;
            }
        }
        return false;
    }

The first two if statements check if the coordinates are beyond the dimensions of the board. I could have put these into one if statement, but I found this easier to read.

The next part loops through the snake segments and checks if our coordinates coincide with any part of the snake.

Generating Food

This function generates random coordinates for our food. We get a random number using the Math.random method, which gives a random value between 0 and 1 (i.e. a percentage), and multiplying by the maximum value.

    function genFood() {
        var x, y;
        do {
            x = Math.floor(Math.random()*(width-1));
            y = Math.floor(Math.random()*(height-1));
        } while(testCollision(x, y));
        food = [x, y];
    }

We put this in a do while loop so we can regenerate in case our coordinates coincide with the snake.

Drawing

This function draws the snake by iterating over the snake array and generating a square for each piece. The fillRect method accepts arguments for the x and y coordinates of the top left corner, and the width and height (pixelsize).

    function drawSnake() {
        var i, l, x, y;
        for(i = 0, l = snake.length; i < l; i++) {
            x = snake[i][0];
            y = snake[i][1];
            c.fillRect(x*pixelsize, y*pixelsize, pixelsize, pixelsize);
        }
    }

This function draws a circle in the location of the food using the arc method, which accepts arguments for the coordinates, radius (half of pixelsize), start angle (0), end angle (Math.PI*2 for a circle), and direction.

    function drawFood() {
        c.beginPath();
        c.arc((food[0]*pixelsize)+pixelsize/2, (food[1]*pixelsize)+pixelsize/2, pixelsize/2, 0, Math.PI*2, false);
        c.fill();
    }

Since these coordinates are for the center of the circle while the snake pieces are based on the corner of the square, we have to shift the coordinates for the circle by half of pixelsize so it lines up with everything.

Running

The last function we need to add is that frame function I mentioned earlier.

If the game is paused or ended, do nothing:

    function frame() {
        if(!gstarted || gpaused) {
            return;
        }

Get the coordinates of the snake's head (the first element in the snake array):

        var x = snake[0][0], y = snake[0][1];

Move the head in the correct direction (up = decrement y, right = increment x, down = increment y, left = decrement x):

        switch(newdir) {
            case 0:
                y--;
                break;
            case 1:
                x++;
                break;
            case 2:
                y++;
                break;
            case 3:
                x--;
                break;
        }

If the head has collided, end the game and stop:

        if(testCollision(x, y)) {
            endGame();
            return;
        }

Add the new coordinates to the beginning of the array using unshift:

        snake.unshift([x, y]);

If the snake has hit food, increment the score and make new food; otherwise, remove the last segment of the snake using pop on the array (not removing it makes the snake grow):

        if(x == food[0] && y == food[1]) {
            score++;
            genFood();
        }
        else {
            snake.pop();
        }

Update the current direction:

        dir = newdir;

Black out the board and redraw everything:

        c.fillStyle = '#000';
        c.fillRect(0, 0, width*pixelsize, height*pixelsize);
        c.fillStyle = '#fff';
        drawFood();
        drawSnake();

Schedule the next frame in rate milliseconds (if stable frame rate was important, we could subtract execution time from the delay):

        setTimeout(frame, rate);
    }

Running

The last step is to start everything by calling that setup function:

    setup(25, 25, 10, 200);

Our board size is now 25x25 with each pixel being 10 real pixels, with a fairly slow delay of 200 ms.

Conclusion

I hope that was easy enough to follow and is useful to someone learning how to use the canvas or JavaScript. It's been a while since I've made a blog post, so I figured I should post something fun. If you have any questions or suggestion, please post a comment.

Posted by Steve

Comments (7) Trackbacks (0)
  1. Hi Steve,
    I’m writing an application for the Windows Phone 7 platform and was looking for sample Javascript code. Could I use your code? Let me know if you have any issues with me using your code/modifying it as needed.
    Regards,
    Sriram

  2. Hi,
    Thank you for the tutorial.
    I have a question: if i just put the next code in a html file and the java script code in the “snake.js” file, wouldn’t be enough for the game to work in a browser? Because it doesn’t.
    The code:

    Snake

    Thank you!
    Isabelle

  3. The code from the HTML section of the tutorial, with a second source snake.js.

  4. hello sir i like ur site
    but i have a request & question:
    1. how i create clock in using c program or html

  5. It doesn’t seem to work, i had it in its own seperate file javascript.js and that didn’t work so i took it out of the .js file and just put it in the tags and that didn’t work so i thought i messed up on something decided to copy and paste you’re code, didn’t work. So overall it didn’t work for me :(. Did full tut though learned some stuff ^^.


Leave a comment

This site uses Akismet to reduce spam. Learn how your comment data is processed.

No trackbacks yet.