Xargus / Game Engine - Exploring Various Methods For Improving Game/Asset Load Time
Currently when starting a new level in Xargus, the game pauses/freezes for approximately 2.5 seconds before the level starts. The reason why this happens is that new resources need to be loaded that are required for the level, and this loading is happening in the main game thread.
In this blog post Iâm going to explore various ways of reducing asset load time, as well as exploring ways of making load times less annoying for the player, through the use of loading animations, threading, etc.
The Asset Loading Process
Currently in my game engine, whenever a new screen is loaded, all assets required by that screen are also loaded from the apk file assets into memory / ram, and then appropriate assets are then sent to opengl, which handles sending them to video card memory as appropriate.
Image assets are stored as large texture atlases that are created by a software package called TexturePacker. Each of these texture atlases consists of a .json file that requires parsing, and a corresponding large .png image. The .json file contains data about which parts of the larger .png image contain each individual image file.
Sound files are also loaded at this stage, but the process for doing so is handled by an Android SoundPool class, is done so in a different thread, and my game engine isnât particularly involved in handling the loading of these.
The most expensive part of loading these assets will be reading the files from the apk into memory - file I/O operations are generally quite expensive. My understanding is that transferring these from memory into openGL is likely to incur some expense, but nowhere near as much as reading them from disk to begin with.
Multi-Thread Loading, Loading Animations
The first thing that can be done in order to significantly improve the user experience is to load all images in another thread, and provide some form of visual animation to the user while this loading process is happening.
Creating a really nice looking animated screen transition would be a great way of hiding the loading process, for example filling the screen with images until it is full, then removing all the images one by one, or having big metal bars come down over the screen. A screen transition can easily be 0.5 - 3 seconds long without the user being annoyed that it is taking too long, meaning that so long as all the loading can be done in this time, it is the only thing required to make the game look seamless. Most cartoons, and lots of games, have quite interesting screen transitions to look at for inspiration, for example:
Youtube - Flowers Screen Transition
Youtube - Superfriends Screen Transition
Youtube - Batman Screen Transition
It is worth mentioning that fade transitions (ie fading smoothly from one screen to the next), or most other windows movie maker style transitions, would not work. Most of these type of transition require the second screen to be loaded for the transition to animate, as they display parts of the second screen at an early stage in the transition, whereas cartoon style transitions mostly donât.
If it is not possible to load all the resources within a 3 second window or thereabouts, it is a good idea to provide a loading screen, as opposed to simply having a really long transition. The following are best practices for loading screens:
It is better to animate into and out of the loading screen, rather than just having a âloadingâ message appear on screen. Fancier animations/transition-style animations are better than simple ones, for example, in a snow-themed game, it would be better to have the entire screen fill with snow from above for 2 seconds, followed by loading text fading in over the snow for 0.5 seconds, and then having a 0.5 second transition after loading where all the snow melts away, rather than simply having a loading popup fade in and then fade out again.
If the loading bar is on screen for more than 5 seconds, it is better to provide a loading bar that shows progress, so the user can see how long loading is going to take. Seeing a loading bar mitigates userâs impatience to some extent.
Simple ongoing animations should play during loading, such as having a simple particle effect, or having the loading text pulsate in some way. Looking at a simple animation is less boring than looking at a static screen. These also indicate to the user that the game is still loading, and has not crashed.
If the game does crash during loading, or one of the resources could not be loaded from file, etc, a message should be shown to the user explaining this, rather than just force-quitting the app, or perpetually staying on the loading screen. In some cases it may be better to allow the user to play without one of the resources being loaded - for example, if one of the sounds doesnt load, this isnât a deal breaker - the game is still playable.
Common Sense - Reducing Surplus Assets
The most simple way of reducing load times is firstly to ensure that no unnecessary assets are being loaded. This is quite common sense, but I thought Iâd mention it anyway.Â
For example, by splitting texture files up into smaller texture files, it is easier to load less info from memory in order to get the images required (although, convesely, it is often better in terms of openGL performance to have larger texture files - there is a balance to be struck)
Reducing the resolution of images is another way of reducing asset load time, among other common sense methods. These will vary for each individual game.
Optimized 256-Colour .PNG Compression
Because the largest impact on asset load time is the time it takes to load the files into RAM, reducing filesize of the images will have a significant impact on load times.
There is a way of significantly reducing the filesize of png images, that has been popularized by sites such as tinypng .
Essentially, in most png images, each pixelâs colour is stored individually, as 4 bytes (alpha, red, green, blue), and then some lossless compression is applied to reduce this filesize.
A more efficient way of doing this is to instead store 256 different colours that the image will use, and then, for each pixel, use only 1 byte to encode which of the 256 colours will be used for that pixel. An algorithm can then determine which 256 colours to use to best recreate the image, and a technique called dithering can also be used to blend colours together better. This results in filesizes that are 70-80% smaller.
This wikipedia article explains how dithering works, and demonstrates this technique in more detail.
It should be mentioned that although the filesizes are much smaller, the images will use the same amount of memory in RAM - my understanding is that images will be stored as full uncompressed bitmap-style images in RAM. This means that although file I/O operations will be much faster using compressed images, communicating these images to openGL and manipulating them in memory will not.
I created versions of my main texture file that were compressed in this way, and I got the following results:
Although the images still look good, they are clearly not the same quality as when uncompressed. On the mushrooms and alien faces in particular, the dithering technique that has been used is very clearly visible, and the colours are slightly off for the mushroom.
This effect is particularly pronounced in this texture file due to the fact that there are a lot of different colours in it, as only 256 different pixel colours can be used for the entire image. Potentially, by splitting it up into smaller texture files, compression would produce results that were of better visual quality. If, for example, all the alien images were on a separate texture file, then a greater proportion of those 256 available colours could be allocated to the bluey-purpley shades of the head, and the result would be better.
Demonstrating this, here is what compression of just the head image on its own looks like:
The two images are pretty much indistinguishable - the noticeable dithering effect seen in the previous compressed image is not present whatsoever.
It should also be noted that this visible dithering effect is likely to be significantly reduced in rendered images due to the fact that it is highly unlikely that images will be being rendered at pixel-perfect 100% scale - they will have some scale factor applied to them, and due to the inherent nature of how bilinear interpolation works, this will result in the graininess of dithering being reduced slightly.
I used the compressed texture file in game to see how it would look when rendered, and the results were as follows:
(Note that many images are on a seperate texture file and therefore were not subject to compression - only the alien, all guns, and all enemies are on the compressed texture file)
Main Menu Screen - Uncompressed:
Main Menu Screen - Compressed:
Main Menu Screen - Uncompressed - Zoomed In:
Main Menu Screen - Compressed - Zoomed In:
In Game - Uncompressed - Zoomed In:
In Game - Compressed - Zoomed In (Note that only the alien has been compressed, background trees have not):
In all the compressed images, the visual graininess artefacts of dithering are visible, and this does damage the crisp, clean aesthetic of the particular art style I am using to a degree. This may not be the case to the same extent in different games with different art styles.
Texture Loading System - Loading Compressed Image at 150% Resolution:
One solution would be to store the image on disk compressed at 150% of its resolution.Â
This would take up 1.5*1.5*0.3 = ~68% of the filesize of the original (100% resolution, uncompressed) image.
Then, when it is loaded from disk, it would be loaded into RAM at 150% resolution (as it exists on disk), and then reduced down to 100% resolution before being sent to openGL. This reduction would go some way to removing the visual graininess artefacts left behind by the dithering process, due to, as I mentioned earlier, the way in which bilinear interpolation (image scaling) works.
However, reducing a very large image such as the texture from 150% to 100% in RAM would potentially have a noticeable effect on loading time - each texture image is about 2000x2000 pixels big at 100% size, which would be 3000x3000 at 150% size. This, combined with the fact that the image would still be 68% of the original filesize, means this is not a great idea.
Complex Texture Loading System - Individually Loading Each Image & Programatically Constructing Texture:
Another improvement over simply compressing the whole texture file is to generate the .json file as normal, storing the best fit positions of each image file in the texture file, but then rather than generating the texture image as one file, saving each image seperately.
A script could then be run compressing each image. Visual artefacts would be vastly reduced over compressing the entire texture file, as each image would have its own 256 colours to work with, rather than having to share these 256 colours with all the other images in the texture.
This is demonstrated above with an alien face image, but to redemonstrate it with the alienâs T shirt (which is an image that is quite difficult to reduce to 256 colours, as it involves vibrant colours all across the rainbow spectrum), here is what the T shirt looks like when compressed as an individual image:
Which isnât amazing, but is a noticeable improvement over how the T shirt looks when compressed as part of the entire texture (also shown in above compressed screenshots):
(I used imagemagick to count the number of colours in this image and got the result 125 - around half of the colours used in the per-image compression. Also, those 125 colours will not have been optimised for the individual image)
Then, when the assets are being loaded, each image can be loaded separately from file, and the entire texture file can be constructed according to the instructions of the .json file.
This could be done in a two thread setup whereby one thread performs file I/O to load images, and another thread takes images that have been loaded into RAM and places them in the appropriate place in the texture (probably using the Android Canvas/Graphics API to do this). This way, the loading process should not be noticeably slowed down by the fact that texture files are also having to be stitched together from their individual image components as part of loading.
I am also unsure whether loading each image separately from different files would be slower in terms of file I/O operations than loading one big texture file. If it is the case that it is slower, this could hinder this methodâs effectiveness.
This looks like a good idea overall, although it would take a bit of time to implement - currently, I use TexturePacker which automatically resizes and packs all my images from their source files (as each image is absolutely massive resolution when created, and then resized down to 35% of that in the texture file). In order to work with individual images rather than TexturePacker texture outputs, I would need to create a compile time script that resized all my images down to 35% size, and compressed each one individually. I would obviously also need to implement the code to load these images from file into memory and stitch them together into a larger texture.
Initially Loading Low-Res/Compressed Images, Then Loading Full-Res Uncompressed Images Afterwards
One way of significantly reducing the initial load time would be to initially load versions of the texture file that were low resolution and/or compressed, and use these. Then, once these have been loaded, the game/level can start, and the higher res versions of the images can be loaded in the background. When the high res versions have been loaded, these can be swapped so that the game uses the high res ones instead.
The following table shows what an image of the cowâs head looks like when reduced to various scales and then rescaled back to 100%. It also shows the filesize required to store each reduced size image:
As is visible, scales of 0.2x to 0.4x leave significant visual artefacts that are very undesirable, leaving these largely unusable. Scales of 0.5x to 0.7x have some visual artefacts, and are a bit blurry/not as crisp as the original, but largely these would be fine to display to the user, and would only require 8%-15% of the filesize (and therefore a similarly reduced percentage of load time) compared to the uncompressed original.
To understand the potential advantage of this fully, consider hypothetically that it takes 10 seconds to load all the images at 100% uncompressed size. This is represented on the following graph:
Now consider that instead, a compressed image of 0.5x scale is loaded first, then the full sized uncompressed image is loaded afterwards:
The game becomes playable at 0.8 seconds (as a compressed image at 0.5x filesize takes 0.5*0.5*0.3=0.08 (8%) of the time to load compared to the original uncompressed). Then, the game starts using the full sized image at around the 10.8 second mark.
By doing this, we have significantly improved the way in which the game loads - now the user can play the game/level from the 0.8 seconds mark, albeit with low res, somewhat blurry images. Playing the game with low res, blurry images for 10 seconds is vastly preferable however to staring at a loading screen.
We can even go one step further than this, by loading 0.5x scale images, then loading 0.7x scale images, and then loading 1.0x uncompressed images:
This way, the game is playable from 0.8 seconds, then at around the 2.4 second mark the image quality gets improved moderately, before the images become full quality at around the 12.4 second mark.
The exact combination of which scales to load would differ for each texture in each game, depending on a whole variety of factors. For example, in Xargus, where the load time is currently approx 2.5 seconds, it might make sense to load the 80% compressed version first, which could load in approx 0.5 seconds, and then have a 0.8 second transition on the screen to cover up this load time and make the loading appear seamless.
Having An Asset Manager That Intelligently Manages *Unloading* Of Unused Textures To Avoid Unnecessarily Unloading & Reloading Textures
Currently, when a new screen is loaded, all the textures required by that screen are loaded into memory and sent to openGL. When the screen is unloaded and a different screen is loaded, all the previous textures are removed from memory, and the new ones are loaded in (with the exception of textures that are used by both).
This is the most simple way of managing assets. There are times however when this approach will make no sense - consider that the user finishes a level, and the game goes back to the level select screen, where the user then has the option to start the next level. OpenGL is told to throw away all of the textures used in gameplay when the game returns to the level select screen. Then, if the user clicks to start the next level, all these same textures, that existed in video memory only moments earlier, have to be reloaded from disk and sent to OpenGL. This is clearly not the best approach.
It is however important to ensure that unused textures are being discarded at some stage - otherwise we risk using up RAM unnecessarily, possibly forcing background apps to have to close, as well as increasing the frequency of garbage collections, etc. We would also risk hitting the limit on the number of textures that could be held by openGL if we did not throw away unused textures.
However, we do not need to be unloading these straight away. We could, for example, have a system that marks them as unused when they are no longer required, and then have some sort of count/timer built into each texture that unloads them at a later stage. This could be used to e.g.:
Unload textures after 3? minutes - this way if the user leaves a screen then returns to the previous one within 3 minutes, no textures will have to be recreated, which would be a common use case.
Unload textures after 2? new screens - ie. if transitioning from the game view to the main menu, this counts as 1 new screen. Donât unload all the textures not in use at this stage, but instead unload these when the main menu transitions to a different screen, e.g. the credits screen. This way, returning to whichever screen was previous will result in textures still being intact.
Keep a fixed number of unused textures in memory (maybe 5?) - a fixed number of the most recently unused textures will be kept in memory. When another texture becomes unused, it will be added to the list, and the oldest texture currently in the list will be unloaded in its place if the list is at maximum capacity.
Keep a fixed amount of unused resolution in memory (maybe 2048x2048x2?) - similar to previously, but instead of keeping a fixed number of textures, a fixed amount of texture resolution is kept. This way, textures are treated according to the space they use in memory rather than being treated equally - for example, 4 1024x1024 textures use the same amount of memory as one 2048x2048 texture.
Such a system could also combine these methods into one hybrid method for texture management. For example, such a system could, theoretically:
List 1: Maintain a list of the two most recently unloaded textures indefinitely
List 2: Maintain a list of the two next most recently unloaded textures, where all the textures in the list have either been unloaded within the last 4 minutes, OR have been unloaded within the last 2 screens.
List 3: Maintain a list of the six next most recently unloaded textures, where all the textures in the list have both been unloaded within the last 2 minutes, AND have been unloaded within the last 2 screens, AND the total amount of unused resolution in memory (from lists 1 2 and 3 combined) is less than 2048x2048
The exact specifics of such a system would have to be set for each game individually, in order to minimise surplus reloading of textures, whilst also minimising unnecessarily storing unused textures in memory.
Having An Asset Manager That Intelligently Manages/Prioritises *Loading* Of Texture Assets
In addition to make such a texture manager intelligently manage unloading of unused assets, it would be beneficial to make it intelligently manage & prioritise loading.
There are likely to be situations whereby textures are required by a screen that are not required immediately, but instead are required after a period of time. Consider for example a situation whereby a level has a boss at the end of it, and the images for this boss are all on their own texture sheet. Loading this texture at the beginning would be nonsensical - doing so would potentially mean it was loaded in place of another one that was immediately required, slowing the loading process down. Instead, it would be much better to load all textures excluding the boss texture at reduced resolution & compressed, then load all textures excluding the boss texture at full resolution, then load the boss texture at full resolution.
Alternatively, consider Xargus - an animation plays at the beginning of each level for at least 5 seconds before any enemies are on screen. This animation involves the screen being zoomed in on the alien, so it would be desirable for full sized versions of the alien assets and background/hill assets to be used for this animation. Conversely, all the enemy assets, etc, would not be required until at least 10 seconds into the game, and these would be required at a lower resolution. It might make sense therefore to load the alien + hill assets in full resolution first.
In order to be able to do this, a system needs to be set up so that game screens can communicate to the asset manager the approximate time that each asset will be required, so that it can load the highest quality assets required as soon as possible, whilst also ensuring that all assets required later are loaded on time.
Having An Asset Manager That Supports Pre-Loading Of Assets That Are Not Currently Required
Another useful feature in an asset manager would be the ability to pre-load assets for use later. For example, assume the user is on the main menu screen, and level 4 is the highest level that is currently unlocked. Chances are, the user it quite likely to play level 4 next. Therefore, it would make sense for the game to pre-load versions of the assets required in order to play level 4. This way, if the user does go on to play level 4, loading times will be reduced; and if the user does not go on to play level 4, these assets can then be easily discarded.
A downside to this method is that it would involve using excess RAM + video memory loading textures that may never be used.
One potential method I could use to mitigate this downside is to, instead of loading the texture from file into an image in RAM (where it would be represented as a bitmap - i.e. uncompressed, 4 bytes per pixel) I could possibly load the file data into RAM, but keep the data intact in its .png format. If this .png was compressed, I would potentially save a lot of RAM versus storing it as a bitmap:
.png files generally use 50% of the filespace of .bmp files due to lossless compression
256-colour compressed .png files generally use 20-30% of the filespace of uncompressed .png files due to using 1 byte per pixel rather than 4
This means that compressed .png files require approx 10-15% of the space to store them compared to if they were uncompressed bitmap images.
By doing this, I would get all the expensive file I/O out of the way, and then if the images were required, I could convert them into bitmap images and send them to openGL. This would be the best of both worlds to some extent - I wouldnât be using much excess RAM, and I would be preloading the images to improve seamlessness of loading. I am not entirely sure this is possible/easy to do on Android however.
Complex Texture Loading System - Individually Loading Each Image & Programatically Constructing Texture PART 2 - Double-Loading Textures
Previously in this blog post, I described how building a system that loaded images individually from file and then assembled them into a texture in RAM could benefit from filespace optimizations of a compressed 256 colour image, without a significant reduction in image quality (compared to compressing the entire texture file, which led to a noticeable reduction in image quality).
When using this method, there is potential to also use another trick to speed things up further.
Essentially, in addition than just creating one massive texture file in RAM, another texture file, half the x-size of the first, is created in RAM (i.e. as big as the left half of the texture). So, for example, assuming the entire texture file is 2000x2000 pixels, another image will be created in RAM that is 1000x2000 pixels.
Then, all the images that are on the left hand side of the texture are loaded first. When they have been loaded from file, they will be placed into both texture files in RAM. This will result in the second image in RAM essentially becoming a copy of the left hand side of the first one.
The advantage of doing this is that once the second image has been loaded (with approx 50% of the images), it can be sent to openGL to be used. This means that 50% of the images are available for use in 50% of the time, and then the full 100% are available in the full time:
Complex Texture Loading System - Individually Loading Each Image & Programatically Constructing Texture PART 3 - Using OpenGL To Assemble Texture Files
Extending even further on this technique, it may be possible to, instead of using Android Canvas/Graphics APIs to construct the texture image from component images in RAM, use OpenGL. This would come with a great deal of advantages.
Essentially, instead of using a two-thread setup whereby one thread loads images from file and another thread assembles these images into a texture, each new image can be added to an arraylist when loaded. Then, the openGL rendering thread can, at the beginning of drawing, test for any images in this arraylist, and for each image that exists:
Load the image into OpenGL as a new texture
Render from the newly loaded texture containing just the image into the full-sized texture
Destroy the image loaded into OpenGL as a new texture
This way, images become available for rendering as soon as they have been loaded from file, without having to wait for the entire texture to be loaded before openGL can use images from it:
This also means that loading of full-res versions of individual images can be prioritized, rather than just being able to prioritize textures. In real use cases, this would be very significant - consider Xargus for example. At the beginning of a level, the alien is on screen, the hippie van is on screen, and hills & clouds are on screen. All of these things are contained in different large texture files that contain many more images than just what is on screen, and these large texture files need to be fully loaded before that can be used. By using this method, it would be possible to load all the alien, hippie van and hill images first at full resolution, and use these images immediately without having to wait for unnecesary parts of the large texture images to load.
Having said that though, this is quite a complex thing to implement, and would possibly take about 2 weeks or something similar, so I am unlikely to be implementing it at this stage.
Conclusion / Which Methods I Am Likely To Implement In Xargus
I am likely to implement the following in Xargus / my game engine:
Implementing a way of handling variable-duration screen transitions used to mask loading times in my game engine
Implementing an asset manager in my game engine that can support delayed unloading, prioritised loading, and pre loading of assets
Also having the asset manager support initially loading compressed, low res versions of images
I may implement more features to reduce load times at a later date, but more complex features than this are unlikely to be required for Xargus