I’ve written a couple of posts on asset management in web-based games already, and I still think they’re valid and useful on most of the points made in them.

However, in putting together my latest endeavour (a hack-n-slash isometric multi-player RPG in HTML5 canvas), I’ve learned a few more things and want to share them.

TLDR; I’ve pushed the code I’m currently using to Github and it can be found here: https://github.com/robashton/swallow

Anyway – moving on we can discover some of these learnings:

Simply waiting for requested assets to load via HTTP on start-up isn’t always enough

In my earlier post, I suggested if you had a resource caretaker which you requested resources like textures, sounds, models, shaders etc from – then it could take care of loading the data and return promises rather than the real things, this looked something like this:

1
2
3
4
5
6
7
8
9
10
11
var modelOne = resources.find('models/hovercraft.json');
var modelTwo = resources.find('models/missile.json');
var explosionSound = resources.find('sounds/explosion.wav');
 
resources.on('fullyLoaded', function() {
  game.start();
 
  // use the resources
  modelOne.get();
  modelTwo.get();
});

And the world is a happy place to live in, we rely on those assets coming down via HTTP, rely on HTTP caching and everything else given to us and it works well.

However, we also rely on the initial state of the game indicating which resources it is going to need – and doesn’t account for other resources that might be requested once the game is under way (for example, explosions, other models/textures further in the world, sounds, etc).

This results in negative artifacts like ‘popping’, or sounds being played after the special effect has finished (or in bad cases, the player walking on an empty background!)

Therefore the answer is to pre-load, but how…?

The Application Cache

Now, I’m not going to give a full description of this, but essentially you can tell the browser “Hey, these are my resources, please download and cache them”, for example:

1
2
3
4
5
6
7
8
9
CACHE MANIFEST
# 2012-03-11:v1
 
CACHE:
/favicon.ico
game.html
models/hovercraft.json
models/missile.json
sounds/explosion.wav

We can generate this kind of application cache automatically as part of our deploy process by enumerating through the assets, and we can choose not to re-generate it if none of the files have changed.

When we re-generate it, we can set the timestamp (therefore indicating to the browser that because the manifest has changed it might like to go through those assets on the server and see which ones it needs to download).

We also have similar code to write against this, “Update the resources if necessary, wait for this process to complete”

1
2
3
appCache.addEventListener('updateready', function() {
  game.start();
}, false);

Ace.

This has its share of issues though – the biggest one is probably that it doesn’t really allow for any granularity in your app.

In this modern age of the internet, our users have short attention spans and a long initial load may well mean losing out on users – if you have different levels for example, you probably want to download each level and the assets for that level before you play that level – not at the beginning of the whole game.

In short, the Application Cache is great if you have a small self contained game (such as puzzle games), or if you want to fully support the game being playable offline immediately after download;  it does however come with its share of issues.

That brings us onto another option

Write your own Application Cache

We can easily emulate what the AppCache does for us by writing our own manifest definition and using that to pre-load files from the server. We can then rely on the browser-cache to carry on working the way it always has (so if files haven’t changed, don’t fetch them etc).

This is essentially like our first solution again, except we now have manifest files which describe what data needs pre-loading.

1
2
3
4
5
6
7
8
9
10
11
12
resources.preloadFromManifest('levels/levelOne.json');
var modelOne = resources.find('models/hovercraft.json');
var modelTwo = resources.find('models/missile.json');
var explosionSound = resources.find('sounds/explosion.wav');
 
resources.on('fullyLoaded', function() {
  game.start();
 
  // use the resources
  modelOne.get();
  modelTwo.get();
});

This still causes some issues – the browser may choose to not cache some objects, may decide that some things aren’t available offline, and weirdly give us some issues with sounds.

For example: One issue I discovered with sounds in my last Ludum Dare attempt was that you have to create a new Audio object every-time you play a sound – and the browser re-requests the asset from the URL you give it each time that happens!

Another problem with this approach is again it doesn’t scale too well – if you’ve got 500 textures, 500 HTTP requests is not really appropriate at with the current technology stack (Until SPDY or something similar is supported universally at least).

That brings us to where I am at the moment with my hack-n-slash multiplayer canvas RPG…

Bundle everything up into a single file!

How things come full circle – desktop game developers have been inventing and consuming package formats since time began, and now web-game developers can get in on that action too.

So hence writing a command line utility in NodeJS to scan a directory and package various files into a JSON file.

1
2
3
4
.json -> keep it as JSON
.png -> Base64 encode it
.wav -> Base64 encode it
.shader -> add it as a string

This works well, as on the client loading these assets means writing a small amount of code like so:

1
2
var image = new Image();
image.src = "data:image/png;base64," + imageResource.get();

In actuality, what we end up doing is loading an entire asset package, say ‘assets.json’ and writing the following code before loading the game.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
$.getJSON('assets.json', function(rawData) {
  preloadAssets(rawData);
});
 
function preloadAssets(rawData) {
    forEachResourceInRawData(rawData, preloadItem);
};
 
function preloadItem(key, itemData) {
  var loader = findLoaderForFiletype(key);
  increaseAwaitCounter();
  loader(key, itemData, decreaseAwaitcounter);
};
 
resources.on('complete', startGame);

Where an implementation of a handler for a PNG might do this

1
2
3
4
5
6
function preloadPng(key, itemData, cb) {
    var preloadedImage = new Image();
    preloadedImage.src = "data:image/png;base64," + itemData;
    preloadedAssets[key] = preloadedImage;
    preloadedImage.onload = cb;
};

This means that once the assets file is loaded, the game can start and there are no delays in playing audio, displaying textures and so on and so forth.

Clearly this is massively simplified, because in the real world yet again what we actually do is.

Do the hybrid approach

  • Load the assets for the current land
  • Request an asset
  • Is the asset in the preloaded package?
  • Yes? -> Return a promise containing the asset
  • No? -> Return a promise without that asset
  • -> Make an HTTP request to get the asset
  • -> Cache the asset

For example, my little RPG pre-loads most of the textures and models used across the land, but downloads the actual tile information (where is a tile, what is on that tile) as the player walks around.

This is similar to the streaming that takes places in any reasonable desktop game, and offers a good compromise in a connected multiplayer game (and would work well for a disconnected one too).

I’ll push out the client code I’m using as a library at some point, but that’s not really a suitable candidate for ‘frameworking’, because homogenising something like asset management on the client side can do more harm than good.