Case Study... Monster Game

The aim of this game is to use the arrow keys to direct the monster to the bullseye.

This game shows:

 

Play game

Download .zip file

monster_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 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";

/* Direction that the skeleton is walking */
/* Note that this matches the row in the gameObject image for the given direction */
const UP = 0;
const LEFT = 1;
const DOWN = 2;
const RIGHT = 3;
const STOPPED = 4;
const START = 5;

/* The various gameObjects */
/* These are the positions that each gameObject is held in the gameObjects[] array */
const BACKGROUND = 0;
const OBSTACLES = 1;
const BULLSEYE = 2;
const MONSTER = 3;
const WIN_MESSAGE = 4;

/******************* 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.                          */
    /* This function will:                                              */
    /* 1. create the various game game gameObjects                   */
    /* 2. store the game gameObjects in an array                     */
    /* 3. create a new Game to display the game gameObjects          */
    /* 4. 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(starsImage, 0, 0, canvas.width, canvas.height);
    gameObjects[OBSTACLES] = new StaticImage(obstaclesImage, 0, 0, canvas.width, canvas.height);
    gameObjects[BULLSEYE] = new Bullseye(bullseyeImage, canvas.width - 50, canvas.height - 50, 50, 50);
    gameObjects[MONSTER] = new Monster(monsterImage);

    /* END OF game specific code. */

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

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

    /* If they are needed, then include any game-specific mouse and keyboard listners */
    document.addEventListener('keydown', function (e)
    {
        if (e.keyCode === 37)  // left
        {
            gameObjects[MONSTER].setDirection(LEFT);
        }
        else if (e.keyCode === 38) // up
        {
            gameObjects[MONSTER].setDirection(UP);
        }
        else if (e.keyCode === 39) // right
        {
            gameObjects[MONSTER].setDirection(RIGHT);
        }
        else if (e.keyCode === 40) // down
        {
            gameObjects[MONSTER].setDirection(DOWN);
        }
        else if (e.keyCode === 32) // space
        {
            gameObjects[MONSTER].setDirection(START);
        }
    });

    document.addEventListener('keyup', function (e)
    {
        gameObjects[MONSTER].setDirection(STOPPED);
    });
}

The game displays a monster, a background, a bullseye and obstacles.

    gameObjects[BACKGROUND] = new StaticImage(starsImage, 0, 0, canvas.width, canvas.height);
    gameObjects[OBSTACLES] = new StaticImage(obstaclesImage, 0, 0, canvas.width, canvas.height);
    gameObjects[BULLSEYE] = new Bullseye(bullseyeImage, canvas.width - 50, canvas.height - 50, 50, 50);
    gameObjects[MONSTER] = new Monster(monsterImage);

The arrow keys are used to control the direction that the monster moves.

 document.addEventListener('keydown', function (e)
    {
        if (e.keyCode === 37)  // left
        {
            gameObjects[MONSTER].setDirection(LEFT);
        }
        else if (e.keyCode === 38) // up
        {
            gameObjects[MONSTER].setDirection(UP);
        }
        else if (e.keyCode === 39) // right
        {
            gameObjects[MONSTER].setDirection(RIGHT);
        }
        else if (e.keyCode === 40) // down
        {
            gameObjects[MONSTER].setDirection(DOWN);
        }
        else if (e.keyCode === 32) // space
        {
            gameObjects[MONSTER].setDirection(START);
        }
    });

The monster stops moving when the arrow keys are released.

    document.addEventListener('keyup', function (e)
    {
        gameObjects[MONSTER].setDirection(STOPPED);
    });

MonsterCanvasGame.js

/* Author: Derek O Reilly, Dundalk Institute of Technology, Ireland.       */
/* A CanvasGame that implements collision detection.                       */
/* The game allows the user to walk a skeleton around a maze.              */
/* If the skeleton is guided to the maze exit, then a win message appears. */


