2012-03-12

#7 - libgdx Tutorial: Texture Packer

Libgdx version used on this post: 0.9.2 (download)
In this post we will implement the last screen of the menu, the Start Game screen.
As of libgdx 0.9.6 some of the classes used on this post were modified or removed. You may want to read the post #13 after reading this post, which covers those changes.

Important changes

  • Inside the pause method of our game (Tyrian#onPause) I'm now calling ProfileService#persist. This causes the game progress to be saved to an external file as I explained in a previous post. It's important to put this method call inside the pause method because we don't know if the player is coming back to the game after this method is executed.
  • I also added a flag to define the current runtime environment (development or production). This way we can change the game's default functionality to ease its development. One example of that is during the persistence of the game progress (ProfileService#persist). If we're in development mode there is no need to encode the output file, so we can easily open and edit it with a text editor. I also had to modify the code that reads the game progress to check if its contents are encoded or not.
  • Many changes were made to the domain entities in order to facilitate its usage. I suggest you browse each domain class using the source code browser.

About the Start Game screen

This screen will have the following roles:
  1. Enable the selection of the level to be played.
  2. Customize the ship.
  3. Display the credits available to the player.
I'm considering you read the previous posts, so I'll summarize the steps involved: create the layout descriptor and write the screen's code that reads it and configures the widgets.

On this screen we used a new widget, the SelectBox. Like the other widgets, it allows us to set a listener so that we get notified when the player change the selection. Inside this listener we coordinate the operation to be executed, delegating most of the checks to the domain objects.

OpenGL and Textures

The Start Game screen works nice, but it is definitely not fun to customize the ship using only form widgets. We should at least show an image of the installed items. We've been using images for some time (in the Splash screen) but we never got into much detail, so let's understand them better.

In 1992 Silicon Graphics Inc. released OpenGL, an API that defines many abstractions easing the work with different graphical hardwares. One of these abstractions is texture mapping. It basically states that for an image to be displayed it must come from a texture currently bound to the GPU's memory. In order to improve performance, we can put multiple images inside a texture and later we tell which region of this texture we want to draw. If the image we want to show is inside another texture (other than the currently bound texture), a texture switching must occur on the GPU before OpenGL can render it. We should try to minimize this texture switching because it can cause us performance issues.

In libgdx we can reference a texture using the com.badlogic.gdx.graphics.Texture class. Note that it's very important to dispose a texture if you're not going to using it, because the Java object that points to it is just a pointer, and the actual resource may be consuming the GPU's memory. The Garbage Collector doesn't know anything about the GPU, so make sure you're always disposing the textures correctly.

Image Atlas

In the previous section we learned that in order to optimize performance we should put multiple images inside the same texture (instead of creating one texture for each image). To do so we create the so called "image atlas", carefully picking related images for it. If the images are not related, we'll end up switching textures too many times at runtime, which is exactly what we want to avoid.

For the Splash Screen we created an image atlas of one image. That's ok because the image is large and we didn't need any other image for this screen. But when we start developping the game screen, we'll have to handle several small images. We should keep track of the dimension and location of each image inside the image atlas in order to create our TextureRegion objects later on. We could have our designer handle this, but we don't have one and event if we had, there is a better approach we can use.

Texture Packer

Libgdx provides a tool called TexturePacker that basically creates image atlases for us. All we have to do is specify where our individual images are, and where to save the output image atlases. If needed, we can also set several settings to customize the output. In order to use it, I followed the following steps:
  1. On Eclipse, I added a new JAR to the tyrian-game's classpath: gdx-tools.jar. You can find this JAR inside the libgdx build you downloaded, or preferably at this link, if you're using the latest libgdx version.
  2. I created a directory to hold the individual images at: tyrian-game/etc/images. This will be our input directory. Then, I created sub-directories based on the screen the images will appear. The TexturePacker uses these sub-directories to group the images, that is, images inside different sub-directories will never be placed on the same image atlas.
  3. I created the image files, which you can see here.
  4. I created the TyrianTexturePacker class, which calls the TexturePacker the way we want. Basically, I told the TexturePacker to read the images from our input directory and save the output image atlases to our resource folder. Please read the comments inside the class for detailed information.
  5. I ran the TyrianTexturePacker class and refreshed my resources folder.

Texture Packer Configuration

Instead of going through each configuration option, I'll share a great link that details all the configuration that can be done. Note that the image files may follow a special nomenclature for setting specific configuration that applies only to the image itself. The underscore character is used to separate these special settings, so don't use it when naming your image files unless you want to use this feature.

As an example of that, I updated the Splash screen to also use an image atlas generated by the Texture Packer. I renamed the splash image to: splash-image_l,l.png. The "l,l" part tells that the linear texture filter should be used when loading the image in runtime.

Using image atlases

Finally, I'll show how to load the image atlases generated by the TexturePacker tool.
  1. I modified the AbstractScreen class to add a new TextureAtlas attribute. I create an instance of it in a lazy manner, and dispose it in the dispose method.
  2. In order to create a TextureAtlas instance, I call:
    atlas = new TextureAtlas( Gdx.files.internal( "image-atlases/pages-info" ) );
    
    This "pages-info" file was generated by the TexturePacker, and it contains all the information of the generated image atlases.
  3. When I want to load an image, all I have to do is call getAtlas().findRegion(), passing in the name of the image file I want. This in turn gives me an instance of AtlasRegion, which extends TextureRegion (the class we used previously in the Splash screen to load the splash image). Check out the source code for the modified Start Game screen for more details.
  4. I modified the layout-descriptor to include the image placeholders for the ship's items. And with the region object in hand I can just call Image#setRegion to display the image I want.
This is the final screen:



Conclusion

We created the Start Game screen and learned more about OpenGL textures. We used the TexturePacker tool of libgdx to automatically create image atlases for us, so we can focus on the game's mechanics. I didn't detail the source code changes too much, but I'm sure you can figure them out using the source code browser. Thanks!

4 comments:

  1. great tuts!
    i already know a bit of libgdx but these tutorials enhanced my knowledge.
    i like reading them.
    keep going!

    ReplyDelete
  2. simply amazing tutorials !! thanks a lot !

    ReplyDelete
  3. Hi! can you help me with setRegion() function? can't find it's implementation, it just doesn't exist in my project. Thanks in advance

    ReplyDelete
  4. I just want to say thank you for your well written and well organized tutorials. It is rare to find somethings as helpful as these have been. I love the power of this library but I think I'd be pretty lost without your work.

    ReplyDelete