Friday, December 28, 2012

Texture Manager for libgdx

Problem

Managing Texture objects in libgdx can be surprisingly annoying. On the one hand, libgdx does some useful optimizing under the hood. On the other hand, you may run into race conditions with garbage collection that manifests as native code errors.

For example:
  • You're using a Texture before a scene change.
  • When you're exiting the scene, you dispose of the Texture objects used.
  • You create a new scene that just happens to use the same Texture.
  • GC finally fires to fully dispose of the Texture.
  • When you go to render the new scene, you hit a native error.
These problems can be difficult to debug, as the stack trace and error output often make no mention of the Texture; in development of Monkey Match, the last piece of our code was somewhere in the render method, before calling into libgdx's Stage object.

Solution

To resolve this, a Texture manager class can be very handy. There are two elements to this:
  1. A bundled texture object that contains a reference to both a Texture and a TextureRegion.
  2. The texture manager itself.
The bundled texture is handy because you'll often find yourself referencing both Texture objects and TextureRegion objects for rendering purposes.

Implementation

First, let's build ourselves the BundledTexture class:


public class BundledTexture {
 
 public final Texture       texture;
 public final TextureRegion region;
 
 public BundledTexture(final Texture texture, int offsetX, int offsetY, int width, int height) {
  this.texture = texture;
  this.region = new TextureRegion(this.texture, offsetX, offsetY, width, height);
 }
 
 public float width() {
  return region.getRegionWidth();
 }
 
 public float height() {
  return region.getRegionHeight();
 }
 
}

The width() and height() methods are just helper methods. libgdx defines many different width/height methods and it's not always clear which version you want. This was used as a shortcut to make the code more clear.

The rest should be pretty clear: We store the Texture object and create a TextureRegion based on the offset and height/width passed in.

Now, for the TextureManager class:



public class TextureManager {
 
 private final Map<String, BundledTexture> mDict = new HashMap<String, BundledTexture>();
 
 public void add(final String key, final BundledTexture texture) {
  if (mDict.containsKey(key)) {
   return;
  }
  
  mDict.put(key, texture);
 }
 
 public BundledTexture get(final String key) {
  return mDict.get(key);
 }
 
 public void dispose(final String key) {
  if (!mDict.containsKey(key)) {
   return;
  }
  
  final BundledTexture t = mDict.get(key);
  t.texture.dispose();
  mDict.remove(key);
 }
 
 public void disposeAll() {
  for (final BundledTexture t : mDict.values()) {
   t.texture.dispose();
  }
  
  mDict.clear();
 }

}

This is also pretty simple, but simple is always better, no? We add BundledTextures to our map with a String identifier, which we can later use to find the texture. We check for error conditions and silently fail (you could throw an exception, if so desired; I prefer not to obfuscate my code with them unless I'm dealing with asynchronous or otherwise truly "exceptional" conditions).

When we're ready to dispose of a Texture, we can either single it out individually, or we can dispose of the entire map. You could override the finalize() method, but with libgdx, we often find ourselves calling these methods on-demand.

Conclusion

That's it! Now, we just initialize our TextureManager, add textures to it, retrieve the desired texture when we want to use it, and dispose with the whole shebang when we're done.

You can find the source code for this example on bitbucket (git repository).

Update 1: Minor edits for clarity, to clean up Blogger formatting.

No comments:

Post a Comment