Offscreen Canvas Games

Example of an offscreen games canvas (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>
#gameCanvas
{
    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;

const GAMESTATE_PLAYING = 0;
const GAMESTATE_LOST = 1;
const GAMESTATE_WON = 2;

let gameState = GAMESTATE_PLAYING;

let canvas = null;
let ctx = null;
let offscreenObstaclesCanvas = null;
let offscreenObstaclesCanvasG = null;

let bullseyeWidth = 50;
let bullseyeHeight = 50;
let bullseyeX = CANVAS_WIDTH - bullseyeWidth;
let bullseyeY = CANVAS_HEIGHT - bullseyeHeight;

let monsterWidth = 40;
let monsterHeight = 40;

let startX = monsterWidth / 2;
let startY = monsterHeight / 2;

let x = startX;
let y = startY;

let obstaclesImage = new Image();
obstaclesImage.src = "images/obstacles.png";

let monsterImage = new Image();
monsterImage.src = "images/monster.png";

let bullseyeImage = new Image();
bullseyeImage.src = "images/bullseye.png";

let starsImage = new Image();
starsImage.src = "images/stars.png";


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('gameCanvas');
    ctx = canvas.getContext('2d');
    canvas.width = CANVAS_WIDTH;
    canvas.height = CANVAS_HEIGHT;

    /* Offscreen Canvas */
    offscreenObstaclesCanvas = document.createElement('canvas');
    offscreenObstaclesCanvasG = offscreenObstaclesCanvas.getContext('2d');
    offscreenObstaclesCanvas.width = CANVAS_WIDTH;
    offscreenObstaclesCanvas.height = CANVAS_HEIGHT;
    offscreenObstaclesCanvasG.drawImage(obstaclesImage, 0, 0, canvas.width, canvas.height);

    renderCanvas();

    document.addEventListener('keydown', keydownHandler);
}


function renderCanvas()
{
    requestAnimationFrame(renderCanvas);

    ctx.drawImage(starsImage, 0, 0, canvas.width, canvas.height);
    ctx.drawImage(offscreenObstaclesCanvas, 0, 0, canvas.width, canvas.height);
    ctx.drawImage(bullseyeImage, bullseyeX, bullseyeY, bullseyeWidth, bullseyeHeight);
    ctx.drawImage(monsterImage, x - (monsterWidth / 2), y - (monsterHeight / 2), monsterWidth, monsterHeight);


    if (gameState === GAMESTATE_WON)
    {
        alert('WON');
        gameState = GAMESTATE_PLAYING;
    }
    else if (gameState === GAMESTATE_LOST)
    {
        alert('LOST');
        gameState = GAMESTATE_PLAYING;
    }
}


function keydownHandler(e)
{
    let stepSize = 10;

    if (e.keyCode === 37)  // left
    {
        x -= stepSize;
    }
    else if (e.keyCode === 38) // up
    {
        y -= stepSize;
    }
    else if (e.keyCode === 39) // right
    {
        x += stepSize;
    }
    else if (e.keyCode === 40) // down
    {
        y += stepSize;
    }

    updateGameState();
}


function updateGameState()
{
    let imageData = offscreenObstaclesCanvasG.getImageData(x, y, 1, 1);
    let data = imageData.data;

    if (data[3] !== 0)
    {
        gameState = GAMESTATE_LOST;
    }
    else if ((x >= bullseyeX) && (x <= bullseyeX + bullseyeWidth) &&
            (y >= bullseyeY) && (y <= bullseyeY + bullseyeHeight))
    {
        gameState = GAMESTATE_WON;
    }
}
</script>
</head>

<body>
<canvas id = "gameCanvas" tabindex="1">
Your browser does not support the HTML5 'Canvas' tag.
</canvas>
<p>Use the four arrow keys to move the monster into the bullseye, but avoid the obstacles.</p>
</body>
</html> 

The example above only tests the centre-point of the monster for collisions against the obstacles and the bullseye. Write code to improve the hit test, as shown here.

Write code so that the canvas shakes whenever the monster hits an obstacle, as shown here.

Write code to allow a user to guide a sprite animated character through a maze, as shown here.

Amend the code above to cause an explosion whenever the sprite hits the maze wall, as shown here.

Case Study: Tank Game

Develop a tank game where the user destroys enemy tanks, as shown here.

Specifically:

The tank game will use the sprite image below:

As per the notes on timers, the code below will move the tank forward. This code takes account of the fact that the sprite images are 90 degrees off (if we do not fix this then the tank faces the wrong direction) and the sprite images are in reverse order (if we do not fix this, it will give the effect of the tracks moving backward).

Example of a tank sprite moving forward (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 = 1000;
const CANVAS_HEIGHT = 1000;

let fieldImage = new Image();
fieldImage.src = "images/field.png";

let canvas = null;
let ctx = null;
/* Animation array */ let animation = []; window.onload = onAllAssetsLoaded; document.write("<div id='loadingMessage'>Loading...</div>"); function onAllAssetsLoaded() { // kill 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; /* Step 1 of 3 */ /* Each animation needs to be declared as an object */ animation[0] = new TankAnimation(ctx, 0, 490, 100, 0, CANVAS_WIDTH); renderCanvas(); } /* Step 2 of 3 */ /* Each animation needs its own code */ /******************************************************************************/ /* TankAnimation object */ function TankAnimation(ctx, centreX, centreY, size, animationStartDelay, canvasWidth) { /* These variables are ALWAYS needed */ this.ctx = ctx; this.animationInterval = null; this.frameRate = 130; // change to suit the animation speed in milliseconds. Smaller numbers give a faster animation */ this.animationIsDisplayed = false; /* These variables depend on the animation */ this.canvasWidth = canvasWidth; this.spriteIncrement = -1; // display the sprite images in reverse order this.stepSizeX = 5; this.centreX = centreX; this.centreY = centreY; this.size = size; this.NUMBER_OF_SPRITES = 8; // the number of sprites in the sprite image this.NUMBER_OF_COLUMNS_IN_SPRITE_IMAGE = 8; // the number of columns in the sprite image this.NUMBER_OF_ROWS_IN_SPRITE_IMAGE = 4; // the number of rows in the sprite image this.START_ROW = 1; this.START_COLUMN = 0; this.currentSprite = 0; // the current sprite to be displayed from the sprite image this.row = this.START_ROW; // current row in sprite image this.column = this.START_COLUMN; // current column in sprite image this.SPRITE_WIDTH; this.SPRITE_HEIGHT; this.tankImage = new Image(); this.tankImage.src = "images/tank.png"; /* Start the animation */ setTimeout(this.start.bind(this), animationStartDelay); } /* Public functions */ TankAnimation.prototype.start = function () { this.animationIsDisplayed = true; this.animationInterval = setInterval(this.update.bind(this), this.frameRate); }; TankAnimation.prototype.stop = function () { this.animationIsDisplayed = true; clearInterval(this.animationInterval); this.animationInterval = null; // set to null when not running }; TankAnimation.prototype.kill = function () { this.stop(); this.animationIsDisplayed = false; }; TankAnimation.prototype.update = function () { this.currentSprite++; this.column += this.spriteIncrement; if (this.currentSprite >= this.NUMBER_OF_SPRITES) { this.currentSprite = 0; this.row = this.START_ROW; this.column = this.START_COLUMN; } if (this.spriteIncrement === 1) { if (this.column >= this.NUMBER_OF_COLUMNS_IN_SPRITE_IMAGE) { this.column = 0; this.row++; } } else // spriteIncrement === -1 { if (this.column < 0) { this.column = this.NUMBER_OF_COLUMNS_IN_SPRITE_IMAGE - 1; this.row--; if (this.row < 0) { this.currentSprite = 0; this.row = this.START_ROW; this.column = this.START_COLUMN; } } } // move the tank forward this.centreX += this.stepSizeX; if (this.centreX > CANVAS_WIDTH) { this.centreX = 0; } }; TankAnimation.prototype.render = function () { if (this.animationIsDisplayed) { this.SPRITE_WIDTH = (this.tankImage.width / this.NUMBER_OF_COLUMNS_IN_SPRITE_IMAGE); this.SPRITE_HEIGHT = (this.tankImage.height / this.NUMBER_OF_ROWS_IN_SPRITE_IMAGE); this.ctx.save(); this.ctx.translate(this.centreX, this.centreY); this.ctx.rotate(Math.radians(90)); this.ctx.translate(-this.centreX, -this.centreY); this.ctx.drawImage(this.tankImage, this.column * this.SPRITE_WIDTH, this.row * this.SPRITE_WIDTH, this.SPRITE_WIDTH, this.SPRITE_HEIGHT, this.centreX - parseInt(this.size / 2), this.centreY - parseInt(this.size / 2), this.size, this.size); this.ctx.restore(); } }; /******************************************************************************/ function renderCanvas() { requestAnimationFrame(renderCanvas); ctx.drawImage(fieldImage, 0, 0, CANVAS_WIDTH, CANVAS_HEIGHT); /* Step 3 of 3 */ /* Draw the animations */ for (let i = 0; i < animation.length; i++) { animation[i].render(); } } Math.radians = function (degrees) { return degrees * Math.PI / 180; }; </script> </head> <body> <canvas id = "canvas" tabindex="1"> Your browser does not support the HTML5 'Canvas' tag. </canvas> </body> </html>

Getting the tank to move in any direction

The above example uses the variable 'this.stepSizeX' to calculate the next x position for the tank. This works because the tank is moving parallel to the the x-axis. To move in any other direction, we need to have a stepSizeX and stepSizeY incrementor. These two values can be set using the sin and cos of the direction that the tank is travelling, as shown below.

TankAnimation.prototype.setDirection = function (newDirection)
{
    this.direction = newDirection;

    this.stepSizeX = this.STEP_SIZE * Math.sin(Math.radians(this.direction));
    this.stepSizeY = -this.STEP_SIZE * Math.cos(Math.radians(this.direction));
};

These two values are calculated using the direction that the tank is travelling. The direction is in the range of 0 to 360 degrees. The direction is incremented and decremented using the left and right arrow keys.

function keydownHandler(e)
{
    const ANGLE_STEP_SIZE = 10;

    if (e.keyCode === 37)  // left
    {
        animation[0].setDirection(animation[0].getDirection() - ANGLE_STEP_SIZE);
    }
    else if (e.keyCode === 39) // right
    {
        animation[0].setDirection(animation[0].getDirection() + ANGLE_STEP_SIZE);
    }
}

Example of a tank moving in any direction (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 = 1000;
const CANVAS_HEIGHT = 1000;
let fieldImage = new Image();
fieldImage.src = "images/field.png";
let canvas = null;
let ctx = null;
/* Animation array */
let animation = [];

window.onload = onAllAssetsLoaded;
document.write("<div id='loadingMessage'>Loading...</div>");
function onAllAssetsLoaded()
{
    // kill 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;
    /* Step 1 of 3 */
    /* Each animation needs to be declared as an object */
    animation[0] = new TankAnimation(ctx, 0, 490, 100, 0, 90, CANVAS_WIDTH, CANVAS_HEIGHT);

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

function keydownHandler(e)
{
    const ANGLE_STEP_SIZE = 10;

    if (e.keyCode === 37)  // left
    {
        animation[0].setDirection(animation[0].getDirection() - ANGLE_STEP_SIZE);
    }
    else if (e.keyCode === 39) // right
    {
        animation[0].setDirection(animation[0].getDirection() + ANGLE_STEP_SIZE);
    }
}


/* Step 2 of 3 */
/* Each animation needs its own code */
/******************************************************************************/
/* TankAnimation object */
function TankAnimation(ctx, centreX, centreY, size, animationStartDelay, direction, canvasWidth, canvasHeight)
{
    /* These variables are ALWAYS needed */
    this.ctx = ctx;
    this.animationInterval = null;
    this.frameRate = 130; // change to suit the animation speed in milliseconds. Smaller numbers give a faster animation */
    this.animationIsDisplayed = false;
    /* These variables depend on the animation */
    this.canvasWidth = canvasWidth;
    this.canvasHeight = canvasHeight;
    this.centreX = centreX;
    this.centreY = centreY;
    this.size = size;
    this.NUMBER_OF_SPRITES; // the number of sprites in the sprite image
    this.NUMBER_OF_COLUMNS_IN_SPRITE_IMAGE = 8; // the number of columns in the sprite image
    this.NUMBER_OF_ROWS_IN_SPRITE_IMAGE = 4; // the number of rows in the sprite image	
    this.START_ROW;
    this.START_COLUMN;

    this.NUMBER_OF_SPRITES = 8; // the number of sprites in the sprite image
    this.START_ROW = 1;
    this.START_COLUMN = 0;

    this.currentSprite = 0; // the current sprite to be displayed from the sprite image  
    this.row = this.START_ROW; // current row in sprite image
    this.column = this.START_COLUMN; // current column in sprite image
    this.spriteIncrement = -1;
    this.SPRITE_WIDTH;
    this.SPRITE_HEIGHT;

    //let direction = 90; // set to 90 to accout for the fact that the sprite image is originally 90 degrees off
    this.stepSizeX = this.STEP_SIZE * Math.sin(Math.radians(this.direction));
    this.stepSizeY = -this.STEP_SIZE * Math.cos(Math.radians(this.direction));

    this.STEP_SIZE = 5; // the number of pixels to move forward
    this.stepSizeX = this.STEP_SIZE;   // originally going from left to right across the screen
    this.stepSizeY = 0;
    this.setDirection(direction);

    this.tankImage = new Image();
    this.tankImage.src = "images/tank.png";

    /* Start the animation */
    setTimeout(this.start.bind(this), animationStartDelay);
}


/* Public functions */
TankAnimation.prototype.start = function ()
{
    this.animationIsDisplayed = true;
    this.animationInterval = setInterval(this.update.bind(this), this.frameRate);
};


TankAnimation.prototype.stop = function ()
{
    this.animationIsDisplayed = true;
    clearInterval(this.animationInterval);
    this.animationInterval = null; // set to null when not running           
};


TankAnimation.prototype.kill = function ()
{
    this.stop();
    this.animationIsDisplayed = false;
};


TankAnimation.prototype.update = function ()
{
    this.currentSprite++;
    this.column += this.spriteIncrement;

    if (this.currentSprite >= this.NUMBER_OF_SPRITES)
    {
        this.currentSprite = 0;
        this.row = this.START_ROW;
        this.column = this.START_COLUMN;
    }


    if (this.spriteIncrement === 1)
    {
        if (this.column >= this.NUMBER_OF_COLUMNS_IN_SPRITE_IMAGE - 1)
        {
            this.column = 0;
            this.row++;
        }
    }
    else  // spriteIncrement === -1
    {
        if (this.column < 0)
        {
            this.column = this.NUMBER_OF_COLUMNS_IN_SPRITE_IMAGE - 1;
            this.row--;
            if (this.row < 0)
            {
                this.currentSprite = 0;
                this.row = this.START_ROW;
                this.column = this.START_COLUMN;
            }
        }
    }

    //centreX++;
    this.centreX += this.stepSizeX;
    this.centreY += this.stepSizeY;
    if (this.centreX > this.canvasWidth)
    {
        this.centreX = 0;
    }
    else if (this.centreY > this.canvasHeight)
    {
        this.centreY = 0;
    }
    else if (this.centreX < 0)
    {
        this.centreX = this.canvasWidth;
    }
    else if (this.centreY < 0)
    {
        this.centreY = this.canvasHeight;
    }
};


TankAnimation.prototype.render = function ()
{
    if (this.animationIsDisplayed)
    {
        this.SPRITE_WIDTH = (this.tankImage.width / this.NUMBER_OF_COLUMNS_IN_SPRITE_IMAGE);
        this.SPRITE_HEIGHT = (this.tankImage.height / this.NUMBER_OF_ROWS_IN_SPRITE_IMAGE);

        this.ctx.save();
        this.ctx.translate(this.centreX, this.centreY);
        this.ctx.rotate(Math.radians(this.direction));
        this.ctx.translate(-this.centreX, -this.centreY);

        this.ctx.drawImage(this.tankImage, this.column * this.SPRITE_WIDTH, this.row * this.SPRITE_HEIGHT, this.SPRITE_WIDTH, this.SPRITE_HEIGHT, this.centreX - parseInt(this.size / 2), this.centreY - parseInt(this.size / 2), this.size, this.size);
        this.ctx.restore();
    }
};


TankAnimation.prototype.getDirection = function ()
{
    return this.direction;
};


TankAnimation.prototype.setDirection = function (newDirection)
{
    this.direction = newDirection;

    this.stepSizeX = this.STEP_SIZE * Math.sin(Math.radians(this.direction));
    this.stepSizeY = -this.STEP_SIZE * Math.cos(Math.radians(this.direction));
};
/******************************************************************************/


function renderCanvas()
{
    requestAnimationFrame(renderCanvas);
    ctx.drawImage(fieldImage, 0, 0, CANVAS_WIDTH, CANVAS_HEIGHT);
    /* Step 3 of 3 */
    /* Draw the animations */
    for (let i = 0; i < animation.length; i++)
    {
        animation[i].render();
    }
}


Math.radians = function (degrees)
{
    return degrees * Math.PI / 180;
};
</script>
</head>

<body>
<canvas id = "canvas" tabindex="1">
Your browser does not support the HTML5 'Canvas' tag.
</canvas>
<p>Use the left and right arrows to steer the tank.</p>
</body>
</html>

Stopping the tank from entering the water

In this game, the tank can only travel forward. Testing the front left, centre and right positions against the river offscreen buffer will be enough to ensure that we can detect that the tank is heading into the river. We can use the image below as the river offscreen buffer.

The collision detection can be done in the 'update()' function, immediately before the tank moves. The collision code is shown below.

    // Collision detection with the river
	  // As the tank can only move forward, we need only do collision detection for the front of the tank
    let imageData = null;
    let data = null;

    /* front left of tank */
    imageData = offscreenObstaclesCanvasG.getImageData(this.centreX + this.SPRITE_HEIGHT / 2.2, this.centreY - this.SPRITE_WIDTH / 2.2, 1, 1);
    data = imageData.data;
    if (data[3] !== 0)
    {
        this.centreX -= this.stepSizeX;
        this.centreY -= this.stepSizeY;
    }

    /* front right of tank */
    imageData = offscreenObstaclesCanvasG.getImageData(this.centreX + this.SPRITE_HEIGHT / 2.2, this.centreY + this.SPRITE_WIDTH / 2.2, 1, 1);
    data = imageData.data;
    if (data[3] !== 0)
    {
        this.centreX -= this.stepSizeX;
        this.centreY -= this.stepSizeY;
    }

    /* front centre of tank */
    imageData = offscreenObstaclesCanvasG.getImageData(this.centreX, this.centreY + this.SPRITE_WIDTH / 2.2, 1, 1);
    data = imageData.data;
    if (data[3] !== 0)
    {
        this.centreX -= this.stepSizeX;
        this.centreY -= this.stepSizeY;
    }

Tank game with river collision detection (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 = 1000;
const CANVAS_HEIGHT = 1000;
let fieldImage = new Image();
fieldImage.src = "images/field.png";
let canvas = null;
let ctx = null;

let riverImage = new Image();
riverImage.src = "images/tankFieldRiver.png";
let offscreenObstaclesCanvas = null;
let offscreenObstaclesCanvasG = null;


/* Animation array */
let animation = [];

window.onload = onAllAssetsLoaded;
document.write("<div id='loadingMessage'>Loading...</div>");
function onAllAssetsLoaded()
{
    // kill 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;

    offscreenObstaclesCanvas = document.createElement('canvas');
    offscreenObstaclesCanvasG = offscreenObstaclesCanvas.getContext('2d');
    offscreenObstaclesCanvas.width = CANVAS_WIDTH;
    offscreenObstaclesCanvas.height = CANVAS_HEIGHT;

    offscreenObstaclesCanvasG.drawImage(riverImage, 0, 0, canvas.width, canvas.height);

    /* Step 1 of 3 */
    /* Each animation needs to be declared as an object */
    animation[0] = new TankAnimation(ctx, 0, 490, 100, 0, 90, CANVAS_WIDTH, CANVAS_HEIGHT);

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

function keydownHandler(e)
{
    const ANGLE_STEP_SIZE = 10;

    if (e.keyCode === 37)  // left
    {
        animation[0].setDirection(animation[0].getDirection() - ANGLE_STEP_SIZE);
    }
    else if (e.keyCode === 39) // right
    {
        animation[0].setDirection(animation[0].getDirection() + ANGLE_STEP_SIZE);
    }
}


/* Step 2 of 3 */
/* Each animation needs its own code */
/******************************************************************************/
/* TankAnimation object */
function TankAnimation(ctx, centreX, centreY, size, animationStartDelay, direction, canvasWidth, canvasHeight)
{
    /* These variables are ALWAYS needed */
    this.ctx = ctx;
    this.animationInterval = null;
    this.frameRate = 130; // change to suit the animation speed in milliseconds. Smaller numbers give a faster animation */
    this.animationIsDisplayed = false;
    /* These variables depend on the animation */
    this.canvasWidth = canvasWidth;
    this.canvasHeight = canvasHeight;
    this.centreX = centreX;
    this.centreY = centreY;
    this.size = size;
    this.NUMBER_OF_SPRITES; // the number of sprites in the sprite image
    this.NUMBER_OF_COLUMNS_IN_SPRITE_IMAGE = 8; // the number of columns in the sprite image
    this.NUMBER_OF_ROWS_IN_SPRITE_IMAGE = 4; // the number of rows in the sprite image	
    this.START_ROW;
    this.START_COLUMN;

    this.NUMBER_OF_SPRITES = 8; // the number of sprites in the sprite image
    this.START_ROW = 1;
    this.START_COLUMN = 0;

    this.currentSprite = 0; // the current sprite to be displayed from the sprite image  
    this.row = this.START_ROW; // current row in sprite image
    this.column = this.START_COLUMN; // current column in sprite image
    this.spriteIncrement = -1;
    this.SPRITE_WIDTH;
    this.SPRITE_HEIGHT;

    //let direction = 90; // set to 90 to accout for the fact that the sprite image is originally 90 degrees off
    this.stepSizeX = this.STEP_SIZE * Math.sin(Math.radians(this.direction));
    this.stepSizeY = -this.STEP_SIZE * Math.cos(Math.radians(this.direction));

    this.STEP_SIZE = 5; // the number of pixels to move forward
    this.stepSizeX = this.STEP_SIZE;   // originally going from left to right across the screen
    this.stepSizeY = 0;
    this.setDirection(direction);

    this.tankImage = new Image();
    this.tankImage.src = "images/tank.png";

    /* Start the animation */
    setTimeout(this.start.bind(this), animationStartDelay);
}


/* Public functions */
TankAnimation.prototype.start = function ()
{
    this.animationIsDisplayed = true;
    this.animationInterval = setInterval(this.update.bind(this), this.frameRate);
};


TankAnimation.prototype.stop = function ()
{
    this.animationIsDisplayed = true;
    clearInterval(this.animationInterval);
    this.animationInterval = null; // set to null when not running           
};


TankAnimation.prototype.kill = function ()
{
    this.stop();
    this.animationIsDisplayed = false;
};


TankAnimation.prototype.update = function ()
{
    this.currentSprite++;
    this.column += this.spriteIncrement;

    if (this.currentSprite >= this.NUMBER_OF_SPRITES)
    {
        this.currentSprite = 0;
        this.row = this.START_ROW;
        this.column = this.START_COLUMN;
    }


    if (this.spriteIncrement === 1)
    {
        if (this.column >= this.NUMBER_OF_COLUMNS_IN_SPRITE_IMAGE - 1)
        {
            this.column = 0;
            this.row++;
        }
    }
    else  // spriteIncrement === -1
    {
        if (this.column < 0)
        {
            this.column = this.NUMBER_OF_COLUMNS_IN_SPRITE_IMAGE - 1;
            this.row--;
            if (this.row < 0)
            {
                this.currentSprite = 0;
                this.row = this.START_ROW;
                this.column = this.START_COLUMN;
            }
        }
    }

    //centreX++;
    this.centreX += this.stepSizeX;
    this.centreY += this.stepSizeY;


    // Collision detection with the river
	  // As the tank can only move forward, we need only do collision detection for the front of the tank
    let imageData = null;
    let data = null;

    /* front left of tank */
    imageData = offscreenObstaclesCanvasG.getImageData(this.centreX + this.SPRITE_HEIGHT / 2.2, this.centreY - this.SPRITE_WIDTH / 2.2, 1, 1);
    data = imageData.data;
    if (data[3] !== 0)
    {
        this.centreX -= this.stepSizeX;
        this.centreY -= this.stepSizeY;
    }

    /* front right of tank */
    imageData = offscreenObstaclesCanvasG.getImageData(this.centreX + this.SPRITE_HEIGHT / 2.2, this.centreY + this.SPRITE_WIDTH / 2.2, 1, 1);
    data = imageData.data;
    if (data[3] !== 0)
    {
        this.centreX -= this.stepSizeX;
        this.centreY -= this.stepSizeY;
    }

    /* front centre of tank */
    imageData = offscreenObstaclesCanvasG.getImageData(this.centreX, this.centreY + this.SPRITE_WIDTH / 2.2, 1, 1);
    data = imageData.data;
    if (data[3] !== 0)
    {
        this.centreX -= this.stepSizeX;
        this.centreY -= this.stepSizeY;
    }
    
    
    if (this.centreX > this.canvasWidth)
    {
        this.centreX = 0;
    }
    else if (this.centreY > this.canvasHeight)
    {
        this.centreY = 0;
    }
    else if (this.centreX < 0)
    {
        this.centreX = this.canvasWidth;
    }
    else if (this.centreY < 0)
    {
        this.centreY = this.canvasHeight;
    }
};


TankAnimation.prototype.render = function ()
{
    if (this.animationIsDisplayed)
    {
        this.SPRITE_WIDTH = (this.tankImage.width / this.NUMBER_OF_COLUMNS_IN_SPRITE_IMAGE);
        this.SPRITE_HEIGHT = (this.tankImage.height / this.NUMBER_OF_ROWS_IN_SPRITE_IMAGE);

        this.ctx.save();
        this.ctx.translate(this.centreX, this.centreY);
        this.ctx.rotate(Math.radians(this.direction));
        this.ctx.translate(-this.centreX, -this.centreY);

        this.ctx.drawImage(this.tankImage, this.column * this.SPRITE_WIDTH, this.row * this.SPRITE_HEIGHT, this.SPRITE_WIDTH, this.SPRITE_HEIGHT, this.centreX - parseInt(this.size / 2), this.centreY - parseInt(this.size / 2), this.size, this.size);
        this.ctx.restore();
    }
};


TankAnimation.prototype.getDirection = function ()
{
    return this.direction;
};


TankAnimation.prototype.setDirection = function (newDirection)
{
    this.direction = newDirection;

    this.stepSizeX = this.STEP_SIZE * Math.sin(Math.radians(this.direction));
    this.stepSizeY = -this.STEP_SIZE * Math.cos(Math.radians(this.direction));
};
/******************************************************************************/


function renderCanvas()
{
    requestAnimationFrame(renderCanvas);
    ctx.drawImage(fieldImage, 0, 0, CANVAS_WIDTH, CANVAS_HEIGHT);
    /* Step 3 of 3 */
    /* Draw the animations */
    for (let i = 0; i < animation.length; i++)
    {
        animation[i].render();
    }
}


Math.radians = function (degrees)
{
    return degrees * Math.PI / 180;
};
</script>
</head>

<body>
<canvas id = "canvas" tabindex="1">
Your browser does not support the HTML5 'Canvas' tag.
</canvas>
<p>Use the left and right arrows to steer the tank.</p>
</body>
</html>

Shell Fire animation and sound

When the user presses the spacebar key, we can play this mp3 file. We can use the small explosion images from the tank sprite image to create the shell fire effect. The shell fire and sound can be implemented as a ShellFireAnimation object. This object will be created whenever the user presses the spacebar, as shown below:

if (e.keyCode === 32) // space
{
    animation[1] = new FireShellAnimation(ctx, animation[0].getX(), animation[0].getY(), animation[0].getDirection());
}

To get the ShellFireAnimation explosion to occur in the same direction that the tank is travelling, we pass the tank direction to the constructor of the ShellFireAnimation object. The direction can be used to correctly position the explosion within the ShellFireAnimation render() function, as shown below:

FireShellAnimation.prototype.render = function ()
{
    if (this.animationIsDisplayed)
    {
        this.SPRITE_WIDTH = (this.tankImage.width / this.NUMBER_OF_COLUMNS_IN_SPRITE_IMAGE);
        this.SPRITE_HEIGHT = (this.tankImage.height / this.NUMBER_OF_ROWS_IN_SPRITE_IMAGE);

        this.ctx.save();
        this.ctx.translate(this.centreX, this.centreY);
        this.ctx.rotate(Math.radians(this.direction));
        this.ctx.translate(-this.centreX, -this.centreY);

        this.ctx.drawImage(this.tankImage, this.column * this.SPRITE_WIDTH, this.row * this.SPRITE_HEIGHT, this.SPRITE_WIDTH, this.SPRITE_HEIGHT, this.centreX - parseInt(this.size / 2), this.centreY - parseInt(this.size), this.size, this.size);
        this.ctx.restore();
    }
};

The ShellFireAnimation will kill itself after it plays its animation. The example below includes a ShellFireAnimation object.

Tank game with shell fire (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 = 1000;
const CANVAS_HEIGHT = 1000;
let fieldImage = new Image();
fieldImage.src = "images/field.png";
let canvas = null;
let ctx = null;

let riverImage = new Image();
riverImage.src = "images/tankFieldRiver.png";
let offscreenObstaclesCanvas = null;
let offscreenObstaclesCanvasG = null;


/* Animation array */
let animation = [];

window.onload = onAllAssetsLoaded;
document.write("<div id='loadingMessage'>Loading...</div>");
function onAllAssetsLoaded()
{
    // kill 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;

    offscreenObstaclesCanvas = document.createElement('canvas');
    offscreenObstaclesCanvasG = offscreenObstaclesCanvas.getContext('2d');
    offscreenObstaclesCanvas.width = CANVAS_WIDTH;
    offscreenObstaclesCanvas.height = CANVAS_HEIGHT;

    offscreenObstaclesCanvasG.drawImage(riverImage, 0, 0, canvas.width, canvas.height);

    /* Step 1 of 3 */
    /* Each animation needs to be declared as an object */
    animation[0] = new TankAnimation(ctx, 0, 490, 100, 0, 90, CANVAS_WIDTH, CANVAS_HEIGHT);

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

function keydownHandler(e)
{
    const ANGLE_STEP_SIZE = 10;

    if (e.keyCode === 37)  // left
    {
        animation[0].setDirection(animation[0].getDirection() - ANGLE_STEP_SIZE);
    }
    else if (e.keyCode === 39) // right
    {
        animation[0].setDirection(animation[0].getDirection() + ANGLE_STEP_SIZE);
    }
    else if (e.keyCode === 32) // space
    {
        animation[1] = new FireShellAnimation(ctx, animation[0].getX(), animation[0].getY(), animation[0].getDirection());
    }
}


/* Step 2 of 3 */
/* Each animation needs its own code */
/******************************************************************************/
/* TankAnimation object */
function TankAnimation(ctx, centreX, centreY, size, animationStartDelay, direction, canvasWidth, canvasHeight)
{
    /* These variables are ALWAYS needed */
    this.ctx = ctx;
    this.animationInterval = null;
    this.frameRate = 130; // change to suit the animation speed in milliseconds. Smaller numbers give a faster animation */
    this.animationIsDisplayed = false;
    /* These variables depend on the animation */
    this.canvasWidth = canvasWidth;
    this.canvasHeight = canvasHeight;
    this.centreX = centreX;
    this.centreY = centreY;
    this.size = size;
    this.NUMBER_OF_SPRITES; // the number of sprites in the sprite image
    this.NUMBER_OF_COLUMNS_IN_SPRITE_IMAGE = 8; // the number of columns in the sprite image
    this.NUMBER_OF_ROWS_IN_SPRITE_IMAGE = 4; // the number of rows in the sprite image	
    this.START_ROW;
    this.START_COLUMN;

    this.NUMBER_OF_SPRITES = 8; // the number of sprites in the sprite image
    this.START_ROW = 1;
    this.START_COLUMN = 0;

    this.currentSprite = 0; // the current sprite to be displayed from the sprite image  
    this.row = this.START_ROW; // current row in sprite image
    this.column = this.START_COLUMN; // current column in sprite image
    this.spriteIncrement = -1;
    this.SPRITE_WIDTH;
    this.SPRITE_HEIGHT;

    //let direction = 90; // set to 90 to accout for the fact that the sprite image is originally 90 degrees off
    this.stepSizeX = this.STEP_SIZE * Math.sin(Math.radians(this.direction));
    this.stepSizeY = -this.STEP_SIZE * Math.cos(Math.radians(this.direction));

    this.STEP_SIZE = 5; // the number of pixels to move forward
    this.stepSizeX = this.STEP_SIZE;   // originally going from left to right across the screen
    this.stepSizeY = 0;
    this.setDirection(direction);

    this.tankImage = new Image();
    this.tankImage.src = "images/tank.png";

    /* Start the animation */
    setTimeout(this.start.bind(this), animationStartDelay);
}


/* Public functions */
TankAnimation.prototype.start = function ()
{
    this.animationIsDisplayed = true;
    this.animationInterval = setInterval(this.update.bind(this), this.frameRate);
};


TankAnimation.prototype.stop = function ()
{
    this.animationIsDisplayed = true;
    clearInterval(this.animationInterval);
    this.animationInterval = null; // set to null when not running           
};


TankAnimation.prototype.kill = function ()
{
    this.stop();
    this.animationIsDisplayed = false;
};


TankAnimation.prototype.update = function ()
{
    this.currentSprite++;
    this.column += this.spriteIncrement;

    if (this.currentSprite >= this.NUMBER_OF_SPRITES)
    {
        this.currentSprite = 0;
        this.row = this.START_ROW;
        this.column = this.START_COLUMN;
    }


    if (this.spriteIncrement === 1)
    {
        if (this.column >= this.NUMBER_OF_COLUMNS_IN_SPRITE_IMAGE - 1)
        {
            this.column = 0;
            this.row++;
        }
    }
    else  // spriteIncrement === -1
    {
        if (this.column < 0)
        {
            this.column = this.NUMBER_OF_COLUMNS_IN_SPRITE_IMAGE - 1;
            this.row--;
            if (this.row < 0)
            {
                this.currentSprite = 0;
                this.row = this.START_ROW;
                this.column = this.START_COLUMN;
            }
        }
    }

    //centreX++;
    this.centreX += this.stepSizeX;
    this.centreY += this.stepSizeY;


    // Collision detection with the river
    // As the tank can only move forward, we need only do collision detection for the front of the tank
    let imageData = null;
    let data = null;

    /* front left of tank */
    imageData = offscreenObstaclesCanvasG.getImageData(this.centreX + this.SPRITE_HEIGHT / 2.2, this.centreY - this.SPRITE_WIDTH / 2.2, 1, 1);
    data = imageData.data;
    if (data[3] !== 0)
    {
        this.centreX -= this.stepSizeX;
        this.centreY -= this.stepSizeY;
    }

    /* front right of tank */
    imageData = offscreenObstaclesCanvasG.getImageData(this.centreX + this.SPRITE_HEIGHT / 2.2, this.centreY + this.SPRITE_WIDTH / 2.2, 1, 1);
    data = imageData.data;
    if (data[3] !== 0)
    {
        this.centreX -= this.stepSizeX;
        this.centreY -= this.stepSizeY;
    }

    /* front centre of tank */
    imageData = offscreenObstaclesCanvasG.getImageData(this.centreX, this.centreY + this.SPRITE_WIDTH / 2.2, 1, 1);
    data = imageData.data;
    if (data[3] !== 0)
    {
        this.centreX -= this.stepSizeX;
        this.centreY -= this.stepSizeY;
    }



    if (this.centreX > this.canvasWidth)
    {
        this.centreX = 0;
    }
    else if (this.centreY > this.canvasHeight)
    {
        this.centreY = 0;
    }
    else if (this.centreX < 0)
    {
        this.centreX = this.canvasWidth;
    }
    else if (this.centreY < 0)
    {
        this.centreY = this.canvasHeight;
    }
};


TankAnimation.prototype.render = function ()
{
    if (this.animationIsDisplayed)
    {
        this.SPRITE_WIDTH = (this.tankImage.width / this.NUMBER_OF_COLUMNS_IN_SPRITE_IMAGE);
        this.SPRITE_HEIGHT = (this.tankImage.height / this.NUMBER_OF_ROWS_IN_SPRITE_IMAGE);

        this.ctx.save();
        this.ctx.translate(this.centreX, this.centreY);
        this.ctx.rotate(Math.radians(this.direction));
        this.ctx.translate(-this.centreX, -this.centreY);

        this.ctx.drawImage(this.tankImage, this.column * this.SPRITE_WIDTH, this.row * this.SPRITE_HEIGHT, this.SPRITE_WIDTH, this.SPRITE_HEIGHT, this.centreX - parseInt(this.size / 2), this.centreY - parseInt(this.size / 2), this.size, this.size);
        this.ctx.restore();
    }
};


TankAnimation.prototype.getDirection = function ()
{
    return this.direction;
};


TankAnimation.prototype.setDirection = function (newDirection)
{
    this.direction = newDirection;

    this.stepSizeX = this.STEP_SIZE * Math.sin(Math.radians(this.direction));
    this.stepSizeY = -this.STEP_SIZE * Math.cos(Math.radians(this.direction));
};


TankAnimation.prototype.getX = function ()
{
    return this.centreX;
};


TankAnimation.prototype.getY = function ()
{
    return this.centreY;
};
/******************************************************************************/


/******************************************************************************/
/* FireShellAnimation object */
function FireShellAnimation(ctx, centreX, centreY, direction)
{
    /* These variables are ALWAYS needed */
    this.ctx = ctx;
    this.animationInterval = null;
    this.frameRate = 50; // change to suit the animation speed in milliseconds. Smaller numbers give a faster animation */
    this.animationIsDisplayed = false;
    /* These variables depend on the animation */

    this.centreX = centreX;
    this.centreY = centreY;
    this.direction = direction;

    this.NUMBER_OF_SPRITES = 3; // the number of sprites in the sprite image
    this.NUMBER_OF_COLUMNS_IN_SPRITE_IMAGE = 8; // the number of columns in the sprite image
    this.NUMBER_OF_ROWS_IN_SPRITE_IMAGE = 4; // the number of columns in the sprite image
    //    this.NUMBER_OF_SPRITES = 3; // the number of sprites in the sprite image
    this.START_ROW = 2;
    this.START_COLUMN = 1;

    this.currentSprite = 0; // the current sprite to be displayed from the sprite image  
    this.row = this.START_ROW; // current row in sprite image
    this.column = this.START_COLUMN; // current column in sprite image
    this.spriteIncrement = 1;
    this.SPRITE_WIDTH;
    this.SPRITE_HEIGHT;
    this.size = 100;
    this.tankImage = new Image();
    this.tankImage.src = "images/tank.png";

    this.fireShellSound = document.createElement('audio');
    this.fireShellSound.src = 'images/tankFire.mp3';

    /* Start the animation */
    setTimeout(this.start.bind(this), 0);
}


/* Public functions */
FireShellAnimation.prototype.start = function ()
{
    this.fireShellSound.play();
    this.animationIsDisplayed = true;
    this.animationInterval = setInterval(this.update.bind(this), this.frameRate);
};


FireShellAnimation.prototype.stop = function ()
{
    this.animationIsDisplayed = true;
    clearInterval(this.animationInterval);
    this.animationInterval = null; // set to null when not running           
};


FireShellAnimation.prototype.kill = function ()
{
    this.stop();
    this.animationIsDisplayed = false;
};


FireShellAnimation.prototype.update = function ()
{
    this.currentSprite++;
    if (this.currentSprite >= this.NUMBER_OF_SPRITES)
    {
        this.kill();
    }

    this.column += this.spriteIncrement;
};


FireShellAnimation.prototype.render = function ()
{
    if (this.animationIsDisplayed)
    {
        this.SPRITE_WIDTH = (this.tankImage.width / this.NUMBER_OF_COLUMNS_IN_SPRITE_IMAGE);
        this.SPRITE_HEIGHT = (this.tankImage.height / this.NUMBER_OF_ROWS_IN_SPRITE_IMAGE);

        this.ctx.save();
        this.ctx.translate(this.centreX, this.centreY);
        this.ctx.rotate(Math.radians(this.direction));
        this.ctx.translate(-this.centreX, -this.centreY);

        this.ctx.drawImage(this.tankImage, this.column * this.SPRITE_WIDTH, this.row * this.SPRITE_HEIGHT, this.SPRITE_WIDTH, this.SPRITE_HEIGHT, this.centreX - parseInt(this.size / 2), this.centreY - parseInt(this.size), this.size, this.size);
        this.ctx.restore();
    }
};
/******************************************************************************/


function renderCanvas()
{
    requestAnimationFrame(renderCanvas);
    ctx.drawImage(fieldImage, 0, 0, CANVAS_WIDTH, CANVAS_HEIGHT);
    /* Step 3 of 3 */
    /* Draw the animations */
    for (let i = 0; i < animation.length; i++)
    {
        animation[i].render();
    }
}


Math.radians = function (degrees)
{
    return degrees * Math.PI / 180;
};
</script>
</head>

<body>
<canvas id = "canvas" tabindex="1">
Your browser does not support the HTML5 'Canvas' tag.
</canvas>
<p>Use the left and right arrows to steer the tank.<br>Use the spacebar to fire a shell</p>
</body>
</html>

Shell Explosion

Shells should explode once they have travelled their maximum range. We create a Shell object. The shell range can be calculated using the direction that the tank is travelling. The shell range can be passed to the shell constructor, so that the shell knows its range. The shell movement is done in the Shell update() function. When the shell reaches its maximum range, an explosion can be created using an ExplosionAnimation object, as shown below:

shell.prototype.update = function ()
{
    if (this.distanceShellTravelled < this.shellRange)
    {
        this.distanceShellTravelled += this.stepSize;

        this.explosionTargetX = this.x + (this.distanceShellTravelled * Math.sin(Math.radians(this.direction)));
        this.explosionTargetY = this.y - (this.distanceShellTravelled * Math.cos(Math.radians(this.direction)));
    }
    else
    {
        animation[EXPLOSION] = new ExplosionAnimation(this.ctx, this.explosionTargetX, this.explosionTargetY, 250, 0);
        this.kill();
    }
};

As with the ShellFireAnimaiton, the ExplosionAnimation takes account of the direction that the tank is travelling. Use this mp3 file and this sprite image file for the ExplosionAnimation.

Below is an example of a tank with an exploding shell (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 = 1000;
const CANVAS_HEIGHT = 1000;
let fieldImage = new Image();
fieldImage.src = "images/field.png";
let canvas = null;
let ctx = null;

let riverImage = new Image();
riverImage.src = "images/tankFieldRiver.png";
let offscreenObstaclesCanvas = null;
let offscreenObstaclesCanvasG = null;

const TANK = 0;
const FIRE_SHELL = 1;
const SHELL = 2;
const EXPLOSION = 3;

const TANK_SIZE = 100; // the size of the tank images that will appear on the screen

/* Animation array */
let animation = [];

window.onload = onAllAssetsLoaded;
document.write("<div id='loadingMessage'>Loading...</div>");
function onAllAssetsLoaded()
{
    // kill 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;

    offscreenObstaclesCanvas = document.createElement('canvas');
    offscreenObstaclesCanvasG = offscreenObstaclesCanvas.getContext('2d');
    offscreenObstaclesCanvas.width = CANVAS_WIDTH;
    offscreenObstaclesCanvas.height = CANVAS_HEIGHT;

    offscreenObstaclesCanvasG.drawImage(riverImage, 0, 0, canvas.width, canvas.height);

    /* Step 1 of 3 */
    /* Each animation needs to be declared as an object */
    animation[TANK] = new TankAnimation(ctx, 0, 490, TANK_SIZE, 0, 90, CANVAS_WIDTH, CANVAS_HEIGHT);

    document.addEventListener('keydown', keydownHandler);
    gameLoop();
}

function keydownHandler(e)
{
    const ANGLE_STEP_SIZE = 10;

    if (e.keyCode === 37)  // left
    {
        animation[TANK].setDirection(animation[TANK].getDirection() - ANGLE_STEP_SIZE);
    }
    else if (e.keyCode === 39) // right
    {
        animation[TANK].setDirection(animation[TANK].getDirection() + ANGLE_STEP_SIZE);
    }
    else if (e.keyCode === 32) // space
    {
        animation[FIRE_SHELL] = new FireShellAnimation(ctx, animation[TANK].getX(), animation[TANK].getY(), animation[TANK].getDirection());
        animation[SHELL] = new shell(ctx, animation[TANK].getX(), animation[TANK].getY(), animation[TANK].direction);
    }
}


/* Step 2 of 3 */
/* Each animation needs its own code */
/******************************************************************************/
/* TankAnimation object */
function TankAnimation(ctx, centreX, centreY, size, animationStartDelay, direction, canvasWidth, canvasHeight)
{
    /* These variables are ALWAYS needed */
    this.ctx = ctx;
    this.animationInterval = null;
    this.frameRate = 130; // change to suit the animation speed in milliseconds. Smaller numbers give a faster animation */
    this.animationIsDisplayed = false;
    /* These variables depend on the animation */
    this.canvasWidth = canvasWidth;
    this.canvasHeight = canvasHeight;
    this.centreX = centreX;
    this.centreY = centreY;
    this.size = size;
    this.NUMBER_OF_SPRITES; // the number of sprites in the sprite image
    this.NUMBER_OF_COLUMNS_IN_SPRITE_IMAGE = 8; // the number of columns in the sprite image
    this.NUMBER_OF_ROWS_IN_SPRITE_IMAGE = 4; // the number of rows in the sprite image	
    this.START_ROW;
    this.START_COLUMN;

    this.NUMBER_OF_SPRITES = 8; // the number of sprites in the sprite image
    this.START_ROW = 1;
    this.START_COLUMN = 0;

    this.currentSprite = 0; // the current sprite to be displayed from the sprite image  
    this.row = this.START_ROW; // current row in sprite image
    this.column = this.START_COLUMN; // current column in sprite image
    this.spriteIncrement = -1;
    this.SPRITE_WIDTH;
    this.SPRITE_HEIGHT;

    //let direction = 90; // set to 90 to accout for the fact that the sprite image is originally 90 degrees off
    this.stepSizeX = this.STEP_SIZE * Math.sin(Math.radians(this.direction));
    this.stepSizeY = -this.STEP_SIZE * Math.cos(Math.radians(this.direction));

    this.STEP_SIZE = 5; // the number of pixels to move forward
    this.stepSizeX = this.STEP_SIZE;   // originally going from left to right across the screen
    this.stepSizeY = 0;
    this.setDirection(direction);

    this.tankImage = new Image();
    this.tankImage.src = "images/tank.png";

    /* Start the animation */
    setTimeout(this.start.bind(this), animationStartDelay);
}


/* Public functions */
TankAnimation.prototype.start = function ()
{
    this.animationIsDisplayed = true;
    this.animationInterval = setInterval(this.update.bind(this), this.frameRate);
};


TankAnimation.prototype.stop = function ()
{
    this.animationIsDisplayed = true;
    clearInterval(this.animationInterval);
    this.animationInterval = null; // set to null when not running           
};


TankAnimation.prototype.kill = function ()
{
    this.stop();
    this.animationIsDisplayed = false;
};


TankAnimation.prototype.update = function ()
{
    this.currentSprite++;
    this.column += this.spriteIncrement;

    if (this.currentSprite >= this.NUMBER_OF_SPRITES)
    {
        this.currentSprite = 0;
        this.row = this.START_ROW;
        this.column = this.START_COLUMN;
    }


    if (this.spriteIncrement === 1)
    {
        if (this.column >= this.NUMBER_OF_COLUMNS_IN_SPRITE_IMAGE - 1)
        {
            this.column = 0;
            this.row++;
        }
    }
    else  // spriteIncrement === -1
    {
        if (this.column < 0)
        {
            this.column = this.NUMBER_OF_COLUMNS_IN_SPRITE_IMAGE - 1;
            this.row--;
            if (this.row < 0)
            {
                this.currentSprite = 0;
                this.row = this.START_ROW;
                this.column = this.START_COLUMN;
            }
        }
    }

    //centreX++;
    this.centreX += this.stepSizeX;
    this.centreY += this.stepSizeY;


    // Collision detection with the river
    // As the tank can only move forward, we need only do collision detection for the front of the tank
    let imageData = null;
    let data = null;

    /* front left of tank */
    imageData = offscreenObstaclesCanvasG.getImageData(this.centreX + this.SPRITE_HEIGHT / 2.2, this.centreY - this.SPRITE_WIDTH / 2.2, 1, 1);
    data = imageData.data;
    if (data[3] !== 0)
    {
        this.centreX -= this.stepSizeX;
        this.centreY -= this.stepSizeY;
    }

    /* front right of tank */
    imageData = offscreenObstaclesCanvasG.getImageData(this.centreX + this.SPRITE_HEIGHT / 2.2, this.centreY + this.SPRITE_WIDTH / 2.2, 1, 1);
    data = imageData.data;
    if (data[3] !== 0)
    {
        this.centreX -= this.stepSizeX;
        this.centreY -= this.stepSizeY;
    }

    /* front centre of tank */
    imageData = offscreenObstaclesCanvasG.getImageData(this.centreX, this.centreY + this.SPRITE_WIDTH / 2.2, 1, 1);
    data = imageData.data;
    if (data[3] !== 0)
    {
        this.centreX -= this.stepSizeX;
        this.centreY -= this.stepSizeY;
    }



    if (this.centreX > this.canvasWidth)
    {
        this.centreX = 0;
    }
    else if (this.centreY > this.canvasHeight)
    {
        this.centreY = 0;
    }
    else if (this.centreX < 0)
    {
        this.centreX = this.canvasWidth;
    }
    else if (this.centreY < 0)
    {
        this.centreY = this.canvasHeight;
    }
};


TankAnimation.prototype.render = function ()
{
    if (this.animationIsDisplayed)
    {
        this.SPRITE_WIDTH = (this.tankImage.width / this.NUMBER_OF_COLUMNS_IN_SPRITE_IMAGE);
        this.SPRITE_HEIGHT = (this.tankImage.height / this.NUMBER_OF_ROWS_IN_SPRITE_IMAGE);

        this.ctx.save();
        this.ctx.translate(this.centreX, this.centreY);
        this.ctx.rotate(Math.radians(this.direction));
        this.ctx.translate(-this.centreX, -this.centreY);

        this.ctx.drawImage(this.tankImage, this.column * this.SPRITE_WIDTH, this.row * this.SPRITE_HEIGHT, this.SPRITE_WIDTH, this.SPRITE_HEIGHT, this.centreX - parseInt(this.size / 2), this.centreY - parseInt(this.size / 2), this.size, this.size);
        this.ctx.restore();
    }
};


TankAnimation.prototype.getDirection = function ()
{
    return this.direction;
};


TankAnimation.prototype.setDirection = function (newDirection)
{
    this.direction = newDirection;

    this.stepSizeX = this.STEP_SIZE * Math.sin(Math.radians(this.direction));
    this.stepSizeY = -this.STEP_SIZE * Math.cos(Math.radians(this.direction));
};


TankAnimation.prototype.getX = function ()
{
    return this.centreX;
};


TankAnimation.prototype.getY = function ()
{
    return this.centreY;
};
/******************************************************************************/


/******************************************************************************/
/* FireShellAnimation object */
function FireShellAnimation(ctx, centreX, centreY, direction)
{
    /* These variables are ALWAYS needed */
    this.ctx = ctx;
    this.animationInterval = null;
    this.frameRate = 50; // change to suit the animation speed in milliseconds. Smaller numbers give a faster animation */
    this.animationIsDisplayed = false;
    /* These variables depend on the animation */

    this.centreX = centreX;
    this.centreY = centreY;
    this.direction = direction;

    this.NUMBER_OF_SPRITES = 3; // the number of sprites in the sprite image
    this.NUMBER_OF_COLUMNS_IN_SPRITE_IMAGE = 8; // the number of columns in the sprite image
    this.NUMBER_OF_ROWS_IN_SPRITE_IMAGE = 4; // the number of columns in the sprite image
    //    this.NUMBER_OF_SPRITES = 3; // the number of sprites in the sprite image
    this.START_ROW = 2;
    this.START_COLUMN = 1;

    this.currentSprite = 0; // the current sprite to be displayed from the sprite image  
    this.row = this.START_ROW; // current row in sprite image
    this.column = this.START_COLUMN; // current column in sprite image
    this.spriteIncrement = 1;
    this.SPRITE_WIDTH;
    this.SPRITE_HEIGHT;
    this.size = 100;
    this.tankImage = new Image();
    this.tankImage.src = "images/tank.png";

    this.fireShellSound = document.createElement('audio');
    this.fireShellSound.src = 'images/tankFire.mp3';

    /* Start the animation */
    setTimeout(this.start.bind(this), 0);
}


/* Public functions */
FireShellAnimation.prototype.start = function ()
{
    this.fireShellSound.play();
    this.animationIsDisplayed = true;
    this.animationInterval = setInterval(this.update.bind(this), this.frameRate);
};


FireShellAnimation.prototype.stop = function ()
{
    this.animationIsDisplayed = true;
    clearInterval(this.animationInterval);
    this.animationInterval = null; // set to null when not running           
};


FireShellAnimation.prototype.kill = function ()
{
    this.stop();
    this.animationIsDisplayed = false;
};


FireShellAnimation.prototype.update = function ()
{
    this.currentSprite++;
    if (this.currentSprite >= this.NUMBER_OF_SPRITES)
    {
        this.kill();
    }

    this.column += this.spriteIncrement;
};


FireShellAnimation.prototype.render = function ()
{
    if (this.animationIsDisplayed)
    {
        this.SPRITE_WIDTH = (this.tankImage.width / this.NUMBER_OF_COLUMNS_IN_SPRITE_IMAGE);
        this.SPRITE_HEIGHT = (this.tankImage.height / this.NUMBER_OF_ROWS_IN_SPRITE_IMAGE);

        this.ctx.save();
        this.ctx.translate(this.centreX, this.centreY);
        this.ctx.rotate(Math.radians(this.direction));
        this.ctx.translate(-this.centreX, -this.centreY);

        this.ctx.drawImage(this.tankImage, this.column * this.SPRITE_WIDTH, this.row * this.SPRITE_HEIGHT, this.SPRITE_WIDTH, this.SPRITE_HEIGHT, this.centreX - parseInt(this.size / 2), this.centreY - parseInt(this.size), this.size, this.size);
        this.ctx.restore();
    }
};
/******************************************************************************/


/******************************************************************************/
/* ExplosionAnimation object */
function ExplosionAnimation(ctx, centreX, centreY, size, animationStartDelay)
{
    /* These variables are ALWAYS needed */
    this.ctx = ctx;
    this.animationInterval = null;
    this.frameRate = 40; // change to suit the animation speed in milliseconds. Smaller numbers give a faster animation */
    this.animationIsDisplayed = false;

    /* These variables depend on the animation */
    this.centreX = centreX;
    this.centreY = centreY;
    this.size = size;
    this.NUMBER_OF_SPRITES = 74; // the number of sprites in the sprite image
    this.NUMBER_OF_COLUMNS_IN_SPRITE_IMAGE = 9; // the number of columns in the sprite image
    this.NUMBER_OF_ROWS_IN_SPRITE_IMAGE = 9; // the number of rows in the sprite image	
    this.START_ROW = 0;
    this.START_COLUMN = 0;

    this.currentSprite = 0; // the current sprite to be displayed from the sprite image  
    this.row = this.START_ROW; // current row in sprite image
    this.column = this.START_COLUMN; // current column in sprite image

    this.SPRITE_WIDTH = 100;
    this.SPRITE_HEIGHT = 100;

    this.explosionImage = new Image();
    this.explosionImage.src = "images/explosion.png";

    this.shellExplosionSound = document.createElement('audio');
    this.shellExplosionSound.src = 'images/shellExplosionSound.mp3';

    /* Make sure that the image has loaded before starting the animation */
    setTimeout(this.start.bind(this), animationStartDelay);
}


/* Public functions */
ExplosionAnimation.prototype.start = function ()
{
    this.shellExplosionSound.play();
    this.animationIsDisplayed = true;
    this.animationInterval = setInterval(this.update.bind(this), this.frameRate);
};


ExplosionAnimation.prototype.stop = function ()
{
    this.animationIsDisplayed = true;
    clearInterval(this.animationInterval);
    this.animationInterval = null; // set to null when not running           
};


ExplosionAnimation.prototype.kill = function ()
{
    this.stop();
    this.animationIsDisplayed = false;
};


ExplosionAnimation.prototype.update = function ()
{
    if (this.currentSprite === this.NUMBER_OF_SPRITES)
    {
        this.kill();
    }
    this.currentSprite++;

    this.column++;
    if (this.column >= this.NUMBER_OF_COLUMNS_IN_SPRITE_IMAGE)
    {
        this.column = 0;
        this.row++;
    }
};


ExplosionAnimation.prototype.render = function ()
{
    if (this.animationIsDisplayed)
    {
        this.SPRITE_WIDTH = (this.explosionImage.width / this.NUMBER_OF_COLUMNS_IN_SPRITE_IMAGE);
        this.SPRITE_HEIGHT = (this.explosionImage.height / this.NUMBER_OF_ROWS_IN_SPRITE_IMAGE);
        this.ctx.drawImage(this.explosionImage, this.column * this.SPRITE_WIDTH, this.row * this.SPRITE_WIDTH, this.SPRITE_WIDTH, this.SPRITE_HEIGHT, this.centreX - parseInt(this.size / 2), this.centreY - parseInt(this.size / 2), this.size, this.size);
    }
};
/******************************************************************************/


/******************************************************************************/
/* shell object */
function shell(ctx, x, y, direction)
{
    /* These variables are ALWAYS needed */
    this.ctx = ctx;
    this.animationInterval = null;
    this.frameRate = 50; // change to suit the animation speed in milliseconds. Smaller numbers give a faster animation */
    this.animationIsDisplayed = false;
    /* These variables depend on the animation */
    this.x = x;
    this.y = y;
    this.explosionTargetX = x;
    this.explosionTargetY = y;
    this.direction = direction;

    /* define the maximum range of the shell */
    /* the shell will explode here if it has not hit a target beforehand */
    this.stepSize = 20;
    this.shellRange = 400;
    this.distanceShellTravelled = 0;

    /* Start the animation */
    setTimeout(this.start.bind(this), 0);
}


/* Public functions */
shell.prototype.start = function ()
{

    //  this.fireShellSound.play();
    this.animationIsDisplayed = true;

    this.animationInterval = setInterval(this.update.bind(this), this.frameRate);
};


shell.prototype.stop = function ()
{
    this.animationIsDisplayed = true;
    clearInterval(this.animationInterval);
    this.animationInterval = null; // set to null when not running           
};


shell.prototype.kill = function ()
{
    this.stop();
    this.animationIsDisplayed = false;
};


shell.prototype.update = function ()
{
    if (this.distanceShellTravelled < this.shellRange)
    {
        this.distanceShellTravelled += this.stepSize;

        this.explosionTargetX = this.x + (this.distanceShellTravelled * Math.sin(Math.radians(this.direction)));
        this.explosionTargetY = this.y - (this.distanceShellTravelled * Math.cos(Math.radians(this.direction)));
    }
    else
    {
        animation[EXPLOSION] = new ExplosionAnimation(this.ctx, this.explosionTargetX, this.explosionTargetY, 250, 0);
        this.kill();
    }
};


shell.prototype.render = function ()
{
    if (this.animationIsDisplayed)
    {

    }
};


shell.prototype.getX = function ()
{
    return this.explosionTargetX;
};


shell.prototype.getY = function ()
{
    return this.explosionTargetY;
};
/******************************************************************************/

function gameLoop()
{
    requestAnimationFrame(gameLoop);
    ctx.drawImage(fieldImage, 0, 0, CANVAS_WIDTH, CANVAS_HEIGHT);
    /* Step 3 of 3 */
    /* Draw the animations */
    for (let i = 0; i < animation.length; i++)
    {
        animation[i].render();
    }
}

Math.radians = function (degrees)
{
    return degrees * Math.PI / 180;
};
</script>
</head>

<body>
<canvas id = "canvas" tabindex="1">
Your browser does not support the HTML5 'Canvas' tag.
</canvas>
<p>Use the left and right arrows to steer the tank.<br>Use the spacebar to fire a shell</p>
</body>
</html>

Destroying Enemy Tanks

We can use an EnemyTank object to create enemy tanks. This object uses a single blue tank image from the original tank sprite image and places it at a random position and angle on the canvas.

We implement collision detection inside the main game loop. A collision occurs if a shell collides with a tank, as shown below:

function processShellCollisions()
{
    if (enemyTank.hitByShell(animation[SHELL].getX(), animation[SHELL].getY()))
    {
        animation[EXPLOSION] = new ExplosionAnimation(ctx, enemyTank.getX(), enemyTank.getY(), 250, 0);
        animation[SHELL].kill();
        enemyTank.positionRandomly();
    }
}

In order to make the code more maintainable, we can use constants to name the various animations in 'animation[]'. Instead of using 'animation[0], we can use 'animation[TANK]', etc.

const TANK = 0;
const ENEMY_TANK = 1;
const FIRE_SHELL = 2;
const SHELL = 3;
const EXPLOSION = 4;

In order to make the game more realistic, we can add this mp3 file. This mp3 file plays the sound of a moving tank. As the tank in our game is continuously moving, we need to loop the mp3 file, so that it continously repeats. The code to do this is shown below. This code should be included in the TankAnimation constructor.

this.movingSound = new Audio();
this.movingSound.src = 'images/tankMoving.mp3';

this.movingSound.addEventListener('ended', function() 
{
    this.currentTime = 0;
    this.play();
}, false);

Example of a tank game where enemy tanks can be destroyed (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 = 1000;
const CANVAS_HEIGHT = 1000;
let fieldImage = new Image();
fieldImage.src = "images/field.png";
let canvas = null;
let ctx = null;

let riverImage = new Image();
riverImage.src = "images/tankFieldRiver.png";
let offscreenObstaclesCanvas = null;
let offscreenObstaclesCanvasG = null;

const TANK = 0;
const ENEMY_TANK = 1;
const FIRE_SHELL = 2;
const SHELL = 3;
const EXPLOSION = 4;

const TANK_SIZE = 100; // the size of the tank images that will appear on the screen

/* Animation array */
let animation = [];

window.onload = onAllAssetsLoaded;
document.write("<div id='loadingMessage'>Loading...</div>");
function onAllAssetsLoaded()
{
    // kill 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;

    offscreenObstaclesCanvas = document.createElement('canvas');
    offscreenObstaclesCanvasG = offscreenObstaclesCanvas.getContext('2d');
    offscreenObstaclesCanvas.width = CANVAS_WIDTH;
    offscreenObstaclesCanvas.height = CANVAS_HEIGHT;

    offscreenObstaclesCanvasG.drawImage(riverImage, 0, 0, canvas.width, canvas.height);

    /* Step 1 of 3 */
    /* Each animation needs to be declared as an object */
    animation[TANK] = new TankAnimation(ctx, 0, 490, TANK_SIZE, 0, 90, CANVAS_WIDTH, CANVAS_HEIGHT);
    animation[ENEMY_TANK] = new EnemyTank(ctx, TANK_SIZE, CANVAS_WIDTH, CANVAS_HEIGHT);

    document.addEventListener('keydown', keydownHandler);
    gameLoop();
}

function keydownHandler(e)
{
    const ANGLE_STEP_SIZE = 10;

    if (e.keyCode === 37)  // left
    {
        animation[TANK].setDirection(animation[TANK].getDirection() - ANGLE_STEP_SIZE);
    }
    else if (e.keyCode === 39) // right
    {
        animation[TANK].setDirection(animation[TANK].getDirection() + ANGLE_STEP_SIZE);
    }
    else if (e.keyCode === 32) // space
    {
        animation[FIRE_SHELL] = new FireShellAnimation(ctx, animation[TANK].getX(), animation[TANK].getY(), animation[TANK].getDirection());

        /* Add explosion at SHELL_RANGE distance away from the tank gun */
        //    let explosionX = animation[TANK].getX() + (SHELL_RANGE * Math.sin(Math.radians(animation[TANK].direction)));
        //    let explosionY = animation[TANK].getY() - (SHELL_RANGE * Math.cos(Math.radians(animation[TANK].direction)));
        //   animation[SHELL] = new ExplosionAnimation(ctx, explosionX, explosionY, 250, 500);
        animation[SHELL] = new shell(ctx, animation[TANK].getX(), animation[TANK].getY(), animation[TANK].direction);
    }
}


/* Step 2 of 3 */
/* Each animation needs its own code */
/******************************************************************************/
/* TankAnimation object */
function TankAnimation(ctx, centreX, centreY, size, animationStartDelay, direction, canvasWidth, canvasHeight)
{
    /* These variables are ALWAYS needed */
    this.ctx = ctx;
    this.animationInterval = null;
    this.frameRate = 130; // change to suit the animation speed in milliseconds. Smaller numbers give a faster animation */
    this.animationIsDisplayed = false;
    /* These variables depend on the animation */
    this.canvasWidth = canvasWidth;
    this.canvasHeight = canvasHeight;
    this.centreX = centreX;
    this.centreY = centreY;
    this.size = size;
    this.NUMBER_OF_SPRITES; // the number of sprites in the sprite image
    this.NUMBER_OF_COLUMNS_IN_SPRITE_IMAGE = 8; // the number of columns in the sprite image
    this.NUMBER_OF_ROWS_IN_SPRITE_IMAGE = 4; // the number of rows in the sprite image	
    this.START_ROW;
    this.START_COLUMN;

    this.NUMBER_OF_SPRITES = 8; // the number of sprites in the sprite image
    this.START_ROW = 1;
    this.START_COLUMN = 0;

    this.currentSprite = 0; // the current sprite to be displayed from the sprite image  
    this.row = this.START_ROW; // current row in sprite image
    this.column = this.START_COLUMN; // current column in sprite image
    this.spriteIncrement = -1;
    this.SPRITE_WIDTH;
    this.SPRITE_HEIGHT;

    //let direction = 90; // set to 90 to accout for the fact that the sprite image is originally 90 degrees off
    this.stepSizeX = this.STEP_SIZE * Math.sin(Math.radians(this.direction));
    this.stepSizeY = -this.STEP_SIZE * Math.cos(Math.radians(this.direction));

    this.STEP_SIZE = 5; // the number of pixels to move forward
    this.stepSizeX = this.STEP_SIZE;   // originally going from left to right across the screen
    this.stepSizeY = 0;
    this.setDirection(direction);

    this.tankImage = new Image();
    this.tankImage.src = "images/tank.png";
    
    this.movingSound = new Audio();
    this.movingSound.src = 'images/tankMoving.mp3';

    this.movingSound.addEventListener('ended', function() 
    {
        this.currentTime = 0;
        this.play();
    }, false);

    /* Start the animation */
    setTimeout(this.start.bind(this), animationStartDelay);
}


/* Public functions */
TankAnimation.prototype.start = function ()
{
    this.movingSound.play();
    this.animationIsDisplayed = true;
    this.animationInterval = setInterval(this.update.bind(this), this.frameRate);
};


TankAnimation.prototype.stop = function ()
{
    this.animationIsDisplayed = true;
    clearInterval(this.animationInterval);
    this.animationInterval = null; // set to null when not running           
};


TankAnimation.prototype.kill = function ()
{
    this.stop();
    this.animationIsDisplayed = false;
};


TankAnimation.prototype.update = function ()
{
    this.currentSprite++;
    this.column += this.spriteIncrement;

    if (this.currentSprite >= this.NUMBER_OF_SPRITES)
    {
        this.currentSprite = 0;
        this.row = this.START_ROW;
        this.column = this.START_COLUMN;
    }


    if (this.spriteIncrement === 1)
    {
        if (this.column >= this.NUMBER_OF_COLUMNS_IN_SPRITE_IMAGE - 1)
        {
            this.column = 0;
            this.row++;
        }
    }
    else  // spriteIncrement === -1
    {
        if (this.column < 0)
        {
            this.column = this.NUMBER_OF_COLUMNS_IN_SPRITE_IMAGE - 1;
            this.row--;
            if (this.row < 0)
            {
                this.currentSprite = 0;
                this.row = this.START_ROW;
                this.column = this.START_COLUMN;
            }
        }
    }

    //centreX++;
    this.centreX += this.stepSizeX;
    this.centreY += this.stepSizeY;


    // Collision detection with the river
    // As the tank can only move forward, we need only do collision detection for the front of the tank
    let imageData = null;
    let data = null;

    /* front left of tank */
    imageData = offscreenObstaclesCanvasG.getImageData(this.centreX + this.SPRITE_HEIGHT / 2.2, this.centreY - this.SPRITE_WIDTH / 2.2, 1, 1);
    data = imageData.data;
    if (data[3] !== 0)
    {
        this.centreX -= this.stepSizeX;
        this.centreY -= this.stepSizeY;
    }

    /* front right of tank */
    imageData = offscreenObstaclesCanvasG.getImageData(this.centreX + this.SPRITE_HEIGHT / 2.2, this.centreY + this.SPRITE_WIDTH / 2.2, 1, 1);
    data = imageData.data;
    if (data[3] !== 0)
    {
        this.centreX -= this.stepSizeX;
        this.centreY -= this.stepSizeY;
    }

    /* front centre of tank */
    imageData = offscreenObstaclesCanvasG.getImageData(this.centreX, this.centreY + this.SPRITE_WIDTH / 2.2, 1, 1);
    data = imageData.data;
    if (data[3] !== 0)
    {
        this.centreX -= this.stepSizeX;
        this.centreY -= this.stepSizeY;
    }



    if (this.centreX > this.canvasWidth)
    {
        this.centreX = 0;
    }
    else if (this.centreY > this.canvasHeight)
    {
        this.centreY = 0;
    }
    else if (this.centreX < 0)
    {
        this.centreX = this.canvasWidth;
    }
    else if (this.centreY < 0)
    {
        this.centreY = this.canvasHeight;
    }
};


TankAnimation.prototype.render = function ()
{
    if (this.animationIsDisplayed)
    {
        this.SPRITE_WIDTH = (this.tankImage.width / this.NUMBER_OF_COLUMNS_IN_SPRITE_IMAGE);
        this.SPRITE_HEIGHT = (this.tankImage.height / this.NUMBER_OF_ROWS_IN_SPRITE_IMAGE);

        this.ctx.save();
        this.ctx.translate(this.centreX, this.centreY);
        this.ctx.rotate(Math.radians(this.direction));
        this.ctx.translate(-this.centreX, -this.centreY);

        this.ctx.drawImage(this.tankImage, this.column * this.SPRITE_WIDTH, this.row * this.SPRITE_HEIGHT, this.SPRITE_WIDTH, this.SPRITE_HEIGHT, this.centreX - parseInt(this.size / 2), this.centreY - parseInt(this.size / 2), this.size, this.size);
        this.ctx.restore();
    }
};


TankAnimation.prototype.getDirection = function ()
{
    return this.direction;
};


TankAnimation.prototype.setDirection = function (newDirection)
{
    this.direction = newDirection;

    this.stepSizeX = this.STEP_SIZE * Math.sin(Math.radians(this.direction));
    this.stepSizeY = -this.STEP_SIZE * Math.cos(Math.radians(this.direction));
};


TankAnimation.prototype.getX = function ()
{
    return this.centreX;
};


TankAnimation.prototype.getY = function ()
{
    return this.centreY;
};
/******************************************************************************/


/******************************************************************************/
/* EnemyTank object */
function EnemyTank(ctx, size, canvasWidth, canvasHeight)
{
    this.ctx = ctx;
    this.size = size;
    this.canvasWidth = canvasWidth;
    this.canvasHeight = canvasHeight;

    this.positionRandomly();

    this.NUMBER_OF_COLUMNS_IN_SPRITE_IMAGE = 8; // the number of columns in the sprite image
    this.NUMBER_OF_ROWS_IN_SPRITE_IMAGE = 4; // the number of rows in the sprite image	
    this.SPRITE_WIDTH = null;
    this.SPRITE_HEIGHT = null;

    // use the first blue sprite from the tank image file as the enemy
    this.row = 1;
    this.column = 1;

    this.tankImage = new Image();
    this.tankImage.src = "images/tank.png";
}


EnemyTank.prototype.positionRandomly = function ()
{
    // note that the enemyTank can be positioned in the river
    this.centreX = Math.random() * (this.canvasWidth - (this.size * 2)) + this.size;
    this.centreY = Math.random() * (this.canvasHeight - (this.size * 2)) + this.size;
    this.direction = Math.random() * 360; // between 0 and 360 degrees
};

EnemyTank.prototype.hitByShell = function (x, y)
{
    let imageTopLeftX = this.centreX - parseInt(this.size / 2);
    let imageTopLeftY = this.centreY - parseInt(this.size / 2);
    if ((x > imageTopLeftX) && (y > imageTopLeftY))
    {
        if (x > imageTopLeftX)
        {
            if ((x - imageTopLeftX) > this.size)
            {
                return false; // to the right of the image
            }
        }

        if (y > imageTopLeftY)
        {
            if ((y - imageTopLeftY) > this.size)
            {

                return false; // below the image
            }
        }
    }
    else // above or to the left of the image
    {
        return false;
    }

    return true; // inside image
};


EnemyTank.prototype.render = function ()
{
    this.SPRITE_WIDTH = (this.tankImage.width / this.NUMBER_OF_COLUMNS_IN_SPRITE_IMAGE);
    this.SPRITE_HEIGHT = (this.tankImage.height / this.NUMBER_OF_ROWS_IN_SPRITE_IMAGE);
    this.ctx.save();
    this.ctx.translate(this.centreX, this.centreY);
    this.ctx.rotate(Math.radians(this.direction));
    this.ctx.translate(-this.centreX, -this.centreY);

    this.ctx.drawImage(this.tankImage, this.column * this.SPRITE_WIDTH, this.row * this.SPRITE_HEIGHT, this.SPRITE_WIDTH, this.SPRITE_HEIGHT, this.centreX - parseInt(this.size / 2), this.centreY - parseInt(this.size), this.size, this.size);
    this.ctx.restore();
};


EnemyTank.prototype.getX = function ()
{
    return this.centreX;
};


EnemyTank.prototype.getY = function ()
{
    return this.centreY;
};
/******************************************************************************/


/******************************************************************************/
/* FireShellAnimation object */
function FireShellAnimation(ctx, centreX, centreY, direction)
{
    /* These variables are ALWAYS needed */
    this.ctx = ctx;
    this.animationInterval = null;
    this.frameRate = 50; // change to suit the animation speed in milliseconds. Smaller numbers give a faster animation */
    this.animationIsDisplayed = false;
    /* These variables depend on the animation */

    this.centreX = centreX;
    this.centreY = centreY;
    this.direction = direction;

    this.NUMBER_OF_SPRITES = 3; // the number of sprites in the sprite image
    this.NUMBER_OF_COLUMNS_IN_SPRITE_IMAGE = 8; // the number of columns in the sprite image
    this.NUMBER_OF_ROWS_IN_SPRITE_IMAGE = 4; // the number of columns in the sprite image
    //    this.NUMBER_OF_SPRITES = 3; // the number of sprites in the sprite image
    this.START_ROW = 2;
    this.START_COLUMN = 1;

    this.currentSprite = 0; // the current sprite to be displayed from the sprite image  
    this.row = this.START_ROW; // current row in sprite image
    this.column = this.START_COLUMN; // current column in sprite image
    this.spriteIncrement = 1;
    this.SPRITE_WIDTH;
    this.SPRITE_HEIGHT;
    this.size = 100;
    this.tankImage = new Image();
    this.tankImage.src = "images/tank.png";

    this.fireShellSound = new Audio();
    this.fireShellSound.src = 'images/tankFire.mp3';

    /* Start the animation */
    setTimeout(this.start.bind(this), 0);
}


/* Public functions */
FireShellAnimation.prototype.start = function ()
{
    this.fireShellSound.play();
    this.animationIsDisplayed = true;
    this.animationInterval = setInterval(this.update.bind(this), this.frameRate);
};


FireShellAnimation.prototype.stop = function ()
{
    this.animationIsDisplayed = true;
    clearInterval(this.animationInterval);
    this.animationInterval = null; // set to null when not running           
};


FireShellAnimation.prototype.kill = function ()
{
    this.stop();
    this.animationIsDisplayed = false;
};


FireShellAnimation.prototype.update = function ()
{
    this.currentSprite++;
    if (this.currentSprite >= this.NUMBER_OF_SPRITES)
    {
        this.kill();
    }

    this.column += this.spriteIncrement;
};


FireShellAnimation.prototype.render = function ()
{
    if (this.animationIsDisplayed)
    {
        this.SPRITE_WIDTH = (this.tankImage.width / this.NUMBER_OF_COLUMNS_IN_SPRITE_IMAGE);
        this.SPRITE_HEIGHT = (this.tankImage.height / this.NUMBER_OF_ROWS_IN_SPRITE_IMAGE);

        this.ctx.save();
        this.ctx.translate(this.centreX, this.centreY);
        this.ctx.rotate(Math.radians(this.direction));
        this.ctx.translate(-this.centreX, -this.centreY);

        this.ctx.drawImage(this.tankImage, this.column * this.SPRITE_WIDTH, this.row * this.SPRITE_HEIGHT, this.SPRITE_WIDTH, this.SPRITE_HEIGHT, this.centreX - parseInt(this.size / 2), this.centreY - parseInt(this.size), this.size, this.size);
        this.ctx.restore();
    }
};
/******************************************************************************/


/******************************************************************************/
/* ExplosionAnimation object */
function ExplosionAnimation(ctx, centreX, centreY, size, animationStartDelay)
{
    /* These variables are ALWAYS needed */
    this.ctx = ctx;
    this.animationInterval = null;
    this.frameRate = 40; // change to suit the animation speed in milliseconds. Smaller numbers give a faster animation */
    this.animationIsDisplayed = false;

    /* These variables depend on the animation */
    this.centreX = centreX;
    this.centreY = centreY;
    this.size = size;
    this.NUMBER_OF_SPRITES = 74; // the number of sprites in the sprite image
    this.NUMBER_OF_COLUMNS_IN_SPRITE_IMAGE = 9; // the number of columns in the sprite image
    this.NUMBER_OF_ROWS_IN_SPRITE_IMAGE = 9; // the number of rows in the sprite image	
    this.START_ROW = 0;
    this.START_COLUMN = 0;

    this.currentSprite = 0; // the current sprite to be displayed from the sprite image  
    this.row = this.START_ROW; // current row in sprite image
    this.column = this.START_COLUMN; // current column in sprite image

    this.SPRITE_WIDTH = 100;
    this.SPRITE_HEIGHT = 100;

    this.explosionImage = new Image();
    this.explosionImage.src = "images/explosion.png";

    this.shellExplosionSound = new Audio();
    this.shellExplosionSound.src = 'images/shellExplosionSound.mp3';

    /* Make sure that the image has loaded before starting the animation */
    setTimeout(this.start.bind(this), animationStartDelay);
}


/* Public functions */
ExplosionAnimation.prototype.start = function ()
{
    this.shellExplosionSound.play();
    this.animationIsDisplayed = true;
    this.animationInterval = setInterval(this.update.bind(this), this.frameRate);
};


ExplosionAnimation.prototype.stop = function ()
{
    this.animationIsDisplayed = true;
    clearInterval(this.animationInterval);
    this.animationInterval = null; // set to null when not running           
};


ExplosionAnimation.prototype.kill = function ()
{
    this.stop();
    this.animationIsDisplayed = false;
};


ExplosionAnimation.prototype.update = function ()
{
    if (this.currentSprite === this.NUMBER_OF_SPRITES)
    {
        this.kill();
    }
    this.currentSprite++;

    this.column++;
    if (this.column >= this.NUMBER_OF_COLUMNS_IN_SPRITE_IMAGE)
    {
        this.column = 0;
        this.row++;
    }
};


ExplosionAnimation.prototype.render = function ()
{
    if (this.animationIsDisplayed)
    {
        this.SPRITE_WIDTH = (this.explosionImage.width / this.NUMBER_OF_COLUMNS_IN_SPRITE_IMAGE);
        this.SPRITE_HEIGHT = (this.explosionImage.height / this.NUMBER_OF_ROWS_IN_SPRITE_IMAGE);
        this.ctx.drawImage(this.explosionImage, this.column * this.SPRITE_WIDTH, this.row * this.SPRITE_WIDTH, this.SPRITE_WIDTH, this.SPRITE_HEIGHT, this.centreX - parseInt(this.size / 2), this.centreY - parseInt(this.size / 2), this.size, this.size);
    }
};
/******************************************************************************/


/******************************************************************************/
/* shell object */
function shell(ctx, x, y, direction)
{
    /* These variables are ALWAYS needed */
    this.ctx = ctx;
    this.animationInterval = null;
    this.frameRate = 50; // change to suit the animation speed in milliseconds. Smaller numbers give a faster animation */
    this.animationIsDisplayed = false;
    /* These variables depend on the animation */
    this.x = x;
    this.y = y;
    this.explosionTargetX = x;
    this.explosionTargetY = y;
    this.direction = direction;

    /* define the maximum range of the shell */
    /* the shell will explode here if it has not hit a target beforehand */
    this.stepSize = 20;
    this.shellRange = 400;
    this.distanceShellTravelled = 0;

    /* Start the animation */
    setTimeout(this.start.bind(this), 0);
}


/* Public functions */
shell.prototype.start = function ()
{

    //  this.fireShellSound.play();
    this.animationIsDisplayed = true;

    this.animationInterval = setInterval(this.update.bind(this), this.frameRate);
};


shell.prototype.stop = function ()
{
    this.animationIsDisplayed = true;
    clearInterval(this.animationInterval);
    this.animationInterval = null; // set to null when not running           
};


shell.prototype.kill = function ()
{
    this.stop();
    this.animationIsDisplayed = false;
};


shell.prototype.update = function ()
{
    if (this.distanceShellTravelled < this.shellRange)
    {
        this.distanceShellTravelled += this.stepSize;


        this.explosionTargetX = this.x + (this.distanceShellTravelled * Math.sin(Math.radians(this.direction)));
        this.explosionTargetY = this.y - (this.distanceShellTravelled * Math.cos(Math.radians(this.direction)));
    }
    else
    {
        animation[EXPLOSION] = new ExplosionAnimation(this.ctx, this.explosionTargetX, this.explosionTargetY, 250, 0);
        this.kill();
    }
};


shell.prototype.render = function ()
{
    if (this.animationIsDisplayed)
    {

    }
};


shell.prototype.getX = function ()
{
    return this.explosionTargetX;
};


shell.prototype.getY = function ()
{
    return this.explosionTargetY;
};
/******************************************************************************/

function gameLoop()
{
    requestAnimationFrame(gameLoop);
    ctx.drawImage(fieldImage, 0, 0, CANVAS_WIDTH, CANVAS_HEIGHT);
    /* Step 3 of 3 */
    /* Draw the animations */
    for (let i = 0; i < animation.length; i++)
    {
        animation[i].render();
    }

    processShellCollisions();
}


function processShellCollisions()
{
    if (animation[ENEMY_TANK].hitByShell(animation[SHELL].getX(), animation[SHELL].getY()))
    {
        animation[EXPLOSION] = new ExplosionAnimation(ctx, animation[ENEMY_TANK].getX(), animation[ENEMY_TANK].getY(), 250, 0);
        animation[SHELL].kill();
        animation[ENEMY_TANK].positionRandomly();
    }
}

Math.radians = function (degrees)
{
    return degrees * Math.PI / 180;
};
</script>
</head>

<body>
<canvas id = "canvas" tabindex="1">
Your browser does not support the HTML5 'Canvas' tag.
</canvas>
<p>Use the left and right arrows to steer the tank.<br>Use the spacebar to fire a shell</p>
</body>
</html>
 
<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>