Canvas Games

A simple game consists of:

In order to periodically display the game in its current state we use requestAnimationFrame();

The example below is a simple game where the user fires a bullet from a bat and tries to hit a target. Two timers are needed; one each for the game rendering and the bullet.

Example of a simple, timer-driven, game (Run Example)

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Worked example from lecture notes</title>
<style>
#canvas
{
    border:1px solid black;
    width:500px;
    height:500px;
}

#loadingMessage
{
    position:absolute;
    top:100px;
    left:100px;
    z-index:100;
    font-size:50px;
}
</style>

<script>
const CANVAS_WIDTH = 500;
const CANVAS_HEIGHT = 500;

let canvas = null;
let ctx = null;

let gameIsOver = false;

window.onload = onAllAssetsLoaded;
document.write("<div id='loadingMessage'>Loading...</div>");
function onAllAssetsLoaded()
{
    // hide the webpage loading message
    document.getElementById('loadingMessage').style.visibility = "hidden";

    canvas = document.getElementById("canvas");
    ctx = canvas.getContext("2d");
    canvas.width = CANVAS_WIDTH;
    canvas.height = CANVAS_HEIGHT;
    setBatY(canvas.height - batHeight);

    renderCanvas();

    window.addEventListener('keydown', keydownHandler);
    renderCanvas();
}


function renderCanvas()
{
    /* Continuously call requestAnimationFrame() to keep rendering the canvas */
    if (gameIsOver)
    {
        return; // end recursion
    }
    else
    {
        requestAnimationFrame(renderCanvas); // recursively call next frame
    }
    
    // draw the background
    drawBackground();

    // draw the bat
    drawBat();

    // draw the bullet
    if (bulletFiring)
    {
        drawBullet();
    }

    // draw the target
    if (!targetDestroyed)
    {
        drawTarget();
    }
    else
    {
        gameIsOver = true;
        alert("You win!");
        clearInterval(renderCanvasInterval);
    }
}


function keydownHandler(e)
{
    if (e.keyCode === 37)  // left
    {
        batX -= batSpeed;
    }
    else if (e.keyCode === 39) // right
    {
        batX += batSpeed;
    }
    else if (e.keyCode === 32) // space
    {
        if (bulletInterval === null)
        {
            bulletX = batX + batWidth / 2;
            bulletY = batY;
            bulletFiring = true;

            bulletInterval = setInterval(moveBullet, 100 / bulletSpeed);  // animate bullet
        }
    }
}


/* Bullet */
let bulletX = 0;
let bulletY = 0;
let bulletWidth = 10;
let bulletHeight = 10;
let bulletSpeed = 100;  // number in range of 1 to 100, where 100 is fastest
let bulletColour = "red";
function drawBullet()
{
    // it is good coding practice to only include draw code inside draw functions
    // and to not include drawing code anywhere else
    ctx.fillStyle = bulletColour;
    ctx.fillRect(bulletX, bulletY, bulletWidth, bulletHeight);
}


let bulletInterval = null;
let bulletFiring = false;
function moveBullet()  // called by bullet timer
{
    bulletY--;
    if (bulletY <= 0)
    {
        clearInterval(bulletInterval);  // destroy bullet timer
        bulletInterval = null;
        bulletFiring = false;
    }
    else
    {
        isTargetHit();
    }
}


/* Bat */
let batX = 0;
let batY = 0;
let batWidth = 100;
let batHeight = 10;
let batSpeed = 10;    // number in range of 1 to 100, where 100 is fastest
let batColour = "black";
function drawBat()
{
    // it is good coding practice to only include draw code inside draw functions
    // and to not include drawing code anywhere else
    ctx.fillStyle = batColour;
    ctx.fillRect(batX, batY, batWidth, batHeight);
}

function setBatY(y)
{
    batY = y;
}


/* Target */
let targetX = 200;
let targetY = 0;
let targetWidth = 100;
let targetHeight = 10;
let targetDestroyed = false;
let targetColour = "green";
function drawTarget()
{
    // it is good coding practice to only include draw code inside draw functions
    // and to not include drawing code anywhere else
    ctx.fillStyle = targetColour;
    ctx.fillRect(targetX, targetY, targetWidth, targetHeight);
}

function isTargetHit()
{
    if ((bulletX >= targetX) && (bulletX <= (targetX + targetWidth))
            && (bulletY >= targetY) && (bulletY <= (targetY + targetHeight)))
    {
        targetDestroyed = true;
    }
}

function drawBackground()
{
    ctx.clearRect(0, 0, canvas.width, canvas.height);
}
</script>
</head>

<body>
<canvas id = "canvas" tabindex="1">
Your browser does not support the HTML5 'Canvas' tag.
</canvas>
<p>Use the left and right arrow keys to move the bat. Use the space bar to fire a bullet. </p>
</body>
</html> 

