2012-03-06

#5 - libgdx Tutorial: Files

Libgdx version used on this post: 0.9.2 (download)
Moving on with our game development, I decided to make a slight change of plans. Instead of having a "Hall Of Fame" screen I think it would be easier to implement a "High Scores" screen. Let's go through all the steps...

Important changes

  • Resources reorganization: by the time the game is finished we'll have many resource files, so it's a good idea to organize them now. I created folders for each type of resource we currently have. That is: skin, image-atlases and layout-descriptors.
  • Splash screen refactoring: A reader of this tutorial shared a tip on how to better use the screen's show/resize methods. In case the screen is resized, our actors don't need to be recreated, but just resized/repositioned. Here is a link containing the changes made to the SplashScreen and AbstractScreen. Thanks xryz!

About the High Scores screen

Following the "design standards" of the Menu screen, the High Scores screen will display the best scores on each of the three levels of the game and a button to go back to the main menu. Using the TableLayout Editor, I ended up with this layout descriptor:
debug
* padding:8
|
| fill:x
---
'High Scores' colspan:2 spacingBottom:20
---
'Episode 1'
[episode1HighScore]
---
'Episode 2'
[episode2HighScore]
---
'Episode 3'
[episode3HighScore]
---
[backButton] colspan:2 width:300 height:60 spacingTop:30
And this preview:

Note: We talked about the TableLayout in a previous post.

The code for the HighScores screen is pretty similar to the Menu screen, so I won't go into details.

The Profile domain entity

We should now think where these high scores will come from. Looking at our domain model, the Profile class seems to be a good candidate since it contains other attributes that should also be persisted across game restarts, such as the "credits" available to the player and the current level she/he is currently at. This is the only domain entity we'll persist to a data store.

We can use a simple Map attribute for holding the current best scores, using level IDs as the keys. Please have a look at the Profile class. The Level class was also changed to include an ID attribute.

Handling preferences in libgdx

One data store available to us is the preferences mechanism. It can hold simple key-value pairs and uses the SharedPreferences class on Android, and a flat file on Desktop. Have a look at the source code for the libgdx's Preferences interface and check out the methods it declares. This is an usage example:
Preferences prefs = Gdx.app.getPreferences( "profile" );
prefs.putInteger( "credits", 1000 );
We could do it using preferences. It's simple and effective. But let's think a little bit more.
  • Some day we might release an updated version of our game. The Profile class may have changed, so we'd have to read the old preferences and updated them to the new preferences format.
  • There is a chance of the user to try and manually edit this preferences file in order to gain advantages in the game.
  • As we're just persisting the state of one domain entity, maybe it's easier to have its whole state serialized and deserialized later on.
  • We could add support for multiple profiles in the future.
Given that, let's see a second data store option.
Edit: in the post #6 I wrote about the preferences mechanism.

Handling files in libgdx

The files module is another abstraction of libgdx. It lets you handle files the same way whether you're running the game on Android or on the Desktop. In the source code repository of libgdx we can also see some new backends, like GWT and iOS. So abstractions are a nice thing to have.

These are the available file types in libgdx:
  1. Classpath: Read-only files that live in the root of the compiled classes.
  2. Internal: Read-only files found in the root of the application (in the case of a Desktop), or inside the assets folder (on Android).
  3. External: Read/Write files located in the user's home directory (in the case of a Desktop), or in the root of the SD Card (on Android).
  4. Absolute: Files referred to through absolute paths, which is normally not a cool thing.
  5. Local: Read/Write files located in the game's root directory (in the case of a Desktop), or in the private application folder (on Android). This file type was added recently and is very interesting. The advantages of using it on Android is that: (1) these files are private to the application, (2) in case the application is removed, so are these files, (3) there is no availability problem (the SD Card is not always available).
