Skip to main content

Loaded

The images in the game are being loaded by creating an Image element and using a .png file, stored as a Base 64 encoded string, to load the data:

var playerSprite = new Image();
playerSprite.src = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAABGdBTUEAALGPC/xhBQAAAAlwSFlzAAAOwwAADsMBx2+oZAAAABh0RVh0U29mdHdhcmUAcGFpbnQubmV0IDQuMS4zjST9ZwAAAQdJREFUWEftl00OgkAMRudIHIelS4/g0qXH4Xizw5T0aSk0EoIV1CbfYn783ks0k1jWzvV66210O28+JlBr7SVd142SJrIbgbY9jfK7Ak3TDPleAYA+kQD7mwnNwSVpApdz7W2WCnBfa9aPhUvSBQD6AKylDPECrLVm/czBJQDeJgCIp9bC7T4BGH0lWrt8PMjC7T7ZTAAAPyIAFi6xcMkrAdaKiQdAugDFHkwsXOLPAUQC9LKv2OdQnC5AYQQmFi7x5xRHAqwnIhQeToAHyD9ExxdgzQcjASsnoTAS4J4//wscR0CfjUIB54R9vfb468b5fgX8IKDLyVBEKAoLdbivy3hyBUq5A/Xp8cOCeCpMAAAAAElFTkSuQmCC';