Write code to move the bat using the mouse, as shown here.

Replace the baground, bat, bullet and target with images, as shown here.

Add code to the above example, so that a win message appears if the user hits the target, as shown here.

Add code to get the player lose the game is they miss three shots, as shown here.

Write your own image-based 2D game.

Object Oriented Game

The difficulty of the above code is that we cannot easily add new bullets or targets to the game. By using object oriented code, we can easily add more bullets and targets, as shown in the code below.

Example of a simple object oriented game (Run Example)

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Worked example from lecture notes</title>
<style>
#canvas
{
    border:1px solid black;
    width:500px;
    height:500px;
}

#loadingMessage
{
    position:absolute;
    top:100px;
    left:100px;
    z-index:100;
    font-size:50px;
}
</style>

<script>
const CANVAS_WIDTH = 500;
const CANVAS_HEIGHT = 500;

let canvas = null;
let ctx = null;

let gameIsOver = false;

const MAX_NUMBER_OF_VISIBLE_BULLETS = 30;  // maximum number of visible bullets on the screen at any time
let bullet = new Array(MAX_NUMBER_OF_VISIBLE_BULLETS);
let target = new Array();


const NUMBER_OF_TARGETS = 2; // the number of targets that the player has to hit
let numberOfTargetsDestroyed = 0; // the number of targets that have been hit so far 
let allTargetsAreDestroyed = false;  // set to true when all of the targets have been hit

const TARGET_WIDTH = 100;

for (let i = 0; i < NUMBER_OF_TARGETS; i++)
{
    target[i] = new Target(Math.random() * (CANVAS_WIDTH - (TARGET_WIDTH / 2)), TARGET_WIDTH);
}

let bat = new Bat();

window.onload = onAllAssetsLoaded;
document.write("<div id='loadingMessage'>Loading...</div>");
function onAllAssetsLoaded()
{
    // hide the webpage loading message
    document.getElementById('loadingMessage').style.visibility = "hidden";

    canvas = document.getElementById("canvas");
    ctx = canvas.getContext("2d");
    canvas.width = CANVAS_WIDTH;
    canvas.height = CANVAS_HEIGHT;

    window.addEventListener('keydown', keydownHandler);
    runGameLoop();
}


function runGameLoop()
{
    if (!gameIsOver)
    {
        processBulletCollisions(); // perform collision detection

        ctx.clearRect(0, 0, canvas.width, canvas.height);

        if (allTargetsAreDestroyed === true)
        {
            gameIsOver = true;
            alert("You win!");
        }
        else if (bat.isDestroyed() === true)
        {
            gameIsOver = true;
            alert("You lose!");
        }
        else
        {
            bat.draw();
            for (let i = 0; i < bullet.length; i++)
            {
                if (bullet[i] !== undefined)
                {
                    bullet[i].draw();
                }
            }

            for (let i = 0; i < target.length; i++)
            {
                if (target[i] !== undefined)
                {
                    target[i].draw();
                }
            }
        }

        requestAnimationFrame(runGameLoop); // call the next loop of the game
    }
}


function processBulletCollisions()
{
    for (let i = 0; i < bullet.length; i++)
    {
        if (bullet[i] !== undefined)
        {
            /* test targets for collision with bullets */
            for (let j = 0; j < target.length; j++)
            {
                if (target[j].isHit(bullet[i].getX(), bullet[i].getY()))
                {
                    target[j].destroyedState = true;
                    bullet[i].destroy();

                    numberOfTargetsDestroyed++;
                    if (numberOfTargetsDestroyed === NUMBER_OF_TARGETS)
                    {
                        allTargetsAreDestroyed = true;
                    }
                }
            }

            /* test bat for collision with bullets */
            if (bat.isHit(bullet[i].getX(), bullet[i].getY()))
            {
                //   alert(bullet[i].getX()+ "    " + bullet[i].getY());
                bat.destroyedState = true;
                bullet[i].destroy();
            }
        }
    }
}


function keydownHandler(e)
{
    if (e.keyCode === 37)  // left
    {
        bat.setX(bat.getX() - bat.getSpeed());
    }
    else if (e.keyCode === 39) // right
    {
        bat.setX(bat.getX() + bat.getSpeed());
    }
    else if (e.keyCode === 32) // space
    {

        for (let i = 0; i < bullet.length; i++)
        {
            if ((bullet[i] === undefined) || (!bullet[i].isFiring()))
            {

                bullet[i] = new Bullet(bat.getX(), bat.getY(), bat.getWidth(), bat.getHeight());
                break;
            }
        }
    }
}