class MonsterCanvasGame extends CanvasGame
{
    constructor(obstaclesImage)
    {
        super();

        /* this.monsterObstalesCtx will be used for collision detection */
        let monsterObstalesOffscreenCanvas = document.createElement('canvas');
        monsterObstalesOffscreenCanvas.width = canvas.width;
        monsterObstalesOffscreenCanvas.height = canvas.height;
        this.monsterObstalesCtx = monsterObstalesOffscreenCanvas.getContext('2d');
        this.monsterObstalesCtx.drawImage(obstaclesImage, 0, 0, canvas.width, canvas.height);

        this.screenShakeInterval = null;
        this.screenIsRotatingToTheLeft = false;
        this.NUMBER_OF_SCREEN_SHAKES_INTERATIONS = 10;
        this.numberOfScreenShakes = 0;
    }

    collisionDetection()
    {
        if (!this.monsterObstalesCtx)
        {
            return;
        }

        let imageData = this.monsterObstalesCtx.getImageData(gameObjects[MONSTER].getX(), gameObjects[MONSTER].getY(), 1, 1);
        let dataTop = imageData.data;
        imageData = this.monsterObstalesCtx.getImageData(gameObjects[MONSTER].getX() + gameObjects[MONSTER].getWidth() * 0.8, gameObjects[MONSTER].getY(), 1, 1);
        let dataRight = imageData.data;
        imageData = this.monsterObstalesCtx.getImageData(gameObjects[MONSTER].getX(), gameObjects[MONSTER].getY() + gameObjects[MONSTER].getHeight() * 0.8, 1, 1);
        let dataBottom = imageData.data;
        imageData = this.monsterObstalesCtx.getImageData(gameObjects[MONSTER].getX() + gameObjects[MONSTER].getWidth() * 0.8, gameObjects[MONSTER].getY() + gameObjects[MONSTER].getHeight() * 0.8, 1, 1);
        let dataLeft = imageData.data;
        if ((dataTop[3] !== 0) || (dataRight[3] !== 0) || (dataBottom[3] !== 0) || (dataLeft[3] !== 0))
        {
            if (gameObjects[MONSTER].getDirection() === UP)
            {
                gameObjects[MONSTER].setDirection(DOWN);
                gameObjects[MONSTER].setY(gameObjects[MONSTER].getY() + 5);
            }
            else if (gameObjects[MONSTER].getDirection() === DOWN)
            {
                gameObjects[MONSTER].setDirection(UP);
                gameObjects[MONSTER].setY(gameObjects[MONSTER].getY() - 5);
            }
            else if (gameObjects[MONSTER].getDirection() === LEFT)
            {
                gameObjects[MONSTER].setDirection(RIGHT);
            }
            else if (gameObjects[MONSTER].getDirection() === RIGHT)
            {
                gameObjects[MONSTER].setDirection(LEFT);
            }

            if (this.screenShakeInterval === null)
            {
                this.screenShakeInterval = setInterval(this.shakeScreen.bind(this), 10);
            }

        }
        else if (gameObjects[BULLSEYE].pointIsInsideBullseyeRectangle(gameObjects[MONSTER].getX() + gameObjects[MONSTER].getWidth() * 0.5, gameObjects[MONSTER].getY() + gameObjects[MONSTER].getHeight() * 0.5))
        {
            /* Player has won */
            for (let i = 0; i < gameObjects.length; i++) /* stop all gameObjects from animating */
            {
                gameObjects[i].stop();
            }
            gameObjects[WIN_MESSAGE] = new StaticText("Well Done!", 20, 280, "Times Roman", 100, "red");
            gameObjects[WIN_MESSAGE].start(); /* render win message */
        }
    }

