Part 1: Sprite Fundamentals
I’ve always loved web games; they’re just fun to make, easy to code (mostly), and there’s something really nice about how accessible a game is when the user just has to click a link to start playing.
Ajax and moving dom elements around made for some fun, but limited in what kind of experience you could create. For game developers, things are changing, and quickly. HTML5 is introducing a bunch of new options for game development purely in the browser, and the browser vendors are competing hard to be the best platform for the new standards.
So from a game developer’s perspective everything is going in the right direction: 2D and 3D hardware-acceleration, high-performance javascript engines, integrated debuggers and profilers, and, probably most importantly, browser vendors who are actively racing to be the best for serious game development.
So the tools are becoming usable, the browsers capable, and the vendors are listening, we can just go make awesome games right? Well, mostly.
HTML5/Javascript game development is still early, and there’s pitfalls to avoid, as well as choices to make on which technology to deploy.
In this article I’ll run through some of the choices to be made developing 2D games, and hopefully give you some ideas for developing your own games using HTML5.
The Basics
First question you’ll have to answer is whether to use the HTML5 Canvas tag for drawing images (a scene-graph), or by manipulating DOM elements.
To do 2D games using the DOM, you basically adjust element styles dynamically in order to move it around the page. Whilst there are some cases where DOM manipulation is good, I’m going to focus on using the HTML5 canvas for graphics since it’s the most flexible for games in a modern browser.
If you’re worried about compatible for older browsers and canvas check out excanvas (http://excanvas.sourceforge.net/).
Page Setup
To get going you’ll need to create an HTML page that contains the canvas tag:
<!doctype html> <html> <head> <title></title> </head> <body style='position: absolute; padding:0; margin:0; height: 100%; width:100%'> <canvas id="gameCanvas"></canvas> </body> </html>
If you load this up, you’ll be rewarded with, well, nothing much. That’s because whilst we have a canvas tag, we haven’t drawn anything on it. Let’s add some simple canvas calls to draw some boxes.
<head> <title></title> <script type='text/javascript'> var canvas = null; function onload() { canvas = document.getElementById('gameCanvas'); var ctx = canvas.getContext("2d"); ctx.fillStyle = '#000000'; ctx.fillRect(0, 0, canvas.width, canvas.height); ctx.fillStyle = '#333333'; ctx.fillRect(canvas.width / 3, canvas.height / 3, canvas.width / 3, canvas.height / 3); } </script> </head> <body onload='onload()' ...
In this example I’ve added an onload event binding to the body tag, and then implemented the function to grab the canvas element and draw some boxes. Simple enough so far.
The boxes are nice, but you’ll notice the canvas doesn’t take up the complete area of the browser window. To accommodate that we can set it’s size by adding a width and height style to the canvas tag. I prefer to keep things dynamic by adjusting the size based on the size of the document element the canvas is contained within.
var canvas = null; function onload() { canvas = document.getElementById('gameCanvas'); canvas.width = canvas.parentNode.clientWidth; canvas.height = canvas.parentNode.clientHeight; ...
Reload and you’ll see the canvas taking up the entire screen. Sweet.
Taking things a little further, let’s handle resizing of the canvas if the browser window is resized by the user.
var canvas = null; function onload() { canvas = document.getElementById('gameCanvas'); resize(); } function resize() { canvas.width = canvas.parentNode.clientWidth; canvas.height = canvas.parentNode.clientHeight; var ctx = canvas.getContext("2d"); ctx.fillStyle = '#000000'; ctx.fillRect(0, 0, canvas.width, canvas.height); ctx.fillStyle = '#333333'; ctx.fillRect(canvas.width/3, canvas.height/3, canvas.width/3, canvas.height/3); }
And add the onresize call to the body tag.
<body onresize='resize()' ...
Now if you resize the browser the rectangles will follow along nicely.
Loading Graphics
Most games are going to need animated sprites, so let’s add some graphics.
First up you’ll need get to an image resource. Since we’re going to be drawing it from within javascript, I find it makes sense to declare the image there and then set its src attribute to be the url of the image you want to load. Please download this image file, which is adapted from SpriteLib GPL: simba.png
var img = null; function onload() { ... img = new Image(); img.src = 'simba.png'; }
You can then draw the image by adding this to the resize method:
ctx.drawImage(img, canvas.width/2 - (img.width/2), canvas.height/2 - (img.height/2));
If you then reload the page, in most cases, you’ll see an image appear. I say most cases, because it depends on how fast your machine is, and whether the browser has cached the image already. That’s because the resize method is being called in between when you’ve started loading the image (setting its src attribute) and when the browser has it ready to go. With one or two images you might get away with it, but as soon as your game expands you’ll need to wait till all the images are loaded before taking action. To wait, add a notification listener to the image so you’ll get a callback when the image is ready. I’ve had to rearrange things a little to make it all work, so here’s the complete updated code:
var canvas = null; var img = null; var ctx = null; var imageReady = false; function onload() { canvas = document.getElementById('gameCanvas'); ctx = canvas.getContext("2d"); img = new Image(); img.src = 'images/simba.png'; img.onload = loaded(); resize(); } function loaded() { imageReady = true; redraw(); } function resize() { canvas.width = canvas.parentNode.clientWidth; canvas.height = canvas.parentNode.clientHeight; redraw(); } function redraw() { ctx.fillStyle = '#000000'; ctx.fillRect(0, 0, canvas.width, canvas.height); if (imageReady) ctx.drawImage(img, canvas.width/2 - (img.width/2), canvas.height/2 - (img.height/2)); }
And the results should be:
This image shows 6 running frames of a little vampire kitty (well, that’s what I think it looks like). To animate the sprite we need to draw each of the frames one at a time.
Sprite Animation
You can draw a single frame using the source parameters of the drawImage call. In effect, only drawing a constrained portion of the source image. So to draw only the first frame use the expanded version of drawImage that let’s you specify a rectangle in the source image. Since our cat animation is made up from 6 frames each 96 x 96 pixels in size, we can do:
ctx.drawImage(img, 0, 0, 96, 54, canvas.width/2 - 48, canvas.height/2 - 48, 96, 54);
The key thing here is the starting 0, 0, 96, 54
. That limits the image being drawn
to just the first frame of our cat animation. I’ve also adjust the centering to be based
on a single frame as well (the 48’s) rather than the entire image size containing all
six frames.
Now the fun bit. To make the animation work we need to track which frame to draw, then as time progresses advance the frame number. To do this we’ll need to go from a static page to one that is cycling on a timed basis.
Let’s start by doing things the old fashioned way. Add an interval timer with a cycle time equivalent to 60 frames per second (1000ms divided by 60). To make sure we only start cycling the animation after the image has loaded, put the call in the loaded function:
function loaded() {
imageReady = true;
setTimeout( update, 1000 / 60 );
}
Adding an update function can then step forward the frame, and call for the redraw:
var frame = 0;
function update() {
redraw();
frame++;
if (frame >= 6) frame = 0;
setTimeout( update, 1000 / 60 );
}
After the draw and frame has been advance the timeout is set again.
Next, modify the draw image to move the source window according to which frame we want to draw (the key piece being the source X position being set to frame multiplied by the size of the frame (in this case frame * 96):
function redraw() {
ctx.fillStyle = '#000000';
ctx.fillRect(0, 0, canvas.width, canvas.height);
if (imageReady)
ctx.drawImage(img, frame*96, 0, 96, 54,
canvas.width/2 - 48, canvas.height/2 - 48, 96, 54);
}
And the result:
Our evil undead-vampire-kitty lives! At super-cat speeds even.
Now we have an animation going let’s do some improvements.
requestAnimFrame
setTimeout
is good, and it works well in just about every browser, but there’s an
even better method, requestAnimFrame.
requestAnimFrame
basically acts as a setTimeout
, but the browser knows you’re
rendering a frame so it can optimize the draw cycle, as well as how that interacts
with the rest of the page reflow. It will even detect if the tab is visible and not
bother drawing if it’s hidden, which saves battery (and yes, web games cycling at
60fps will burn battery). Under the hood, the browsers also get the opportunity to
optimize in other mysterious ways they don’t tell us much about. In my experience
with heavier frame loads (hundreds of sprites especially) there can be substantial
gains in performance; especially on recent browser builds.
One caveat I’d add is that in some cases setTimeout
will outperform requestAnimFrame
,
notably on mobile. Test it out and config your app based on the device.
The call to use requestAnimFrame
is distinct across different browsers so the
standard shim (thanks to Paul Irish) to detect this is:
window.requestAnimFrame = (function(){
return window.requestAnimationFrame ||
window.webkitRequestAnimationFrame ||
window.mozRequestAnimationFrame ||
window.oRequestAnimationFrame ||
window.msRequestAnimationFrame ||
function( callback ){
window.setTimeout(callback, 1000 / 60);
};
})();
There’s also a built-in fall back to plain old setTimeout
if requestAnimFrame support
is not available.
You then need to modify the update method to repeatedly make the request:
function update() {
requestAnimFrame(update);
redraw();
frame++;
if (frame >= 6) frame = 0;
}
Calling the requestAnimFrame
before you actually carry out the render/update tends
to provide a more consistent result.
On a side note, when I first started using requestAnimFrame
I searched around for
how it would be timed, but couldn’t find anyting. That’s because it isn’t. There’s
no equivalent to setting the MS delay you’ll find with setTimeout
, which means
you can’t actually control the frame rate. Just do your work, and let the browser
take care of the rest.
Another thing to watch out for is if you are using requestAnimFrame
from within
your own closure, then you’ll need to do a native wrapping to call it, such as:
my.requestAnimFrame = (function () {
var func = window.requestAnimationFrame ||
window.webkitRequestAnimationFrame ||
window.mozRequestAnimationFrame ||
window.oRequestAnimationFrame ||
window.msRequestAnimationFrame ||
function (callback, element)
{
window.setTimeout(callback, 1000 / this.fps);
};
// apply to our window global to avoid illegal invocations (it's a native)
return function (callback, element) {
func.apply(window, [callback, element]);
};
})();
Time-based Animation
Next we need to solve the speed at which poor kitty has been running. Right now the animation frame advances according to the frame rate, which is going to jump around on different devices. That’s bad; if you’re moving a character and animating at the same time, things are going to look weird and inconsistent across different frame rates. You can try to control the frame rate but in the end basing animation on real timing is going to make for a better all round experience.
You’ll also find that timing in general in games is going to apply to everything you do: firing rate, turning speed, accerlation, jumping, they’ll all be better handled using proper timing.
To advance kitty at a regulated speed we need to track how much time has passed, and then advance the frames according to the time allocated to each one. The basics of this is:
- Set an animation speed in terms of frames per second. (msPerFrame)
- As you cycle the game, figure out how much time has passed since the last frame (delta).
- If enough time has passed to move the animation frame forward, then advance the frame and set the accumulated delta to 0.
- If enough time hasn’t passed, remember (accumulate) the delta time (acDelta).
Here’s this in our code:
var frame = 0;
var lastUpdateTime = 0;
var acDelta = 0;
var msPerFrame = 100;
function update() {
requestAnimFrame(update);
var delta = Date.now() - lastUpdateTime;
if (acDelta > msPerFrame)
{
acDelta = 0;
redraw();
frame++;
if (frame >= 6) frame = 0;
} else
{
acDelta += delta;
}
lastUpdateTime = Date.now();
}
If you load this up, our little kitty has calmed down to a more reasonable speed.
Scaling and Rotating
You can also use the 2D canvas to perform a variety of operations on the image as it’s rendered, such as rotation and scaling.
For example, let’s make some kittens by scaling the image down by half. You
can do this by adding a ctx.scale(0.5, 0.5)
to the draw call:
function redraw()
{
ctx.fillStyle = '#000000';
ctx.fillRect(0, 0, canvas.width, canvas.height);
if (imageReady)
{
ctx.save();
ctx.scale(0.5,0.5);
ctx.drawImage(img, frame*96, 0, 96, 54,
canvas.width/2 - 48, canvas.height/2 - 48, 96, 54);
ctx.restore();
}
}
Since the scaling is changing, you’ll notice I also added a ctx.save()
before the
scale call, then a ctx.restore() at the end. Without this, the calls to scale will
accumulate and poor kitty will quickly shrink into oblivion (try it, its fun).
Scaling also works using negative values in order to reverse and image. If you change the scale values from (0.5, 0.5) to (-1, 1) the cat image will be flipped horizontally, so he’ll run in the opposite direction. Notice that translate is used to flip the starting X position to offset the reversal of the image.
function redraw() {
ctx.fillStyle = '#000000';
ctx.fillRect(0, 0, canvas.width, canvas.height);
if (imageReady) {
ctx.save();
ctx.translate(img.width, 0);
ctx.scale(-1, 1);
ctx.drawImage(img, frame*96, 0, 96, 54,
canvas.width/2 - 48, canvas.height/2 - 48, 96, 54);
ctx.restore();
}
}
You can use rotate to do (duh) rotation. Here’s kitty climbing the walls:
ctx.rotate( 270*Math.PI/180 );
ctx.drawImage(img, frame*96, 0, 96, 54,
-(canvas.width/2 - 48), (canvas.height/2 - 48), 96, 54);
In this case, by rotating the context, the coordinates are rotated as well, not just the image, so the drawImage call offset for this by making the inverting the x position of where the kitty will be drawn.
Such a talented kitty (though vampires are supposed to be able to climb walls right).
The scaling and rotation is cool. Man I can do anything! Well, not really. It’s awesome, but it’s also slow and will have a pretty dramatic impact on rendering performance. In a production game there’s another trick to handling this, and a bunch of other rendering performance issues you might encounter: prerendering.
Prerendering
Pre-rendering is just taking images that you would have rendered during your regular draw cycle and assembling them or manipulating them before hand. You do the expensive rendering operation once, then draw the prerendered result in the regular draw cycle.
In HTML5, you need to draw on a separate invisible canvas, and then instead of drawing an image, you draw the other canvas in its place.
Here’s an example of a function that prerenders the kitty as a reversed image.
var reverseCanvas = null;
function prerender() {
reverseCanvas = document.createElement('canvas');
reverseCanvas.width = img.width;
reverseCanvas.height = img.height;
var rctx = reverseCanvas.getContext("2d");
rctx.save();
rctx.translate(img.width, 0);
rctx.scale(-1, 1);
rctx.drawImage(img, 0, 0);
rctx.restore();
}
Notice a canvas object is created, but not added to the DOM, so it wont be displayed. The height and width is set to the original spritesheet, and then the original image is drawn using the render buffer’s 2D context.
To setup the prerender you can call it from the loaded function.
function loaded() {
imageReady = true;
prerender();
requestAnimFrame(update);
}
Then when you make the regular redraw call, use the reverseCanvas, instead of the original:
function redraw() {
ctx.fillStyle = '#000000';
ctx.fillRect(0, 0, canvas.width, canvas.height);
if (imageReady) {
ctx.save();
ctx.drawImage(reverseCanvas, frame*96, 0, 96, 96,
(canvas.width/2 - 48), (canvas.height/2 - 48), 96, 96);
ctx.restore();
}
}
Unfortunately, when we reversed the image the animation now plays backwards as well, so you’ll need to reverse the animation sequence as well:
function update() {
requestAnimFrame(update);
var delta = Date.now() - lastUpdateTime;
if (acDelta > msPerFrame) {
acDelta = 0;
redraw();
frame--;
if (frame < 0) frame = 5;
} else {
acDelta += delta;
}
lastUpdateTime = Date.now();
}
If you need to, you can convert the canvas into an image by setting its source to use a data url containing the encoded image data. Canvas has a method to do this, so its as easy as:
newImage = new Image();
newImage.src = reverseCanvas.toDataURL("image/png");
Another nice image manipulation is to play with the actual pixel data. The HTML5 canvas elements exposes the image data as an array of pixels in RGBA format. You can gain access to the data array form a context using:
var imageData = ctx.getImageData(0, 0, width, height);
Which will return an ImageData structure containing width, height and data members. The data element is the array of pixels we’re after.
The data array is made up of all the pixels, with each pixel being represented by 4 entries, red, green, blue and the alpha level, all ranging from 0 to 255. Thus an image which is 512 wide by 512 high will result in an array that has 1048576 elements in it – 512x512 equals 262,144 pixels, multiplied by 4 entries per pixel.
Using this data array, here’s an example where the specific red component of image is increased, whilst the red and blue components are reduced, thus creating our level 2 monster, the hell-spawn-demon-kitty.
function prerender() {
reverseCanvas = document.createElement('canvas');
reverseCanvas.width = img.width;
reverseCanvas.height = img.height;
var rctx = reverseCanvas.getContext("2d");
rctx.save();
rctx.translate(img.width, 0);
rctx.scale(-1, 1);
rctx.drawImage(img, 0, 0);
// modify the colors
var imageData = rctx.getImageData(0, 0, reverseCanvas.width, reverseCanvas.height);
for (var i=0, il = imageData.data.length; i < il; i+=4) {
if (imageData.data[i] != 0) imageData.data[i] = imageData.data[i] + 100; // red
if (imageData.data[i+1] != 0) imageData.data[i+1] = imageData.data[i+1] - 50; // green
if (imageData.data[i+1] != 0) imageData.data[i+2] = imageData.data[i+2] - 50; // blue
}
rctx.putImageData(imageData, 0, 0);
rctx.restore();
}
The for loop is interating over the data array in steps of 4, each time modifying the three primary colors. The 4th channel, alpha, is left as is, but if you like you can use this to vary the transparency of certain pixels. (Note: in the JSFiddle example below, we use a dataURL for the image data, specifically to avoid cross-domain issues with direct pixel manipulation. You won’t need to do that on your own server.)
Here’s our level 2 boss kitty:
Since manipulating an image using the pixel array requires iterating over all the elements, this the case of hell kitty, that’s over a million times, you should keep things pretty optimized: precalulate as much as possible, don’t create variables/objects and skip pixels as much as possible.
Conclusion
The combination of canvas drawing, scaling, rotating, translating and pixel manipulation, along with the performance option of using prerendering gives a range of powers to make cool, dynamic games.
As an example, I used these techniques in one of Playcraft’s demo games recently, a 2D 4-way scrolling space shooter. The artists produced only a single frame of each ship (player and enemy fighters), which I would then rotate and prerender according to how many degrees, and thus how smooth, we wanted the ships to turn. I could adjust the number of angles based on the type of ship at run time – by default, player ships rendered with 36 turning angles (very smooth), whereas enemy and opponent ships at only 16 angles (choppy). I also added an option to let players on more powerful computers choose to increase the smoothness angles to 72 all round (super smooth). In addition, I dynamically recolor the emblems and markings on the ships (the cool big stripes along the wings) according to the team you’re on. This again saves on rendering and resources, but also allows the ship colors to be dynamically adjusted based on a user selected team color.
For more information on what you can do with canvas check out the Canvas Element API.
Comments