Every time I need to change a sprite in the game the process is:
  1. Edit the source .png image file in a paint program and save it
  2. Convert the file to a Base 64 encoded string (I use https://www.base64-image.de/ for this)
  3. Copy the encoded string
  4. Paste it into the source code
  5. Refresh the game in the browser
This works ok when there are only a few graphics, but it doesn't scale very well. The workflow that I really want is:
  1. Edit the source .png image file in a paint program and save it
  2. Refresh the game in the browser
Much better! So how do I do that? I'm going to add a resource manager that loads the .png files directly from the server.

Loading Files From The Server

I'm going to use XMLHttpRequests to load the files from the server. These requests are performed asynchronously, so I have to set up callback handlers to handle the response when the file is loaded. I also have to convert the binary data that gets loaded into an Image element. Some basic code for this would look like:

var loaded_image_element = null;
let request = new XMLHttpRequest();
request.open('GET', url);
request.responseType = 'arraybuffer';
request.onreadystatechange = () => {
// readState 4 means that the request has finished
if (request.readyState == 4) {
// HTTP code 200 or 304 means that the data loaded successfully
if (request.status == 200 || request.state == 304) {
// Now we have the data, we can create an Image with it
let image = new Image();
image.onload = () => {
// Image has finished loaded from the Base64 data
loaded_image_elemt = image;
}
// Convert the binary data into a Base64 encoded string and load it into the image
image.src = 'data:image;base64,' + btoa([].reduce.call(binaryData, function (p, c) { return p + String.fromCharCode(c) }, ''));
}
}
request.send(null);

I don't want to limit myself to only being able to create Image elements - I could also create other object types. For example, this would create a Font object:

var loaded_font_object = null;
let request = new XMLHttpRequest();
request.open('GET', url);
request.responseType = 'arraybuffer';
request.onreadystatechange = () => {
// readState 4 means that the request has finished
if (request.readyState == 4) {
// HTTP code 200 or 304 means that the data loaded successfully
if (request.status == 200 || request.state == 304) {
// Now we have the data, we can create an Image with it
let image = new Image();
image.onload = () => {
// Use that Image to create a Font object once it has loaded
loaded_font_object = new Font(image);
}
// Convert the binary data into a Base64 encoded string and load it into the image
image.src = 'data:image;base64,' + btoa([].reduce.call(binaryData, function (p, c) { return p + String.fromCharCode(c) }, ''));
}
}
request.send(null);

Resource Handlers

So now that we understand the concept of different handlers for different resource types, let's see how I use that in practice. In resourceHandlers.js you can see three different handlers for three different resource types. There's also a mapping table at the top that maps resource types to their handlers:

// List of handlers for the various resource types. Each handler is given a binary data blob which
// it must transform and return to the doneCallback
const resourceTypeHandlers = {
'collisionMap': _handleCollisionMapResource,
'font': _handleFontResource,
'image': _handleImageResource,
};
// image => Image element
function _handleImageResource(binaryData, doneCallback) {
// Create an Image element
let image = new Image();
image.onload = () => {
// Return the Image data once it has loaded
doneCallback(image);
}
image.src = 'data:image;base64,' + btoa([].reduce.call(binaryData, function (p, c) { return p + String.fromCharCode(c) }, ''));
}
// font => Font object
function _handleFontResource(binaryData, doneCallback) {
// Create an Image element
let image = new Image();
image.onload = () => {
// Use that Image to create a Font object once it has loaded
doneCallback(new Font(image));
}
image.src = 'data:image;base64,' + btoa([].reduce.call(binaryData, function (p, c) { return p + String.fromCharCode(c) }, ''));
}
// collisionMap => CollisionMap object
function _handleCollisionMapResource(binaryData, doneCallback) {
// Create an Image element
let image = new Image();
image.onload = () => {
// Use that Image to create a CollisionMap object once it has loaded
doneCallback(makeCollisionMap(image));
}
image.src = 'data:image;base64,' + btoa([].reduce.call(binaryData, function (p, c) { return p + String.fromCharCode(c) }, ''));
}

Resource List

Once I've got the resources into the Resource Manager, how do I retrieve them from other parts of the code? I need a get() function, and I need to pass in some sort of unique ID or name. I could just use the filename, but it's better to use something more human readable. It's also good to decouple the actual file from the code that references it. This means that I could change the path or filename of the resource later on and, because the ID would remain the same, I wouldn't have to change any code. This list goes in resourceList.js

// List of resources to load. Stored as an object where the Key is the resource id and Value is an
// object containing 'src' (filename) and 'type' (image, font, etc.)
const resourceList = {
'font': {src:'font.png', type:'font'},
'player_sprite': {src:'player_sprite.png', type:'image'},
'player_collision': {src:'player_collision.png', type:'collisionMap'},
'simple_bullet_sprite': {src:'simple_bullet_sprite.png', type:'image'},
'simple_bullet_collision': {src:'simple_bullet_collision.png', type:'collisionMap'},
'score_bullet_sprite': {src:'score_bullet_sprite.png', type:'image'},
'score_bullet_collision': {src:'score_bullet_collision.png', type:'collisionMap'},
'spawner_sprite': {src:'spawner_sprite.png', type:'image'},
}

The Resource Manager

Finally we get onto the Resource Manager itself. The code might look a little bit scary, but you've seen the basics already:

class ResourceManager {
constructor() {
// Counts the number of loads in progress
this.loadsPending = 0;
// This is where we store the resources once they are loaded
this.availableResources = {}
// Load all resource from the resourceList
for (const id of Object.keys(resourceList)) {
let resource = resourceList[id];
const url = 'data/' + resource.src;
logManager.info('ResourceManager loading resource ' + id + ' from ' + url);
this.loadsPending++;
// Create an XMLHttp request to get the resource data
let _this = this;
let request = new XMLHttpRequest();
request.open('GET', url);
request.responseType = 'arraybuffer';
request.onreadystatechange = () => {
// This is called when the request changes state. State 4 means that the request
// is finished. Other states aren't important to us
if (request.readyState == 4) {
logManager.info('ResourceManager loading resource ' + id + ' completed, response: ' + request.status + ' ' + request.statusText);
if (request.status == 200 || request.state == 304) {
// Create resource data according to its type
const resourceType = resource.type;
const handler = resourceTypeHandlers[resourceType];
if (handler) {
const binaryData = new Uint8Array(request.response);
handler(binaryData, (result) => {
// Save the transformed data
_this.availableResources[id] = result;
// One less load to do..
_this.loadsPending--;
});
} else {
logManager.error('ResourceManager has no handler for resource ' + id + ', type ' + resourceType);
_this.availableResources[id] = null;
_this.loadsPending--;
}
} else {
logManager.error('ResourceManager failed to load resource ' + id);
_this.availableResources[id] = null;
_this.loadsPending--;
}
}
}
request.send(null);
}
}
areAllResourcesLoaded() {
return this.loadsPending == 0;
}
getResource(id) {
if (id in this.availableResources) {
return this.availableResources[id];
} else {
logManager.error('Resource id ' + id + ' unknown or not loaded');
return null;
}
}
isResourceAvailable(id) {
return id in this.availableResources;
}
}

Most of the work in done in the constructor. On line 10 I loop through all of the resources listed in the resourceList and kick of a request for each one. When the request completes I call the appropriate handler on line 32. Once the handler is finished, I save it in the resource manager's availableResources list. I also decrement a loadsPening counter (which is incremented when the load starts). I use this counter to quickly tell whether all resources are loaded or if any are still in progress.

Down at line 56 you'll see the getResource() function that retrieves the resource data. There's also an isResourceAvailable() function that let's me test if a specific resource is loaded. I use this in the LoadingState to display a loading message as soon as the font is loaded. This message is shown (for a split second!) while the other resources are still loading:

class LoadingState extends State {
constructor() {
super('Loading');
}
render() {
// Show loading message as soon as the font resource is available
if (resourceManager.isResourceAvailable('font')) {
resourceManager.getResource('font').print(config.canvasSize / 2, config.canvasSize / 2, 'Loading', FONT_XALIGN.middle, FONT_YALIGN.middle);
}
}
isReadyToExit() {
// Exit when resource loading is complete
return resourceManager.areAllResourcesLoaded();
}
exit() {
// Set up the next state
stateManager.pushState(new TitleScreenState());
}
}

Wrap Up

I added a Resource Manager that loads files directly from the server. This will make it quicker and easier to add new resources or edit existing ones. There was a lot of code this time, including some more changes in the codebase that I didn't cover here.

Check out the code in BitBucket, and the online playable version here.. and leave me a comment if you'd like me to clarify any of the work.

Next Time..

Continuing my promise to work on the gameplay side of things I'm going to work on improving the player control next time.

Comments

Popular posts from this blog

World In Motion

In the last post I touched on how easy it is to score in this game - you can just camp out on the score bullet spawner and watch the points rack up. I'm going to add a little delay to the score bullets so that they start out in a deactivated and uncollectable state, and then change to active and collectable after a short time - say, half a second. To make it clear what state they are in I'm going to add code to let me change the look of Objects and allow them to be animated.

Funky Boss

A quick one this time. I'm heading towards getting collision into the game, but before I do that I need to get the game objects better managed. So this time I'm going to refactor the code a little bit to add an object manager. It's going to mean several small changes across the codebase..

Word Up

Dodging bullets is fun, but we need to start communicating information to the player. One of the most basic ways to do this is with text. In this post I'll add old school mono-spaced pixel font support to the game, and use that to show some simple placeholder info. It all starts with a font sheet: