Games Development in JavaScript
Games Development in JavaScript
Abstract
The course prepare participants to create games and animations in
JavaScript. In the materials students can learn how to create a browser
game, what is an animation and how to create one. The course uses code
examples that can be reused by students and on that base the participants
can create new games.
The original version of this material can be found on project web page:
http://geniusgamedev.eu/teaching.php.
Type: E-learning module, online-training, MOOC
Domain: Software development
Requirements: Basic knowledge in JavaScript programming
Target group: Computer Science students
License
The material is distributed under Creative Commons Attribution license.
Disclaimer
The European Commission’s support for the production of this publica-
tion does not constitute an endorsement of the contents, which reflect the
views only of the authors, and the Commission cannot be held responsible
for any use which may be made of the information contained therein.
HTML Canvas
The HTML <canvas> provides a rectangular area on a webpage on which graphics can be drawn. Canvas is
used with javascript to create animated browser games.
Every canvas needs to have an id assigned to it. This will be used in the javascript code to access the canvas.
<canvas id="gameCanvas"></canvas>
Use the javascript getElementById() function to access the canvas id within the javascript code.
<script>
let canvas = document.getElementById("gameCanvas");
</script>
The physical width and height of the canvas element are set in the stylesheet. These values define how much
physical space the canvas occupies on the webpage.
<style>
#gameCanvas
{
outline:1px solid darkgrey;
width:500px;
height:500px;
}
</style>
The logical width and height of the canvas are defined using javascript. These are the values that are will be
used in the game. If the canvas size does not match the style size, then the canvas drawing will be scaled. It
is usually a good idea to ensure that the physical and logical sizes match. The physical size of the canvas can
be got using the clientWidth and clientHeight properties of the javascript canvas. In these notes, the logical
width and height will always be set to match the physical width and height, as shown below.
<script>
let canvas = document.getElementById("gameCanvas");
canvas.width = canvas.clientWidth;
canvas.height = canvas.clientHeight;
</script>
In order to draw onto the canvas, we need to assign a graphics context to it. This is done in javascript code
using the getContext("2d") function. The code is implemented in the Game object constructor.
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>GENIUS worked example</title>
<style>
#gameCanvas
{
/* the canvas styling usually does not change */
outline:1px solid darkgrey;
width:500px;
height:500px;
}
</style>
</head>
<body>
<canvas id = "gameCanvas"></canvas>
<script>
let canvas = document.getElementById("gameCanvas");
canvas.width = canvas.clientWidth;
canvas.height = canvas.clientHeight;
let ctx = canvas.getContext("2d");
Exercises
Copyright Genius.
Text
We can draw text or outlined (stroke) text on a canvas using the code below:
ctx.fillStyle = "red";
ctx.font = "100px Times Roman";
ctx.fillText("Some Text", 10, 150);
ctx.strokeStyle = "blue";
ctx.font = "50px Arial";
ctx.lineWidth = 3;
ctx.strokeText("Some Text Outline", 10, 350);
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>GENIUS worked example</title>
<link rel="shortcut icon" type="image/png" href="images/genius_icon.png"/>
<style>
#gameCanvas
{
/* the canvas styling usually does not change */
outline:1px solid darkgrey;
width:500px;
height:500px;
}
</style>
</head>
<body>
<canvas id = "gameCanvas"></canvas>
<script>
let canvas = document.getElementById("gameCanvas");
canvas.width = canvas.clientWidth;
canvas.height = canvas.clientHeight;
let ctx = canvas.getContext("2d");
Exercises
Items will be draw on the canvas in the order that they are listed in the code. Any drawing outside of a canvas's
logical area will be clipped.
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>GENIUS worked example</title>
<link rel="shortcut icon" type="image/png" href="images/genius_icon.png"/>
<style>
#gameCanvas
{
/* the canvas styling usually does not change */
outline:1px solid darkgrey;
width:500px;
height:500px;
}
</style>
</head>
<body>
<canvas id = "gameCanvas"></canvas>
<script>
let canvas = document.getElementById("gameCanvas");
canvas.width = canvas.clientWidth;
canvas.height = canvas.clientHeight;
let ctx = canvas.getContext("2d");
Images
We can draw an image onto a canvas, as shown below:
window.onload = function ()
{
ctx.drawImage(img, 100, 100, 300, 300);
}
This will draw the image at position, 100, 100 with a width and height of 300. An image will only be drawn on a canvas if the image has been loaded into the
webpage. For this reason, we should always wrap the canvas code inside a "window.onload()" function. The image must be declared outside of the window.onload()
function. That way, the window.onload() function will only be called after the image has loaded. This is shown in the example below.
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>GENIUS worked example</title>
<style>
#gameCanvas
{
/* the canvas styling usually does not change */
outline:1px solid darkgrey;
width:500px;
height:500px;
}
</style>
<script>
let img = new Image(); // note that the offscreen image must be declared OUTSIDE of the window.onload() func
img.src = "images/city.png";
window.onload = function ()
{
let canvas = document.getElementById("gameCanvas");
canvas.width = canvas.clientWidth;
canvas.height = canvas.clientHeight;
let ctx = canvas.getContext("2d");
<body>
<canvas id = "gameCanvas"></canvas>
</body>
</html>
Exercises
Write code to draw a centred image and four corner images on the canvas, as shown here.
Copyright Genius.
Rotations
Rotations are made around the origin. In order to rotate around any other point, we translate that point to the
origin, perform the rotation and translate back to the point's original position again, as shown below.
Rotations are performed in radiants. We can convert from user friendly degrees to radiants using the formula
below:
By default, all drawing on the canvas after the rotation will be affected by the rotation. Normally, we do not
want to do this.
Use the save() and restore() functions to limit the scope of a rotation, as shown below:
ctx.save();
ctx.translate(canvas.width / 2, canvas.height / 2);
ctx.rotate(Math.radians(90));
ctx.translate(-canvas.width / 2, -canvas.height / 2);
ctx.restore();
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>GENIUS worked example</title>
<style>
#gameCanvas
{
/* the canvas styling usually does not change */
<script>
/* Convert from degrees to radians */
Math.radians = function (degrees)
{
return degrees * Math.PI / 180;
};
</script>
</head>
<body>
<canvas id = "gameCanvas"></canvas>
<script>
let canvas = document.getElementById("gameCanvas");
canvas.width = canvas.clientWidth;
canvas.height = canvas.clientHeight;
let ctx = canvas.getContext("2d");
ctx.fillStyle = "black";
ctx.font = "50px Times Roman";
ctx.save();
ctx.translate(canvas.width / 2, canvas.height / 2);
ctx.rotate(Math.radians(45));
ctx.translate(-canvas.width / 2, -canvas.height / 2);
Code Explained
Exercises
Write code to rotate an image by 90 degrees, as shown here. Remember that you need to ensure that the
image is loaded before you attempt to draw it.
Offscreen Canvas
An offscreen canvas is a canvas that is not visible on the screen. It can be used to construct the contents of a canvas prior to displaying the contents.
An offscreen canvas is just a canvas element. To create a canvas element we use the code below:
As with any other canvas, we can associate a 2d graphic context with an offscreen canvas, as shown in the code below
We need to ensure that the offscreen canvas is the exact same size as the on-screen canvas that it is being associated with, as shown below:
offscreenCanvas.width = canvas.width;
offscreenCanvas.height = canvas.height;
To draw an offscreen canvas onto another canvas, pass the offscreen canvas name as the element to be drawn, as shown below:
ctx.drawImage(offscreenCanvas,0, 0, width, height); // draw the offscreen canvas and not the graphic context
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>GENIUS worked example</title>
<style>
#gameCanvas
{
/* the canvas styling usually does not change */
outline:1px solid darkgrey;
width:500px;
height:500px;
}
</style>
</head>
<body>
<canvas id = 'gameCanvas'></canvas>
<script>
let cityImage = new Image();
cityImage.src = "images/city.png";
window.onload = function ()
{
let canvas = document.getElementById("gameCanvas");
canvas.width = canvas.clientWidth;
canvas.height = canvas.clientHeight;
let ctx = canvas.getContext("2d");
offscreenCanvas = document.createElement('canvas');
offscreenCanvasCtx = offscreenCanvas.getContext('2d');
offscreenCanvas.width = canvas.width;
offscreenCanvas.height = canvas.height;
Code Explained
offscreenCanvas = document.createElement('canvas');
offscreenCanvasCtx = offscreenCanvas.getContext('2d');
offscreenCanvas.width = canvas.width;
offscreenCanvas.height = canvas.height;
The code above creates an offscreen canvas and a graphics context up the offscreen canvas. It does not have to be the same size as the HTML
canvas, but it usually makes sense to match the size.
// draw onto the offscreen canvas
offscreenCanvasCtx.drawImage(beachImage, 0, 0, CANVAS_WIDTH, CANVAS_HEIGHT);
Drawing on an offscreen canvas is done in exactly the same way as for a HTML canvas.
// draw the offscreen buffer onto the screen's canvas
ctx.drawImage(offscreenCanvas, 0, 0, 200, 200);
Any canvas can be drawn onto another canvas using the drawImage() funciton. In this example, the offscreen canvas is drawn onto the HTML
canvas.
Pixels
It is possible to read and write to individual pixels on a canvas.
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>GENIUS worked example</title>
<style>
#gameCanvas
{
/* the canvas styling usually does not change */
outline:1px solid darkgrey;
width:500px;
height:500px;
}
</style>
</head>
<body>
<canvas id = "gameCanvas"></canvas>
<script>
let img = new Image(); // note that the offscreen image must be declared OUTSIDE of the window.onload() func
img.src = "images/city.png";
window.onload = function ()
{
let canvas = document.getElementById("gameCanvas");
canvas.width = canvas.clientWidth;
canvas.height = canvas.clientHeight;
let ctx = canvas.getContext("2d");
const RED = 0;
const GREEN = 1;
const BLUE = 2;
const ALPHA = 3;
ctx.putImageData(imageData, 0, 0);
};
</script>
</body>
</html>
Code Explained
let imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
let data = imageData.data;
The for-loop above steps through each of the selected pixels. Each pixel has a red, green, blue and alpha value, each of which is in the range 0-255.
ctx.putImageData(imageData, 0, 0);
The putImageData() function allows us to write the selected pixels on the canvas.
Exercises
<!doctype html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>GENIUS worked example</title>
<style>
#gameCanvas
{
/* the canvas styling usually does not change */
outline:1px solid darkgrey;
width:500px;
height:500px;
}
</style>
</head>
<body>
<canvas id = "gameCanvas"></canvas>
<script>
let image = new Image();
image.src = "images/city.png";
window.onload = function ()
{
let canvas = document.getElementById("gameCanvas");
canvas.width = canvas.clientWidth;
canvas.height = canvas.clientHeight;
let ctx = canvas.getContext("2d");
Code Explained
imageData = ctx.getImageData(canvas.width / 3, 0, canvas.width / 3, canvas.height); // x,y position and regtangle width
The code above shows that we only manipulate the middle one-third of the canvas.
Exercises
Write code to display a greyscaled image with a coloured border, as shown here.
if (data[ALPHA] !== 0)
{
// This pixel is not transparent
}
We can use an offscreen canvas to test the mouse pointer against the transparent and non-transparent parts of an image. This is especially useful for accurate
collision detection. We shall look at this in more detail later.
Copyright Genius.
Canvas Timers
Timers can be used to call a javascript function at set intervals. There are two ways to call a timer function:
setTimeout(functionName, milliseconds)
This will cause a function to be called once. The function will be called after waiting a specified number of milliseconds.
setInterval(functionName, milliseconds)
This will cause a function to be called repeatedly at specified time intervals.
clearInterval(timerVariable)
This will cause timerVariable to stop calling its associated timer function.
In order to use clearInterval(), a timer variable needs to be associated with a call to setInterval. This is done below:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>GENIUS worked example</title>
<link rel="shortcut icon" type="image/png" href="images/genius_icon.png"/>
<style>
#gameCanvas
{
/* the canvas styling usually does not change */
outline:1px solid darkgrey;
width:500px;
height:500px;
}
</style>
</head>
<body>
<canvas id = "gameCanvas"></canvas>
<script>
let mapImage = new Image();
mapImage.src = "images/map.png";
window.onload = function ()
{
let canvas = document.getElementById("gameCanvas");
canvas.width = canvas.clientWidth;
canvas.height = canvas.clientHeight;
let ctx = canvas.getContext("2d");
let x = 0;
function renderCanvas()
{
ctx.drawImage(mapImage, 0, 0, canvas.width, canvas.height); // clear any previous drawing
RequestAnimationFrame
In the above example, the rendering code is tied to the moving image (gameObject) code. This solution is fine when there is only one gameObject. It is not practical
when there are many gameObjects. Ideally, we should seperate the rendering code from the gameObject code.
In order to ensure smooth animation display, the rendering code should be called as often as possible. Javascript has an additional timer, called
requestAnimationFrame(). This timer will run as fast as the device allows. Faster devices will call this timer more frequently that slower devices when running the
same program. Using requestAnimationFrame will always result in the best game rendering. The example below separates the gameObject update code from the
rendering code.
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>GENIUS worked example</title>
<link rel="shortcut icon" type="image/png" href="images/genius_icon.png"/>
<style>
#gameCanvas
{
/* the canvas styling usually does not change */
outline:1px solid darkgrey;
width:500px;
height:500px;
}
</style>
</head>
<body>
<canvas id = "gameCanvas"></canvas>
<script>
let imgImage = new Image();
imgImage.src = "images/map.png";
window.onload = function ()
{
let canvas = document.getElementById("gameCanvas");
canvas.width = canvas.clientWidth;
canvas.height = canvas.clientHeight;
let ctx = canvas.getContext("2d");
let x = 0;
function updateCarState()
{
if (x > (canvas.width + 1))
{
x = 0;
}
else
{
x++;
}
}
function renderCanvas()
{
ctx.drawImage(imgImage, 0, 0, canvas.width, canvas.height); // clear any previous drawing
ctx.drawImage(carImage, x, 240, 40, 20);
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:
index.js
game.css
CanvasGame.js
GameObject.js
Game-specific files:
example_game.html
example_game.js
Index.Html
index.html
/***************************************************************************/
/* This file is the same for every game. */
/* DO NOT EDIT THIS FILE */
/***************************************************************************/
/************** Declare data and functions that are needed for all games ************/
/*********** 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
document.write("<div id='loadingMessage'>Loading...</div>");
function onAllAssetsLoaded()
{
/* hide the webpage loading message */
document.getElementById('loadingMessage').style.visibility = "hidden";
playGame(); // Each game will include its own .js file, which will hold the game's palyGame() function
}
/* global functions */
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 ************/
/*********** 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 */
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
#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;
}
Code Explained
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. */
/* */
/*******************************************************************************************************/
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();
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-cla
/* If you do not need to implement collision detection, then you do not need to overwrite this method.
}
}
Code Explained
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();
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);
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 funciton 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-cla
/* 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. */
/* */
/*******************************************************************************************************/
class GameObject
{
constructor(updateStateMilliseconds, delay = 0)
{
/* These 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 millise
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
}
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.
Code Explained
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 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 millise
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
}
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.
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 */
}
example_game.html
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Genius worked example</title>
<link rel="shortcut icon" type="image/png" href="images/genius_icon.png"/>
<!-- 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>
<!-- 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 -->
<body>
</div>
</body>
</html>
Code Explained
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 outsid
<canvas id = "gameCanvas" tabindex="1"></canvas>
</div>
</body>
/* If they are needed, then include any game-specific mouse and keyboard listners */
}
Code Explained
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 */
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 *********/
Static Images
Quite often, there will be some static images in a game. If we want to display static images, then we can use a gameObject that does not override the GameObject
class's update() method. An example is shown here. The code for this example is shown below.
StaticImage.js
render()
{
ctx.drawImage(this.image, this.x, this.y, this.width, this.height);
}
}
Code Explained
The render() method displays the image at the position and with the width and height that were passed into the constructor. As there is no state change, we do not
need to override the update() method.
Along with the StaticImage class, we need to make some changes to the static_image.html and static_image.js files. All other files remain unchanged.
The majority of the code in static_image.html and static_image.js remain unchanged from previous examples. The changes are highlighted in the code below.
four_corner_images.html
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>GENIUS worked example</title>
<link rel="shortcut icon" type="image/png" href="images/genius_icon.png"/>
<!-- 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>
<!-- Always include the game javascript that matches the html file name -->
<script src="js/four_corner_images.js" type="text/javascript"></script>
<!-- Include any classes that extend from GameObject that are used in this game -->
<script src="js/StaticImage.js" type="text/javascript"></script>
<body>
<div id="gameContainer"> <!-- having a container will allow us to have a game that includes elements that are ou
<canvas id="gameCanvas" tabindex="1"></canvas>
four_corner_images.js
/* If they are needed, then include any game-specific mouse and keyboard listners */
}
Exercises
Write code to draw four coloured blocks on the canvas, as shown here.
Write code to draw a block that fades out from white to red, as shown here.
Static Text
Quite often, there will be some static text in a game. If we want to display static images, then we can use a gameObject that does
not override the GameObject class's update() method. An example is shown here. The code for this example is shown below.
StaticText.js
render()
{
ctx.fillStyle = this.colour;
ctx.fillText(this.text, this.x, this.y);
}
}
Code Explained
STATIC_TEXT_CENTRE is used as a flag that will set the text to be horrizontally centred.
const STATIC_TEXT_CENTRE = -1;
In the constructor() method, we measure the width of the text using the highlighted code below:
constructor(text, x, y, font, fontSize, colour)
{
super(null); /* as this class extends from GameObject, you must always call super() */
If "this.x" has been set to be the value STATIC_TEXT_CENTRE, then we centre the text, as shown in the highlighted code
below.
constructor(text, x, y, font, fontSize, colour)
{
super(null); /* as this class extends from GameObject, you must always call super() */
Pulsating Images
A pulsating image is similar to a static image in that it will have a position, a width and a height. Unlike a static image, a pulsating image will continuously update
its position, as shown here. The code for this example is shown below.
PulsatingImage.js
this.stepSize = stepSize; // can be positive or negative. Negative decreases the original image size
if (this.stepSize > 0) // image is pulsating between original size to a larger size
{
this.incrementing = true;
}
else // image is pulsating between original size to a larger size
{
this.incrementing = false;
}
this.currentStep = 0;
}
updateState()
{
if (this.stepSize > 0) // image pulses into a bigger image
{
if (this.incrementing) // incrementing to increase size of original image
{
this.currentStep++;
if (this.currentStep === this.numberOfSteps)
{
this.incrementing = false;
}
}
else // drecrementing to return to original image size
{
this.currentStep--;
if (this.currentStep === 0)
{
this.incrementing = true;
}
}
}
else // image pulses into a smaller image
{
if (!this.incrementing) // drecrementing to decrease size of original image
{
this.currentStep++;
if (this.currentStep === this.numberOfSteps)
{
this.incrementing = true;
}
}
else // drecrementing to return to original image size
{
this.currentStep--;
if (this.currentStep === 0)
{
this.incrementing = false;
render()
{
ctx.drawImage(this.image, this.x - (this.currentStep * this.stepSize) / 2, this.y - (this.currentStep * this.ste
}
}
Code Explained
The intervalTime is passed to the Visual superclass (as shown below in green). This determines the frequency that updateState() method is called. Smaller values of
intervalTime will result in a faster pulsating effect.
The image pulsates about its centre. The initialisation of this is shown below in blue.
The stepSize (as shown below in red) is the number of pixels to increment/decrement the width and height of the displayed image on each call of updateState().
The image can pulsate in a positive or negative direction. A positive value for stepSize will cause the image to pulsate between the original and a larger image. A
negavite value for stepSize will cause the image to pulsate between the original and a smaller image.
constructor(image, x, y, originalWidth, originalHeight, numberOfSteps, stepSize, intervalTime, centreX = null, centr
{
super(intervalTime); /* as this class extends from GameObject, you must always call super() */
this.currentStep = 0;
}
The updateState() method deals with the cases of the image incrementing (i.e. pulsating between the original and a larger image) or decrementing (i.e. pulsating
between the original and a smaller image). The two sets of code are very similar. The incrementing code is shown below in red and the decrementing code is shown
below in blue.
updateState()
{
if (this.stepSize > 0) // image pulses into a bigger image
{
if (this.incrementing) // incrementing to increase size of original image
{
this.currentStep++;
if (this.currentStep === this.numberOfSteps)
{
this.incrementing = false;
}
}
else // drecrementing to return to original image size
{
this.currentStep--;
if (this.currentStep === 0)
{
this.incrementing = true;
}
}
}
else // image pulses into a smaller image
{
if (!this.incrementing) // drecrementing to decrease size of original image
{
this.currentStep++;
if (this.currentStep === this.numberOfSteps)
{
The render() method draws the image on the canvas. It is straight forward and does not need explaining.
render()
{
ctx.drawImage(this.image, this.x - (this.currentStep * this.stepSize) / 2, this.y - (this.currentStep * this.ste
}
pulsating_image.js
/* If they are needed, then include any game-specific mouse and keyboard listners */
}
Code Explained
Exercises
Write a class to cause an image to rotate about a given centre point, as shown here. If no centre point is given, then the image should rotate about its own centre.
The user should be able to specify a positive or negative rotation stepSize.
Buttons
Buttons are very similar to static text and static images. If we want to display a button, then we can use a gameObject that does not override the GameObject class's
update() method. The Button class can contain text and/or an image.
Button.js
constructor(x, y, width, height, text, backgroundImage = null, fontSize = 50, font = "Times Roman", textColour = "Bl
{
super(null); /* as this class extends from GameObject, you must always call super() */
this.isHovering = false;
render()
{
/* the ImageButton's border */
ctx.beginPath();
ctx.strokeStyle = this.strokeStyle;
ctx.lineWidth = this.borderWidth;
ctx.moveTo(this.x, this.y);
ctx.lineTo(this.x + this.width, this.y);
ctx.lineTo(this.x + this.width, this.y + this.height);
ctx.lineTo(this.x, this.y + this.height);
ctx.lineTo(this.x, this.y);
ctx.stroke();
ctx.fillStyle = this.backgroundColour;
ctx.fillRect(this.x, this.y, this.width, this.height);
ctx.closePath();
pointIsInsideBoundingRectangle(pointX, pointY)
{
if (!this.gameObjectIsDisplayed)
{
this.isHovering = false;
return false;
}
if ((pointX > this.x) && (pointY > this.y))
{
if (pointX > this.x)
{
if ((pointX - this.x) > this.width)
{
this.isHovering = false;
return false; // to the right of this gameObject
}
}
Code Explained
BUTTON_CENTRE is used as a flag that will set the button to be horrizontally centred.
const TEXT_WIDTH is used as a flag to indicate that the Button should be the width of the text that is on the Button.
const TEXT_HEIGHT is used as a flag to indicate that the Button should be the height of the text that is on the Button.
The render() method will draw the button's border, then its image and then its text. If the flag "this.isHovering" is set to true, then the render() method will brighten
the button's image and text.
We detect mouse over and mouse pressing of the button in the main js file, as shown in the example below.
demo_button.js
/* If they are needed, then include any game-specific mouse and keyboard listners */
document.getElementById("gameCanvas").addEventListener("mousedown", function (e)
{
if (e.which === 1) // left mouse button
{
let canvasBoundingRectangle = document.getElementById("gameCanvas").getBoundingClientRect();
let mouseX = e.clientX - canvasBoundingRectangle.left;
let mouseY = e.clientY - canvasBoundingRectangle.top;
if (gameObjects[TEXT_BUTTON].pointIsInsideBoundingRectangle(mouseX, mouseY))
{
gameObjects[MESSAGE] = new StaticText("Text button was pressed", STATIC_TEXT_CENTRE, 490, "Times Roman",
gameObjects[MESSAGE].start();
}
else if (gameObjects[SMALL_TEXT_BUTTON].pointIsInsideBoundingRectangle(mouseX, mouseY))
{
gameObjects[MESSAGE] = new StaticText("Small text button was pressed", STATIC_TEXT_CENTRE, 490, "Times R
gameObjects[MESSAGE].start();
}
else if (gameObjects[IMAGE_BUTTON].pointIsInsideBoundingRectangle(mouseX, mouseY))
{
gameObjects[MESSAGE] = new StaticText("Image button was pressed", STATIC_TEXT_CENTRE, 490, "Times Roman"
gameObjects[MESSAGE].start();
}
else if (gameObjects[TEXT_AND_IMAGE_BUTTON].pointIsInsideBoundingRectangle(mouseX, mouseY))
{
gameObjects[TEXT_BUTTON].pointIsInsideBoundingRectangle(mouseX, mouseY);
gameObjects[SMALL_TEXT_BUTTON].pointIsInsideBoundingRectangle(mouseX, mouseY);
gameObjects[IMAGE_BUTTON].pointIsInsideBoundingRectangle(mouseX, mouseY);
gameObjects[TEXT_AND_IMAGE_BUTTON].pointIsInsideBoundingRectangle(mouseX, mouseY);
}
});
}
Code Explained
If the left mouse button is clicked and the mouse is inside a Button object, then we fire some event, as highlighted in red.
document.getElementById("gameCanvas").addEventListener("mousedown", function (e)
{
if (e.which === 1) // left mouse button
{
let canvasBoundingRectangle = document.getElementById("gameCanvas").getBoundingClientRect();
let mouseX = e.clientX - canvasBoundingRectangle.left;
let mouseY = e.clientY - canvasBoundingRectangle.top;
if (gameObjects[TEXT_BUTTON].pointIsInsideBoundingRectangle(mouseX, mouseY))
{
gameObjects[MESSAGE] = new StaticText("Text button was pressed", STATIC_TEXT_CENTRE, 490, "Times Roman",
gameObjects[MESSAGE].start();
}
else if (gameObjects[SMALL_TEXT_BUTTON].pointIsInsideBoundingRectangle(mouseX, mouseY))
{
gameObjects[MESSAGE] = new StaticText("Small text button was pressed", STATIC_TEXT_CENTRE, 490, "Times R
gameObjects[MESSAGE].start();
}
else if (gameObjects[IMAGE_BUTTON].pointIsInsideBoundingRectangle(mouseX, mouseY))
{
gameObjects[MESSAGE] = new StaticText("Image button was pressed", STATIC_TEXT_CENTRE, 490, "Times Roman"
gameObjects[MESSAGE].start();
}
else if (gameObjects[TEXT_AND_IMAGE_BUTTON].pointIsInsideBoundingRectangle(mouseX, mouseY))
{
gameObjects[MESSAGE] = new StaticText("Text and image button was pressed", STATIC_TEXT_CENTRE, 490, "Tim
gameObjects[MESSAGE].start();
}
}
});
If the mouse hovers over a Button object and no mouse button has been pressed, then the Button object is highlighted by changing its colour. The colour change
happens inside the Button object. Whenever the Button object returns true, it sets its "this.isHovering" flag to true, which in turn causes the Button's render()
method to change the Button object's colour. This is highlighted in red in the code below.
document.getElementById("gameCanvas").addEventListener("mousemove", function (e)
{
if (e.which === 0) // no button selected
{
let canvasBoundingRectangle = document.getElementById("gameCanvas").getBoundingClientRect();
let mouseX = e.clientX - canvasBoundingRectangle.left;
let mouseY = e.clientY - canvasBoundingRectangle.top;
gameObjects[TEXT_BUTTON].pointIsInsideBoundingRectangle(mouseX, mouseY);
gameObjects[SMALL_TEXT_BUTTON].pointIsInsideBoundingRectangle(mouseX, mouseY);
gameObjects[IMAGE_BUTTON].pointIsInsideBoundingRectangle(mouseX, mouseY);
gameObjects[TEXT_AND_IMAGE_BUTTON].pointIsInsideBoundingRectangle(mouseX, mouseY);
}
});
External Inputs
It is possible to have interaction between the game canvas and external HTML elements, as shown here.
StaticImageWithInputs.js
render()
{
ctx.drawImage(this.image, this.x, this.y, this.width, this.height);
}
setX(newX)
{
this.x = newX;
}
setY(newY)
{
this.y = newY;
}
setWidth(newWidth)
{
this.width = newWidth;
}
setHeight(newHeight)
{
this.height = newHeight;
}
}
Code Explained
The class has four setter methods, one for each of x, y, width and height, as highlighted in the code above.
setX(newX)
{
this.x = newX;
}
setY(newY)
{
this.y = newY;
}
setWidth(newWidth)
{
this.width = newWidth;
}
setHeight(newHeight)
{
this.height = newHeight;
}
static_image_with_inputs.html
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>GENIUS worked example</title>
<link rel="shortcut icon" type="image/png" href="images/genius_icon.png"/>
<!-- 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>
<!-- Always include the game javascript that matches the html file name -->
<script src="js/static_image_with_inputs.js" type="text/javascript"></script>
<!-- Include any classes that extend from GameObject that are used in this game -->
<script src="js/StaticImageWithInputs.js" type="text/javascript"></script>
<body>
<div id="gameContainer"> <!-- having a container will allow us to have a game that includes elements that are ou
<canvas id="gameCanvas" tabindex="1"></canvas>
<br>
<label for="x">X: </label><input type="range" min="0" max="490" value="125" id="x"><br>
<label for="y">Y: </label><input type="range" min="0" max="490" value="125" id="y"><br>
<label for="width">Width: </label><input type="range" min="10" max="500" value="250" id="width"><br>
<label for="height">Height: </label><input type="range" min="10" max="500" value="250" id="height">
</div>
</body>
</html>
Code Explained
The various html input elements that accept the user input are all included inside the "gameContainer" div. This way, we can keep the game code separate to any
other code that a developer might include on a webpage.
<!-- ***************************************************************************** -->
<!-- *************** THE CODE BELOW CAN BE DIFFERENT FOR EACH GAME *************** -->
<br>
<label for="x">X: </label><input type="range" min="0" max="490" value="125" id="x"><br>
<label for="y">Y: </label><input type="range" min="0" max="490" value="125" id="y"><br>
<label for="width">Width: </label><input type="range" min="10" max="500" value="250" id="width"><br>
<label for="height">Height: </label><input type="range" min="10" max="500" value="250" id="height">
Note that the "onchange" functionality for the html input elements is not included in the HTML file. Instead, it will be included in the game's
static_image_with_inputs.js file.
static_image_with_inputs.js
/* If they are needed, then include any game-specific mouse and keyboard listners */
/* set the initial x,y, width and height of the image */
gameObjects[0].setX(document.getElementById("x").value);
gameObjects[0].setY(document.getElementById("y").value);
gameObjects[0].setWidth(document.getElementById("width").value);
gameObjects[0].setHeight(document.getElementById("height").value);
document.getElementById("x").addEventListener("change", function ()
{
gameObjects[0].setX(document.getElementById("x").value);
});
document.getElementById("y").addEventListener("change", function ()
{
gameObjects[0].setY(document.getElementById("y").value);
});
document.getElementById("width").addEventListener("change", function ()
{
gameObjects[0].setWidth(document.getElementById("width").value);
});
document.getElementById("height").addEventListener("change", function ()
{
gameObjects[0].setHeight(document.getElementById("height").value);
});
}
Code Explained
The image's original position is set using the data from the four HTML input elements.
gameObjects[0].setX(document.getElementById("x").value);
gameObjects[0].setY(document.getElementById("y").value);
gameObjects[0].setWidth(document.getElementById("width").value);
gameObjects[0].setHeight(document.getElementById("height").value);
document.getElementById("y").addEventListener("change", function ()
{
gameObjects[0].setY(document.getElementById("y").value);
});
document.getElementById("width").addEventListener("change", function ()
{
gameObjects[0].setWidth(document.getElementById("width").value);
});
document.getElementById("height").addEventListener("change", function ()
{
gameObjects[0].setHeight(document.getElementById("height").value);
});
Exercises
Write code to allow the user to adjust the size of multiple blocks, as shown here.
Write code to generate a piece of block art, as shown here. Note that the user should be able to specify how many blocks they would like to have.
Write code to adjust the brightness of the red, green and blue components of an image, as shown here.
Moving Images
A moving image is similar to a static image in that it will have a position, a width and a height. Unlike a static image, a moving image will continuously update its
position, as shown here. The code for this example is shown below.
Car.js
updateState()
{
this.x++;
if (this.x > canvas.width)
{
this.x = -this.width;
}
}
render()
{
ctx.drawImage(this.image, this.x, this.y, this.width, this.height);
}
}
Code Explained
The updateState() method increments the x coordinate of the car until it moves off the right-side of the canvas. It then reappears at "-this.width" on the original side
of the screen. Without this, the car would sudenly reappear at x coordinate 0, which does not look smooth.
updateState()
{
this.x++;
if (this.x > canvas.width)
{
this.x = -this.width;
}
}
Along with the Car class, we need to make some changes to the moving_image.html and moving_image.js files. All other files remain unchanged.
The majority of the code in moving_image.html and static_image.js remain unchanged from previous examples. The changes are highlighted in the code below.
moving_image.html
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>GENIUS worked example</title>
<link rel="shortcut icon" type="image/png" href="images/genius_icon.png"/>
<!-- 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"/>
<!-- Always include the game javascript that matches the html file name -->
<script src="js/moving_image.js" type="text/javascript"></script>
<!-- Include any classes that extend from GameObject that are used in this game -->
<script src="js/StaticImage.js" type="text/javascript"></script>
<script src="js/Car.js" type="text/javascript"></script>
<body>
<div id="gameContainer"> <!-- having a container will allow us to have a game that includes elements that are ou
<canvas id="gameCanvas" tabindex="1"></canvas>
</div>
</body>
</html>
In this case, we have two different GameObject sub-classes: StaticImage and Car. Therefore, we need to include a link to both files
<!-- Include any classes that extend from GameObject that are used in this game -->
<script src="js/StaticImage.js" type="text/javascript"></script>
<script src="js/Car.js" type="text/javascript"></script>
moving_image.js
const MAP = 0;
const CAR = 1;
/* If they are needed, then include any game-specific mouse and keyboard listners */
}
Code Explained
If there is more than one type of gameObject, it can be useful to assign constants to the various gameObjects. The red highlighted code above shows how to do this.
Exercises
Write code to allow a user to stop and restart a car image. The user should also be able to adjust the speed that the car travels, as shown here.
Moving Text
Text can be scrolled, scaled, rotated or animated in various other ways.
ScrollingText.js
updateState()
{
this.x--;
if (this.x <= this.endX)
{
this.stop();
}
}
render()
{
ctx.fillStyle = this.textColour;
ctx.font = this.fontSize + "px Times Roman";
ctx.fillText(this.text, this.x, this.y);
}
}
Code Explained
The updateState() method scrolls the text until it reaches it endX location.
updateState()
{
this.x--;
if (this.x <= this.endX)
{
this.stop();
}
}
scrolling_text.js
/* If they are needed, then include any game-specific mouse and keyboard listners */
}
Code Explained
scrolling_background_image.js
/* If they are needed, then include any game-specific mouse and keyboard listners */
}
ScrollingBackgroundImage.js
constructor(image, updateStateMilliseconds)
this.x = 0;
}
updateState()
{
this.x--;
if (this.x <= -canvas.width)
{
this.x = 0;
}
}
render()
{
ctx.drawImage(this.image, this.x, 0, canvas.width, canvas.height);
ctx.drawImage(this.image, this.x + canvas.width, 0, canvas.width, canvas.height);
}
}
Code Explained
In updateState(), x is continuously decremented until the first image is fully off the canvas. At this point, x is reset to 0, as shown below.
updateState()
{
this.x--;
if (this.x <= -canvas.width)
{
this.x = 0;
}
}
scrolling_parallax_images.js
/* If they are needed, then include any game-specific mouse and keyboard listners */
}
Code Explained
Animated Images
Animated images (gameObject images) are images that contain multiple smaller images, which can be displayed in sequence to produce an animation. An example
of an animated image is shown below:
Explosion
updateState()
{
if (this.isFirstCallOfUpdateState)
{
this.explosionSound.currentTime = 0;
this.explosionSound.play();
this.isFirstCallOfUpdateState = false;
}
this.column++;
if (this.column >= this.NUMBER_OF_COLUMNS_IN_SPRITE_IMAGE)
{
this.column = 0;
this.row++;
}
}
render()
{
ctx.drawImage(this.explosionImage, this.column * this.SPRITE_WIDTH, this.row * this.SPRITE_WIDTH, this.SPRITE_WI
}
}
Code Explained
In order to animate a gameObject, we need to be able step through each sub-image in turn. We initialise the gameObject image based on the number of columns
and rows that it contains, as shown in the code below:
this.NUMBER_OF_SPRITES = 74; // the number of gameObjects in the gameObject image
this.NUMBER_OF_COLUMNS_IN_SPRITE_IMAGE = 9; // the number of columns in the gameObject image
this.NUMBER_OF_ROWS_IN_SPRITE_IMAGE = 9; // the number of rows in the gameObject image
this.START_ROW = 0;
this.START_COLUMN = 0;
We step through the various sub-images in turn, as shown in red in the code below. After all of the sub-images have been displayed, we kill the Explosion object,
so that it no longer displays on the canvas.
updateState()
{
if (this.isFirstCallOfUpdateState)
{
this.explosionSound.currentTime = 0;
this.explosionSound.play();
this.isFirstCallOfUpdateState = false;
}
this.column++;
if (this.column >= this.NUMBER_OF_COLUMNS_IN_SPRITE_IMAGE)
{
this.column = 0;
this.row++;
}
}
this.column++;
if (this.column >= this.NUMBER_OF_COLUMNS_IN_SPRITE_IMAGE)
{
this.column = 0;
this.row++;
}
}
Exercises
Write code to show three animated birds flying across a scrolling background, as shown here. The bird image is:
Mouse Events
It is possible to detect mouse and keyboard events for a canvas (or any other HTML element). There are seven mouse events:
click
The event occurs when the mouse is clicked on the canvas.
dblclick
The event occurs when the mouse is double-clicked on the canvas.
mousemove
The event occurs when the mouse is moved while it is over the canvas.
mouseover
The event occurs when the mouse is moved onto the canvas.
mouseout
The event occurs when the mouse is moved out of the canvas.
mousedown
The event occurs when any mouse button is pressed over the canvas.
mouseup
The event occurs when any mouse button is released over the canvas.
We need to associate the event function with the canvas. This is done by adding an event listener to the canvas.
canvas.addEventListener('click', function(e){});
The variable, 'e' is provided by the system. It provides access to the x and y location of the screen pixel that was clicked. To get the canvas pixel location, we need to
subtract the canvas top left corner x and y value from the screen x and y. The example below gets the correct canvas x and y location.
Example showing how to get the canvas x and y position (Run Example).
Note that this example does not need to use the framework classes, as nothing is being drawn on the canvas!
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>GENIUS worked example</title>
<link rel="shortcut icon" type="image/png" href="images/genius_icon.png"/>
<link href="css/game.css" rel="stylesheet" type="text/css"/>
<style>
#gameCanvas
{
margin-top:100px;
margin-left: 100px;
}
</style>
<script>
let mouseX = 0;
let mouseY = 0;
window.onload = function ()
{
let canvas = document.getElementById("gameCanvas");
canvas.width = canvas.clientWidth;
canvas.height = canvas.clientHeight;
let ctx = canvas.getContext("2d");
Code Explained
The canvas is moved away from the document top-left corner for this example, so as to show that our code can detect the mouse coordinates inside the canvas.
<style>
#gameCanvas
{
margin-top:100px;
margin-left: 100px;
}
</style>
If the left mouse button has been clicked, then e.which will hold the value 1.
canvas.addEventListener('click', function (e)
{
if (e.which === 1)
{
}
}
The mouse click returns the screen x and y values rather than the canvas values. The highlighted code below converts from screen to canvas coordinates.
canvas.addEventListener('click', function (e)
{
if (e.which === 1)
{
var canvasBoundingRectangle = canvas.getBoundingClientRect();
mouseX = e.clientX - canvasBoundingRectangle.left;
mouseY = e.clientY - canvasBoundingRectangle.top;
event_mouse_click.js
gameObjects[0].setX(mouseX);
gameObjects[0].setY(mouseY);
});
}
Code Explained
In the file "event_mouse_click.js", whenever the user clicks the mouse, we convert it from screen coordinates to canvas coordinates (shown in red). We then pass
the canvas coordinates to the the gameObject (shown in blue).
/* add event listners for input changes */
document.addEventListener("click", function (e)
{
let canvasBoundingRectangle = document.getElementById("gameCanvas").getBoundingClientRect();
let mouseX = e.clientX - canvasBoundingRectangle.left;
let mouseY = e.clientY - canvasBoundingRectangle.top;
gameObjects[0].setX(mouseX);
gameObjects[0].setY(mouseY);
});
In the file "EventMouseClick.js", we use two setter methods to set the position of the gameObject whenever the user clicks the mouse. In the code below, this.x
and this.y are the top-left corner of the gameObject.
constructor()
{
super(null); /* as this class extends from GameObject, you must always call super() */
render()
{
ctx.fillStyle = 'black';
ctx.fillRect(this.x, this.y, this.width, this.height);
}
setX(newCentreX)
{
this.x = newCentreX - (this.width / 2);
}
setY(newCentreY)
{
this.y = newCentreY - (this.height / 2);
}
}
Code Explained
The black rectangle has its centre set to be the new x and y positions, as shown below.
setX(newCentreX)
{
this.x = newCentreX - (this.width / 2);
}
setY(newCentreY)
{
this.y = newCentreY - (this.height / 2);
}
Exercises
Write code to draw an image in a new location whenever the user clicks on the canvas, as shown in this link.
Dragging An Image
Example that detects if the mouse is inside an image and takes the x and y offsets into account when dragging an image (Run Example)
drag_image.js
if (gameObjects[0].pointIsInsideBoundingRectangle(mouseX, mouseY))
{
gameObjects[0].setOffsetX(mouseX);
gameObjects[0].setOffsetY(mouseY);
}
}
});
if (gameObjects[0].pointIsInsideBoundingRectangle(mouseX, mouseY))
{
gameObjects[0].setX(mouseX);
gameObjects[0].setY(mouseY);
}
}
});
}
Code Explained
The mousedown event handler function is used to initialise the offset of the mouse from the top-left corner of the image. This is needed to ensure that the dragging
of the image is smooth. Without this, the image top-left corner would jump to the current mouse position when we start to drag an image.
DragImage.js
constructor(image)
{
super(null); /* as this class extends from GameObject, you must always call super() */
render()
{
ctx.drawImage(this.image, this.x, this.y, this.width, this.height);
ctx.strokeStyle = 'black';
ctx.strokeRect(this.x - 1, this.y - 1, this.width + 2, this.height + 2);
}
setX(newMouseX)
{
this.x = newMouseX - this.offsetX;
}
setY(newMouseY)
{
this.y = newMouseY - this.offsetY;
}
setOffsetX(newMouseX)
{
this.offsetX = newMouseX - this.x;
}
setOffsetY(newMouseY)
{
this.offsetY = newMouseY - this.y;
}
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
}
}
Code Explained
The code below will detect if the location (x, y) is positioned inside an image.
DragImage.js
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
}
}
The offset needs to be calculated when the mouse is pressed down on an image. The code below will calculate the offsetX and offsetY of the mouse within an
image.
drag_image.js
if (gameObjects[0].pointIsInsideBoundingRectangle(mouseX, mouseY))
{
gameObjects[0].setOffsetX(mouseX);
gameObjects[0].setOffsetY(mouseY);
}
}
});
Whenever an image changes position, the offset needs to be taken into account when calculating the new image top-left x and y positions. This is done by
subtracting the offset values that were calculated in the mousedownHandler(e) code above.
setOffsetX(newMouseX)
{
this.offsetX = newMouseX - this.x;
}
setOffsetY(newMouseY)
{
this.offsetY = newMouseY - this.y;
}
Exercises
Write code to make a drawing tool that is similar to the one shown at this link. Hint: Use two gameObjects, one each for the image and the scribble and use an
offscreen canvas for the Scribble
// Firefox
document.getElementById('gameCanvas').addEventListener("DOMMouseScroll", mouseWheelHandler, false);
The function is provided with a system variable, e. This variable e.wheelDelta contains information relating to the mouse wheel. This value increments/decrements
in units of 120. Therefore, we need to devide it by 120 to get a unit increment/decrement value.
function mouseWheelHandler(e)
{
unitChange = e.wheelDelta / 120; // unitChange will be equal to either +1 or -1
Example using the mouse wheel to scale an image (Run Example). In this example, the image will only scale if the mouse is over the image.
scale_image.js
function mouseWheelHandler(e)
{
let canvasBoundingRectangle = document.getElementById("gameCanvas").getBoundingClientRect();
let mouseX = e.clientX - canvasBoundingRectangle.left;
let mouseY = e.clientY - canvasBoundingRectangle.top;
if (gameObjects[0].pointIsInsideBoundingRectangle(mouseX, mouseY))
{
unitChange = e.wheelDelta / 120;
gameObjects[0].changeWidthAndHeight(unitChange);
}
}
}
ScaleImage.js
constructor(image)
{
super(null); /* as this class extends from GameObject, you must always call super() */
/* These variables depend on the object */
this.image = image;
this.x = 200;
this.y = 200;
this.width = 100;
this.height = 100;
}
render()
{
ctx.drawImage(this.image, this.x, this.y, this.width, this.height);
ctx.strokeStyle = 'black';
ctx.strokeRect(this.x - 1, this.y - 1, this.width + 2, this.height + 2);
}
/* Scaling is about the centre of the image, so adust the x and y position to match new size */
this.x -= sizeChange / 2;
this.y -= sizeChange / 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
}
Code Explained
The image has its width and height changed by the amount sizeChange. The image's x and y coordinates are adjusted to ensure that the scaling happens about the
centre of the image.
changeWidthAndHeight(sizeChange) // note that stepSize will be negative for scaling down
{
this.width += sizeChange;
this.height += sizeChange;
/* Scaling is about the centre of the image, so adust the x and y position to match new size */
this.x -= sizeChange / 2;
this.y -= sizeChange / 2;
}
The pointIsInsideBoundingRectangle() method ensures that scaling only occurs if the mouse if inside the image when the user attempts to scale.
pointIsInsideBoundingRectangle(pointX, pointY)
{
...
}
Exercises
Write code to move, drag and scale an image, as shown in this link.
Keyboard Events
There are three keyboard events:
onkeydown
The event occurs when as soon as any key has been pressed.
onkeypress
The event occurs when any key is being pressed.
onkeyup
The event occurs when a key is being released.
As with the mouse event, we need to insert two pieces of code in order to use a keyboard event: a function containing the action and an event listener. Unlike
the mouse event, the canvas cannot detect keyboard events. Therefore, we tie the keyboard event to the html document.
document.addEventListener("keydown", function(e){});
When using keyboard events, we need to be able to detect which key has been pressed. The code below will test if the left arrow has been pressed.
In the example above, the variable, 'e' is provided by the system. It provides access to the keyCode of the key that has been pressed. The complete set of
keyCodes can be found at this link.
The example below allows the user to move an image around the canvas using the four arrow keys.
key_down.js
/************** Declare data and functions that are needed for all games ************/
/*********** END OF Declare data and functions that are needed for all games *********/
Code Explained
document.addEventListener("keydown", function (e)
{
var stepSize = 10;
The keydown event checks against the four arrow keys. It changes the x or y position of the image depending on which key was pressed.
constructor(image)
{
super(null); /* as this class extends from GameObject, you must always call super() */
render()
{
ctx.drawImage(this.image, this.x, this.y, this.width, this.height);
ctx.strokeStyle = 'black';
ctx.strokeRect(this.x - 1, this.y - 1, this.width + 2, this.height + 2);
}
changeX(changeSize)
{
this.x += changeSize;
}
changeY(changeSize)
{
this.y += changeSize;
}
}
Code Explained
changeX(changeSize)
{
this.x += changeSize;
}
changeY(changeSize)
{
this.y += changeSize;
}
Exercises
Amend the above code to stop the image moving off the canvas.
Write code to animate a gameObject character walking, as shown here. The gameObject image is below:
Play game
fireball_game.js
const BACKGROUND = 0;
const WIN_LOSE_MESSAGE = 1;
/* If they are needed, then include any game-specific mouse and keyboard listners */
document.addEventListener("keydown", function (e)
{
var stepSize = 10;
Code Explained
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
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()));
render()
{
super.render();
bat.render();
target.render();
for (let i = 0; i < fireballs.length; i++)
{
fireballs[i].render();
}
}
}
Code Explained
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()));
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
constructor(image, centreX)
{
super(5); /* as this class extends from GameObject, you must always call super() */
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;
}
}
}
getCentreX()
{
return this.centreX;
}
getCentreY()
{
return this.centreY;
}
}
Code Explained
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);
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
constructor(x, y, width)
{
super(null); /* as this class extends from GameObject, you must always call super() */
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
}
}
Code Explained
/* 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
constructor(image, x, y, width)
{
super(null); /* as this class extends from GameObject, you must always call super() */
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;
}
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
}
}
Code Explained
Play game
maze_game.js
/* If they are needed, then include any game-specific mouse and keyboard listners */
document.addEventListener('keydown', function (e)
{
if (e.keyCode === 37) // left
{
gameObjects[SKELETON].setDirection(LEFT);
}
else if (e.keyCode === 38) // up
{
gameObjects[SKELETON].setDirection(UP);
}
else if (e.keyCode === 39) // right
{
gameObjects[SKELETON].setDirection(RIGHT);
}
else if (e.keyCode === 40) // down
{
gameObjects[SKELETON].setDirection(DOWN);
}
});
}
Code Explained
Because we have collision detection, we need to have our own CanvasGame class.
let game = new MazeSkeletonCanvasGame(mazeGrid);
The keyboard is used to control the direction that the skeleton travels. The skeleton will continue to travel in the same direction until another arrow key is hit.
document.addEventListener('keydown', function (e)
{
if (e.keyCode === 37) // left
{
gameObjects[SKELETON].setDirection(LEFT);
}
else if (e.keyCode === 38) // up
{
MazeCanvasGame.js
collisionDetection()
{
if (!this.mazeCtx)
{
return;
}
if (gameObjects[SKELETON].getDirection() === UP)
{
let imageData = this.mazeCtx.getImageData(gameObjects[SKELETON].getCentreX(), gameObjects[SKELETON].getCentr
if (imageData.data[3] !== 0)
{
gameObjects[SKELETON].setDirection(DOWN);
}
}
else if (gameObjects[SKELETON].getDirection() === LEFT)
{
let imageData = this.mazeCtx.getImageData(gameObjects[SKELETON].getCentreX() - 15, gameObjects[SKELETON].get
if (imageData.data[3] !== 0)
{
gameObjects[SKELETON].setDirection(RIGHT);
}
}
else if (gameObjects[SKELETON].getDirection() === DOWN)
{
let imageData = this.mazeCtx.getImageData(gameObjects[SKELETON].getCentreX(), gameObjects[SKELETON].getCentr
if (imageData.data[3] !== 0)
{
gameObjects[SKELETON].setDirection(UP);
}
Code Explained
Set up an offscreen canvas to hold the grid image that is used for collision detection.
/* this.mazeCtx will be used for collision detection */
let mazeOffscreenCanvas = document.createElement('canvas');
this.mazeCtx = mazeOffscreenCanvas.getContext('2d');
mazeOffscreenCanvas.width = canvas.width;
mazeOffscreenCanvas.height = canvas.height;
this.mazeCtx.drawImage(mazeGridImage, 0, 0, canvas.width, canvas.height);
Do collision detection for the four mid-side points. For example, if we are detecting a collision as the skeleton moves up the screen, then we need to use the
skeleton's (centreX, centreY - 20).
The reason why the '20' is a different number for UP, DOWN, LEFT and RIGHT is to allow for the differences of the gameObject size when it is displayed moving
in different directions.
Stop all gameObjects from animating and display the win message.
/* 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 */
MazeSkeleton.js
this.centreX = centreX; /* set the start position of the skeleton in the maze */
this.centreY = centreY;
Play game
monster_game.js
/* 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);
}
});
Code Explained
The arrow keys are used to control the direction that the monster moves.
The monster stops moving when the arrow keys are released.
document.addEventListener('keyup', function (e)
{
gameObjects[MONSTER].setDirection(STOPPED);
});
MonsterCanvasGame.js
this.screenShakeInterval = null;
this.screenIsRotatingToTheLeft = false;
this.NUMBER_OF_SCREEN_SHAKES_INTERATIONS = 10;
this.numberOfScreenShakes = 0;
}
collisionDetection()
{
if (!this.monsterObstalesCtx)
{
return;
}
}
else if (gameObjects[BULLSEYE].pointIsInsideBullseyeRectangle(gameObjects[MONSTER].getX() + gameObjects[MONSTER]
{
/* 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;
}
}
}
Code Explained
Monster.js
constructor(monsterImage)
{
super(5); /* as this class extends from GameObject, you must always call super() */
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;
}
setX(newX)
{
this.x = newX;
}
setY(newY)
{
this.y = newY;
}
getWidth()
{
return this.width;
}
getHeight()
{
return this.height;
}
}
Code Explained
Bullseye.js
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 */
Play game
jigsaw_game.js
const BACKGROUND = 0;
const ANCHOR_PIECE = 1;
const BUTTON = 2;
const MESSAGE = 3;
/* Hide the "Continue" button and win message until they are needed */
gameObjects[BUTTON].stopAndHide();
gameObjects[MESSAGE].stopAndHide();
/* If they are needed, then include any game-specific mouse and keyboard listners */
document.getElementById("gameCanvas").addEventListener("mousedown", function (e)
{
if (e.which === 1) // left mouse button
{
let canvasBoundingRectangle = document.getElementById("gameCanvas").getBoundingClientRect();
let mouseX = e.clientX - canvasBoundingRectangle.left;
let mouseY = e.clientY - canvasBoundingRectangle.top;
if (gameObjects[BUTTON].pointIsInsideBoundingRectangle(mouseX, mouseY))
{
gameObjects[BUTTON].stopAndHide();
game.createNewWordJigsaw();
}
for (let i = pieces.length - 1; i >= 0; i--)
{
if (pieces[i].pointIsInsideBoundingRectangle(mouseX, mouseY))
{
pieces[i].setOffsetX(mouseX);
pieces[i].setOffsetY(mouseY);
selectedPiece = i;
break;
}
}
}
});
There are six different jigsaw colours. Each colour has its own image file.
let jigsawPiece = new Image();
redPiece.src = "images/jigsaw_piece.png";
The ANCHOR_PIECE is the jigsaw piece that that is placed at the start position of where the jigsaw word is placed.
const BACKGROUND = 0;
const ANCHOR_PIECE = 1;
const BUTTON = 2;
const MESSAGE = 3;
JIGSAW_Y is the vertical position where the jigsaw word will be placed.
JIGSAW_PIECE_SIZE is the width and height of each jigsaw piece.
const JIGSAW_Y = 200; // the y-position of the jigsaw on the canvas
const JIGSAW_PIECE_SIZE = 100; // width and height of each jigsaw piece
JigsawCanvasGame takes in an array of words. The game will allow the user to complete one word jigsaw at a time. Once all of the jigsaw have been completed,
the player wins the game.
let game = new JigsawCanvasGame(["one", "two"], , jigsawPieceImage, JIGSAW_PIECE_SIZE, JIGSAW_Y);
After each word is completed, the user will be presented with a "Continue" BUTTON. When the user clicks the "Continue" BUTTON, the next jigsaw word will be
created, as highlighted in red.
Whenever the user selects a jigsaw piece, then selectedPiece is set to hold the number of that jigsaw piece, as shown in blue.
/* If they are needed, then include any game-specific mouse and keyboard listners */
document.getElementById("gameCanvas").addEventListener("mousedown", function (e)
{
if (e.which === 1) // left mouse button
{
let canvasBoundingRectangle = document.getElementById("gameCanvas").getBoundingClientRect();
let mouseX = e.clientX - canvasBoundingRectangle.left;
let mouseY = e.clientY - canvasBoundingRectangle.top;
if (gameObjects[BUTTON].pointIsInsideBoundingRectangle(mouseX, mouseY))
{
gameObjects[BUTTON].stopAndHide();
game.createNewWordJigsaw();
}
for (let i = pieces.length - 1; i >= 0; i--)
{
if (pieces[i].pointIsInsideBoundingRectangle(mouseX, mouseY))
{
pieces[i].setOffsetX(mouseX);
pieces[i].setOffsetY(mouseY);
selectedPiece = i;
break;
}
}
}
});
As the mouse is moved, the selected jigsaw piece's x and y positions are updated, as shown in red.
If no mouse button is selected (e.which === 0) and the mouse hovers over the "Continue" BUTTON, then the button will change colour. The changing of its colour
takes place inside the Button object. However, we need to call gameObjects[BUTTON].pointIsInsideBoundingRectangle(mouseX, mouseY) to fire the colour
change event.
document.getElementById("gameCanvas").addEventListener("mousemove", function (e)
{
if (e.which === 1) // left mouse button
{
let canvasBoundingRectangle = document.getElementById("gameCanvas").getBoundingClientRect();
let mouseX = e.clientX - canvasBoundingRectangle.left;
let mouseY = e.clientY - canvasBoundingRectangle.top;
JigsawCanvasGame.js
let pieces = []; /* These two variables are used in both this class and in the JigsawPiece class */
let currentPiece = 0; /* Therefore, they need to be global to both classes. */
this.wordList = wordList;
this.jigsawPieceImage = jigsawPieceImage;
this.pieceSize = pieceSize;
this.wordY = wordY;
this.currentWord = 0;
this.createNewWordJigsaw();
}
createNewWordJigsaw()
{
currentPiece = 0;
let colours = [[255, 0, 0], [0, 255, 0], [0, 0, 255], [255, 0, 255], [255,255,0], [255, 100, 0]];
let currentColour = -1; // the fixed jigsaw start piece is not contained in the colours array
let newColour = null; // newColour is randomly assigned below
render()
{
super.render();
for (let i = 0; i < pieces.length; i++)
{
pieces[i].render();
}
}
collisionDetection()
{
if ((pieces.length === 0) || (this.wordList.length === 0))
{
return;
}
Code Explained
The pieces[] array holds the various letter jigsaw pieces that make up the current word.
currentPiece is the piece that is currently able to be anchored to build up the jigsaw word. The player can move any piece, but only the currentPiece can be clicked
into place in the jigsaw word. Once a piece has been clicked into place, it can no longer be moved.
selectedPiece is the currently selected piece.
let pieces = [];
let currentPiece = 0;
this.wordList = wordList;
this.jigsawPieceImage = jigsawPieceImage;
this.pieceSize = pieceSize;
this.wordY = wordY;
this.currentWord = 0;
this.createNewWordJigsaw();
}
The createNewWordJigsaw() method creates the pieces that are needed for the currentWord in the jigsaw's list of words. This method uses a for loop to fill the
pieces[] array with the individual jigsaw pieces that contain each of the letters in a jigsaw word.
A jigsaw piece can be any of the colours in the colours[] array.
The do...while loop is used to assign a colour to each jigsaw piece. The do...while loop is used to ensure that no two piece beside each other have the same colour.
createNewWordJigsaw()
{
currentPiece = 0;
let colours = [[255, 0, 0], [0, 255, 0], [0, 0, 255], [255, 0, 255], [255,255,0], [255, 100, 0]];
let currentColour = -1; // the fixed jigsaw start piece is not contained in the colours array
let newColour = null; // newColour is randomly assigned below
The jigsaw pieces are not contained in the gameObjects[] array. Instead, they are contained in an array called pieces[]. Therefore, we must override the render()
method, so that the jigsaw pieces (in the pieces[] array) can be drawn.
render()
{
super.render();
for (let i = 0; i < pieces.length; i++)
{
pieces[i].render();
}
}
JigsawPiece.js
constructor(jigsawPieceImage, colour, letter, id, size, granulatity, startX, startY, finalX, finalY)
{
super(null); /* as this class extends from GameObject, you must always call super() */
this.finalX = finalX; // this is the position where the jigsaw piece needs to end up at
this.finalY = finalY;
this.granulatity = granulatity; // the +/- resolution of the accuracy of where the piece needs to end up
this.isLocked = false; // set to true when the piece is at its final place
this.jigsawCanvas = document.createElement('canvas');
this.jigsawCanvasCtx = this.jigsawCanvas.getContext("2d");
this.jigsawCanvas.width = jigsawPieceImage.width;
this.jigsawCanvas.height = jigsawPieceImage.height;
this.jigsawCanvasCtx.drawImage(jigsawPieceImage, 0, 0, jigsawPieceImage.width, jigsawPieceImage.height); /* As a
this.jigsawCanvasCtx.strokeStyle = "black";
this.jigsawCanvasCtx.font = this.height * 0.6 + "px Arial"; // scale the font to match the size of the jigsaw p
this.jigsawCanvasCtx.fillText(this.letter, this.height * 0.35, this.height * 0.70); // position the letter in
}
isPieceAtFinalPosition()
{
if (this.id !== currentPiece)
{
return false;
}
if (this.isLocked)
{
return false;
}
if ((this.x > this.finalX - this.granulatity) &&
(this.x < this.finalX + this.granulatity) &&
(this.y > this.finalY - this.granulatity) &&
(this.y < this.finalY + this.granulatity))
{
this.x = this.finalX;
this.y = this.finalY;
this.isLocked = true;
return true;
}
return false;
}
setX(newMouseX)
{
if (this.isLocked)
{
return;
}
if (!this.isPieceAtFinalPosition())
{
this.x = newMouseX - this.offsetX;
}
}
setY(newMouseY)
{
if (this.isLocked)
{
return;
}
this.y = newMouseY - this.offsetY;
}
setOffsetX(newMouseX)
{
if (this.isLocked)
{
return;
}
this.offsetX = newMouseX - this.x;
}
setOffsetY(newMouseY)
{
if (this.isLocked)
{
return;
}
this.offsetY = newMouseY - this.y;
}
pointIsInsideBoundingRectangle(pointX, pointY)
{
if (this.isLocked)
{
return;
}
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
Code Explained
An offscreen canvas is used to hold the jigsaw piece. The jigsaw piece colour is determined by the colour that is passed into the constructor. The offscreen buffer
jigsaw piece is filled with 'colour'. The jigsaw piece will also used for tansparancy testing when the user tries to click the mouse on the jigsaw piece.
Each jigsaw piece has various data, such as width and startX associated with it. The id of the piece (this.id) is its position within the word array. The id is used to
give the order that the letters need to be put together in order to create the jigsaw word.
The granularity of the jigsaw piece (this.granularity) allows for the piece to be detected as hitting the target pixel when it is close to the target. A higher granularity
will mean that the piece is more easily placed at its target. The granularity is the number of pixels around the final x,y position of the jigsaw piece that the user
needs to place the jigsaw piece.
Once a jigsaw piece has been locked in place, the this.isLocked flag is set. This will be used to stop the jigsaw piece from being moved again.
constructor(jigsawPieceImage, colour, letter, id, size, granulatity, startX, startY, finalX, finalY)
{
super(null); /* as this class extends from GameObject, you must always call super() */
this.finalX = finalX; // this is the position where the jigsaw piece needs to end up at
this.finalY = finalY;
this.granulatity = granulatity; // the +/- resolution of the accuracy of where the piece needs to end up
this.isLocked = false; // set to true when the piece is at its final place
this.jigsawCanvas = document.createElement('canvas');
this.jigsawCanvasCtx = this.jigsawCanvas.getContext("2d");
this.jigsawCanvas.width = jigsawPieceImage.width;
this.jigsawCanvas.height = jigsawPieceImage.height;
this.jigsawCanvasCtx.drawImage(jigsawPieceImage, 0, 0, jigsawPieceImage.width, jigsawPieceImage.height); /* As a
this.jigsawCanvasCtx.strokeStyle = "black";
this.jigsawCanvasCtx.font = this.height * 0.6 + "px Arial"; // scale the font to match the size of the jigsaw p
this.jigsawCanvasCtx.fillText(this.letter, this.height * 0.35, this.height * 0.70); // position the letter in
}
The isPieceAtFinalPosition() method test if the current x and y are within the granulatity of the final x and y positions, as shown in red.
If the jigsaw piece is within the granularity, then the jigsaw piece is set to the final x and y position, the isLocked flag is set and the currentPiece is incremented to
the next jigsaw piece.
isPieceAtFinalPosition()
{
if (this.id !== currentPiece)
{
return false;
}
if (this.isLocked)
{
return false;
}
if ((this.x > this.finalX - this.granulatity) &&
(this.x < this.finalX + this.granulatity) &&
(this.y > this.finalY - this.granulatity) &&
(this.y < this.finalY + this.granulatity))
{
this.x = this.finalX;
this.y = this.finalY;
this.isLocked = true;
return true;
}
return false;
}
The pointIsInsideBoundingRectangle() method does the same rectangular bounding test that we have seen in previous examples. If this test is passed, then a second
test is done to check if the mouse is on a transparent pixel within the jigsaw piece. The transparency test code is highlighted in red.
pointIsInsideBoundingRectangle(pointX, pointY)
{
if (this.isLocked)
{
return;
}
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
}
}
Getting user input and updating the game state. Input is got via the keyboard.
Rotation of objects on the canvas
Collision detection between multiple moving objects
Collision detection of rotated bounding rectangles
Play game
tank_game.js
const BACKGROUND = 0;
const TANK = 1;
const FIRE_SHELL = 2; // animation showing initial firing of shell
const SHELL = 3; // a shell that is fired from the tank
const EXPLOSION = 4; // the explosion that results from the firing of the shell
const WIN_MESSAGE = 5;
/* If they are needed, then include any game-specific mouse and keyboard listners */
document.addEventListener("keydown", function (e)
{
var ANGLE_STEP_SIZE = 10;
Code Explained
The game conssts of various gameObjects. In particular, when a shell is fired, three different things happen. Firstly, a small explosion occurs at the top of the tank's
gun barrel. Secondly, the shell moves along its path. Thirdly, as it moves, it performs collision with the enemy tanks. If it collides with an enamy tank, then a large
explosion occurs at the point of collision. If the shell does not collide with any enemy tanks within its range, then a large explosion occurs when the shell reaches
its range.
The small explosion is contained in FIRE_SHELL, the shell movement is contained in SHELL and the large explosion is contained in EXPLOSION.
const BACKGROUND = 0;
const TANK = 1;
const FIRE_SHELL = 2; // animation showing initial firing of shell
const SHELL = 3; // a shell that is fired from the tank
The enemyTanks are held in their own array, called enemyTanks[]. In this game, there are 3 enemy tanks.
As the enemy tanks are not being held in gameObjects[], the game code will be responsible for their rendering.
let enemyTanks = [];
const numberOfEnemyTanks = 3;
When they are created, the enemy tanks are placed in random positions on the canvas. When creating the enemy tanks, we use a do...while loop to ensure that the
tanks do get spawned inside the river.
// create the enemy tanks
for (let i = 0; i < numberOfEnemyTanks; i++)
{
do
{
// make sure that the enemy tanks do not randomly spawn in the river
enemyTanks[i] = new EnemyTank(tankImage, riverImage, tankMovingSound, Math.random() * (canvas.width - 25) +
}while (enemyTanks[i].collidedWithRiver())
enemyTanks[i].start();
enemyTanks[i].startMoving();
}
The left and right arrow keys are used to turn the player tank
The space bar is used to fire a shell. When a shell is fired, a small explosion occurs at the top of the tank's gun barrel, as shown in red below.
The shell is fired in the direction that the tank is facing, as shown in blue below.
document.addEventListener("keydown", function (e)
{
var ANGLE_STEP_SIZE = 10;
TankCanvasGame.js
this.numberOfEnemytanksDestroyed = 0;
}
collisionDetection()
{
/* Collision detection for the player tank bumping into an enemy tank */
for (let i = 0; i < enemyTanks.length; i++)
{
if ((enemyTanks[i].pointIsInsideTank(gameObjects[TANK].getFrontLeftCornerX(), gameObjects[TANK].getFrontLeft
(enemyTanks[i].pointIsInsideTank(gameObjects[TANK].getFrontRightCornerX(), gameObjects[TANK].getFron
{
enemyTanks[i].reverse(5);
enemyTanks[i].setDirection(enemyTanks[i].getDirection() + 10); // turn away from the river, so that the
}
}
this.numberOfEnemytanksDestroyed++;
if (this.numberOfEnemytanksDestroyed === numberOfEnemyTanks)
{
/* Player has won
/* Have a two second delay to show the last enemy tank blowing up beofore displaying the 'Game O
setInterval(function ()
{
for (let j = 0; j < gameObjects.length; j++) /* stop all gameObjects from animating */
{
gameObjects[j].stopAndHide();
}
gameObjects[TANK].stopMoving(); // turn off tank moving sound
gameObjects[BACKGROUND].start();
gameObjects[WIN_MESSAGE] = new StaticText("Game Over!", 5, 270, "Times Roman", 100, "red");
gameObjects[WIN_MESSAGE].start(); /* render win message */
}, 2000);
}
}
}
}
}
render()
{
super.render();
for (let i = 0; i < enemyTanks.length; i++)
Code Explained
Each enemy tank needs to be tested for collision against the player tank all other enemy tanks. This is done in the same way as the player tank was tested for
collision above, were the front-left and front-right corners of the enemy tank are tested againsts the bounding rectangles of the player tank and the other enemy
tanks.
/* Collision detection for the enemy tanks bumping into each other */
for (let i = 0; i < enemyTanks.length; i++)
{
/* check if enemy tank bumps into player tank */
if ((gameObjects[TANK].pointIsInsideTank(enemyTanks[i].getFrontLeftCornerX(), enemyTanks[i].getFrontLeftCorn
(gameObjects[TANK].pointIsInsideTank(enemyTanks[i].getFrontRightCornerX(), enemyTanks[i].getFrontRig
{
enemyTanks[i].reverse(5);
enemyTanks[i].setDirection(enemyTanks[i].getDirection() + 10); // turn away from the river, so that the
}
If the player's tank collides with the river, then it is reversed back out of the river.
/* Collision detection of the player tank with the river */
if (gameObjects[TANK].collidedWithRiver())
{
gameObjects[TANK].reverse();
}
If an enemy tank collides with the river, it is reversed back out of the river and it changes direction by 10 degrees in a clockwise direction. In this way the tank will
eventually move away from the river.
/* Collision detection for enemy tanks with the river */
for (let i = 0; i < enemyTanks.length; i++)
{
if (enemyTanks[i].collidedWithRiver())
{
enemyTanks[i].reverse();
enemyTanks[i].setDirection(enemyTanks[i].getDirection() + 10); // turn away from the river, so that the
}
Collision detection of a shell can only happen if there is a shell. The test 'gameObjects[SHELL] === undefined' is needed to stop a code crash when there is no
shell firing. The test 'gameObjects[SHELL].isFiring()' is used to detect that a shell is firing.
A shell can only strike a tank that is being displayed. The test '(enemyTanks[i].isDisplayed())' is used to detect this.
A shell will strike a tank if it is insdie the tank's bounding rectangle. The test '(enemyTanks[i].pointIsInsideTank(gameObjects[SHELL].getX(),
gameObjects[SHELL].getY()))' is used to detect this.
If a shell hits an enemy tank, then:
When the game ends, all of the gameObjects except the BACKGROUND are hidden. The two-second interval allows the gameObject effect of the last enemy tank
blowing up to be displayed prior to the game ending.
this.numberOfEnemytanksDestroyed++;
if (this.numberOfEnemytanksDestroyed === numberOfEnemyTanks)
{
/* Player has won
/* Have a two second delay to show the last enemy tank blowing up beofore displaying the 'Game O
setInterval(function ()
{
for (let j = 0; j < gameObjects.length; j++) /* stop all gameObjects from animating */
{
gameObjects[j].stopAndHide();
}
gameObjects[TANK].stopMoving(); // turn off tank moving sound
gameObjects[BACKGROUND].start();
gameObjects[WIN_MESSAGE] = new StaticText("Game Over!", 5, 270, "Times Roman", 100, "red");
gameObjects[WIN_MESSAGE].start(); /* render win message */
}, 2000);
}
}
}
}
Tank.js
this.tankImage = tankImage;
this.tankMovingSound = tankMovingSound;
this.centreX = centreX;
this.centreY = centreY;
this.size = 50; // the width and height of the tank
this.halfSize = this.size / 2;
this.START_ROW = startRow;
this.START_COLUMN = startColumn;
updateState()
{
if (!this.isMoving)
{
return;
}
this.currentSprite++;
this.column += this.SPRITE_INCREMENT;
if (this.currentSprite >= this.NUMBER_OF_SPRITES)
{
this.currentSprite = 0;
this.row = this.START_ROW;
this.column = this.START_COLUMN;
}
if (this.column < 0)
{
this.column = this.NUMBER_OF_COLUMNS_IN_SPRITE_IMAGE - 1;
this.row--;
}
this.centreX += this.stepSizeX;
this.centreY += this.stepSizeY;
/* if the tank goes off the canvas, then make it reappear at the opposite side of the canvas */
if ((this.centreX - this.halfSize) > canvas.width)
{
this.centreX = -this.halfSize;
}
else if ((this.centreY - this.halfSize) > canvas.height)
{
this.centreY = -this.halfSize;
}
else if ((this.centreX + this.halfSize) < 0)
{
this.centreX = canvas.width + this.halfSize;
}
else if ((this.centreY + this.halfSize) < 0)
{
this.centreY = canvas.height + this.halfSize;
}
}
render()
{
ctx.save();
ctx.translate(this.centreX, this.centreY);
ctx.rotate(Math.radians(this.direction));
ctx.translate(-this.centreX, -this.centreY);
if (y > imageTopLeftY)
{
if ((y - imageTopLeftY) > this.size)
{
return false; // below the tank image
}
}
}
else // above or to the left of the tank image
{
return false;
}
return true; // inside tank image
}
collidedWithRiver()
{
/* test the front-left corner and the front-right corner of the tank for collision with the river */
/* we only need to test the front of the tank, as the tank can only move forward
if ((this.pointCollisionWithRiver(this.getFrontLeftCornerX(), this.getFrontLeftCornerY())) ||
(this.pointCollisionWithRiver(this.getFrontRightCornerX(), this.getFrontRightCornerY())))
{
return true;
}
return false;
}
pointCollisionWithRiver(x, y)
{
positionRandomly()
{
this.centreX = Math.random() * (canvas.width - (this.size * 2)) + this.size;
this.centreY = Math.random() * (canvas.height - (this.size * 2)) + this.size;
this.setDirection(Math.random() * 360);
startMoving()
{
this.isMoving = true;
this.tankMovingSound.currentTime = 0;
this.tankMovingSound.play();
stopMoving()
{
this.isMoving = false;
this.tankMovingSound.pause();
}
tankIsMoving()
{
return this.isMoving;
}
reverse(numberOfReverseSteps = 1)
{
// move in reverse direction
for (let i = 0; i < numberOfReverseSteps; i++)
{
this.setX(this.getX() - this.getStepSizeX());
this.setY(this.getY() - this.getStepSizeY());
}
}
getDirection()
{
return this.direction;
}
setDirection(newDirection)
{
this.direction = newDirection;
getX()
{
return this.centreX;
}
getY()
{
return this.centreY;
}
setX(x)
{
this.centreX = x;
}
setY(y)
{
this.centreY = y;
}
getStepSizeX()
{
return this.stepSizeX;
}
getStepSizeY()
{
return this.stepSizeY;
}
getSize()
{
return this.size;
}
getFrontLeftCornerX()
{
return this.centreX - this.getSize() / 2.8;
}
getFrontLeftCornerY()
{
getFrontRightCornerX()
{
return this.centreX + this.getSize() / 2.8;
}
getFrontRightCornerY()
{
return this.centreY - this.getSize() / 2.8;
}
getFrontCentreX()
{
return this.centreX;
}
getFrontCentreY()
{
return this.centreY - this.getSize() / 2.8;
}
}
Code Explained
Inside the constructor() method, an offscreen canvas is created to hold an image of the river. This will be used for collision detection between the tank and the river.
/* this.offscreenObstaclesCtx will be used for collision detection with the river */
this.offscreenObstacles = document.createElement('canvas');
this.offscreenObstaclesCtx = this.offscreenObstacles.getContext('2d');
this.offscreenObstacles.width = canvas.width;
this.offscreenObstacles.height = canvas.height;
this.offscreenObstaclesCtx.drawImage(riverImage, 0, 0, canvas.width, canvas.height);
The direction that the tank is moving in is given in degrees. The variable 'this.STEP_SIZE' determines how many pixels the tank will move forward on each call to
updateState(). Higher values will cause the tank to move faster.
this.STEP_SIZE = 2; // the number of pixels to move forward
this.setDirection(direction);
The tank code includes methods for making the tank move and stop. The tank is initially set to be stopped.
this.isMoving = false; // the tank is initially stopped
checks if the tank is stopped or moving. If the tank is stopped, then it does not need to update its state. This is shown in green in the code below.
move through the sprite image, to animate the tank movement. This is shown in blue in the code below.
move the tank forward. If the tank moves off the edge of the canvas, then it is made to reappear on the opposite side of the canvas. This is shown in red in the
code below.
updateState()
{
if (!this.isMoving)
{
return;
}
this.currentSprite++;
this.column += this.SPRITE_INCREMENT;
if (this.currentSprite >= this.NUMBER_OF_SPRITES)
{
this.currentSprite = 0;
this.row = this.START_ROW;
this.column = this.START_COLUMN;
}
if (this.column < 0)
{
this.column = this.NUMBER_OF_COLUMNS_IN_SPRITE_IMAGE - 1;
this.row--;
}
/* if the tank goes off the canvas, then make it reappear at the opposite side of the canvas */
if ((this.centreX - this.halfSize) > canvas.width)
{
this.centreX = -this.halfSize;
}
else if ((this.centreY - this.halfSize) > canvas.height)
{
this.centreY = -this.halfSize;
}
else if ((this.centreX + this.halfSize) < 0)
{
this.centreX = canvas.width + this.halfSize;
}
else if ((this.centreY + this.halfSize) < 0)
{
this.centreY = canvas.height + this.halfSize;
}
}
When rendering the tank, we must rotate the canvas so that the tank is made to point in its current direction.
render()
{
ctx.save();
ctx.translate(this.centreX, this.centreY);
ctx.rotate(Math.radians(this.direction));
ctx.translate(-this.centreX, -this.centreY);
We need to account for the fact that the tank is displayed in a rotated position on the canvas. The best way to achieve this is to rotate the tank and the bullet by an
amount that will bring the tank back to a position where it is not rotated. The rotation of the point is done by rotating the point by the minus angle of 'this.direction'.
The rotation is about the tank's centre point. This is shown in red in the code below.
The rest of the code is a standard test of a point against a rectangle.
pointIsInsideTank(x, y)
{
/* transform the shell into the enemy tank's coordinate system */
let transformedX = x - this.centreX;
let transformedY = y - this.centreY;
x = transformedX * Math.cos(Math.radians((this.direction))) - transformedY * Math.sin(Math.radians(this.directio
y = transformedX * Math.sin(Math.radians((this.direction))) + transformedY * Math.cos(Math.radians(this.directio
x += this.centreX;
y += this.centreY;
if (y > imageTopLeftY)
{
if ((y - imageTopLeftY) > this.size)
{
return false; // below the tank image
}
}
}
else // above or to the left of the tank image
{
return false;
}
return true; // inside tank image
}
The The collidedWithRiver() method tests the front-left and front-right corners of the tank against the offscreen canvas that contains the river image.
The pointCollisionWithRiver() method is used for the collision detection of one point against the offscreen canvas containing the river image.
pointCollisionWithRiver(x, y)
{
The positionRandomly() method places the tank ramdomly on the canvas. The calculation in red ensures that the entire tank is displayed on the canvas.
positionRandomly()
{
this.centreX = Math.random() * (canvas.width - (this.size * 2)) + this.size;
this.centreY = Math.random() * (canvas.height - (this.size * 2)) + this.size;
this.setDirection(Math.random() * 360);
PlayerTank.js
EnemyTank.js
Code Explained
The player tank and the enemy tank use a different set of sprites, so that they are two different colours. This is done using the last two parameters of the Tank
constructor, as shown in red in the code above.
Shell.js
this.explosionImage = explosionImage;
this.shellExplosionSound = shellExplosionSound;
this.x = x;
this.y = y;
this.explosionTargetX = x;
this.explosionTargetY = y;
this.direction = direction;
updateState()
{
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
{
this.stopAndHide();
gameObjects[EXPLOSION] = new Explosion(this.explosionImage, this.shellExplosionSound, this.explosionTargetX,
gameObjects[EXPLOSION].start();
}
}
getX()
{
return this.explosionTargetX;
}
getY()
{
return this.explosionTargetY;
}
getRange()
{
return this.shellRange;
}
isFiring()
{
return this.gameObjectIsDisplayed;
}
}
Code Explained
In the constructor() method, we need to declare the range of the shell. The shell starts from the top of the barrel of the tank's gun.
/* define the maximum range of the shell */
/* the shell will explode here if it has not hit a target beforehand */
this.shellRange = 200;
The shell will travel until it either collides with an enemy tank or reaches its range. The collision with an enemy tank is dealt with inside the TankCanvasGame
collisionDetection() method. Therefore, the updateState() method below only needs to test if the shell has reached its maximum range. If the shell has not reached
its maximum range, then move it to its next position. If the shell has reached it maximum range, then hide the shell and show an explosion.
updateState()
{
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
{
this.stopAndHide();
gameObjects[EXPLOSION] = new Explosion(this.explosionImage, this.shellExplosionSound, this.explosionTargetX,
gameObjects[EXPLOSION].start();
}
}
FireShellAnimation.js
this.tankImage = tankImage;
this.centreX = centreX;
this.centreY = centreY;
this.direction = direction;
fireShellSound.currentTime = 0;
fireShellSound.play();
updateState()
{
this.currentSprite++;
if (this.currentSprite >= this.NUMBER_OF_SPRITES)
{
this.stopAndHide();
}
this.column += this.spriteIncrement;
}
render()
{
Code Explained
Play game
The game data is held in a json file, as shown below. Each answer can contain text and/or an image.
multiple_choice_quiz.json.js
{
"questions":
[
{
"question": "How many countries are there in the EU?",
"correct": 4,
"answers":
[
{
"text": "17",
"image": ""
},
{
"text": "18",
"image": ""
},
{
"text": "27",
"image": ""
},
{
"text": "28",
"image": ""
}
]
},
{
"question": "Click on the map that highlights Poland",
"correct": 2,
"answers":
[
{
"text": "",
"image": "images/map_czech_republic.png"
},
{
]
},
{
"question": "Click on the Finnish flag",
"correct": 3,
"answers":
[
{
"text": "",
"image": "images/flag_denmark.png"
},
{
"text": "",
"image": "images/flag_sweden.png"
},
{
"text": "",
"image": "images/flag_finland.png"
},
{
"text": "",
"image": "images/flag_norway.png"
}
]
}
]
}
multiple_choice_quiz_game.js
/* The game must be loaded from the json file before it is played */
fetch('json/multiple_choice_quiz.json').then(function (response)
{
return response.json();
}).then(function (jsonData)
{
game = new MultipleChoiceQuizCanvasGame(jsonData);
/* If they are needed, then include any game-specific mouse and keyboard listners */
document.getElementById("gameCanvas").addEventListener("mousedown", function (e)
{
if (e.which === 1) // left mouse button
{
let canvasBoundingRectangle = document.getElementById("gameCanvas").getBoundingClientRect();
let mouseX = e.clientX - canvasBoundingRectangle.left;
let mouseY = e.clientY - canvasBoundingRectangle.top;
game.mousedown(mouseX, mouseY);
}
});
game.mousemove(mouseX, mouseY);
}
});
}
Code Explained
An AJAX fetch is done on the json file. The MultipleChoiceQuizCanvasGame game object cannot be created and the game cannot start until the json data has been
fetched, as the jsonData is needed to create the MultipleChoiceQuizCanvasGame (as shown in red).
/* The game must be loaded from the json file before it is played */
fetch('json/multiple_choice_quiz.json').then(function (response)
{
return response.json();
}).then(function (jsonData)
{
game = new MultipleChoiceQuizCanvasGame(jsonData);
The mousedown and mousemove events are passed to the game for handling, as shown in red below.
/* If they are needed, then include any game-specific mouse and keyboard listners */
document.getElementById("gameCanvas").addEventListener("mousedown", function (e)
{
if (e.which === 1) // left mouse button
{
let canvasBoundingRectangle = document.getElementById("gameCanvas").getBoundingClientRect();
let mouseX = e.clientX - canvasBoundingRectangle.left;
let mouseY = e.clientY - canvasBoundingRectangle.top;
game.mousedown(mouseX, mouseY);
}
});
game.mousemove(mouseX, mouseY);
}
});
MultipleChoiceQuizCanvasGame.js
/* If the current question has images on the buttons, they will be stored here. */
this.currentImage = [new Image(), new Image(), new Image(), new Image()];
this.score = 0;
this.currentQuestion = 0;
render()
{
super.render();
for (let i = 0; i < buttons.length; i++)
{
if (buttons[i].isDisplayed())
{
buttons[i].render();
}
}
}
mousedown(x, y)
{
for (let i = 0; i < buttons.length; i++)
{
if (buttons[i].isDisplayed())
{
if (buttons[i].pointIsInsideBoundingRectangle(x, y))
{
if (buttons[i].isThisTheCorrectAnswer())
{
this.score++;
}
this.currentQuestion++;
gameObjects[WIN_MESSAGE] = new StaticText("Your score is: " + this.score, 15, 260, "Times Roman"
gameObjects[WIN_MESSAGE].start(); /* render win message */
}
else /* Load the next question */
{
mousemove(x, y)
{
for (let i = 0; i < buttons.length; i++)
{
if (buttons[i].isDisplayed())
{
buttons[i].pointIsInsideBoundingRectangle(x, y); /* Used to highlight the button that the mouse is curre
}
}
}
gameIsOver()
{
/* Test for end-of-game */
return (this.currentQuestion === this.jsonData.questions.length);
}
loadQuestion()
{
/* Load the question from the json */
gameObjects[QUESTION] = new StaticText(this.jsonData.questions[this.currentQuestion].question, STATIC_TEXT_CENTR
gameObjects[QUESTION].start();
/* Buttons might or might not have an image. We need to deal with both cases */
if (this.jsonData.questions[this.currentQuestion].answers[i].image !== "")
{
this.currentImage[i].src = this.jsonData.questions[this.currentQuestion].answers[i].image;
Code Explained
the buttons[] array will hold the four buttons for the current question.
let buttons = [];
The json file that hold the game data can only contain strings. However, the Button class takes an image object rather than a string filename. Therefore, we need to
store the images that are contained in the json strings into Image objects prior to creating Button objects. The array 'this.currentImage' will be used to hold the four
buttons' images.
constructor(jsonData)
{
super();
this.jsonData = jsonData;
/* If the current question has images on the buttons, they will be stored here. */
this.currentImage = [new Image(), new Image(), new Image(), new Image()];
this.score = 0;
this.currentQuestion = 0;
This game extend from the class CanvasGame. The render() method will call super.render() to render the gameObjects[] that are contained in the game.
The game-specific button objects are then rendered.
render()
{
super.render();
The mousedown() method steps through each button and checks if it has been clicked.
If the button contains the correct answer, then the score is incremented (shown in green).
The current question is incremented (shown in blue).
A test is then done to see if the game is over. If the game is over, then the score is displayed. Otherwise, the next question is loaded (shown in red).
mousedown(x, y)
{
for (let i = 0; i < buttons.length; i++)
{
if (buttons[i].isDisplayed())
{
if (buttons[i].pointIsInsideBoundingRectangle(x, y))
{
if (buttons[i].isThisTheCorrectAnswer())
{
this.score++;
}
this.currentQuestion++;
gameObjects[WIN_MESSAGE] = new StaticText("Your score is: " + this.score, 15, 260, "Times Roman"
gameObjects[WIN_MESSAGE].start(); /* render win message */
}
else /* Load the next question */
{
this.loadQuestion();
}
The mousemove highlights the button that the mouse is currently hovering over
mousemove(x, y)
{
for (let i = 0; i < buttons.length; i++)
{
if (buttons[i].isDisplayed())
{
buttons[i].pointIsInsideBoundingRectangle(x, y); /* Used to highlight the button that the mouse is curre
}
}
}
isThisTheCorrectAnswer()
{
return this.isCorrectAnswer;
}
}
Code Explained
The game is over if all of the questions from the json file have been completed.
gameIsOver()
{
/* Test for end-of-game */
return (this.currentQuestion === this.jsonData.questions.length);
}
The loadQuestion() method displays the 'this.currentQuestion' from jsonData. Each question consists of a question and four answer buttons.
The question is placed in gameObjects[QUESTION] (shown in blue)
The four answers are placed in four buttons. When creating the buttons, we need to account for the fact that an answer might or might not contain an image (as
shown in green). If a button does contain an image, then the json file will hold the image's name. The MultipleChoiceQuizButton class requires an image rather
than an image filename. Therefore, we need to create an Image() object to hold the image, so that we can create a new MultipleChoiceQuizButton object. This is
shown in red.
loadQuestion()
{
/* Load the question from the json */
gameObjects[QUESTION] = new StaticText(this.jsonData.questions[this.currentQuestion].question, STATIC_TEXT_CENTR
gameObjects[QUESTION].start();
/* Buttons might or might not have an image. We need to deal with both cases */
if (this.jsonData.questions[this.currentQuestion].answers[i].image !== "")
{
this.currentImage[i].src = this.jsonData.questions[this.currentQuestion].answers[i].image;
Play game
teddy_game.js
let oldMouseX = 0; // oldMouseX and oldMouseY hold the previous mouse position. This is needed to calculate the
let oldMouseY = 0; // direction that the mouse is moving in, so that we can rotate around a body part's centre of rotati
const BACKGROUND = 0;
const TEDDY1 = 1;
const TEDDY2 = 2;
/* If they are needed, then include any game-specific mouse and keyboard listners */
let NO_BODYPART_SELECTED = -1;
let selectedTeddy = TEDDY1;
let selectedBodyPart = NO_BODYPART_SELECTED;
document.getElementById('gameCanvas').addEventListener('mousedown', function (e)
{
if (e.which === 1) // left mouse button pressed
{
// mouseIsDown = true;
}
let canvasBoundingRectangle = document.getElementById("gameCanvas").getBoundingClientRect();
let mouseX = e.clientX - canvasBoundingRectangle.left;
let mouseY = e.clientY - canvasBoundingRectangle.top;
document.body.style.cursor = "move";
for (let i = 0; i < gameObjects[TEDDY1].getNumberOfBodyParts(); i++)
let ROTATION_STEP_SIZE = 1;
document.getElementById('gameCanvas').addEventListener('mousemove', function (e)
{
// only process 'mousemove' when a body part has been selected
if (selectedBodyPart !== NO_BODYPART_SELECTED)
{
let canvasBoundingRectangle = document.getElementById("gameCanvas").getBoundingClientRect();
let mouseX = e.clientX - canvasBoundingRectangle.left;
let mouseY = e.clientY - canvasBoundingRectangle.top;
Code Explained
We need to keep track of the last mouse, so that we can detect the direction that the mouse is moving. This is needed when we drag on a hotspot. It allows us to
know what way the hotspot should rotate.
let oldMouseX = 0; // oldMouseX and oldMouseY hold the previous mouse position. This is needed to calculate the
let oldMouseY = 0; // direction that the mouse is moving in, so that we can rotate around a body part's centre of rotati
When the mouse is pressed down (inside the 'mousedown' event handler), we check all of the hotspots on each Teddy. If we find that the mouse was clicked on a
hotspot, then we set selectedTeddy and selectedBodyPart to identify the Teddy and BodyPart.
When the mouse is moved (inside the 'mousemove' event handler), we check if a bodyPart was selected. If yes, then we rotate that bodyPart. The selected bodyPart
is rotated (by calling its changeRotationValue() method in the code below). The rotation is based on the direction that the mouse is moving. The oldMouseX,
oldMouseY, mouseX and mouseY positions will be used to calculate the direction of that the mouse is moving.
/* If they are needed, then include any game-specific mouse and keyboard listners */
let NO_BODYPART_SELECTED = -1;
let selectedTeddy = TEDDY1;
let selectedBodyPart = NO_BODYPART_SELECTED;
document.getElementById('gameCanvas').addEventListener('mousedown', function (e)
{
if (e.which === 1) // left mouse button pressed
{
// mouseIsDown = true;
}
let canvasBoundingRectangle = document.getElementById("gameCanvas").getBoundingClientRect();
let mouseX = e.clientX - canvasBoundingRectangle.left;
let mouseY = e.clientY - canvasBoundingRectangle.top;
document.body.style.cursor = "move";
for (let i = 0; i < gameObjects[TEDDY1].getNumberOfBodyParts(); i++)
{
for (let teddy = FIRST_TEDDY; teddy <= LAST_TEDDY; teddy++)
{
if (gameObjects[teddy] !== undefined)
{
if (gameObjects[teddy].getBodyPart(i).isHotSpot(mouseX, mouseY) === true)
{
selectedTeddy = teddy;
selectedBodyPart = i;
}
}
}
}
oldMouseX = mouseX;
oldMouseY = mouseY;
});
let ROTATION_STEP_SIZE = 1;
Teddy.js
this.scale = scale; // the scale is needed to convert the physical image data into the required size on the canv
this.bodyPart = [];
this.bodyPart[this.LEFT_LEG_UPPER] = new TeddyBodyPart(scale, leftLegUpperImage, 100, 100, 0.0, 50, 10, 60, 50, 4
this.bodyPart[this.LEFT_LEG_LOWER] = new TeddyBodyPart(scale, leftLegLowerImage, 50, 100, 0.0, 25, 10, 25, 50, 4
this.bodyPart[this.LEFT_LEG_FOOT] = new TeddyBodyPart(scale, leftLegFootImage, 60, 50, 0.0, 30, 5, 30, 55, 30);
this.bodyPart[this.RIGHT_LEG_UPPER] = new TeddyBodyPart(scale, rightLegUpperImage, 100, 100, 0.0, 50, 10, 40, 50
this.bodyPart[this.RIGHT_LEG_LOWER] = new TeddyBodyPart(scale, rightLegLowerImage, 50, 100, 0.0, 25, 10, 25, 50,
this.bodyPart[this.RIGHT_LEG_FOOT] = new TeddyBodyPart(scale, rightLegFootImage, 60, 50, 0.0, 30, 5, 30, 55, 30)
// all other body parts are dependent on the position of their parent
// head
this.bodyPart[this.TORSO].setChild(this.bodyPart[this.HEAD], 100, 10);
// left arm
this.bodyPart[this.TORSO].setChild(this.bodyPart[this.LEFT_ARM_UPPER], 180, 50);
this.bodyPart[this.LEFT_ARM_UPPER].setChild(this.bodyPart[this.LEFT_ARM_LOWER], 80, 25);
this.bodyPart[this.LEFT_ARM_LOWER].setChild(this.bodyPart[this.LEFT_ARM_HAND], 100, 25);
// right arm
this.bodyPart[this.TORSO].setChild(this.bodyPart[this.RIGHT_ARM_UPPER], 20, 50);
this.bodyPart[this.RIGHT_ARM_UPPER].setChild(this.bodyPart[this.RIGHT_ARM_LOWER], 80, 25);
this.bodyPart[this.RIGHT_ARM_LOWER].setChild(this.bodyPart[this.RIGHT_ARM_HAND], 100, 25);
// left leg
this.bodyPart[this.TORSO].setChild(this.bodyPart[this.LEFT_LEG_UPPER], 135, 190);
this.bodyPart[this.LEFT_LEG_UPPER].setChild(this.bodyPart[this.LEFT_LEG_LOWER], 60, 90);
this.bodyPart[this.LEFT_LEG_LOWER].setChild(this.bodyPart[this.LEFT_LEG_FOOT], 25, 70);
// right leg
this.bodyPart[this.TORSO].setChild(this.bodyPart[this.RIGHT_LEG_UPPER], 70, 190);
this.bodyPart[this.RIGHT_LEG_UPPER].setChild(this.bodyPart[this.RIGHT_LEG_LOWER], 40, 90);
this.bodyPart[this.RIGHT_LEG_LOWER].setChild(this.bodyPart[this.RIGHT_LEG_FOOT], 25, 70);
}
render()
{
// place all of the bodyParts in the correct position
this.bodyPart[this.TORSO].setChildrenPositions();
getBodyPart(i)
{
return this.bodyPart[i];
}
setHotSpotDrawState(state)
{
for (let i = 0; i < this.NUMBER_OF_BODY_PARTS; i++)
{
this.bodyPart[i].setDrawHotSpot(state);
}
}
getNumberOfBodyParts()
{
return this.NUMBER_OF_BODY_PARTS;
}
}
Code Explained
The parameter 'scale' is used to set the size of a Teddy. It is sized with a sclae that is relative to the canvas width and height.
Each of the bodyParts of the Teddy are given an identifier (as shown in green below).
The bodyPart[] array is used to hold the bodyParts (as shown in red below).
The child dependencies of each bodyParts are set up using the BodyPart's setChild() method (as shown in blue below). The linking of parent and child bodyParts
allows us to rotate child bodyParts whenever a parent bodyPart has been rotated.
constructor(torsoCentreX, torsoCentreY, scale, torsoImage, headImage, leftArmUpperImage, leftArmLowerImage, leftArmH
{
super(delay); /* as this class extends from GameObject, you must always call super() */
this.scale = scale; // the scale is needed to convert the physical image data into the required size on the canv
this.bodyPart = [];
this.bodyPart[this.LEFT_LEG_UPPER] = new TeddyBodyPart(scale, leftLegUpperImage, 100, 100, 0.0, 50, 10, 60, 50,
this.bodyPart[this.LEFT_LEG_LOWER] = new TeddyBodyPart(scale, leftLegLowerImage, 50, 100, 0.0, 25, 10, 25, 50, 4
this.bodyPart[this.LEFT_LEG_FOOT] = new TeddyBodyPart(scale, leftLegFootImage, 60, 50, 0.0, 30, 5, 30, 55, 30);
this.bodyPart[this.RIGHT_LEG_UPPER] = new TeddyBodyPart(scale, rightLegUpperImage, 100, 100, 0.0, 50, 10, 40, 50
this.bodyPart[this.RIGHT_LEG_LOWER] = new TeddyBodyPart(scale, rightLegLowerImage, 50, 100, 0.0, 25, 10, 25, 50,
this.bodyPart[this.RIGHT_LEG_FOOT] = new TeddyBodyPart(scale, rightLegFootImage, 60, 50, 0.0, 30, 5, 30, 55, 30)
// all other body parts are dependent on the position of their parent
// head
this.bodyPart[this.TORSO].setChild(this.bodyPart[this.HEAD], 100, 10);
// left arm
this.bodyPart[this.TORSO].setChild(this.bodyPart[this.LEFT_ARM_UPPER], 180, 50);
this.bodyPart[this.LEFT_ARM_UPPER].setChild(this.bodyPart[this.LEFT_ARM_LOWER], 80, 25);
this.bodyPart[this.LEFT_ARM_LOWER].setChild(this.bodyPart[this.LEFT_ARM_HAND], 100, 25);
// right arm
this.bodyPart[this.TORSO].setChild(this.bodyPart[this.RIGHT_ARM_UPPER], 20, 50);
this.bodyPart[this.RIGHT_ARM_UPPER].setChild(this.bodyPart[this.RIGHT_ARM_LOWER], 80, 25);
this.bodyPart[this.RIGHT_ARM_LOWER].setChild(this.bodyPart[this.RIGHT_ARM_HAND], 100, 25);
// left leg
this.bodyPart[this.TORSO].setChild(this.bodyPart[this.LEFT_LEG_UPPER], 135, 190);
this.bodyPart[this.LEFT_LEG_UPPER].setChild(this.bodyPart[this.LEFT_LEG_LOWER], 60, 90);
this.bodyPart[this.LEFT_LEG_LOWER].setChild(this.bodyPart[this.LEFT_LEG_FOOT], 25, 70);
// right leg
this.bodyPart[this.TORSO].setChild(this.bodyPart[this.RIGHT_LEG_UPPER], 70, 190);
this.bodyPart[this.RIGHT_LEG_UPPER].setChild(this.bodyPart[this.RIGHT_LEG_LOWER], 40, 90);
this.bodyPart[this.RIGHT_LEG_LOWER].setChild(this.bodyPart[this.RIGHT_LEG_FOOT], 25, 70);
}
The render() method steps through the bodyParts[] array and renders each of the Teddy bodyParts.
render()
{
// place all of the bodyParts in the correct position
this.bodyPart[this.TORSO].setChildrenPositions();
TeddyBodyPart.js
class TeddyBodyPart
this.width = width * scale; // width and height of the bodyPart in canvas coordinates
this.height = height * scale;
this.radiants = radiants; // the angle of rotation (in radiants)
this.centreOfRotationX = centreOfRotationX * scale; // the point around which rotations occur
this.centreOfRotationY = centreOfRotationY * scale;
this.hotSpotX = hotSpotX * scale; // hot spot for identifying where the user can drag the mouse to
this.hotSpotY = hotSpotY * scale; // rotate this bodyPart
this.hotSpotRadius = hotSpotRadius * scale; // size of the hotspot
this.children = [];
this.numberOfChildren = 0; // used to keep track of the number of children in the this.children array above
render()
{
// draw rotated bodyPart onto its offscreen canvas
this.offscreenBodyPartCanvasCtx.clearRect(0, 0, canvas.width, canvas.height);
this.rotateOffscreenCanvas(this.radiants, this.parentX, this.parentY);
this.offscreenBodyPartCanvasCtx.drawImage(this.bodyPartImageFile, this.parentX - this.centreOfRotationX, this.pa
this.rotateOffscreenCanvas(-this.radiants, this.parentX, this.parentY);
setChildrenPositions()
{
// This method adjusts the postion of this bodyPart's children to take account of this bodyPart's postion.
// To achieve the adjustment of position, this method sets the 'this.parentX', 'this.parentY' and 'this.radiant'
// recursively rotate each child bodyPart about its own centre of rotation
this.children[i].setChildrenPositions();
}
}
}
isRotatingClockwise(centerX, centerY, x, y)
{
// return true if the mouse is dragging in a clockwise direction, else return false
if (Math.abs(x - oldMouseX) >= Math.abs(y - oldMouseY))
{
// mouse is being moved primarily along the x-axis
if ((x < oldMouseX)) // mouse is moving to the left
{
if (y < centerY) // mouse is above the image
{
return false;
}
else // mouse is below the image
{
return true;
}
}
else // mouse is moving to the right
{
if (y < centerY) // mouse is to the left of the image
{
return true;
}
else // mouse is to the right of the image
{
return false;
}
}
}
else // mouse is being moved primarily along the y-axis
{
// mouse is being moved primarily along the y-axis
if (y < oldMouseY) // mouse is moving to the left
{
if (x < centerX) // mouse is above the image
{
return true;
}
else // mouse is below the image
{
return false;
}
}
else // mouse is moving to the right
{
if (x < centerX) // mouse is to the left of the image
{
return false;
}
else // mouse is to the right of the image
{
return true;
}
}
}
}
isHotSpot(canvasX, canvasY)
{
hotSpotX += this.parentX;
hotSpotY += this.parentY;
if ((canvasX > hotSpotX - this.hotSpotRadius) && (canvasX < hotSpotX + this.hotSpotRadius) &&
(canvasY > hotSpotY - this.hotSpotRadius) && (canvasY < hotSpotY + this.hotSpotRadius))
{
return true;
}
else
{
return false;
}
}
drawHotSpotOnOffscreenCanvas()
{
// draw a bodyPart's hotspot on the offscreenBodyPartCanvas
if (this.hotSpotIsDrawn === false)
{
return;
}
hotSpotX += this.parentX;
hotSpotY += this.parentY;
setInitialParentXandY(newParentX, newParentY)
{
// this method ties the parent's coordinates to the child's centreOfRotation coordinates
// The parent's coordinates are the x,y values of the joint on the parent where
// the child's rotation x,y coordinates will attach to
this.parentX = newParentX;
this.parentY = newParentY;
this.originalParentX = this.parentX;
this.originalParentY = this.parentY;
}
setDrawHotSpot(state)
{
// Not strictly necessary, but might be useful as a guide to showing younger users where to drag
this.hotSpotIsDrawn = state;
}
getDegrees()
setRadiants(newDegrees)
{
// set the rotation angle as radiants
// note that the input parameter, newDegrees, is given in degrees
this.radiants = Math.radians(newDegrees);
}
setDrawHotSpot(state)
{
this.hotSpotIsDrawn = state;
}
}
Code Explained
Teddy is made up of 14 BodyParts, which are linked together in a parent-child structure. Each BodyPart can have zero or more child BodyParts. Parents and
children are linked at the child's point 'this.centreOfRotationX,this.centreOfRotationX' (as shown in red below). The parent's connecting point is provided an a
parameter input to the method setInitialParentXandY(), which we shall look at further down in these notes.
Hotspots can be used to rotate BodyParts. Hopspots will be highlighted if the 'this.hotSpotIsDrawn' flag is set to true (as shown in blue below).
Each BodyPart has an offscreen canvas (as shown in green below). Rotations of a BodyPart will be implemented by rotating the offscreen canvas. The offscreen
canvas is the same size as the main game canvas. This means that any rotations that occur on the offscreen canvas will be reflected in the final canvas when the
offscreen canvas is drawn onto the main canvas.
constructor(scale, sourceImage, width, height, radiants, centreOfRotationX, centreOfRotationY, hotSpotX, hotSpotY, h
{
// There needs to be one bodyPart for each seperate part of the body
// For example, the left arm consists of 3 bodyParts: LEFT_ARM_UPPER, LEFT_ARM_LOWER and LEFT_HAND
this.width = width * scale; // width and height of the bodyPart in canvas coordinates
this.height = height * scale;
this.radiants = radiants; // the angle of rotation (in radiants)
this.centreOfRotationX = centreOfRotationX * scale; // the point around which rotations occur
this.centreOfRotationY = centreOfRotationY * scale;
this.hotSpotX = hotSpotX * scale; // hot spot for identifying where the user can drag the mouse to
this.hotSpotY = hotSpotY * scale; // rotate this bodyPart
this.hotSpotRadius = hotSpotRadius * scale; // size of the hotspot
this.children = [];
this.numberOfChildren = 0; // used to keep track of the number of children in the this.children array above
The render() method draws the rotated offscreen canvas on the main game canvas. It also draws the hotspot, if it is enabled.
render()
{
// draw rotated bodyPart onto its offscreen canvas
this.offscreenBodyPartCanvasCtx.clearRect(0, 0, canvas.width, canvas.height);
this.rotateOffscreenCanvas(this.radiants, this.parentX, this.parentY);
this.offscreenBodyPartCanvasCtx.drawImage(this.bodyPartImageFile, this.parentX - this.centreOfRotationX, this.pa
this.rotateOffscreenCanvas(-this.radiants, this.parentX, this.parentY);
// recursively rotate each child bodyPart about its own centre of rotation
this.children[i].setChildrenPositions();
}
}
}
The changeRotationValue() method adds/subtracts 'degrees' based on the current mouse position on the canvas. The code is straightforward and does not need
explaining.
changeRotationValue(mouseX, mouseY, degrees)
{
// depending on the position of the mouse relative to the position of this.parentX and this.parentY, add or subt
if (this.isRotatingClockwise(this.parentX, this.parentY, mouseX, mouseY) === true)
{
this.radiants += Math.radians(degrees);
}
else
{
this.radiants -= Math.radians(degrees);
}
}
The isRotatingClockwise() method is used to determine the direction that the mouse is being moved on the canvas. The code is straightforward and does not need
explaining.
isRotatingClockwise(centerX, centerY, x, y)
{
// return true if the mouse is dragging in a clockwise direction, else return false
if (Math.abs(x - oldMouseX) >= Math.abs(y - oldMouseY))
{
// mouse is being moved primarily along the x-axis
if ((x < oldMouseX)) // mouse is moving to the left
{
if (y < centerY) // mouse is above the image
{
return false;
}
else // mouse is below the image
{
return true;
}
}
else // mouse is moving to the right
{
if (y < centerY) // mouse is to the left of the image
{
return true;
}
else // mouse is to the right of the image
{
return false;
}
}
The isHotSpot() method determines if an canvasX, canvasY coordinate is indide the hotspot area of a bodyPart. The location on the canvas must be rotated to
account for the rotation of the offscreen canvas of this bodyPart.
isHotSpot(canvasX, canvasY)
{
// return true if the canvasX, canvasY is in this bodyPart's hotspot
var hotSpotX = this.parentX - this.centreOfRotationX + this.hotSpotX;
var hotSpotY = this.parentY - this.centreOfRotationY + this.hotSpotY;
hotSpotX += this.parentX;
hotSpotY += this.parentY;
if ((canvasX > hotSpotX - this.hotSpotRadius) && (canvasX < hotSpotX + this.hotSpotRadius) &&
(canvasY > hotSpotY - this.hotSpotRadius) && (canvasY < hotSpotY + this.hotSpotRadius))
{
return true;
}
else
{
return false;
}
}
The setChild() method sets a bodyPart as being a child of this bodyPart. This is needed to allow us to adjust child bodyParts whenever we rotate their parent
bodyPart.
The setInitialParentXandY() method connects a child back to its parent. It is called once for each child (as shown in red below)
setChild(child, parentX, parentY)
{
// set a direct child of this bodyPart
// For example, LEFT_ARM_UPPER has one direct child, which is LEFT_ARM_LOWER
this.children[this.numberOfChildren] = child;
child.setInitialParentXandY(parentX * this.scale, parentY * this.scale);
this.numberOfChildren++;
}
setInitialParentXandY(newParentX, newParentY)
{
// this method ties the parent's coordinates to the child's centreOfRotation coordinates
// The parent's coordinates are the x,y values of the joint on the parent where
// the child's rotation x,y coordinates will attach to
this.parentX = newParentX;
this.parentY = newParentY;
this.originalParentX = this.parentX;
getDegrees()
{
// return the rotation angle as degrees
return this.radiants * 180 / Math.PI;
}
setRadiants(newDegrees)
{
// set the rotation angle as radiants
// note that the input parameter, newDegrees, is given in degrees
this.radiants = Math.radians(newDegrees);
}
setDrawHotSpot(state)
{
this.hotSpotIsDrawn = state;
}
Exercises
Extend Teddy to make a DancingTeddy, as shown here. Hint: The animation is created by randomly moving some of the DancingTeddy bodyParts[] inside the
DancingTeddy updateState() method.