ZetCode

Snake in HTML5 canvas

last modified July 17, 2023

In this part of the HTML5 canvas tutorial we create a Snake game clone.

Snake

Snake is an older classic video game. It was first created in late 70s. Later it was brought to PCs. In this game the player controls a snake. The objective is to eat as many apples as possible. Each time the snake eats an apple, its body grows. The snake must avoid the walls and its own body. This game is sometimes called Nibbles.

Development

The size of each of the joints of a snake is 10 px. The snake is controlled with the cursor keys. Initially, the snake has three joints. If the game is finished, the "Game Over" message is displayed in the middle of the canvas.

snake.html
<!DOCTYPE html>
<html>
<head>
<title>Snake in HTML5 canvas</title>
<style>
    canvas {background: black}
</style>
<script>
    var canvas;
    var ctx;

    var head;
    var apple;
    var ball;

    var dots;
    var apple_x;
    var apple_y;

    var leftDirection = false;
    var rightDirection = true;
    var upDirection = false;
    var downDirection = false;
    var inGame = true;

    const DOT_SIZE = 10;
    const ALL_DOTS = 900;
    const MAX_RAND = 29;
    const DELAY = 140;
    const C_HEIGHT = 300;
    const C_WIDTH = 300;

    const LEFT_KEY = 37;
    const RIGHT_KEY = 39;
    const UP_KEY = 38;
    const DOWN_KEY = 40;

    var x = new Array(ALL_DOTS);
    var y = new Array(ALL_DOTS);


    function init() {

        canvas = document.getElementById('myCanvas');
        ctx = canvas.getContext('2d');

        loadImages();
        createSnake();
        locateApple();
        setTimeout("gameCycle()", DELAY);
    }

    function loadImages() {

        head = new Image();
        head.src = 'head.png';

        ball = new Image();
        ball.src = 'dot.png';

        apple = new Image();
        apple.src = 'apple.png';
    }

    function createSnake() {

        dots = 3;

        for (var z = 0; z < dots; z++) {
            x[z] = 50 - z * 10;
            y[z] = 50;
        }
    }

    function checkApple() {

        if ((x[0] == apple_x) && (y[0] == apple_y)) {

            dots++;
            locateApple();
        }
    }

    function doDrawing() {

        ctx.clearRect(0, 0, C_WIDTH, C_HEIGHT);

        if (inGame) {

            ctx.drawImage(apple, apple_x, apple_y);

            for (var z = 0; z < dots; z++) {
                if (z == 0) {
                    ctx.drawImage(head, x[z], y[z]);
                } else {
                    ctx.drawImage(ball, x[z], y[z]);
                }
            }
        } else {

            gameOver();
        }
    }

    function gameOver() {

        ctx.fillStyle = 'white';
        ctx.textBaseline = 'middle';
        ctx.textAlign = 'center';
        ctx.font = 'normal bold 18px serif';

        ctx.fillText('Game over', C_WIDTH/2, C_HEIGHT/2);
    }

    function checkApple() {

        if ((x[0] == apple_x) && (y[0] == apple_y)) {

            dots++;
            locateApple();
        }
    }

    function move() {

        for (var z = dots; z > 0; z--) {
            x[z] = x[(z - 1)];
            y[z] = y[(z - 1)];
        }

        if (leftDirection) {
            x[0] -= DOT_SIZE;
        }

        if (rightDirection) {
            x[0] += DOT_SIZE;
        }

        if (upDirection) {
            y[0] -= DOT_SIZE;
        }

        if (downDirection) {
            y[0] += DOT_SIZE;
        }
    }

    function checkCollision() {

        for (var z = dots; z > 0; z--) {

            if ((z > 4) && (x[0] == x[z]) && (y[0] == y[z])) {
                inGame = false;
            }
        }

        if (y[0] >= C_HEIGHT) {
            inGame = false;
        }

        if (y[0] < 0) {
            inGame = false;
        }

        if (x[0] >= C_WIDTH) {
            inGame = false;
        }

        if (x[0] < 0) {
            inGame = false;
        }
    }

    function locateApple() {

        var r = Math.floor(Math.random() * MAX_RAND);
        apple_x = r * DOT_SIZE;

        r = Math.floor(Math.random() * MAX_RAND);
        apple_y = r * DOT_SIZE;
    }

    function gameCycle() {

        if (inGame) {

            checkApple();
            checkCollision();
            move();
            doDrawing();
            setTimeout("gameCycle()", DELAY);
        }
    }

    onkeydown = function(e) {

        var key = e.keyCode;

        if ((key == LEFT_KEY) && (!rightDirection)) {
            leftDirection = true;
            upDirection = false;
            downDirection = false;
        }

        if ((key == RIGHT_KEY) && (!leftDirection)) {
            rightDirection = true;
            upDirection = false;
            downDirection = false;
        }

        if ((key == UP_KEY) && (!downDirection)) {
            upDirection = true;
            rightDirection = false;
            leftDirection = false;
        }

        if ((key == DOWN_KEY) && (!upDirection)) {
            downDirection = true;
            rightDirection = false;
            leftDirection = false;
        }
    };