/******************************************************************************/
/* Bullet Object */
function Bullet(batX, batY, batWidth, batHeight)
{
    /* private member variables */
    this.firingState = true;
    this.bulletWidth = 10;
    this.bulletHeight = 10;
    this.x = batX + batWidth / 2;
    this.y = batY - batHeight;
    this.bulletColour = "red";
    this.speed = 100;  // number in range of 1 to 100, where 100 is fastest  
    this.isMovingUp = true;
    this.interval = setInterval(this.move.bind(this), 100 / this.getSpeed());
    // note that we have to bind the interval above
}


/* public methods */
Bullet.prototype.move = function ()
{
    if (this.isMovingUp)
    {
        this.y--;
        if (this.y <= 0)
        {
            this.isMovingUp = false;
            for (let i = 0; i < NUMBER_OF_TARGETS; i++)
            {
                target[i].isHit();
            }
        }
    }
    else  // moving down
    {
        this.y++;
        if (this.y >= canvas.height)
        {
            bat.isHit();
        }
    }
};


Bullet.prototype.destroy = function ()
{
    clearInterval(this.interval);
    this.firingState = false;
};


Bullet.prototype.draw = function ()
{
    if (this.isFiring())
    {
        ctx.fillStyle = this.bulletColour;
        ctx.fillRect(this.x, this.y, this.bulletWidth, this.bulletHeight);
    }
};


Bullet.prototype.setFiringState = function (newState)
{
    this.firingState = newState;
};


Bullet.prototype.isFiring = function ()
{
    return this.firingState;
};


Bullet.prototype.setX = function (newX)
{
    this.x = newX;
};


Bullet.prototype.getX = function ()
{
    return this.x;
};


Bullet.prototype.getY = function ()
{
    return this.y;
};


Bullet.prototype.getSpeed = function ()
{
    return this.speed;
};
/******************************************************************************/


/******************************************************************************/
/* Bat Object */
function Bat()
{
    /* private member variables */
    this.batColour = "black";
    this.width = 100;
    this.height = 10;
    this.speed = 10;    // number in range of 1 to 100, where 100 is fastest  
    this.destroyedState = false;
    this.x = 0;
    this.y = CANVAS_HEIGHT - this.height;
}


/* public methods */
Bat.prototype.draw = function ()
{
    if (!this.isDestroyed())
    {

        ctx.fillStyle = this.batColour;
        ctx.fillRect(this.x, this.y, this.width, this.height);
    }
};


Bat.prototype.isDestroyed = function ()
{
    return this.destroyedState;
};


Bat.prototype.setX = function (newX)
{
    this.x = newX;
};


Bat.prototype.setY = function (newY)
{
    this.y = newY;
};


Bat.prototype.getX = function ()
{
    return this.x;
};


Bat.prototype.getY = function ()
{
    return this.y;
};


Bat.prototype.getWidth = function ()
{
    return this.width;
};


Bat.prototype.getHeight = function ()
{
    return this.height;
};


Bat.prototype.getSpeed = function ()
{
    return this.speed;
};


Bat.prototype.isHit = function (bulletX, bulletY)
{
    return ((bulletX >= this.x) && (bulletX <= (this.x + this.width))
            && (bulletY >= this.y) && (bulletY <= (this.y + this.height)));
};
/******************************************************************************/



/******************************************************************************/
/* Target Object */
function Target(newX, newWidth)
{
    /* private member variables */
    this.x = newX;
    this.y = 0;
    this.width = newWidth;
    this.height = 10;
    this.destroyedState = false;
    this.targetColour = "green";
}


/* public methods */
Target.prototype.draw = function ()
{
    if (!this.isDestroyed())
    {
        ctx.fillStyle = this.targetColour;
        ctx.fillRect(this.x, this.y, this.width, this.height);
    }
};


Target.prototype.isDestroyed = function ()
{
    return this.destroyedState;
};


Target.prototype.isHit = function (bulletX, bulletY)
{
    if (this.isDestroyed())
    {
        return;
    }

    return ((bulletX >= this.x) && (bulletX <= (this.x + this.width))
            && (bulletY >= this.y) && (bulletY <= (this.y + this.height)));
};
/******************************************************************************/


</script>
</head>

<body>
<canvas id = "canvas" tabindex="1">
Your browser does not support the HTML5 'Canvas' tag.
</canvas>
<p>Use the left and right arrow keys to move the bat. Use the space bar to fire a bullet. </p>
</body>
</html>

Change the game so that the targets move to a new position and reduce their size by 1/4 of the original size each time they are hit, as shown here.

Change the code above so that the bullets bounce up when they hit the bottom of the canvas, as shown here.

Write your own object oriented 2D game.

 
<div align="center"><a href="../../versionC/index.html" title="DKIT Lecture notes homepage for Derek O&#39; Reilly, Dundalk Institute of Technology (DKIT), Dundalk, County Louth, Ireland. Copyright Derek O&#39; Reilly, DKIT." target="_parent" style='font-size:0;color:white;background-color:white'>&nbsp;</a></div>