    render()
    {
        ctx.save();
        if (this.screenShakeInterval !== null) // hit an obstacle
        {
            if (this.screenIsRotatingToTheLeft)
            {
                ctx.translate(canvas.width / 2, canvas.height / 2);
                ctx.rotate(Math.radians(1));
                ctx.translate(-canvas.width / 2, -canvas.height / 2);
            }
            else
            {
                ctx.translate(canvas.width / 2, canvas.height / 2);
                ctx.rotate(Math.radians(-1));
                ctx.translate(-canvas.width / 2, -canvas.height / 2);
            }
        }

        super.render();
        ctx.restore();
    }

    shakeScreen()
    {
        if (this.screenIsRotatingToTheLeft)
        {
            this.screenIsRotatingToTheLeft = false;
        }
        else // screen is rotating to the right
        {
            this.screenIsRotatingToTheLeft = true;
        }
        this.numberOfScreenShakes++;
        if (this.numberOfScreenShakes >= this.NUMBER_OF_SCREEN_SHAKES_INTERATIONS)
        {
            this.numberOfScreenShakes = 0;
            clearInterval(this.screenShakeInterval);
            this.screenShakeInterval = null;
        }
    }
}

Whenever the skeleton hits the maze grid, if changes direction.

            /* Player has won */
            for (let i = 0; i < gameObjects.length; i++) /* stop all gameObjects from animating */
            {
                gameObjects[i].stop();
            }
            gameObjects[WIN_MESSAGE] = new StaticText("Well Done!", 20, 280, "Times Roman", 100, "red");
            gameObjects[WIN_MESSAGE].start(); /* render win message */

Monster.js

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

class Monster 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(monsterImage)
    {
        super(5); /* as this class extends from GameObject, you must always call super() */

        /* These variables depend on the object */
        this.startX = 20;
        this.startY = 20;
        this.x = this.startX;
        this.y = this.startY;

        this.monsterImage = monsterImage;
        this.width = 40;
        this.height = 40;
        this.setDirection(STOPPED);
    }

    updateState()
    {
        if (this.direction === UP)
        {
            this.y--;
        }
        else if (this.direction === LEFT)
        {
            this.x--;
        }
        else if (this.direction === DOWN)
        {
            this.y++;
        }
        else if (this.direction === RIGHT)
        {
            this.x++;
        }
    }

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

    setDirection(newDirection)
    {
        if (this.direction !== START)
        {
            this.direction = newDirection;
        }
        else // spacebar hit, so set monster back to start
        {
            this.x = this.startX;
            this.y = this.startY;
            this.direction = STOPPED;
        }
    }

    getDirection()
    {
        return(this.direction);
    }

    getX()
    {
        return this.x;
    }

    getY()
    {
        return this.y;
    }

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

    setY(newY)
    {
        this.y = newY;
    }
    getWidth()
    {
        return this.width;
    }

    getHeight()
    {
        return this.height;
    }
}


Bullseye.js

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

class Bullseye extends StaticImage
{
    /* 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, height)
    {
        super(image, x, y, width, height); /* as this class extends from GameObject, you must always call super() */

        /* These variables depend on the object */
        this.image = image;
        this.width = width;
        this.height = height;
        this.x = x;
        this.y = y;
        
        this.bullseyeSize = 10; // the granularity of the bullseye target.
    }

    getX()
    {
        return this.x;
    }

    getY()
    {
        return this.y;
    }

    getWidth()
    {
        return this.width;
    }

    getHeight()
    {
        return this.height;
    }

    pointIsInsideBullseyeRectangle(pointX, pointY)
    {
        /* The bullseye is set to have a width and height of bullseySize */
        /* The bulleseye is set from the centre of the bullseye image    */
        
        let bullseyeX = this.x + ((this.width - this.bullseyeSize) / 2);
        let bullseyeY = this.y + ((this.height - this.bullseyeSize) / 2);  

        if ((pointX > bullseyeX) && (pointY > bullseyeY))
        {
            if (pointX > bullseyeX)
            {
                if ((pointX - bullseyeX) > this.bullseyeSize)
                {
                    return false; // to the right of this gameObject
                }
            }

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