Whether you're taking part in the discussion, or reading articles about it, you must be aware that, even though it's still a specification, HTML5 is ready to be used...right now.
It not only improves the semantic structure of the web, but it also brings new JavaScript API's to life to make it easier to embed video, audio and even geolocation technologies (which is particularly interesting for mobile web applications) with ease.
That's not where the APIs stop though. They can additionally be used to build games, by making use of the Canvas 2D API. And that's exactly what we're going to do today.
The Concept
The preview image of this article might point you in the direction of what we are going to build, but it still rather vague, right?
To show the potential of the Canvas 2D API, we'll build a game, which has been created in Flash numerous times before: a "catch the falling objects" game.
As you may know, most of those games are always bound to a specific theme, such as catching fruits in your basket while avoiding rotten apples, or catching golden coins while avoiding worthless rocks.
For the sake of keeping things simple and because most of you with a background in webdesign and development already know what RGB stands for, we'll be catching falling blocks with a specific color: red, green or blue.
We will also implement a somewhat unique feature: the color of the basket indicates which blocks you should catch. For example, if the basket is blue, you are only supposed to catch blue blocks.
Now that we've got our concept ready, we must think of a name to bind it all together. At this point, you could make use of your creativity and brainstorm names, like 'Zealof', 'Xymnia' and 'Doogle' - but a better idea is to come up with a name that is related to the game itself.
As the main point of the game is 'catching' 'RGB' colored 'blocks', a good name could be something along the lines of 'RGBlock' or 'CatchBlock'. I chose to call the game 'RGBCatcher', referring to a possible job title for the player of the game.
A Simple Sketch
Whether you're in the initial stages of building a game, or designing your new portfolio website, a simple sketch is very useful for quickly capturing the rough edges of an idea for later usage.
Long before I began to write a single line of code, I first made a sketch of how things should look.
I surrounded objects with arrows to indicate their movement capabilities, and also added colored dots to help me remember that the color of the basket and the blocks are not supposed to be static.
The whole thing should also have some kind of an interface, where the player can read his health (HP) and score (PT) from. Though I did the original sketch with a simple pencil on paper, it looked similar to the image below.
Don't overdo your sketches; you should use it as a tool to capture the rough edges of an idea - not to paint or describe in words exactly each step of how things should look or work. That's what mockups and prototypes are for.
Getting Started
Before we even start writing a single line of JavaScript, we should first get our HTML5 canvas tag ready. canvas
is the HTML5 tag which cooperates with the Canvas 2D API. That's right: cooperates.
It's a common misconception that the new HTML5 functionality does not require JavaScript to run. HTML5 is — just like previous versions — a markup language and is not capable of making the web dynamic all on its own.
Organization of our project
Lack of organization is definitely a no-go, so let's create a new directory and call it exactly the same as the name of the game — it will be holding all the required files for our project.
In this directory, you should create a child directory, called 'js', which will hold the 'rgbcatcher.js' file. We'll be writing JavaScript later, too, so it's a wise idea to have a text editor with at least syntax highlighting and line number support for your own comfort.
In the root of the 'RGBCatcher' directory, create a textfile, called 'index' with the regular HTML extension and open it with your favorite text editor — index.html will function as the starting point for our game.
The HTML
There's no fancy things going on with the markup - just the HTML5 doctype, a reference to rgbcatcher.js, and the canvas tag with very limited styling applied should do the trick.
<!DOCTYPE html> <html> <head> <title>RGBCatcher</title> <script src="js/rgbcatcher.js"></script> <style type="text/stylesheet"> body, html { margin: 0px; padding: 0px; } #canvas { border: 1px solid #eee; margin: 10px; } </style> </head> <body> <canvas id="canvas" width="398" height="198"></canvas> </body> </html>
As you can see, the HTML5 doctype is significantly simpler, compared to most of the current main stream doctypes. Among other useful things, this new doctype allows for quicker instantiation of a standards compliant HTML document.
The JavaScript
In the sketch, each instantiated object has a cross in it. Keeping this in mind: we can easily make a list of which function definitions we will need:
- A health indicator (countable)
- A score counter (countable)
- A block (movable)
- A basket (controlled by the player, also movable)
So why is there only one block in the list, while we can see five in the sketch? Even though JavaScript is prototype based, we can instantiate multiple block instances from one base block definition.
The Object Wrappers
Now that we've got our object list ready, it shouldn't be too hard to create wrappers for them.
var Basket = function() { } var Block = function() { } var Health = function() { } var Score = function() { }
Hang on — the block and basket object are both of the type 'movable' (their coordinates can change), so wouldn't it be useful to have a base movable object, which the block and basket object can later inherit from?
var Movable = function() { }
The block and basket object are not the only objects which have certain functionality in common — the health indicator and the score counter are both designed to keep track of a specific value: the score and health, respectively.
var Countable = function() { }
That's it. Even though they have no functionality at all, our wrappers are done!
Global Definitions
Before we go any further, we need two things: a few global variables which are accessible from any function requiring it, and a global wrapper. This global wrapper functions as a main handler for our game.
In general, global definitions are a bad thing, due to JavaScript having only one single namespace. This makes it easier for variable and function names to collide between various scripts on the same page, but as this game is pretty much supposed to run as a stand-alone, we shouldn't worry too much about it.
All global variables will be put at the top of the rgbcatcher.js file. The global RGBCatcher wrapper will be placed at the bottom.
// This will hold the DOM object of our canvas element var canvas; // This will hold the Canvas 2D API object var context; // This object will hold pressed keys to check against var keyOn = []; // [object definitions] RGBCatcher = new function() { // This is the object which holds our game-colors, the 'this' keyword is used to make it accessible from outside this function (aka a public property) this.colors = [ '#f00', // Red '#0f0', // Green '#00f', // Blue ]; }
We also define a new global rand
function, which will make it easier for us to grab a random number in a specific range. This will, for example, be used to select a random color from the RGBCatcher.colors
array.
function rand(min, max) { return Math.random() * (max-min) + min; }
The Movable Base Function
Because both the basket and the block object are movables, which means that their coordinates are not constant, we will first write a base object they can inherit from.
The movable base object should only contain functionality, which will be present in both the block and the basket function definition.
The constructor
A constructor is the routine, which is called as soon as a new object is instantiated and before the object has a prototype assigned to it. This makes a constructor particularly useful for initializing object specific variables, which are then available in the scope of the object.
Because the basket and block object will hold different properties, we will write a constructor which accepts an array and then uses that array to set object specific variables.
// Add a parameter called 'data' so we can access the contents of an argument used at the instantiation of the Player object in the constructor var Movable = function(data) { if (data === undefined) return; for (var i = 0; i < data.length; i++) { var setting = data[i]; // By accessing 'this' (which refers to this very instance) as an array, we can set a new object-specific variable with the name of 'setting' to 'setting' its value this[setting[0]] = setting[1]; } // When this object is succesfully instantiated; it's alive! this.alive = true; }
Updating the Movable object
The update process of a movable object, which consists of the movement and drawing phase, should only occur when a movable object is still alive.
Because the actual movement and drawing process of a movable object probably depends on its inheritor, the base movable object should not define them unless all the inheritors share the same update or drawing method.
Movable.prototype = { update: function() { if (this.alive) { this.move(); this.draw(); } } };
The Basket
Now that we've defined the movable object, we only need to write an additional move
and draw
method for the basket object which inherits from it.
Basket specific variables
Before we start with the basket object's methods, we should first define a few variables specifically related to this object. These variables will be held by the RGBCatcher wrapper.
The basket
variable will hold an instance of the basket object. The basketData
variable is an array which holds data about the basket, like its height and horizontal movement speed.
var basket; var basketData = [ ['width', 30], // Width in pixels of the basket ['height', 10], // Height in pixels of the basket ['xSpeed', 1.1], // Horizontal movement speed in pixels ['color', undefined], // The color of the basket ['oldColor', undefined] // The old color of the basket, we can check against to prevent having the same basket color twice ];
The constructor
As the movable base already has the constructor and update method required for the basket function, we're an inheritance away from implementing it.
var Basket = function(data) { Movable.call(this, data); } Basket.prototype = new Movable();
Resetting the Basket
We want to both center the basket at the bottom of the screen, and change the color of the basket as soon as a new game or level begins.
By dividing the width of our basket by two and subtracting this value from the canvas's width, also divided by two, as the result, we will get a perfectly centered basket.
To position the basket at the bottom of the screen, we set the y
coordinate to the basket's height, subtracted from the canvas's height.
Basket.prototype.reset = function() { this.x = canvas.width / 2 - this.width / 2; this.y = canvas.height - this.height; }
Resetting the basket's color is a little bit more difficult.
We use a while loop which will randomly select a color from the public RGBCatcher.color
array on each spin.
As soon as a color is picked which does not equal the old color of the basket, the loop stops spinning and a new oldColor
is assigned.
Basket.prototype.reset = function() { // Reset the position this.x = canvas.width / 2 - this.width / 2; this.y = canvas.height - this.height; // Reset the color while (this.color == this.oldColor) this.color = RGBCatcher.colors[Math.round(rand(0, (RGBCatcher.colors.length-1)))]; // Change the old color to the current color (so that the while loop will stil work the next time this method is called) this.oldColor = this.color; }
Updating the Basket
As the movable object already brings functionality for an update
method, we only need to define specific methods to move and draw the basket.
The method which will move the basket, will, depending on the input of the user, add or subtract the horizontal movement speed (basket.xSpeed
) from the basket's current x coordinate.
The move method is also responsible for moving the basket back to its latest possible position if the basket moves out of the canvas element its viewport — this requires us to implement a very simple routine for collision detection.
Collision detection should never be implemented by solely checking on whether an object's coordinate matches a specific expression. This might work for graphics which are 1px by 1px, but the graphical view of anything bigger than that will be completely off as there should also be held account with the width and height.
That's why most of the time you'll need to subtract or add the shape its width or height to respectively the x or y coordinate to match the graphical representation of a collision.
Basket.prototype.move = function() { // 37 is the keycode representation of a left keypress if (keyOn[37]) this.x -= this.xSpeed; // 39 is the keycode representation of a right keypress if (keyOn[39]) this.x += this.xSpeed; // If the x coordinate is lower than 0, which is less than the outer left position of the canvas, move it back to the outer left position of the canvas if (this.x > 0) this.x = 0; // If the x coordinate plus the basket's width is greater than the canvas's width, move it back to the outer right position of the canvas if (this.x + this.width < canvas.width) this.x = canvas.width - this.width; }
Now that the user is able to move its basket, the only thing we still have to do is to write a method which has the ability to draw the basket on the screen — this is where the Canvas 2D API jumps in.
You might think that making use of this API is extremely sophisticated, but really, it is not! The Canvas 2D API is designed to be as simple, but yet as functional as possible.
We only require two Canvas 2D API dependencies: the fillStyle
property and the fillRect
method.
The fillStyle
property is used to define a fill color for shapes.
The value should follow the CSS3 specification for colors — this means you are allowed to use 'black' but also its HEX equivalent, '#000' — but also works with gradients and patterns but we will use a simple fill color.
The fillRect
method draws a filled rectangle, by making use of fillStyle
as its fill color, at position (x, y) and a defined width and height in pixels.
Note that the required fillRect
parameters can be found as properties in an instantiated basket object, so we can just use the this
keyword in combination with the name of the parameter.
Basket.prototype.draw = function() { // The Basket object's 'color' atribute holds the color which our basket needs to be context.fillStyle = this.color; // The C2A fillRect method draws a filled rectangle (with fillStyle as its fill color) at position (x, y) with a set height and width. // All these arguments can be found in the atributes of our basket object context.fillRect(this.x, this.y, this.width, this.height); }
Final function definition
That's it; we've got our entire basket object up and running.
Well, I wouldn't exactly call it running, so let's set up the game loop and other various core elements of our game, so we can put this basket to use!
var Basket = function(data) { Movable.call(this, data); } Basket.prototype = new Movable(); Basket.prototype.reset = function() { // Reset the position this.x = canvas.width / 2 - this.width / 2; this.y = canvas.height - this.height; // Reset the color while (this.color == this.oldColor) this.color = RGBCatcher.colors[Math.round(rand(0, (RGBCatcher.colors.length-1)))]; // Change the old color to the current color (so that the while loop will stil work the next time this method is called) this.oldColor = this.color; } Basket.prototype.move = function() { // 37 is the keycode representation of a left keypress if (keyOn[37]) this.x -= this.xSpeed; // 39 is the keycode representation of a right keypress if (keyOn[39]) this.x += this.xSpeed; // If the x coordinate is lower than 0, which is less than the outer left position of the canvas, move it back to the outer left position of the canvas if (this.x < 0) this.x = 0; // If the x coordinate plus the basket's width is greater than the canvas's width, move it back to the outer right position of the canvas if (this.x + this.width < canvas.width) this.x = canvas.width - this.width; } Basket.prototype.draw = function() { // The Basket object's 'color' atribute holds the color which our basket needs to be context.fillStyle = this.color; // The C2A fillRect method draws a filled rectangle (with fillStyle as its fill color) at position (x, y) with a set height and width. // All these arguments can be found in the atributes of our basket object context.fillRect(this.x, this.y, this.width, this.height); }
The RGBCatcher Wrapper
To actually implement the functions and prototype we just wrote, they have to be instantiated and called somewhere. This is what the main handler of our game is going to handle: instantiating and calling the right things at the right time.
The run method
The public run
method of the RGBCatcher wrapper, will initialize the game components (like the Canvas 2D API, an instance of the basket object and so forth).
It will also set up global event listeners for the keyup
and keydown
event so that current key presses can be easily retrieved from the global keyOn
array.
RGBCatcher = new function() { // Public this.colors = [ '#f00', '#0f0', '#00f', ]; // Private var basketData = [ ['width', 30], ['height', 10], ['xSpeed', 1.1], ['color', '#f00'], ['oldColor', '#f00'] ]; var basket; this.run = function() { // Set the global 'canvas' object to the #canvas DOM object to be able to access its width, height and other attributes are canvas = document.getElementById('canvas'); // This is where its all about; getting a new instance of the C2A object — pretty simple huh? context = canvas.getContext('2d'); // Add an eventListener for the global keydown event document.addEventListener('keydown', function(event) { // Add the keyCode of this event to the global keyOn Array // We can then easily check if a specific key is pressed by simply checking whether its keycode is set to true keyOn[event.keyCode] = true; }, false); // Add another eventListener for the global keyup event document.addEventListener('keyup', function(event) { // Set the keyCode of this event to false, to avoid an inifinite keydown appearance keyOn[event.keyCode] = false; }, false); // Instantiate the basket object and feed it the required basketData basket = new Basket(basketData); // At the start of a new game, this method is called to set dynamic variables to their default values resetGame(); } }
Resetting the game
As we only have one single game related object to reset so far, which is the basket object, we only need to call this object's reset methods to reset the game.
RGBCatcher = new function() { // [...] function resetGame() { basket.resetPosition(); basket.resetColor(); } }
Note that this method should only be called at the start of a new game. Because we do not have any level management yet, for now it's called from the RGBCatcher's run
function.
The game loop
The game loop is responsible for updating and redrawing all the objects of a game until a specific event occurs, like the user running out of health.
Game loops come in all sorts of formats and sizes, but the general order of a game loop is checking for input, updating and moving game objects, clearing the screen and then redrawing all the game objects to the screen.
By simply calling the basket object's update
method, we already process most of the steps found in the game loop.
The only additional line of code to write is the line which clears the screen. As our screen is the canvas element, the Canvas 2D API is required to achieve this.
Clearing a canvas by making use off the Canvas 2D API is done via the clearRect
method. It accepts four parameters: the x and y coordinate to start clearing from and the width and height to clear.
Because we need to clear a rectangle which overlaps the entire canvas, we start at the point (0, 0) and set the height and width to respectively the height and width of the canvas element.
RGBCatcher = new function() { // [...] function gameLoop() { // Clear the entire canvas context.clearRect(0, 0, canvas.width, canvas.height); } }
Now that our game loop clears the canvas at each spin, also called a frame, we only need to call the basket object's update method as it handles the move, update and redraw process all by itself.
RGBCatcher = new function() { // [...] function gameLoop() { context.clearRect(0, 0, canvas.width, canvas.height); basket.update(); } }
An entry point
The only thing still missing for performing a first test run of our game, is an entry point for the game loop.
The entry point's sole purpose is to start the game loop and define an interval for each loop its spin — setInterval
is exactly what we need.
RGBCatcher = new function() { // [...] // The variable associated with the setInterval ID var interval; this.run = function() { // [...] // Set the interval variable to the interval its ID so we can easily abort the game loop later // The speed of the interval equals 30 frames per second, which should be enough to keep things running smoothly interval = setInterval(gameLoop, 30/1000); } }
As you might have noticed, once entered, our game loop runs infinitely. We will definitely change this later, but for now we are still in the early stages of the development process.
Let's Give it a Go!
Now that the master RGBCatcher wrapper and basket object have been defined, we can finally test our premature game!
Because our game depends on the canvas element, we must wait until it's rendered before we call the RGBCatcher its public run
method.
This could be done by including the JavaScript file after the markup of the canvas element, but if you want to keep your JavaScript in the head-element, just attach the run
method to the window
its onload
event.
window.onload = function() { RGBCatcher.run(); }
Fire up the index.html file located in the RGBCatcher directory and try to move the basket by making use of the left and right arrow keys.
Also, try to refresh the page a few times to check whether the basket's color is actually changing or not. Don't worry if you see a repeating color — the old color is not being saved on a page refresh.
Clearly it's not very spectacular yet, so feel free to carry on because we are going to add some falling blocks.
The Block
A catch-the-falling-objects game is not really worth playing without falling objects to catch so let's continue and make those blocks fall!
We will start with writing up the code for the block object and in no time you will see red, green and blue blocks falling down your screen.
Block specific variables
Just like the basket object, a block object needs an array filled with data to store specific properties of a block, like its width and vertical movement speed.
This data array goes in the RGBCatcher wrapper.
var blockData = [ ['width', 10], // Width in pixels of this block ['height', 10], // Height in pixels of this block ['ySpeed', 2], // Vertical movement speed in pixels ['color', undefined] // The color of this block ];
So why is there, unlike the basket
variable, no block
variable which will hold an instance of the block object?
Because a level should contain more than just one block to catch.
In fact, we will make use of some sort of a block manager which will take care of managing blocks — more about that later.
The constructor
The constructor of our block object is exactly the same as the basket object's constructor.
var Block = function(data) { Movable.call(this, data); } Block.prototype = new Movable();
Notice how we would have quite some lines of duplicate code if we would not have been making use of a base movable object definition?
Resetting the Block
As we want to prevent that all blocks fall down at exactly the same position, we use the rand
function to put a block at a random horizontal position.
Instead of having a resetPosition
method, we will call the method which achieves the random positioning initPosition
as this method is about initializing the position, not resetting it.
Initialization? Isn't the constructor supposed to do that? It is, so we will only call the initPosition
method once, in the constructor.
var Block = function(data) { Movable.call(this, data); this.initPosition(); } Block.prototype = new Movable(); Block.prototype.initPosition = function() { // Only allow to set the position of this block once if (this.x !== undefined || this.y !== undefined) return; // By picking a rounded number between 0 and the canvas.width subtracted by the block's width, we have a position for this block which is still inside the block's viewport this.x = Math.round(rand(0, canvas.width - this.width)); // By setting the vertical position of the block to 0 subtracted by the block's height, the block will look like it slides into the canvas's viewport this.y = 0 - this.height; }
A block will now automatically initialize its position on instantiation, but it does not yet initialize its color.
First, we must alter the block function's constructor so that, next to initPosition
, a call will be made to initColor
at instantiation.
var Block = function(data) { Movable.call(this, data); this.initPosition(); this.initColor(); }
For now, this will throw an "Object #initColor
method to the prototype.
Block.prototype.initColor = function() { if (this.color !== undefined) return; this.color = RGBCatcher.colors[Math.round(rand(0, (RGBCatcher.colors.length-1)))]; }
Updating a Block
The movable prototype already brings functionality for a collective update method, so yet again that's something we should not be worried about.
Because there's no input required for a block to fall down, the method which makes a block move, is very straight forward: keep adding the vertical fall speed to the current vertical position.
Block.prototype.move = function() { // Add the vertical speed to the block's current position to move it this.y += this.ySpeed; }
The only thing left to write for our block object is the draw method, which is yet another task for the Canvas 2D API.
To draw our block we must first set a fill color and then we should draw a rectangle with parameters fetched from the block's properties.
Sounds familiair? It should. We did exactly the same trick with the basket object's draw
method, remember?
Because the basket and block object are both movables, we'll just move the basket's draw
method to the movable prototype so that all inheritors will share this draw
method.
Movable.prototype = { // [...] draw: function() { context.fillStyle = this.color; context.fillRect(this.x, this.y, this.width, this.height); } }
Final function definition
Those blocks are not going to fall all by themselves so let's proceed and add block handling to the RGBCatcher wrapper.
var Block = function(data) { Movable.call(this, data); this.initPosition(); this.initColor(); } Block.prototype = new Movable(); Block.prototype.initPosition = function() { // Only allow to set the position of this block once if (this.x !== undefined || this.y !== undefined) return; // By picking a rounded number between 0 and the canvas.width subtracted by the block's width, we have a position for this block which is still inside the block's viewport this.x = Math.round(rand(0, canvas.width - this.width)); // By setting the vertical position of the block to 0 subtracted by the block's height, the block will look like it slides into the canvas's viewport this.y = 0 - this.height; } Block.prototype.initColor = function() { if (this.color !== undefined) return; this.color = RGBCatcher.colors[Math.round(rand(0, (RGBCatcher.colors.length-1)))]; } Block.prototype.move = function() { // Add the vertical speed to the block's current position to move it this.y += this.ySpeed; }
Handling the Blocks
Although the basket is able to move without the help of a computer controlled handler, a block is definitely not.
It requires a supervisor or rather a manager, which is, among other things, able to remove blocks from the screen as soon as they hit the ground. This supervisor will find its place in the RGBCatcher master wrapper.
- Initialize: Initializing variables related specifically to the blocks, like a variable which indicates how many blocks are still on the screen.
- Reset: Resetting all the initialized variables to their default values.
- Update blocks: As soon as the initialization is finished, the game loop is entered and we'll start with looping through all the current available blocks and updating them.
- Check for collision: For each block we should check whether it is currently in a collision or not and perform specific actions on specific collisions.
- Add blocks if required: When all the blocks have been updated succesfully and collisions have been handled, new blocks should be spawned if this is required.
Block handling specific variables
We require five more variables for handling the blocks. They all go in the RGBCatcher function definition as private properties.
// The amount of blocks there should be per level (example: level 3 equals has (3*4) 12 blocks to process) var blocksPerLevel = 4; // The time in seconds there should be between the fall of a new block. This value decreases when the user goes up a level var blocksSpawnSec; // The amount of blocks already spawned var blocksSpawned; // The amount of blocks currently on the canvas var blocksOnScreen; // The array which holds the blocks to process var blocks = [];
Reset
The RGBCatcher object wrapper already has a resetGame
method. We now only have to add default values for the variables defined above, so they are reset at the start of a new game.
RGBCatcher = new function() { // [...] function resetGame() { basket.resetPosition(); basket.resetColor(); blocksSpawnSec = 2.5; blocksSpawned = 0; blocksOnScreen = 0; blocks = []; } }
Update blocks
The update blocks phase comes down on looping through all the blocks and updating them. It's also responsible for initiating the next phase of the block management which consists of calling a method to handle the collision detection.
RGBCatcher = new function() // [...] function updateBlocks() { for (var i = 0; i < blocks.length; i++) { // Assign a local copy var block = blocks[i]; block.update(); checkCollision(block); } } }
Now we are getting somewhere. Each block in the blocks
variable is updated by making use of the block object's update
method.
When the block has been updated, it will be pushed to the checkCollision
method which will then handle collision detection.
Check for collision
Collision detection is one of the harder parts of game development. Underestimating it, might result in unexpected results.
Have you ever come across a game in which you were able to walk through walls and could shoot your enemies without properly aiming at them? Blame the developers who worked on the collision detection!
Those games were probably 3D, but, lucky for us, we only have to worry about two possible dimensions colliding with each other so practically there's a whole dimension less to go wrong with!
In our RGBCatcher game there is only a total of four collisions to worry about and we already dealt with the first two — remember the possible collision between the basket and the right or left bounds of the screen?
The last two collisions involve the falling blocks and whether they hit the basket or the ground.
Before we are checking whether there is an actual collision with the ground or the basket, we'll check whether the block has passed or is currently on the y line on which the basket resides.
As you can see in the image below, basing collision detection solely on the objects their coordinates is a bad thing as the visual representation of the block has already hit the basket before the coordinates match the collision.
RGBCatcher = new function() { // [...] function checkCollision(block) { // If the block is not defined or not alive, return if (block === undefined || block.alive === false) return; // If the block hasn't passed the vertical line the basket resides on, there's no way we are dealing with a collision yet if (block.y + block.height < basket.y) return; } }
When we are sure the block is visually on the same or on a higher y coordinate than the basket, we can safely check whether the x coordinate of the block is in the correct range.
Other than the y coordinate, there's not just one spot on which a block can horizontally collide with the basket. That's why we need to check if a block is in the range of the basket's total width to determine whether we're dealing with a collision or not.
The range of the basket cannot be described in just one static value, so we'll use a very basic algorithm.
This algorithm will determine whether the block's x position is greater or equal to the x position of the basket and whether the block's x position plus its width is lower than the basket's x
position plus its width.
RGBCatcher = new function() { // [...] function checkCollision(block) { // If the block is not defined or not alive, return if (block === undefined || block.alive === false) return; // If the block hasn't passed the vertical line the basket resides on, we're not dealing with a collision (yet) if (block.y + block.height < basket.y) return; // If the block's x coordinate is in the range of the basket's width, then we've got a collision if (block.x >= basket.x && block.x + block.width <= basket.x + basket.width) { } // If it's not, the block has missed the basket and will thus, eventually, collide with the ground. else { } } }
Editing and adding some game variables
Before we go on, we should first add and edit a few variables found in the scope of the RGBCatcher wrapper.
The variable to edit is the blockData
variable. We'll add a block property called strength — this value will be inflicted to the user's health if the user misses a correctly colored block or if it catches an incorrectly colored block.
On the other hand, the amount of strength will be added to the user's score if the user catches a block which has the same color as the basket.
Two other variables to add are called health
and score
. These variables will hold objects representing the graphical health bar and score counter later on, but for now they are just simple integer values.
// This is a percentage which starts at 100% var health = 100; // This is just a regular integer value var score = 0; var blockData = [ ['width', 10], ['height', 10], ['ySpeed', 2], ['color', undefined], ['strength', 30] // The strength of this block *new* ];
Catching a block
As our game's objective requires the user to only catch blocks, which have the same color as the basket, we'll first compare the block's color to the basket's color.
If they are the same, we've got a correct catch and the score should increase with the block's strength. On the other hand should an incorrectly colored block decrease the user's health by the block's strength.
In both cases the block should disappear from the screen and the blocksOnScreen
variable should decrease with one
RGBCatcher = new function() { // [...] // By passing a reference of the block object to the function, we can use the current very block to perform our collision detection function checkCollision(block) { // [...] // If the block's x-coordinate is in the range of the basket's width, then we've got a collision if (block.x >= basket.x && block.x + block.width <= basket.x + basket.width) { // Whether it's a correctly colored block or not, the current block should disappear and the amount of blocks on the screen should decrease with one if (block.alive == true) { block.alive = false; blocksOnScreen--; } // If the block's color matches the basket's current color, we've got a correct catch if (block.color === basket.color) // So give the player some points score += block.strength; else // Otherwise, inflict damage to the health of the player health -= block.strength; } // If it's not, the block has missed the basket and will thus, eventually, collide with the ground else { } } }
Missing a block
If the user misses a correctly colored block, the strength of the block should be inflicted to the player's health. However, there's nothing wrong with missing an incorrectly colored block and if that happens the game should just continue without further action.
In opposite to catching a block, a block should not immediately disappear from the screen. It should first fall down the view port of the canvas and then be removed from the blocks
array.
RGBCatcher = new function { // [...] function checkCollision() { // [...] // If it's not, the block has missed the basket and will thus, eventually, collide with the ground else { // The player missed a correctly colored block and no damage has been inflicted yet if (block.color === basket.color && block.strength > 0) { // So lets inflict damage to the health of the player health -= block.strength; // To prevent this block from inflicting damage again, we set its strength to 0 block.strength = 0; } // If the block's y coordinate is greater than the canvas's height, it has disappeared from the viewport and can be removed if (block.alive === true && block.y > canvas.height) { block.alive = false; blocksOnScreen--; } } } }
Add blocks if required
So how should our addBlock
method work?
It first checks whether the amount of required blocks for the current level (blocksPerLevel * level
) equals the amount of already spawned (blocksSpawned
) blocks.
If it doesn't, a new block object is spawned, it will be added to the blocks
array and the blocksSpawned
and blocksOnScreen
variables will be increased with one.
If it does, an expression is ran to determine if there are still blocks on the screen (blocksOnScreen
) which should land somewhere first.
If this is not the case and blocksOnScreen == 0
, we're safe to say that the level has been completed.
The addBlock
method returns true if there are still blocks to process and false if the correct amount of blocks have been spawned and removed from the screen.
RGBCatcher = new function // [...] function addBlock() { if (blocksSpawned != blocksPerLevel * level) { // Add a new block the the blocks array blocks[blocks.length] = new Block(blockData); // Both increase the amount of blocks on the screen and the amount of spawned blocks blocksSpawned++; blocksOnScreen++; } else { // Check whether all blocks have been processed if (blocksOnScreen == 0) return false; } // Return true if there's still something to work with return true; } }
The Info Screen
We could implement the just written functions in the gameloop and start playing immediately, but wouldn't it be a lot nicer to have a title screen which waits on a response of the user instead of immediately starting with blocks falling down the screen?
Let's write up a function which displays a fancy title screen and waits for input from the user — the space bar sounds appropriate.
We could use the Canvas 2D API to render the title screen, but why not just use a HTML tag and use JavaScript to alter it.
Updating the HTML
The title screen will be put in a common div element tagged with an ID of info as it will function as an info screen too, not just for displaying the title screen.
With the dimensions of 200px * 26px, we use an absolute position and simply calculate the 'top' and 'left' margin of the info element to nicely center it above the canvas element.
For the top position we add the top margin of the #canvas
element to the total height, minus the height of the info element and divide it by two.
We do the same thing for the left position, except that instead of using the height, we use the width of both the #canvas
and the #info
element to calculate the correct position.
<!DOCTYPE html> <html> <head> <title>RGBCatcher</title> <script src="http://tutsplus.s3.amazonaws.com/tutspremium/web-development/172_game/js/js.js"></script> <style> body, html, p { margin: 0px; padding: 0px; } #canvas { border: 1px solid #eee; margin: 10px; } #info { position: absolute; top: 92px; /* (10px top margin + 200px of total height for the #canvas element - 26px of total height for the #info element) / 2 */ left: 105px; /* (10px left margin + 400px of total width for the #canvas element - 200px of total width for the #info element / 2 */ text-align: center; font: 10px sans-serif; background-color: #fff; width: 200px; height: 26px; } /* Some styling for the title screen */ span.red { color: #f00; } span.green { color: #0f0; } span.blue { color: #00f; } span.red, span.green, span.blue { font-weight: bold; } </style> </head> <body> <canvas id="canvas" width="398" height="198"></canvas> <div id="info"></div> </body> </html>
Now we got our HTML and CSS sorted again, we can return to the much more interesting process of writing the code which displays the actual title screen.
The titleScreen function
The titleScreen
function is a child-function (method) of the RGBCatcher function wrapper.
Before we write it up, we need to add two private variables — info
and infoScreenChange
.
info
holds a DOM object which gives us the necessary tools to alter the info element.
The infoScreenChange
boolean, which defaults to true, helps us to determine whether the info screen should be updated or not.
// A DOM Element of the info screen var info; // Should the info screen be changed? var infoScreenChange = true;
Summing it up: if the info screen has not yet been updated (infoScreenChange === true
) , do so.
Then just wait for a press on the space bar (keyOn[32]
) to hide the info screen, reset the game and enter the game loop.
RGBCatcher = new function() { // [...] function titleScreen() { // Should the info screen be updated? if (infoScreenChange) { // Set the HTML value of the info DOM object so it displays a fancy titlescreen info.innerHTML = '<p><span class="red">R</span><span class="green">G</span><span class="blue">B</span>Catcher</p> <p>Press spacebar to start</p>'; // Only update the info screen once infoScreenChange = false; } // 32 is the key code representation of a press on the spacebar if (keyOn[32]) { // Set the infoScreenChange variable to its default value again infoScreenChange = true; // Set the CSS 'display' rule of the info element to none so it disappears info.style.display = 'none'; // The player wants to start playing so the current 'titleScreen loop' will be cleared clearInterval(interval); // Reset the game resetGame(); // And enter the game loop at 30 frames per second interval = setInterval(gameLoop, 30/1000) } } }
There should be made some changes to the run
method for setting the local info
variable is set and hooking setInterval
up to the title screen instead of the game loop.
RGBCatcher = new function() { // [...] this.run = function() { // Set the global 'canvas' object to the #canvas DOM object to be able to access its width, height and other attributes are canvas = document.getElementById('canvas'); // Set the local 'info' object to the #info DOM object info = document.getElementById('info'); // This is where it's all about; getting a new instance of the C2A object — pretty simple huh? context = canvas.getContext('2d'); // Add an eventListener for the global keydown event document.addEventListener('keydown', function(event) { // Add the keyCode of this event to the global keyOn Array // We can then easily check if a specific key is pressed by simply checking whether its keycode is set to true keyOn[event.keyCode] = true; }, false); // Add another eventListener for the global keyup event document.addEventListener('keyup', function(event) { // Set the keyCode of this event to false, to avoid an inifinite keydown appearance keyOn[event.keyCode] = false; }, false); // Instantiate the basket object and feed it the required basketData basket = new Basket(basketData); // Go to the title screen at 30 frames per second interval = setInterval(titleScreen, 30/1000); } }
Feel free to give it a try.
Though there are no blocks falling down yet, we now have a title screen which is being displayed before the game starts and disappears as soon as the user presses the space bar.
Level Management
Now we've got a title screen up and running, it's about time to let those blocks fall.
A bit back, you might have noticed the yet undefined local level
variable in the RGBCatcher's private addBlock
method. This variable is a requisition of the level management routine.
Let's define it together with a new variable called levelUp
. With this boolean we are able to determine whether the user has recently completed a level or not.
// The level the player is currently at var level; // Has the player recently leveled up? var levelUp = false;
Looking back at the scheme for block management, we see a loop of updating, collision checking and adding blocks if this is required. As our game already has a spinning loop, which is the game loop, the repeating pattern goes in there.
RGBCatcher = new function() { // [...] function gameLoop() { context.clearRect(0, 0, canvas.width, canvas.height); basket.update(); updateBlocks(); } }
But what blocks will be updated if no calls are made to the addBlock
function?
Adding timer functionality
When a new block should fall down, depends on the blocksSpawnSec
variable. We'll add a simple timer to check against so we can determine whether we should call the addBlock
function or not.
The variables required to achieve this, are called frameTime
and startTime
.
var startTime = 0; var frameTime = 0;
frameTime
will be set at each spin of the game loop whereas startTime
should only be set at the start of a new timer. This first timer should start to run at the start of a new game.
We already have a function called resetGame
, which is called at the start of a new game, so we define the startTime
variable there. While we are at it, we might as well add a reset for the level
variable.
Note that the function we use to retrieve the current time, getTime
, returns an integer value representing the current time in milliseconds since 1970.
RGBCatcher = new function() { // [...] function resetGame() { basket.reset(); health = 100; score = 0; blocksSpawnSec = 2; blocksSpawned = 0; blocksOnScreen = 0; blocks = []; level = 1; startTime = new Date().getTime(); } }
Now we have an initial startTime
value, we can easily find out how much milliseconds have elapsed by simply extracting this value from the current frameTime
.
So after calling the updateBlocks
method, we check whether the current frame time is greater than the start time plus the seconds after which a new block should be spawned. If this is the case, we reset the timer and make a call to the addBlock
function — depending on its return value, appropriate actions will be performed.
RGBCatcher = new function() { // [...] function gameLoop() { frameTime = new Date().getTime(); context.clearRect(0, 0, canvas.width, canvas.height); basket.update(); updateBlocks(); // blocksSpawnSec * 1000 because getTime() returns a value in miliseconds if (frameTime >= startTime + (blocksSpawnSec*1000)) { // If all blocks have been processed if (addBlock() === false) { } // Reset the timer startTime = frameTime; } } }
Going up a level
If the addBlock
function returns false
, we can tell that the current level has been completed, so we set the levelUp
variable to true.
We also increase the difficulty of the game by slightly decreasing the time between blocks fall (blocksSpawnSec
) and slightly increasing the speed at which blocks fall (blockData['ySpeed']
). These in- and decrements are part of the game play — feel free to adjust them later if you think these are too low or too high.
As the level has been completed, a few variables should be reset. This routine is not exactly the same as the reset procedure of a game, so writing a fitting function for that is the next thing we'll do.
RGBCatcher = new function() { // [...] function gameLoop() { frameTime = new Date().getTime(); context.clearRect(0, 0, canvas.width, canvas.height); basket.update(); updateBlocks(); // blocksSpawnSec * 1000 because the timer values are in miliseconds if (frameTime >= startTime + (blocksSpawnSec*1000)) { // If all blocks have been added if (addBlock() === false) { // The player should go up a level levelUp = true; level++; // Increase difficulty blocksSpawnSec *= 0.99; blockData['ySpeed'] *= 1.01; basketData['xSpeed'] *= 1.02; // Reset level specific variables resetLevel(); } // The timer is finished, reset it startTime = frameTime; } } }
Resetting a level
Considering the concept of our game, the color and the position of the basket should be reset at the start of a new level. To award the player for completing the level, we will reset its health.
Obviously, the blocksSpawned
, blocksOnScreen
and blocks
variable should also be reset.
RGBCatcher = new function() { // [...] function resetLevel() { basket.reset(); health = 100; blocksSpawned = 0; blocksOnScreen = 0; blocks = []; } }
Game Over
Now we have a resetLevel
implementation, we can continue with working on the game loop.
What's still missing from it is a check on the user's health to determine whether it still has enough health to continue, and a routine which lets the user know that a new level is starting.
The first thing is relatively simple, so let's start with that. Basically, we only have to check if the player's health is lower than 1.
RGBCatcher = new function() { // [...] function gameLoop() { frameTime = new Date().getTime(); if (health < 1) { basket.alive = false; // Game over } // [...] } }
So how should the game-over-procedure be implemented? At the point at which the user has less than 1/100 HP left, the game should end and a game over message should be shown.
This is best accomplished by quitting the current game loop and entering a new game over loop which will flash a message for a specific amount of time, three seconds sounds fine, after which the title screen is shown again.
RGBCatcher = new function() { // [...] function gameLoop() { frameTime = new Date().getTime(); if (health < 1) { basket.alive = false; // Abort the game loop and set a new loop for the game over screen clearInterval(interval); interval = setInterval(gameOverScreen, 30/1000); return; } // [...] } }
A new gameOverScreen
function should be defined which first clears the entire canvas, displays the game-over-message and after three seconds have elapsed, goes back to the title screen.
The game-over-message will be put on the info screen. Displaying the message for just three seconds is easily achieved by using the timer functionality you should be familiar with now.
RGBCatcher = new function() { // [...] function gameOverScreen() { frameTime = new Date().getTime(); // Should the info screen be changed? if (infoScreenChange) { // First clear the canvas with the basket and blocks from the background context.clearRect(0, 0, canvas.width, canvas.height); // Change the text of the info screen and show it info.innerHTML = '<p>Game over!</p>'; info.style.display = 'block'; // Do not update the info screen again infoScreenChange = false; } // If three seconds have passed if (frameTime > startTime + (3*1000)) { // A new info screen should be pushed next time infoScreenChange = true; // Reset the timer startTime = frameTime; // Quit this loop and set a new the loop for the title screen clearInterval(interval); interval = setInterval(titleScreen, 30/1000); } } }
Get ready for level 2!
What's missing from our updated game loop is a routine which handles the flashing of a level up message.
We already have a variable called levelUp
which helps us to determine whether a message should be shown or not.
RGBCatcher = new function() { // [...] function gameLoop() { frameTime = new Date().getTime(); if (health < 1) { basket.alive = false; // Abort the game loop and set a new loop for the game over screen clearInterval(interval); interval = setInterval(gameOverScreen, 30/1000); return; } if (levelUp) { return; } // [...] } }
So we just copy the gameOverScreen
code to the if-statement, change the message to be flashed and the actions performed after the three seconds mark is reached.
RGBCatcher = new function() { // [...] function gameLoop() { frameTime = new Date().getTime(); if (health < 1) { basket.alive = false; // Abort the game loop and set a new loop for the game over screen clearInterval(interval); interval = setInterval(gameOverScreen, 30/1000); return; } if (levelUp) { if (infoScreenChange) { // First clear the canvas with the basket and blocks from the background context.clearRect(0, 0, canvas.width, canvas.height); // Change the text of the info screen and show it info.innerHTML = '<p>Level ' + (level-1) + ' cleared!</p><p>Get ready for level ' + level + '!</p>'; info.style.display = 'block'; // Do not update the info screen again infoScreenChange = false; } // If three seconds have passed if (frameTime > startTime + (3*1000)) { // Flashing of the message has been completed levelUp = false; // Hide the info screen and force an update next time info.style.display = 'none'; infoScreenChange = true; // Set a new timer startTime = frameTime; } return; } // [...] } }
Feel free to give the game a go again. It's pretty much functional, except that there are still two things missing: a score counter and a health bar. We are making progress, now!
The Countable Base Function
A score counter and a health bar have one thing in common: they count a specific value. Therefore it's a good idea for them to have an object they can inherit from: a countable base object — just like we did with the movable base definition for the basket and block object.
Starting with the fact that both the health and score object should supply us with a method for changing their value, they should also have a method which returns the current value of the instantiated object and they will both have an update and movement routine.
The constructor
The constructor of the countable base, should only define properties which are shared by all the inheritors.
These are x
and y
for positioning, the value
property which holds the value of a countable (like an amount of health), the targetValue
property and the speed
property.
The speed
and targetValue
properties will be used for a graphical animation, but we will come back to that later.
var Countable = function() { this.x = 0; this.y = 0; this.speed = 2; this.value = 0; this.targetValue = 0; } Countable.prototype = { };
A reset
method should be declared by an inheritor which will set the object's properties to custom values for initialization.
Updating the Countable object
The update method of the countable prototype is identical to the update method of the movable prototype except that it does not require an 'is alive' check.
Countable.prototype = { update: function() { this.move(); this.draw(); } };
Setting the value of the Countable object
The targetValue
and value
property will be used to perform a basic graphical animation in which the health bar or score counter slides from its old value
to its new targetValue
.
This is achieved by not directly setting the value
property, but by using the sub-station called targetValue
which we can use to set up an animation.
Countable.prototype = { // [...] set: function(amount) { this.targetValue += amount; } };
By directly adding the desired amount of change to the target value, we can also use a negative amount. This gives us the ability to subtract, but also add a certain amount to the target value.
Moving the Countable object
The movement process of a countable object is not about changing its coordinates, like we did with the block and basket's move
routine, but about changing the current value
so it will eventually be the same as targetValue
.
Practically this will result in a process of the value
property slightly increasing or decreasing in value until it's at the same value as targetValue
.
As the value
property will be used to draw the score or the health bar, changing it over time instead of setting directly, results in an animation.
We should first determine whether the value is already equal to the target value. If this is the case, nothing should happen. If it isn't, the speed should be added (value < targetValue
) or subtracted (value > targetValue
) from the actual value.
As soon as the difference between the target and actual value is lower than the animation speed, the value should be set to the target value as adding or subtracting the speed from the value will be more than sufficient.
Countable.prototype = { // [...] move: function() { // If the difference between the target and actual value is lower than the animation speed, set the value to the target value if (Math.abs(this.value - this.targetValue) < this.speed) this.value = this.targetValue; else if (this.targetValue > this.value) this.value += this.speed; else this.value -= this.speed; } };
Final function definition
Now that we have a base Countable function definition, an inheritor only needs to define a reset
and draw
method... and it's ready to go! How convenient is that?
var Countable = function() { this.x = 0; this.y = 0; this.speed = 2; this.value = 0; this.targetValue = 0; } Countable.prototype = { update: function() { this.move(); this.draw(); }, change: function(amount) { this.targetValue += amount; }, move: function() { // If the difference between the target and actual value is lower than the animation speed, set the value to the target value if (Math.abs(this.value - this.targetValue) < this.speed) this.value = this.targetValue; else if (this.targetValue > this.value) this.value += this.speed; else this.value -= this.speed; } };
The Health Object
Obvious as it is, the health object is responsible for keeping track the health points of the user and drawing the health bar to the screen.
As the base countable function already supplies us with an update
, change
and get
method, we only need to define a reset
and draw
method.
The constructor
Though the value
and targetValue
properties of this object should be reset in the reset
method, the constructor defines the static x
and y
property.
If we look back at the sketch, we see that we planned to position the health bar in the top right corner with some additional margin from the canvas's bounds.
This is achieved by setting the x
property to the canvas's width subtracted by the width of the health bar (52px) and the desired right margin (10px). Secondly, we will set the y
property to 10, which is nothing more than the top margin.
We also make a call to the yet to be defined reset
method which will set the value
and targetValue
property at instantiation.
var Health = function() { Countable.call(this); this.x = canvas.width - 52 - 10; this.y = 10; this.reset(); } Health.prototype = new Countable();
Resetting the Health object
The base countable object already defines a few properties, but as not all the inheritors share the same exact property values, the inheritor its reset
method will set those properties to the correct values.
As we want to start the game with an animation of the health bar filling up, we'll set the starting health value at 1 and the target value at 100 instead of leaving them at their default values.
Health.prototype.reset = function() { // If we would leave it at a default of 0, the game would immediately end as it equals a loss of the game this.value = 1; this.targetValue = 100; }
Drawing the Health object
The health bar consists of three collaborating drawings; a container, an actual health bar which its width fluctuates and a small piece of text for the interface.
The container
The container is fairly simple to draw even though it introduces a new method of the Canvas 2D API: the strokeRect
method; a method for drawing a rectangle with a border specified by the current stroke style.
strokeRect
requires exactly the same parameters as fillRect
: x, y, width and height. To define the stroke its color, the Canvas 2D API its strokeStyle
property is used. We will leave it at its default value of black.
Just like the fillRect
method, a fill color can be defined by setting the fillStyle
property.
Health.prototype.draw = function() { // The container context.fillStyle = '#fff'; context.strokeRect(this.x, this.y, 50 + 2, 5 + 2); }
Note that we add two pixels to both the width and the height of the container. This is to actually make the container 50x5 pixels as the Canvas 2D API puts the border inside the defined width and height.
The health bar
The animation its logic is already handled by the inherited move
method of the countable object. In combination with the varying color, this will give changes made to the health percentage a nice transition.
Setting the Canvas 2D API its fillStyle
property accordingly to the health percentage is incredibly simply when using a few if statements.
Health.prototype.draw = function() { // The container context.fillStyle = '#fff'; context.strokeRect(this.x, this.y, 50 + 2, 5 + 2); // The bar if (this.value >= 50) context.fillStyle = '#00ff00'; else if (this.value >= 25) context.fillStyle = '#fa6600'; else if (this.value >= 0) context.fillStyle = '#ff0000'; }
The process of drawing the health bar isn't much more difficult to do; we only have to draw a filled rectangle (fillRect
) at the same position of the container, except that we add one pixel to both the width and the height to position it within the borders of the container.
The width of the health bar is simply found by multiplying the current health percentage (value
) with the scale ratio of the container its blank space (50) and the possible maximum amount of health (100). 5 pixels should be sufficient as its height.
Health.prototype.draw = function() { // The container context.fillStyle = '#fff'; context.strokeRect(this.x, this.y, 50 + 2, 5 + 2); // The bar if (this.value >= 50) context.fillStyle = '#00ff00'; else if (this.value >= 25) context.fillStyle = '#fa6600'; else if (this.value >= 0) context.fillStyle = '#ff0000'; context.fillRect(this.x + 1, this.y + 1, this.value * (50/100), 5); }
The 'HP' text
Rendering text to the canvas can be achieved by making use of the fillText
or strokeText
method of the Canvas 2D API. A font definition can be set with the font
property (defaults to '10px sans-serif').
Just like rectangles, the fillStyle
and strokeStyle
properties are used to respectively set a fill or border color. Other than that, there are also properties for setting the align (textAlign
, defaults to 'start') and the baseline (textBaseline
, defaults to 'alphabetic') of the text.
We will use the fillText
method as we do not want to have a border around our text. fillText
requires the following parameters: a piece of text to print and an x and y position.
To simplify the process of putting the text on the right spot, we'll set the text its baseline to 'top'. There's still some padding inserted before the text is being drawn and therefore we will subtract 3 pixels from the y position to put it in align with the health bar.
Health.prototype.draw = function() { // [...] // The text context.fillStyle = '#000'; context.textBaseline = 'top'; context.fillText('HP', this.x - 25, this.y - 3); }
Final function definition
And with that, we have completed our health object.
The final element for us to put together before we can call it an end is the score object.
var Health = function() { Countable.call(this); this.x = canvas.width - 52 - 10; this.y = 10; } Health.prototype = new Countable(); Health.prototype.reset = function() { // If we would leave it at a default of 0, the game would immediately end as it equals a loss of the game this.value = 1; this.targetValue = 100; } Health.prototype.draw = function() { // The container context.fillStyle = '#fff'; context.strokeRect(this.x, this.y, 50 + 2, 5 + 2); // The bar if (this.value >= 50) context.fillStyle = '#00ff00'; else if (this.value >= 25) context.fillStyle = '#fa6600'; else if (this.value >= 0) context.fillStyle = '#ff0000'; context.rect(this.x + 1, this.y + 1, this.value * (50/100), 5); // The text context.fillStyle = '#000'; context.textBaseline = 'top'; context.fillText('HP', this.x - 25, this.y - 3); }
The Score Object
The score object inherits from the countable base object which, again, gives us the advantage of having a few methods less to write. This means we only have to write two methods.
One for drawing the text 'PT' (short-hand for points) and the current value
to the canvas and one for resetting the object's properties so the player will start fresh at the beginning of a new level.
The constructor
As the position of the score counter is static and is not required to be reset at the start of a new game, it's fine to define it in the constructor.
Whereas the x position should be the same as the health bar to align the interface nicely, the y position should be a bit lower. This comes down to 10 pixels for the top margin, an additional 7 pixels for the health bar its height and another 5 pixels for the margin between the health bar and the score counter.
var Score = function() { Countable.call(this); this.x = canvas.width - 50 - 10; this.y = 10 + 7 + 5; } Score.prototype = new Countable();
Resetting the Score object
The reset
method of the Score object should only be called at the start of a new game as the score should be taken along with the levels the player completes — setting the value
and targetValue
properties to zero is sufficient.
Score.prototype.reset = function() { this.value = this.targetValue = 0; }
Drawing the Score object
The entire drawing phasing of the score object consists of nothing more than putting two pieces of text on the canvas; 'PT' and the current score.
The 'PT' text can be positioned just like we did with the 'HP' text; altering the already defined x property a bit to position it with some whitespace from the actual score counter.
Score.prototype.draw = function() { context.textBaseline = 'top'; context.fillStyle = '#000'; context.fillText(this.value, this.x, this.y); context.fillText('PT', this.x - 25, this .y); }
Final function definition
That is that for the score object.
Let's continue to the final stage and actually implement the health and score object into our game.
var Score = function() { Countable.call(this); this.x = canvas.width - 52 - 10; this.y = 10 + 7 + 5; } Score.prototype = new Countable(); Score.prototype.reset = function() { this.value = this.targetValue = 0; } Score.prototype.draw = function() { context.textBaseline = 'top'; context.fillStyle = '#000'; context.fillText(this.value, this.x, this.y); context.fillText('PT', this.x - 25, this .y); }
Putting it All Together
To actually integrate the health and score counter into our game, we should update the master RGBCatcher function wrapper.
Starting with the run
method, we should add two lines to it for instantiating a new health and score counter object.
RGBCatcher = new function() { // [...] this.run = function() { // [...] basket = new Basket(basketData); health = new Health(); score = new Score(); // [...] } }
In the resetGame
and resetLevel
method the lines which set the health and score variable to zero, when they were still simple integer values, should be replaced with a call to the health and score object their reset
method.
score.reset(); // old definition: score = 0; health.reset(); // old definition: health = 100;
The gameLoop
method its routine to determine whether the user still has enough health to continue playing, still works with the health
variable as if it were an integer (health ).
RGBCatcher = new function() { // [...] function gameLoop() { // [...] if (health.value < 1) { clearInterval(interval); interval = setInterval(gameOver, 1000/targetFPS); } // [...] } }
To the gameLoop
method there should also be added a call to the health and score objects their update
methods. It fits nicely under the call to the basket object's update
method.
RGBCatcher = new function() { // [...] function gameLoop() { // [...] basket.update(); health.update(); score.update(); // [...] } }
The last thing we should change are increment and decrement calls to the old health and score integer values in the checkCollision
function in the RGBCatcher wrapper.
Those two variables are no longer integers and we should therefore use the health and score object their change
method to respectively inflict damage or add points to the score.
score += block.strength;; // becomes score.change(block.strength); // and health -= block.strength; // becomes health.change(- block.strength);
And we're done!
Conclusion
By using some common sense and dividing the elements of a game into separate tasks, it's not too difficult to hack up a basic game using HTML5 technologies.
Feel free to go on and continue exploring the marvelous world of the Canvas 2D API. For example, you could try to alter the drawing phase of the basket so it actually has a gap.
You could also think about improving the game by adding blocks, which will increase the user's health or add top scores by making use of HTML5's local storage functionality.
Whatever you do, have fun with it!
Comments