Forest Racer - A HTML5 Game in Under 13K
Tuesday, 11th September 2012, 20:46
So, earlier today I finished and submitted my entry to the Js13kGames competition. If you remember in my previous entry, I said I planned to make a Deatchchase style racing game, and in the end with barely half a kilobyte to spare, I managed to implement everything I wanted.
With more time, it would have been great to play with Mozilla's Audio Data API or whatever Chrome has as an alternative or equivalent. But I only found out about the competition last week, and with things being so busy I only had a few days to throw at it.
Overall I'm quite pleased with how it turned out, with more time and space I'd be able to do a lot more, but every single thing I wanted to get in, I managed to.
Thoughts on HTML5
Well, this really does remind me of the old IE5/IE6 and Netscape days, where so many things don't work properly in one browser yet do in another. It's frustrating that some things don't do what you expect anywhere, and others are very broken on some.
Like there is no way to manipulate directly the pixels of a Canvas area, the only way to read a pixel is to getImageData to pull a 1x1 pixel, which results in a completely unnecessary overhead. It's as if the people designing these APIs were smaller than a foetus in the golden age of home computing. Just as ridiculous is how you can use getImageData/createImageData to create an image buffer that you can manipulate directly, and then provide no simple way of setting/reading those pixels without accessing the 8 bit RGBA values.
I don't mind this too much, it's a very old school way of doing things that gives you the illusion of "hitting the hardware directly" without actually really doing that. But the advantage of doing that is that... you are hitting it directly to save time.
Another stupidism is with the putImageData method, which makes the alpha channel quite useless when it comes to overlaying sprites with it. The method should really have an option or sister method to merge the RGB data based on the alpha channel and pre-existing values on the canvas, rather than just overwriting them. To work around it I had to do the following long winded method:
- Create a new Canvas element
- Use putImageData to copy the image to the canvas
- Use toDataURL to copy the canvas to an image encoded URL
- Then create a new Image() element and assign that URL as the src
Only then could I write my sprites with transparencies to the main canvas.
Support for the <audio> tag, in particular currentTime property proved very sketchy. In Chrome the whole thing works most of the time, but sometimes you have to refresh the page to get it going properly, and you also have to put setting that property in a try/catch block otherwise it sometimes randomly throws an error.
Firefox doesn't seem to handle that property properly at all, you can set it to 0 as much as you like, but most of the time Firefox ignores your plee and just finishes the sample and then does nothing for ages despite requests to play() it.
Some Tricks I Used
One technique I used which may be of interest, relates to frame rate. I set a target framerate of 30 per second, and use some code to try achieving this as accurately as possible. The key is ONLY to rely on setTimeout() and use a bit of maths.
Firstly, take your 1000 milliseconds and divide that by your frame rate, so in this case we get roughly 33ms between frames. Then what you do is at the start of your function which is called every frame, you set a variable to the current time with the Date() object. At the end of your frame update, you calculate how long the frame took to generate, and then subtract that from the next setTimeout value.
That way, if a frame takes 5ms to calculate, the next update will be called in 28ms (33ms - 5ms), simple!
ForestRacer.prototype.processFrame = function() {
var dtStart = new Date();
var self = this;
dosomething();
var dtEnd = new Date();
var msDiff = dtEnd - dtStart;
var msNextFrame = (1000 / 30) - msDiff;
if (msNextFrame < 1)
msNextFrame = 1;
this.timeout = setTimeout(function() {
self.processFrame();
}, msNextFrame);
};
The audio is broken up into two files, one is a crash sound, the other is a revving engine. I keep a counter that causes the engine sound to loop faster depending on the speed, which gives you the illusion that the engine is going faster when really it isn't.
Keys are handled using an onkeydown/onkeyup routine, that sets properties of an object to true or false, whenever they match an object that lists keys with their keycodes. This should allow easy redefining of them, if I'd had room to add it.
this.keys = { left: 78, right: 77, up: 65, down: 90, start: 83, debug: 89 };
this.keysdown = {};
var self = this;
window.onkeydown = function(event) {
for (key in self.keys)
{
if (event.keyCode == self.keys[key])
self.keysdown[key] = true;
}
};
window.onkeyup = function(event) {
for (key in self.keys)
{
if (event.keyCode == self.keys[key])
self.keysdown[key] = false;
}
};
So you can see, this lets us check at any point with something like this
if (this.keysdown.right)
{
dosomething();
}
The added bonus is (for example) you could also then set the this.keysdown.right to false and that key would not be triggered again until either a key repeat or being repressed.
One last thing I did, was to have one single setTimeout function which performs all the generic requirements for each frame, and then just calls a member property which is set to whatever current game mode function is required. That way to swap game modes, all you have to do is re-assign the function to another member property.
Download
You can download the source code and game at Github, or you can just click and play it here, best use Chrome for better sound though.