blog.mailson.org


Simple pong game using HTML5 and canvas

I've spend some time working on a simple game using canvas 2d. I had some problems while doing this (some hard to find) and I thought it would be great to share my experiences on this work.

This is a simple version of the well know game pong. Throughout this article I'll give some tips on how I treated the user's input (both keyboard and touch events), a word about collision detection and also troubleshooting some issues that came to me.

According to wikipedia:

Pong is a two-dimensional sports game that simulates table tennis. The player controls an in-game paddle by moving it vertically across the left side of the screen, and can compete against either a computer controlled opponent or another player controlling a second paddle on the opposing side. Players use the paddles to hit a ball back and forth. The aim is for a player to earn more points than the opponent; points are earned when one fails to return the ball to the other.

Introduction

I've used a basic object-oriented approach on top of JavaScript found on Mozilla developer network. In fact, that's not the only way of using object-oriented programming in JavaScript.

In the game we have a MainLoop() function which calls the game update and draw functions (in that order). The main loop is repeatedly called through a setTimeout.

The Game class* is the actual game. It controls each player movement, listens to user input and also call the draw methods.

*Although JavaScript doesn't actually have classes I'll use the term for a better understanding.


Setting the basics

First we need to create a HTML file with a canvas element and source the JavaScript.

<!DOCTYPE HTML>
<html>
    <head>
        <title>Pong</title>
        <script defer src="pong.js"></script>
        <link rel="stylesheet" href="style.css" />
    </head>
    <body>
        <canvas id="game" width="512" height="256"></canvas>
    </body>
</html>

There's a HTML5 page (you can tell by the first line) with a 512x256 canvas element. The defer keyword in the script tag tells the browser to only execute the script after all elements are loaded. This way we avoid referring to a canvas element that weren't created yet.

The background is painted using CSS instead of calling a canvas draw function to do the job. The reason is that painting on canvas is quite expensive and the browser can do some performance tuning on CSS. So painting a background onto a canvas should be avoided.

#game {
    background-color: #353535;
}

Game mockup

As we discussed before there's a Game which has two main functions: update() and draw().

function Game() {
    var canvas = document.getElementById("game");
    this.width = canvas.width;
    this.height = canvas.height;
    this.context = canvas.getContext("2d");
    this.context.fillStyle = "white";
}

In the constructor we get the canvas element and a 2d context out of it. We don't actually need to save the canvas element itself because we will rarely use it and we can always get it back from the context using context.canvas.

The other Game methods are implemented below:

Game.prototype.draw = function()
{
    this.context.clearRect(0, 0, this.width, this.height);
    this.context.fillRect(this.width/2, 0, 2, this.height);
};

Game.prototype.update = function()
{
    if (this.paused)
        return;
};

Structuring the main loop

Our game main loop is a function which is called every 33ms. That function has a simple job: call the game update() and draw() functions. To call a function continuously we could use either setInterval, setTimeout or the new requestAnimationFrame.

// Initialize our game instance
var game = new Game();

function MainLoop() {
    game.update();
    game.draw();
    // Call the main loop again at a frame rate of 30fps
    setTimeout(MainLoop, 33.3333);
}

// Start the game execution
MainLoop();

For now our game looks like this. A gray rectangle with a white net in the middle.

The players

There are two players here, each on a side of the field. To represent a player we create the Paddle class to store its position, score and size.

function Paddle(x,y) {
    this.x = x;
    this.y = y;
    this.width = 2;
    this.height = 28;
    this.score = 0;
}

We better create a draw function for the player. Thereby Game don't need to worry on how the player is drawn.

Paddle.prototype.draw = function(p)
{
    p.fillRect(this.x, this.y, this.width, this.height);
};

Finally we put the players on each side of the canvas. The 5 pixels margin is purely aesthetic.

function Game() {
    var canvas = document.getElementById("game");
    this.width = canvas.width;
    this.height = canvas.height;
    this.context = canvas.getContext("2d");
    this.context.fillStyle = "white";

    this.p1 = new Paddle(5, 0);
    this.p1.y = this.height/2 - this.p1.height/2;
    this.p2 = new Paddle(this.width - 5 - 2, 0);
    this.p2.y = this.height/2 - this.p2.height/2;
}
Game.prototype.draw = function()
{
    this.context.clearRect(0, 0, this.width, this.height);
    this.context.fillRect(this.width/2, 0, 2, this.height);

    this.p1.draw(this.context);
    this.p2.draw(this.context);
};

