Framework

Many canvas games consist of multiple gameObjects moving around and interacting on a canvas. We shall develop a simple object oriented framework that we can use to develop gameObject-based canvas games. The framework consists of the basic html code, various javascript classes and global variables. The minimum requirement to use this framework is to include the four framework read-only files below and two game-specific files:

Framework read-only files:

 

Game-specific files:

index.js

index.js

/***************************************************************************/
/* This file is the same for every game.                                   */
/* DO NOT EDIT THIS FILE                                                   */
/***************************************************************************/


/* Author: Derek O Reilly, Dundalk Institute of Technology, Ireland.       */
/* This file holds the global variables that will be used in all games.    */
/* This file always calls the playGame function().                         */
/* It also holds game specific code, which will be different for each game */

/************** Declare data and functions that are needed for all games ************/

/* Always create a canvas and a ctx */
let canvas = null;
let ctx = null;

/* Always create an array that holds the default game gameObjects */
let gameObjects = [];

/*********** END OF Declare data and functions that are needed for all games *********/

/* Wait for all game assets, such as audio and images to load before starting the game */
/* The code below will work for both websites and Cordova mobile apps                  */
window.addEventListener("load", onAllAssetsLoaded);           // needed for websites
document.addEventListener("deviceready", onAllAssetsLoaded);  // needed for Cordova mobile apps

let game_is_loaded = false;
document.write("<div id='loadingMessage'>Loading...</div>");
function onAllAssetsLoaded()
{
    // Both "load" and "deviceready" will call this function.
    // The game_is_loaded flag is used to make sure that this function will only be executed once 
    if(game_is_loaded)
    {
        return;
    }
    game_is_loaded = true;

    /* hide the webpage loading message */
    document.getElementById('loadingMessage').style.visibility = "hidden";

    /* Initialise the canvas and associated variables */
    /* This code never changes                        */
    canvas = document.getElementById("gameCanvas");
    ctx = canvas.getContext("2d");
    canvas.width = canvas.clientWidth;
    canvas.height = canvas.clientHeight;

    playGame(); // Each game will include its own .js file, which will hold the game's palyGame() function
}


/* global functions */

/* Convert from degrees to radians */
Math.radians = function (degrees)
{
    return degrees * Math.PI / 180;
};

This file sets the global variables that are used in all games. In particular, it sets "canvas" and "ctx" to be global. As these are used in both the CanvasGame and GameObject class, making them global improves performance.
This gameObjects[] array holds the game's gameObjects. This ties into the render() method code within CanvasCode. Having all of the gameObjects in one array makes it easy for us to step thgough them all, ensuring that all gameObjects are continuously rendered during gameplay. Using the gameObjects[] array makes writing rendering code much simpler for the developer.

/************** Declare data and functions that are needed for all games ************/

/* Always create a canvas and a ctx */
let canvas = null;
let ctx = null;

/* Always create an array that holds the default game gameObjects */
let gameObjects = [];

/*********** END OF Declare data and functions that are needed for all games *********/

 

This code displays a "Loading..." message while the game assets, such as images, are loading. Once all of the webpage assets have loaded, the onAllAssetsLoaded() function hides the load message and calls the playGame() function.

/* Wait for all game assets, such as audio and images to load before starting the game */
/* The code below will work for both websites and Cordova mobile apps                  */
window.addEventListener("load", onAllAssetsLoaded);           // needed for websites
document.addEventListener("deviceready", onAllAssetsLoaded);  // needed for Cordova mobile apps

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

    ...

The global variables canvas and ctx can only be initialised once the game's webpage has been loaded loaded.

    /* Initialise the canvas and associated variables */
    /* This code never changes                        */
    canvas = document.getElementById("gameCanvas");
    ctx = canvas.getContext("2d");
    canvas.width = canvas.clientWidth;
    canvas.height = canvas.clientHeight;

The function playGame() plays the game. The code for playGame() will be contained in the .js game file that was included in the game html file (in this example example_game.js and example_game.html).

    playGame(); // Each game will include its own .js file, which will hold the game's palyGame() function

Any global functions should be added to the index.js file. For these notes, the only global function is Math.radians().

/* global functions */

/* Convert from degrees to radians */
Math.radians = function (degrees)
{
    return degrees * Math.PI / 180;
};

Game Stylesheet

The game stylesheet is always the same. If game-specific css code is needed, then create another css file to hold it.

game.css

/* Author: Derek O Reilly, Dundalk Institute of Technology, Ireland. */
/* style class for game's container, canvas and loading message      */

#gameContainer
{
    /* style the gameContainer if the game includes non-canvas elements */
}

#gameCanvas
{
    /* the canvas styling usually does not change */
    outline:1px solid darkgrey;
    width:500px;
    height:500px;
}

