Case Study... Fireball Game

The aim of this game is to use the fireballs to destroy the log without letting any falling fireball hit the bat.

This game shows:

Play game

Download .zip file

fireball_game.js

/* Author: Derek O Reilly, Dundalk Institute of Technology, Ireland.             */
/* There should always be a javaScript file with the same name as the html file. */
/* This file always holds the playGame function().                               */
/* It also holds game specific code, which will be different for each game       */





/******************** Declare game specific global data and functions *****************/
/* images must be declared as global, so that they will load before the game starts  */
let backgroundImage = new Image();
backgroundImage.src = "images/grass.png";

let logImage = new Image();
logImage.src = "images/log.png";

let fireballImage = new Image();
fireballImage.src = "images/fireball.png";

const BACKGROUND = 0;
const WIN_LOSE_MESSAGE = 1;

/* Instead of using gameObject[], we can declare our own gameObject variables */
let bat = null; // we cannot initialise gameObjects yet, as they might require images that have not yet loaded
let target = null;

let fireballs = [];
let numberOfBulletsFired = 0; // no bullets fired yet
/******************* END OF Declare game specific data and functions *****************/







/* Always have a playGame() function                                     */
/* However, the content of this function will be different for each game */
function playGame()
{
    /* We need to initialise the game objects outside of the Game class */
    /* This function does this initialisation.                          */
    /* Specifically, this function will:                                */
    /* 1. initialise the canvas and associated variables                */
    /* 2. create the various game gameObjects,                   */
    /* 3. store the gameObjects in an array                      */
    /* 4. create a new Game to display the gameObjects           */
    /* 5. start the Game                                                */



    /* Create the various gameObjects for this game. */
    /* This is game specific code. It will be different for each game, as each game will have it own gameObjects */

    gameObjects[BACKGROUND] = new StaticImage(backgroundImage, 0, 0, canvas.width, canvas.height);
    bat = new Bat(0, canvas.height - 10, 125);
    target = new Target(logImage, 100, 0, 100);
    /* END OF game specific code. */


    /* Always create a game that uses the gameObject array */
    let game = new FireballCanvasGame();

    /* Always play the game */
    game.start();

    
    /* If they are needed, then include any game-specific mouse and keyboard listners */
    document.addEventListener("keydown", function (e)
    {
        let stepSize = 10;

        if (e.keyCode === 37)  // left
        {
            bat.changeX(-stepSize);
        }
        else if (e.keyCode === 39) // right
        {
            bat.changeX(stepSize);
        }
        else if (e.keyCode === 32) // space bar
        {
            fireballs[numberOfBulletsFired] = new Fireball(fireballImage, bat.getCentreX());
            fireballs[numberOfBulletsFired].start();
            numberOfBulletsFired++;
            bat.setWidth(bat.getWidth() + 10);
        }
    });
}

The fireballs are not contained in the gameObjects[] array. Instead, they are stored in a fireballs[] array.

let fireballs = [];

A new fireball is added each time the user hits the space bar. The fireball fires from the centre of the bat.
In this game, the bat gets bigger each time a fireball is fired.

        else if (e.keyCode === 32) // space bar
        {
            fireballs[numberOfBulletsFired] = new Fireball(fireballImage, bat.getCentreX());
            fireballs[numberOfBulletsFired].start();
            numberOfBulletsFired++;
            bat.setWidth(bat.getWidth() + 10);
        }

FireballCanvasGame.js

/* Author: Derek O Reilly, Dundalk Institute of Technology, Ireland.                                                   */
/* The CanvasGame class is responsible for rendering all of the gameObjects and other game graphics on the canvas.         */
/* If you want to implement collision detection in your game, then you MUST overwrite the collisionDetection() method. */
/* This class will usually not change.                                                                                 */



class FireballCanvasGame extends CanvasGame
{
    constructor()
    {
        super();
    }

    collisionDetection()
    {
        for (let i = 0; i < numberOfBulletsFired; i++)
        {
            if (target.pointIsInsideBoundingRectangle(fireballs[i].getCentreX(), fireballs[i].getCentreY()))
            {
                target.setWidth(target.getWidth() - 10);
                target.setX(Math.random() * (canvas.width - target.getWidth()));

                if (target.getWidth() < target.getMinimumSize())
                {
                    /* Player has won */
                    for (let i = 0; i < fireballs.length; i++) /* stop all gameObjects from animating */
                    {
                        fireballs[i].stop();
                    }
                    gameObjects[WIN_LOSE_MESSAGE] = new StaticText("Win!", 150, 270, "Times Roman", 100, "black");
                    gameObjects[WIN_LOSE_MESSAGE].start(); /* render win message */
                }
            }
            else if (bat.pointIsInsideBoundingRectangle(fireballs[i].getCentreX(), fireballs[i].getCentreY()))
            {
                /* Player has lost */
                for (let i = 0; i < fireballs.length; i++) /* stop all gameObjects from animating */
                {
                    fireballs[i].stop();
                }
                gameObjects[WIN_LOSE_MESSAGE] = new StaticText("LOSE!", 100, 270, "Times Roman", 100, "red");
                gameObjects[WIN_LOSE_MESSAGE].start(); /* render lose message */
            }
        }
    }