And here's how it's looking like:

Keyboard input

The players only move vertically so we assign W and S to move the left player and UP and DOWN to the right one.

For this purpose we should listen to the keydown event and every time it occurs we'll check the event's keyCode to execute the movement.

If the user keeps holding a key, the new keydown events are made and new calls to the callback are executed. It should fit perfectly for a game. However only one key sends a keydown event at a time. Let the first player hold down the S key. When the second player starts holding the DOWN key we won't receive the keydown event for the first player anymore.

In the following example, try to hold down 2 keys at the same time and see it for yourself: http://jsfiddle.net/mailson/TupBQ/

So how do you know the first player is pressing down the S key after the second player made its move? Well, although we may not receive the keydown anymore we should know the key is being pressed if we don't get a keyup event for that.

See another example using the latter approach: http://jsfiddle.net/mailson/jreG6/

To get our jobs easily done I've created a KeyListener class to handle this. It just listens to keydown/keyup events and keeps track of what keys are being pressed.

function KeyListener() {
    this.pressedKeys = [];

    this.keydown = function(e) {
        this.pressedKeys[e.keyCode] = true;
    };

    this.keyup = function(e) {
        this.pressedKeys[e.keyCode] = false;
    };

    document.addEventListener("keydown", this.keydown.bind(this));
    document.addEventListener("keyup", this.keyup.bind(this));
}

KeyListener.prototype.isPressed = function(key)
{
    return this.pressedKeys[key] ? true : false;
};

KeyListener.prototype.addKeyPressListener = function(keyCode, callback)
{
    document.addEventListener("keypress", function(e) {
        if (e.keyCode == keyCode)
            callback(e);
    });
};

A word about the bind() function

When a event callback is called the this variable points to the element that triggered the event. Yet we need a reference to the KeyListener object in both keyup and keydown callbacks. To achieve that we use a bind() to tell the callback that this actually points to our KeyListener instance (this), not the event caller.

For further information, please read the addEventListener documentation on MDN.

Although useful the bind() function is quite recent and won't work in Internet Explorer <9 or Safari <6. Fortunately someone has already came up with a work around for that.

Using KeyListener

To move the paddles we simply create a new instance of KeyListener and check, inside the game update, if our respective keys are being pressed.

function Game() {
    var canvas = document.getElementById("game");
    this.width = canvas.width;
    this.height = canvas.height;
    this.context = canvas.getContext("2d");
    this.context.fillStyle = "white";
    this.keys = new KeyListener();

    this.p1 = new Paddle(5, 0);
    this.p1.y = this.height/2 - this.p1.height/2;
    this.p2 = new Paddle(this.width - 5 - 2, 0);
    this.p2.y = this.height/2 - this.p2.height/2;
}

Game.prototype.update = function()
{
    if (this.paused)
        return;

    // To which Y direction the paddle is moving
    if (this.keys.isPressed(83)) { // DOWN
        this.p1.y = Math.min(this.height - this.p1.height, this.p1.y + 4);
    } else if (this.keys.isPressed(87)) { // UP
        this.p1.y = Math.max(0, this.p1.y - 4);
    }

    if (this.keys.isPressed(40)) { // DOWN
        this.p2.y = Math.min(this.height - this.p2.height, this.p2.y + 4);
    } else if (this.keys.isPressed(38)) { // UP
        this.p2.y = Math.max(0, this.p2.y - 4);
    }
};

Our progress so far:

The ball

The ball element is not controlled by the user. Instead, it moves around the viewport with an X and Y velocity. When the ball hits a paddle, it changes to the opposite X direction but keeps the Y velocity. When the ball hits the top or bottom of the screen, it moves in the opposite Y direction while the X velocity remains the same. If it crosses the left or right side of the screen, the player of the opposite side scores.

For now we're going to implement a very basic collision detection for the ball.

function Ball() {
    this.x = 0;
    this.y = 0;
    this.vx = 0;
    this.vy = 0;
    this.width = 4;
    this.height = 4;
}

Ball.prototype.update = function()
{
    this.x += this.vx;
    this.y += this.vy;
};

Ball.prototype.draw = function(p)
{
    p.fillRect(this.x, this.y, this.width, this.height);
};

This is a simple class which stores the ball's attributes. The position is updated on each update() call.

function Game() {
    // [...]
    this.ball = new Ball();
    this.ball.x = this.width/2;
    this.ball.y = this.height/2;
    this.ball.vy = Math.floor(Math.random()*12 - 6);
    this.ball.vx = 7 - Math.abs(this.ball.vy);
}

Game.prototype.update = function()
{
    if (this.paused)
        return;

    // [...]

    this.ball.update();
    if (this.ball.x > this.width || this.ball.x + this.ball.width < 0) {
        this.ball.vx = -this.ball.vx;
    } else if (this.ball.y > this.height || this.ball.y + this.ball.height < 0) {
        this.ball.vy = -this.ball.vy;
    }
};

Game.prototype.draw = function()
{
    this.context.clearRect(0, 0, this.width, this.height);
    this.context.fillRect(this.width/2, 0, 2, this.height);

    this.ball.draw(this.context);

    this.p1.draw(this.context);
    this.p2.draw(this.context);
};

It's starting to look like a game, isn't it?

Improving the collision detection

You probably noticed that the ball is not actually colliding with the paddles. We're not even checking where the paddle is on our collision detection algorithm.

A simple solution would be checking on game's update if the ball intersects with either of the paddles, then change the X velocity accordingly.

However this is not a safe check. The ball moves more than 1 pixel per loop and situations like the illustrated below often occurs.

To avoid this tunneling effect, we need to "guess" where the ball was at the instant of collision and translate the ball to that position.

I won't spend much time talking about the collision detection. The following code explains for itself.

Game.prototype.update = function()
{
    if (this.paused)
        return;

    this.ball.update();

    // To which Y direction the paddle is moving
    if (this.keys.isPressed(83)) { // DOWN
        this.p1.y = Math.min(this.height - this.p1.height, this.p1.y + 4);
    } else if (this.keys.isPressed(87)) { // UP
        this.p1.y = Math.max(0, this.p1.y - 4);
    }

    if (this.keys.isPressed(40)) { // DOWN
        this.p2.y = Math.min(this.height - this.p2.height, this.p2.y + 4);
    } else if (this.keys.isPressed(38)) { // UP
        this.p2.y = Math.max(0, this.p2.y - 4);
    }

    if (this.ball.vx > 0) {
        if (this.p2.x <= this.ball.x + this.ball.width &&
                this.p2.x > this.ball.x - this.ball.vx + this.ball.width) {
            var collisionDiff = this.ball.x + this.ball.width - this.p2.x;
            var k = collisionDiff/this.ball.vx;
            var y = this.ball.vy*k + (this.ball.y - this.ball.vy);
            if (y >= this.p2.y && y + this.ball.height <= this.p2.y + this.p2.height) {
                // collides with right paddle
                this.ball.x = this.p2.x - this.ball.width;
                this.ball.y = Math.floor(this.ball.y - this.ball.vy + this.ball.vy*k);
                this.ball.vx = -this.ball.vx;
            }
        }
    } else {
        if (this.p1.x + this.p1.width >= this.ball.x) {
            var collisionDiff = this.p1.x + this.p1.width - this.ball.x;
            var k = collisionDiff/-this.ball.vx;
            var y = this.ball.vy*k + (this.ball.y - this.ball.vy);
            if (y >= this.p1.y && y + this.ball.height <= this.p1.y + this.p1.height) {
                // collides with the left paddle
                this.ball.x = this.p1.x + this.p1.width;
                this.ball.y = Math.floor(this.ball.y - this.ball.vy + this.ball.vy*k);
                this.ball.vx = -this.ball.vx;
            }
        }
    }

    // Top and bottom collision
    if ((this.ball.vy < 0 && this.ball.y < 0) ||
            (this.ball.vy > 0 && this.ball.y + this.ball.height > this.height)) {
        this.ball.vy = -this.ball.vy;
    }
};

Now we have a game:

Counting the score

The goal in this game is very simple: a player needs to make the ball cross the wall on the opposite side.

Game.prototype.update = function()
{
    // [...]
    if (this.ball.x >= this.width)
        this.score(this.p1);
    else if (this.ball.x + this.ball.width <= 0)
        this.score(this.p2);
};

Game.prototype.score = function(p)
{
    // player scores
    p.score++;
    var player = p == this.p1 ? 0 : 1;

    // set ball position
    this.ball.x = this.width/2;
    this.ball.y = p.y + p.height/2;

    // set ball velocity
    this.ball.vy = Math.floor(Math.random()*12 - 6);
    this.ball.vx = 7 - Math.abs(this.ball.vy);
    if (player == 1)
        this.ball.vx *= -1;
};

We also need a display to show the score

function Display(x, y) {
    this.x = x;
    this.y = y;
    this.value = 0;
}

Display.prototype.draw = function(p)
{
    p.fillText(this.value, this.x, this.y);
};
function Game() {
    // [...]

    this.p1 = new Paddle(5, 0);
    this.p1.y = this.height/2 - this.p1.height/2;
    this.display1 = new Display(this.width/4, 25);
    this.p2 = new Paddle(this.width - 5 - 2, 0);
    this.p2.y = this.height/2 - this.p2.height/2;
    this.display2 = new Display(this.width*3/4, 25);
}

Game.prototype.draw = function()
{
    // [...]
    this.display1.draw(this.context);
    this.display2.draw(this.context);
};

Game.prototype.update = function()
{
    if (this.paused)
        return;

    this.ball.update();
    this.display1.value = this.p1.score;
    this.display2.value = this.p2.score;
    // [...]
};

Using touch events

In order to make it work on mobile devices we need to listen to touch events such as touchstart, touchend or touchmove. Each of those events has a changedTouches attribute of type TouchList which is a list of Touch objects.

Please note that mouse clicks won't dispatch a touch event. Fortunately you can easily simulate touch events in Chrome.

I noticed that in my browser I always got a new Touch object on each event call. However when I tested the game on my iPad, initially I couldn't make it work. I was storing the previous Touch object to compare to the new one and calculate a movement. After some troubleshooting I found out that the browser wasn't giving me a new Touch instance, instead it was just editing the attributes of the Touch reference I already got. So I couldn't just store the Touch object to compare with the next event, I had to actually store the values I wanted from it (clientX and clientY).

Given that I came out with a TouchListener

function TouchListener(element) {
    this.touches = [];
    this.touchMoveListener = function(touch) {};

    element.addEventListener("touchstart", (function(e) {
        e.preventDefault();
        for (var i = 0; i < e.changedTouches.length; i++) {
            var touch = e.changedTouches[i];
            this.touches[touch.identifier] = {x: touch.clientX, y: touch.clientY};
        }
    }).bind(this));

    element.addEventListener("touchmove", (function(e) {
        e.preventDefault();
        for (var i = 0; i < e.changedTouches.length; i++) {
            var touch = e.changedTouches[i];
            var previousTouch = this.touches[touch.identifier];
            this.touches[touch.identifier] = {x: touch.clientX, y: touch.clientY};

            var offset = {x: touch.clientX - previousTouch.x, y: touch.clientY - previousTouch.y}
            this.touchMoveListener({x: touch.clientX, y: touch.clientY, offset: offset});
        }
    }).bind(this));

    element.addEventListener("touchend", (function(e) {
        e.preventDefault();
        for (var i = 0; i < e.changedTouches.length; i++) {
            delete this.touches[e.changedTouches[i].identifier];
        }
    }).bind(this));
}

This helper class stores each touch event in a hash table using the Touch identifier as a key. The offset of the touch movement is calculated comparing the current touch position with the previously stored one. The result is then passed as an argument of a callback.

Conclusion

Making HTML5 games requires the use of some functionalities that are not fully implemented even on modern browsers. Also, since browsers development are happening really fast, a few months can make a huge difference in browsers versions. So if you plan to support a variety of platforms, you'll have to take some time to check if that awesome functionality X were supported a few months ago (Safari 5 and Safari 6 are just one year apart and the difference between them is huge).

Making this game had taught me that:

If you have any question, please use the comments section below.


Comments