Like in Java, a directory is considered to be a File in libgdx, but instead of re-using the java.io.File class, the com.badlogic.gdx.files.FileHandle class is used (independent of the file's type). The following lines show how to create a FileHandle for each of the available file types:
  1. Gdx.files.classpath( "config.properties" );
  2. Gdx.files.internal( "data/config.properties" );
  3. Gdx.files.external( ".tyrian/game-progress.xml" );
  4. Gdx.files.absolute( "/Users/gustavo.steigert/.tyrian/game-progress.xml" );
  5. Gdx.files.local( "data/game-progress.xml" );
Now that we know how to create file handles, the following list shows what we can do with them:
  • Read: there are many read methods, but the simplest one is readString(), which returns a String with the contents of the file.
  • Write: there are also many write methods. The simplest is writeString(content, append/overwrite).
  • List: in case it points to a directory, calling list() retrieves an array of FileHandle with the children.
  • Delete: erases the file with delete(), or the directory with deleteDirectory().
  • Copy/Move: copies/moves the file/directory with copyTo(destination)/moveTo(destination).
In case some of these operations fail, a GdxRuntimeException will be thrown (which is a RuntimeException).
Edit: this section was updated to cover the new file type "internal". In a next post we'll use this new file type instead of "external".

Using the JSON format

When writing to a text file we'll use the JSON notation to model our data. If you don't know JSON yet you should really take the time to read about it. JSON is gradually replacing XML since its output is easier for humans and machines to read, and the output is not so lengthy.

We'll make the Profile class know how to generate a JSON representation of its state, and also how to recover it later on. The first thing to do is make the Profile class implement com.badlogic.gdx.utils.Json.Serializable, which declares the following methods:
public interface Serializable {
  void write (Json json);
  void read (Json json, OrderedMap<String, Object> jsonData);
}
The Javadocs for the JSON classes in libgdx are missing, so reading the source code and analysing other projects I came up with the following implementation in our Profile class:
    @SuppressWarnings( "unchecked" )
    @Override
    public void read(
        Json json,
        OrderedMap<String,Object> jsonData )
    {
        currentLevelId = json.readValue( "currentLevelId", Integer.class, jsonData );
        credits = json.readValue( "credits", Integer.class, jsonData );

        // libgdx handles the keys of JSON formatted HashMaps as Strings, but we
        // want it to be integers instead (because the levelIds are integers)
        Map<String,Integer> highScores = json.readValue( "highScores", HashMap.class,
            Integer.class, jsonData );
        for( String levelIdAsString : highScores.keySet() ) {
            int levelId = Integer.valueOf( levelIdAsString );
            Integer highScore = highScores.get( levelIdAsString);
            this.highScores.put( levelId, highScore );
        }
    }

    @Override
    public void write(
        Json json )
    {
        json.writeValue( "currentLevelId", currentLevelId );
        json.writeValue( "credits", credits );
        json.writeValue( "highScores", highScores );
    }
The method calls are pretty intuitive. We're writing key-values in write() and recovering them with the correct types in read(). These methods still lack the code for saving/restoring the Ship's items. I think you can easily handle it, so I won't go into details.

The Profile service

The last piece of the puzzle is to create a service to coordinate the reading and writing of the Profile's state. It should define the location of the target file and expose read/write operations, also handling eventual unexpected states. These are the detailed requirements for the Profile service:
  1. Retrieve Profile operation
    • The target file that will hold the state of the Profile class will be an external file located at: .tyrian/profile-v1.json; The "-v1" suffix allows us to update the file model when new versions of the game get installed.
    • If the target file does not exist, one should be created based on an empty Profile state.
    • If the target file exists, it should be read and supplied to a fresh Profile instance, so that it can restore its state.
    • The content of the file will be encoded with the com.badlogic.gdx.utils.Base64Coder utility of libgdx, making it harder for players trying to edit it manually.
    • Should any problem occur while reading the file or restoring the Profile state, a fresh new Profile should be created and retrieved to the caller.
  2. Persist Profile operation
    • Have the given Profile instance generate its own JSON representation.
    • Encode the outcome with libgdx's com.badlogic.gdx.utils.Base64Coder.
    • Write the result to the target file.
If the player opens the target file, something like this will be displayed:
e2N1cnJlbnRMZXZlbElkOjAsY3JlZGl0czowLGhpZ2hTY29yZXM6eyIwIjoxMDAwLCIxIjoyNDAwLCIyIjo1MjAwfX0=
Editing it will likely invalidate the structure, making the decode operation impossible. In this case, we just create a new Profile and move on with life.

You can find the source code for the Profile class here. Going back to the HighScore screen, it should now retrieve the Profile through the Profile service, and simply display the high scores for each level. This is the resulting screen for all this hard work (amazing, isn't it?):


Conclusion

When dealing with the High Scores screen (old Hall Of Fame screen) we learned how to use files in libgdx. We went a bit further and talked about the other persistence mechanism (preferences), did some basic encoding to the game progress data and played with some JSON utility classes. This is the tag on the Subversion for this post. Thanks for reading!

17 comments:

  1. when I try the new abstract and splash screens i get a fatal error on the switch from splash screen to main menu. It does look like the resize is being called twice.

    #
    # A fatal error has been detected by the Java Runtime Environment:
    #
    # EXCEPTION_ACCESS_VIOLATION (0xc0000005) at pc=0x0000000069b4ff54, pid=8080, tid=6848
    #
    # JRE version: 7.0_02-b13
    # Java VM: Java HotSpot(TM) 64-Bit Server VM (22.0-b10 mixed mode windows-amd64 compressed oops)
    # Problematic frame:
    # C [nvoglv64.DLL+0x64ff54]

    ReplyDelete
    Replies
    1. I'm also facing the same issue, but the problematic DLL for me is "ntdll.dll". I created a topic on the official forum of libgdx to try and solve it. What I realised is that ignoring the disposal of the stage inside AbstractScreen#dispose seems to fix the problem. Does it also work for you? Thanks for the feedback.

      Delete
    2. I saw te same issue under Android at this stage, after a moment past splash screen the game would crash but there was no useful error in logcat.

      I commented out disposal of stage and found as well AbstractScreen was the culprit.

      Delete
    3. Thanks for your feedback! Could you please tell me what android version you were running?

      Delete
    4. I am having the same issue on Windows. I think it has something to do with Java 7 policies.
      Log: http://pastebin.com/X8DXaaGr
      System: Win 8 x64, JRE: 7.0_10-b18
      Hope it helps.

      Delete
    5. It usually get crashed with error like that, when you dispose something that has already been disposed(maybe not by yourself), e.g. scene2d and box2d.

      Delete
  2. Interesting... removing the stage disposal didn't seem to help but the problem goes away when you use the image atlas refactor on the splash page you did in lesson #7. I'm really enjoying your tutorial :)

    ReplyDelete
    Replies
    1. Tested on a 32-bit Mac and the problem does not appear. I'm starting to think it's a issue with 64-bit CPUs.

      Delete
  3. Hi Rawan,
    I can see your comments get deleted sometimes.. even some of my comments get deleted. This Blogspot is not reliable.. :(

    I ask you to go to the libgdx forum first ( http://www.badlogicgames.com/forum/ ) and post your question, since I don't have spare time to look into this.

    Sorry!

    ReplyDelete
  4. Hello Gustavo,
    Great tutorials buddy!!!
    I am able to avert the crash if i do a stage.clear() before stage.dispose()

    ReplyDelete
    Replies
    1. Tried it with no success. Thanks for the feedback anyway!

      Delete
  5. Sweet and simple tutorial, keet it up!

    ReplyDelete
  6. This comment has been removed by the author.

    ReplyDelete
  7. Thanks for your tutorial! thanks.

    ReplyDelete
  8. Really nice tutorial a little hard to follow up due to the changes in libgdx but this is awesome

    ReplyDelete