    render()
    {
        super.render();

        bat.render();
        target.render();
        for (let i = 0; i < fireballs.length; i++)
        {
            fireballs[i].render();
        }
    }
}

The collision detection checks each of the fireballs in the fireballs[] array to see if it collides with the target.

    collisionDetection()
    {
        if (this.gameState === PLAYING)
        {
            for (let i = 0; i < numberOfBulletsFired; i++)
            {
                if (target.pointIsInsideBoundingRectangle(fireballs[i].getCentreX(), fireballs[i].getCentreY()))
                {
                   ...

The fireballs are checked against the bounding rectangle of the target. If there is a collision between a fireball and the target, then the target reduces in size and moves.
If the target becomes smaller that target.getMinimumSize(), then the player wins.

                if (target.pointIsInsideBoundingRectangle(fireballs[i].getCentreX(), fireballs[i].getCentreY()))
                {
                    target.setWidth(target.getWidth() - 10);
                    target.setX(Math.random() * (500 - target.getWidth()));

                    if (target.getWidth() < target.getMinimumSize())
                    {
                        this.gameState = WON;
                    }
                }

The fireballs are checked against the bat's bounding rectangle. If a fireball hits the bat, then the player loses.

                else if (bat.pointIsInsideBoundingRectangle(fireballs[i].getCentreX(), fireballs[i].getCentreY()))
                {
                    
                    this.gameState = LOST;
                }

The game needs to render the background gameObjects. This is done by calling its parent class's super.render() method.
The bat, target and fireballs rendering methods are called directly, as shown below.

    render()
    {
        if (this.gameState === PLAYING)
        {
            super.render();
            
            bat.render();
            target.render();
            for(let i=0;i < fireballs.length;i++)
            {
                fireballs[i].render();
            }  
        }
        ...

Fireball.js

/* Author: Derek O Reilly, Dundalk Institute of Technology, Ireland. */

class Fireball extends GameObject
{
    /* Each gameObject MUST have a constructor() and a render() method.        */
    /* If the object animates, then it must also have an updateState() method. */

    constructor(image, centreX)
    {
        super(5); /* as this class extends from GameObject, you must always call super() */

        /* These variables depend on the object */
        this.image = image;
        this.width = 30;
        this.height = 30;
        this.centreX = centreX;
        this.centreY = canvas.height - this.height - 1;
        this.stepSize = -1;
        this.rotation = 360;
    }

    updateState()
    {
        this.rotation -= 3;
        if (this.rotation < 1)
        {
            this.rotation = 360;
        }

        if (this.stepSize < 0)
        {
            this.centreY--;
            if (this.centreY < 0)
            {
                this.stepSize = 1;
            }
        }
        else // this.stepSize >= 0
        {
            this.centreY++;
            if (this.centreY > canvas.height)
            {
                this.stepSize = -1;
            }
        }
    }

    render()
    {
        ctx.save();
        ctx.translate(this.centreX, this.centreY);
        ctx.rotate(Math.radians(this.rotation));
        ctx.translate(-this.centreX, -this.centreY);

        ctx.drawImage(this.image, this.centreX - this.width / 2, this.centreY - this.width / 2, this.width, this.height);
        ctx.restore();
    }

    getCentreX()
    {
        return this.centreX;
    }

    getCentreY()
    {
        return this.centreY;
    }
}

The fireballs rotate when they are moving. Firstly, the rotation amount is set inside the updateState() method. The -3 means that the fireball will rotate -3 degrees each time updateState() is called.

    updateState()
    {        
        this.rotation -= 3;
        if (this.rotation < 1)
        {
            this.rotation = 360;
        }
        ...

Secondly, the canvas is rotated by the rotation amount when the fireball is being drawn on the canvas.

    render()
    {
        ctx.save();
        ctx.translate(this.centreX, this.centreY);
        ctx.rotate(Math.radians(this.rotation));
        ctx.translate(-this.centreX, -this.centreY);

        ctx.drawImage(this.image, this.centreX - this.width / 2, this.centreY - this.width / 2, this.width, this.height);
        ctx.restore();
    }

The fireballs change direction when they hit the top or bottom of the canvas.

        if (this.stepSize < 0)
        {
            this.centreY--;
            if (this.centreY < 0)
            {
                this.stepSize = 1;
            }
        }
        else // this.stepSize >= 0
        {
            this.centreY++;
            if (this.centreY > canvas.height)
            {
                this.stepSize = -1;
            }
        }

Bat.js

/* Author: Derek O Reilly, Dundalk Institute of Technology, Ireland. */

class Bat extends GameObject
{
    /* Each gameObject MUST have a constructor() and a render() method.        */
    /* If the object animates, then it must also have an updateState() method. */

    constructor(x, y, width)
    {
        super(null); /* as this class extends from GameObject, you must always call super() */

        /* These variables depend on the object */
        this.x = x;
        this.y = y;
        this.width = width;
        this.height = 10;
    }

    render()
    {
        ctx.fillStyle = 'black';
        ctx.fillRect(this.x, this.y, this.width, this.height);
    }

    changeX(changeAmount)
    {
        this.x += changeAmount;
        
        /* Ensure that only half of the bat can be off the screen                               */
        /* This ensures that the bat can still fire at a log that is on the edge of the screen, */
        /* while at the same time the bat cannot hide fully from oncoming fireballs.            */
        if(this.x > canvas.width - (this.width / 2))
        {
            this.x = canvas.width - (this.width / 2);
        }
        else if(this.x < -(this.width / 2))
        {
            this.x = -(this.width / 2);
        }
    }
    
    getWidth()
    {
        return this.width;
    }
    
    setWidth(newWidth)
    {
        this.width = newWidth;
    }

    getCentreX()
    {
        return this.x + this.width / 2;
    }

    pointIsInsideBoundingRectangle(pointX, pointY)
    {
        if ((pointX > this.x) && (pointY > this.y))
        {
            if (pointX > this.x)
            {
                if ((pointX - this.x) > this.width)
                {
                    return false; // to the right of this gameObject
                }
            }

            if (pointY > this.y)
            {
                if ((pointY - this.y) > this.height)
                {
                    return false; // below this gameObject
                }
            }
        }
        else // above or to the left of this gameObject
        {
            return false;
        }
        return true; // inside this gameObject
    }
}

The changeX() method ensures that the bat can never move fully off the canvas. As the fireballs are fired from the centre of the bat, it must be possible to move the centre of the bat to either "0" or "canvas.width". The changeX() method implements this.

    changeX(changeAmount)
    {
        this.x += changeAmount;
        
        /* Ensure that only half of the bat can be off the screen                               */
        /* This ensures that the bat can still fire at a log that is on the edge of the screen, */
        /* while at the same time the bat cannot hide fully from oncoming fireballs.            */
        if(this.x > canvas.width - (this.width / 2))
        {
            this.x = canvas.width - (this.width / 2);
        }
        else if(this.x < -(this.width / 2))
        {
            this.x = -(this.width / 2);
        }
    }

Target.js

/* Author: Derek O Reilly, Dundalk Institute of Technology, Ireland. */

class Target extends GameObject
{
    /* Each gameObject MUST have a constructor() and a render() method.        */
    /* If the object animates, then it must also have an updateState() method. */

    constructor(image, x, y, width)
    {
        super(null); /* as this class extends from GameObject, you must always call super() */

        /* These variables depend on the object */
        this.image = image;
        this.x = x;
        this.y = y;
        this.width = width;
        this.height = 30;
        
        this.minimumSize = 20; 
    }

    render()
    {
        ctx.drawImage(this.image, this.x, this.y, this.width, this.height);
    }

    getX()
    {
        return this.x;
    }

    getY()
    {
        return this.y;
    }

    getWidth()
    {
        return this.width;
    }

    setX(newX)
    {
        this.x = newX;
    }

    setY(newY)
    {
        this.y = newY;
    }

    setWidth(newWidth)
    {
        this.width = newWidth;
    }
    
    getMinimumSize()
    {
        return this.minimumSize;
    }

    pointIsInsideBoundingRectangle(pointX, pointY)
    {
        if ((pointX > this.x) && (pointY > this.y))
        {
            if (pointX > this.x)
            {
                if ((pointX - this.x) > this.width)
                {
                    return false; // to the right of this gameObject
                }
            }

            if (pointY > this.y)
            {
                if ((pointY - this.y) > this.height)
                {
                    return false; // below this gameObject
                }
            }
        }
        else // above or to the left of this gameObject
        {
            return false;
        }
        return true; // inside this gameObject
    }
}

The code for Target.js is straightforward and needs no explaining.