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:
Every time I need to change a sprite in the game the process is:
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:
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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:
- Edit the source .png image file in a paint program and save it
- Convert the file to a Base 64 encoded string (I use https://www.base64-image.de/ for this)
- Copy the encoded string
- Paste it into the source code
- Refresh the game in the browser
- Edit the source .png image file in a paint program and save it
- Refresh the game in the browser
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:
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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:
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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:
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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:
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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:
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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.
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
Post a Comment