#loadingMessage
{
    /* the loading message styling usually does not change */
    position:absolute;
    top:100px;
    left:100px;
    z-index:100;
    font-size:50px;
}

The gameContainer and gameCanvas hold the game.

The loadingMessage is the message that appears while the game assets are loading.

CanvasGame.js

This class holds the game loop, renders the gameObjects and looks after collision detection.

/*******************************************************************************************************/
/* This file is the same for every game.                                                               */
/* DO NOT EDIT THIS FILE.                                                                              */
/*                                                                                                     */
/* If you need to modify the methods, then you should create a sub-class that extends from this class. */
/*                                                                                                     */
/*******************************************************************************************************/


/* 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. */

class CanvasGame
{
    constructor()
    {
        /* render the game on the canvas */
        this.playGameLoop();
    }

    start()
    {
        for (let i = 0; i < gameObjects.length; i++)
        {
            gameObjects[i].start();
        }
    }

    playGameLoop()
    {
        this.collisionDetection();
        this.render();
        
        /* recursively call playGameLoop() */
        requestAnimationFrame(this.playGameLoop.bind(this));
    }
    
    render()
    {
        /* clear previous rendering from the canvas */
        ctx.clearRect(0, 0, canvas.width, canvas.height);

        /* render game gameObjects on the canvas */
        for (let i = 0; i < gameObjects.length; i++)
        {
            /* Use an empty gameObject to ensure that there are no empty entries in the gameObjects[] array */
            /* This is needed because an empty entry in the gameObjects[] array will cause the program to freeze */
            if(gameObjects[i] === undefined)
            {
                gameObjects[i] = new GameObject();
            }
            
            if (gameObjects[i].isDisplayed())
            {
                gameObjects[i].render();
            }
        }
    }
    
    collisionDetection()
    {
        /* If you need to implement collision detection in your game, then you can overwrite this method in your sub-class. */
        /* If you do not need to implement collision detection, then you do not need to overwrite this method.              */
    }
}
constructor()
    {
        /* render the game on the canvas */
        this.playGameLoop();
    }

The start() function is called inside the game's main js file (in this webpage, it is the file example_game.js).
It starts the various gameObjects' timers.

    start()
    {
        for (let i = 0; i < this.gameObjects.length; i++)
        {
            this.gameObjects[i].start();
        }

    }

The playGameLoop() continuously checks the gameObjects for collisions and renders the gameObjects.

 playGameLoop()
    {
        this.collisionDetection();
        this.render();
        
        /* recursively call playGameLoop() */
        requestAnimationFrame(this.playGameLoop.bind(this));
    }

 

The render() function renders all of the gameObjects that are included in the game.

Because of the way that entries are added to the gameObjects[] array, it is possible that an entry in the array will be empty. When this occurs, we need to place an empty placeholder gameObject in the array, as shown in red.

Each gameObject contains an isDisplayed() method, which will return true whenever the gameObject's start() method is called and false whenever the gameObject's stopAndHide() method is called. In this way, we can show or hide gameObjects on the canvas. We test against the isDisplayed() method to display only those gameObjects that are visible, as shown in blue.

    render()
    {
        /* clear previous rendering from the canvas */
        ctx.clearRect(0, 0, canvas.width, canvas.height);

        /* render game gameObjects on the canvas */
        for (let i = 0; i < gameObjects.length; i++)
        {
            /* Use an empty gameObject to ensure that there are no empty entries in the gameObjects[] array */
            /* This is needed because an empty entry in the gameObjects[] array will cause the program to freeze */
            if(gameObjects[i] === undefined)
            {
                gameObjects[i] = new GameObject();
            }
            
            if (gameObjects[i].isDisplayed())
            {
                gameObjects[i].render();
            }
        }
    }

 

The collisionDetection() function will be specific to each game. If a game does not involve gameObject collision detection, then leave this function empty.
If your game has collision detection, then you should create your own sub-class that extends from this class. Inside your new sub-class you should override this function.

    collisionDetection()
    {
        /* If you need to implement collision detection in your game, then you can overwrite this method in your sub-class. */
        /* If you do not need to implement collision detection, then you do not need to overwrite this method.              */
    }

GameObject.js

GameObject.js is the GameObject super-class. All of the gameObjects in a game must extend from GameObject.

/*******************************************************************************************************/
/* This file is the same for every game.                                                               */
/* DO NOT EDIT THIS FILE.                                                                              */
/*                                                                                                     */
/* If you need to modify the methods, then you should create a sub-class that extends from this class. */
/*                                                                                                     */
/*******************************************************************************************************/


/* Author: Derek O Reilly, Dundalk Institute of Technology, Ireland.                                                                                */
/* This is the superclass that gameObjects extend from.                                                                                                 */
/* It holds the data and functionality that is common to all gameObjects.                                                                               */
/* If you want to implement state change in your game, then you MUST overwrite the updateState() method in your sub-class that extends from GameObject. */
/* You MUST overwrite the code inside this if-statement with your own render() code in your sub-class that extends GameObject.                          */
/* This class will usually not change.                                                                                                              */

class GameObject
{
    constructor(updateStateMilliseconds, delay = 0)
    {
        /* These variables are ALWAYS needed */
        this.gameObjectInterval = null; /* set to null when not running */
        this.gameObjectIsDisplayed = false;
        this.updateStateMilliseconds = updateStateMilliseconds; /* change to suit the gameObject state update in milliseconds. Smaller numbers give a faster gameObject update. */
        this.delay = delay; /* delay the start of the updateState() method */
    }

    start()
    {
        if ((this.updateStateMilliseconds !== null) && (this.gameObjectInterval === null))
        {
            setTimeout(startUpdateStateInterval.bind(this), this.delay);
        }
        else if (this.updateStateMilliseconds === null)
        {
            this.gameObjectIsDisplayed = true; // by default, gameObjects that have no updateState() interval should be visible
        }

        function startUpdateStateInterval() // this function is only ever called from inside the start() method 
        {
            this.gameObjectInterval = setInterval(this.updateState.bind(this), this.updateStateMilliseconds);
            this.gameObjectIsDisplayed = true;
        }
    }

    stop()
    {
        if (this.gameObjectInterval !== null)
        {
            clearInterval(this.gameObjectInterval);
            this.gameObjectInterval = null; /* set to null when not running */
        }
        this.gameObjectIsDisplayed = true;
    }

    stopAndHide()
    {
        this.stop();
        this.gameObjectIsDisplayed = false;
    }

    isDisplayed()
    {
        return (this.gameObjectIsDisplayed);
    }

    updateState()
    {
        /* If you need to change state data in your game, then you can overwrite this method in your sub-class. */
        /* If you do not need to change data state, then you do not need to overwrite this method.              */
    }

    render()
    {
        /* If your gameObject renders, then you overwrite this method with your own render() code */
    }
}

You will always need to create one or more of your own sub-classes that extend from the GameObject class.

The constructor is always given a updateStateMilliseconds value. This is the length of the interval between each call to the class's update() method.
A delay (in milliseconds) can also be provided. The first call of update will wait for the delay has passed before executing.
The code in this method never changes for different gameObjects.

    constructor(updateStateMilliseconds, delay = 0)
    {
        /* These variables are ALWAYS needed */
        this.gameObjectInterval = null; /* set to null when not running */
        this.gameObjectIsDisplayed = false;
        this.updateStateMilliseconds = updateStateMilliseconds; /* change to suit the gameObject state update in milliseconds. Smaller numbers give a faster gameObject update. */
        this.delay = delay; /* delay the start of the updateState() method */
    }

The start() method waits until the delay has passed before calling the startUpdateStateInterval() function. The start() method will not call startUpdateStateInterval() if the gameObject is already started.

    start()
    {
        if ((this.updateStateMilliseconds !== null) && (this.gameObjectInterval === null))
        {
            setTimeout(startUpdateStateInterval.bind(this), this.delay);
        }
        else if (this.updateStateMilliseconds === null)
        {
            this.gameObjectIsDisplayed = true; // by default, gameObjects that have no updateState() interval should be visible
        }

        function startUpdateStateInterval() // this function is only ever called from inside the start() method 
        {
            this.gameObjectInterval = setInterval(this.updateState.bind(this), this.updateStateMilliseconds);
            this.gameObjectIsDisplayed = true;
        }
    }

Both stop() and stopAndHide() stop the gameObject's timer. If you stop() a gameObject, it still remains visible. If you stopAndHide() a gameObject, it is not rendered.
The isDisplayed() method returns true if the gameObject should be rendered.
The code in all three of these methods never change for different gameObjects.

    stop()
    {
        if (this.gameObjectInterval !== null)
        {
            clearInterval(this.gameObjectInterval);
            this.gameObjectInterval = null; /* set to null when not running */
        }
        this.gameObjectIsDisplayed = true;
    }

    stopAndHide()
    {
        this.stop();
        this.gameObjectIsDisplayed = false;
    }

    isDisplayed()
    {
        return (this.gameObjectIsDisplayed);
    }

The code for updateState() and render() will be determined by the game.
You should always create your own sub-class that extends from the GameObject class. If your gameObject can change state, then you need to override the updateState() method. If your gameObject does not change state, then do nothing.

Your gameObject will usually render. If your gameObject renders, then so you need to override the render() method.

    updateState()
    {
        /* If you need to change state data in your game, then you can overwrite this method in your sub-class. */
        /* If you do not need to change data state, then you do not need to overwrite this method.              */
    }

    render()
    {
        /* If your gameObject renders, then you overwrite this method with your own render() code */
    }

game HTML file

This is the HTML that holds the canvas. In this example, assume that the filename is "example_game.html". The template for the HTML is shown below:

example_game.html

<!-- Author: Derek O Reilly, Dundalk Institute of Technology, Ireland. -->

<!DOCTYPE html>
<html>
    <head>
        <title>Course notes example code</title>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <!-- Always include the game stylesheet, the Game class, the GameObject class and index.js --> <!-- These four must be included in all games. This code never changes. --> <link href="css/game.css" rel="stylesheet" type="text/css"/> <script src="js/CanvasGame.js" type="text/javascript"></script> <script src="js/GameObject.js" type="text/javascript"></script> <script src="js/index.js" type="text/javascript"></script> <!-- ***************************************************************************** --> <!-- *************** THE CODE BELOW CAN BE DIFFERENT FOR EACH GAME *************** --> <!-- Always include the game javascript that matches the html file name --> <script src="js/example_game.js" type="text/javascript"></script> <!-- Include any classes that extend from GameObject that are used in this game --> <!-- ************************* END OF GAME SPECIFIC CODE ************************* --> <!-- ***************************************************************************** --> </head> <body> <div id="gameContainer"> <!-- having a container will allow us to have a game that includes elements that are outside of the canvas --> <canvas id="gameCanvas" tabindex="1"></canvas> <!-- ***************************************************************************** --> <!-- *************** THE CODE BELOW CAN BE DIFFERENT FOR EACH GAME *************** --> <!-- ************************* END OF GAME SPECIFIC CODE ************************* --> <!-- ***************************************************************************** --> </div> </body> </html>

It is useful to note that most of the code never changes between games. All non-highlighted code in the listing above will be the same for every game.

We need to always include the four game framework read-only files. The workings of these four files was described at the start of this webpage.

        <!-- Always include the game stylesheet, the Game class, the GameObject class and index.js -->
        <!-- These four must be included in all games. This code never changes.                -->
        <link href="css/game.css" rel="stylesheet" type="text/css"/>
        <script src="js/CanvasGame.js" type="text/javascript"></script>
        <script src="js/GameObject.js" type="text/javascript"></script>
        <script src="js/index.js" type="text/javascript"></script>

 

We always have to include a main game javascript file and at least one class that extends from GameObject.
Although it is not strictly necessary, it makes sense that the filename example_game.js matches the name of the HTML file (in this case example_game.HTML).

<!-- Always include the game javascript that matches the html file name --> 
<script src="js/example_game.js" type="text/javascript"></script>

In this example, MygameObject is a class that extends from GameObject (which is included in GameObject.js). There will always be at least one type of gameObject in every game. Otherwise, it is pointless to use this framework. There can be more than one gameObject sub-class in a game. In this case, each gameObject sub-class will have its own .js file included in the above code.

<!-- Include any classes that extend from GameObject that are used in this game -->
<script src="js/MygameObject.js" type="text/javascript"></script>

The canvas must always have the id "gameCanvas", as it is used in other parts of the code to connect the HTML webpage to the javascript game.
The canvas is wrapped in a div. This will alllow us to develop games that include HTML elements outside of the canvas. Later on, we shall see that these other HTML elements are able to communicate with the canvas. The div must always be called "gameContainer", as it is used in other parts of the code to connect the HTML webpage to the javascript game.

<body>
    <div id="gameContainer"> <!-- having a container will allow us to have a game that includes elements that are outside of the canvas -->
        <canvas id = "gameCanvas" tabindex="1"></canvas>
    </div>
</body>

game .js file

example_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  */

/******************* 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 */

    /* END OF game specific code. */


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

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


    /* If they are needed, then include any game-specific mouse and keyboard listners */
}

Each game can have its own global variables and functions. These will be declared here:

/******************** Declare game specific global data and functions *****************/
/* images must be declared as global, so that they will load before the game starts  */



/******************* END OF Declare game specific data and functions *****************/
  

 

 

Every gameObject-based game must have an array of gameObjects. It is declared as shown below. The gameObjects[] array is accessed by the game's render() and collisionDetection() methods in CanvasGame class.

/* Always create an array that holds the default game gameObjects */
let gameObjects = []; 

 

Every game will have a playGame() function. This is the beginning point of the game. This function is called from the game's HTML file above. Most of the playGame() function code below is the same for every game. Only the highlighted code below will change between games.

/*********** END OF Declare data and functions that are needed for all games *********/



/* 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 */


    /* END OF game specific code. */


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

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

 
<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>