</script>
</head>

<body onload="init();">
    <canvas id="myCanvas" width="300" height="300">
    </canvas>
</body>
</html>

First, we define the constants used in the game.

const DOT_SIZE = 10;
const ALL_DOTS = 900;
const MAX_RAND = 29;
const DELAY = 140;
const C_HEIGHT = 300;
const C_WIDTH = 300;

The DOT_SIZE is the size of the apple and the dot of the snake. The ALL_DOTS constant defines the maximum number of possible dots on the canvas (900 = 300*300/10*10). The MAX_RAND constant is used to calculate a random position for an apple. The DELAY constant determines the speed of the game. The C_HEIGHT and C_WIDTH constants store the size of the canvas.

const LEFT_KEY = 37;
const RIGHT_KEY = 39;
const UP_KEY = 38;
const DOWN_KEY = 40;

These constants store the values of arrow keys. They are used for better readability.

var x = new Array(ALL_DOTS);
var y = new Array(ALL_DOTS);

These two arrays store the x and y coordinates of all joints of a snake.

function init() {

    canvas = document.getElementById('myCanvas');
    ctx = canvas.getContext('2d');

    loadImages();
    createSnake();
    locateApple();
    setTimeout("gameCycle()", DELAY);
}

The init function gets the reference to the canvas object and its context. The loadImages, createSnake, and locateApple functions are called to perform specific tasks. The setTimeout starts the animation.

function loadImages() {

    head = new Image();
    head.src = 'head.png';

    ball = new Image();
    ball.src = 'dot.png';

    apple = new Image();
    apple.src = 'apple.png';
}

In the loadImages function we retrieve the images for the game.

function createSnake() {

    dots = 3;

    for (var z = 0; z < dots; z++) {
        x[z] = 50 - z * 10;
        y[z] = 50;
    }
}

In the createSnake function we create the snake object. At the start, it has three joints.

function checkApple() {

    if ((x[0] == apple_x) && (y[0] == apple_y)) {

        dots++;
        locateApple();
    }
}

If the head collides with the apple, we increase the number of joints of the snake. We call the locateApple method which randomly positions a new apple object.

In the move method we have the key algorithm of the game. To understand it, look at how the snake is moving. We control the head of the snake. We can change its direction with the cursor keys. The rest of the joints move one position up the chain. The second joint moves where the first was, the third joint where the second was etc.

for (var z = dots; z > 0; z--) {
    x[z] = x[(z - 1)];
    y[z] = y[(z - 1)];
}

This code moves the joints up the chain.

if (leftDirection) {
    x[0] -= DOT_SIZE;
}

This line moves the head to the left.

In the checkCollision method, we determine if the snake has hit itself or one of the borders.

for (var z = dots; z > 0; z--) {

    if ((z > 4) && (x[0] == x[z]) && (y[0] == y[z])) {
        inGame = false;
    }
}

If the snake hits one of its joints with its head, the game is over.

if (y[0] >= C_HEIGHT) {
    inGame = false;
}

The game is finished if the snake hits the bottom of the canvas.

function locateApple() {

    var r = Math.floor(Math.random() * MAX_RAND);
    apple_x = r * DOT_SIZE;

    r = Math.floor(Math.random() * MAX_RAND);
    apple_y = r * DOT_SIZE;
}

The locateApple randomly selects x and y coordinates for the apple object. The apple_x and apple_y are the coordinates of the uppler-left point of the apple image.

function gameCycle() {

    if (inGame) {

        checkApple();
        checkCollision();
        move();
        doDrawing();
        setTimeout("gameCycle()", DELAY);
    }
}

The gameCycle function forms a game cycle. Provided that the game has not finished, we perform collision detection, do movement and drawing. The setTimeout function calls recursively the gameCycle function.

if ((key == LEFT_KEY) && (!rightDirection)) {
    leftDirection = true;
    upDirection = false;
    downDirection = false;
}

If we hit the left cursor key, we set the leftDirection variable to true. This variable is used in the move function to change the coordinates of the snake object. Notice also that when the snake is heading to the right, we cannot turn immediately to the left.

Snake
Figure: Snake

This was the Snake game in HTML5 canvas.