2012-03-07

#6 - libgdx Tutorial: Preferences

Libgdx version used on this post: 0.9.2 (download)
Our next target is the Options screen.

About the Options screen

Once again we'll implement a very simple screen with the following options:
  • Sound Effects (enabled/disabled)
  • Music (enabled/disabled)
  • Volume (0-100)
Using the TableLayout Editor I wrote the following layout descriptor:
debug
* padding:8
---
'Options' colspan:3 spacingBottom:20
---
'Sound Effects'
[soundEffectsCheckbox] colspan:2 align:left
---
'Music'
[musicCheckbox] colspan:2 align:left
---
'Volume'
[volumeSlider] fill:x
[volumeValue] width:40
---
[backButton] colspan:3 width:300 height:60 spacingTop:30
Now we should think where to save these values.

Preferences mechanism

It makes no sense to store these option values in our domain model. The Profile class deals specifically with domain attributes that must be saved. Using files is a valid option, but we can use something simpler: the preferences mechanism. We can add just key-value pairs to it and we don't need to care on how and where this information is going to be persisted. Seems enough.

The first thing we should do is implement a class that abstracts the access to the preferences mechanism. The following class does the job:
package com.blogspot.steigert.tyrian;

import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.Preferences;

/**
 * Handles the game preferences.
 */
public class TyrianPreferences
{
    // constants
    private static final String PREF_VOLUME = "volume";
    private static final String PREF_MUSIC_ENABLED = "music.enabled";
    private static final String PREF_SOUND_ENABLED = "sound.enabled";
    private static final String PREFS_NAME = "tyrian";

    public TyrianPreferences()
    {
    }

    protected Preferences getPrefs()
    {
        return Gdx.app.getPreferences( PREFS_NAME );
    }

    public boolean isSoundEffectsEnabled()
    {
        return getPrefs().getBoolean( PREF_SOUND_ENABLED, true );
    }

    public void setSoundEffectsEnabled(
        boolean soundEffectsEnabled )
    {
        getPrefs().putBoolean( PREF_SOUND_ENABLED, soundEffectsEnabled );
        getPrefs().flush();
    }

    public boolean isMusicEnabled()
    {
        return getPrefs().getBoolean( PREF_MUSIC_ENABLED, true );
    }

    public void setMusicEnabled(
        boolean musicEnabled )
    {
        getPrefs().putBoolean( PREF_MUSIC_ENABLED, musicEnabled );
        getPrefs().flush();
    }

    public float getVolume()
    {
        return getPrefs().getFloat( PREF_VOLUME, 0.5f );
    }

    public void setVolume(
        float volume )
    {
        getPrefs().putFloat( PREF_VOLUME, volume );
        getPrefs().flush();
    }
}
These are the codes I'd like you to notice:
  1. Gdx.app.getPreferences( PREFS_NAME );
  2. getPrefs().flush();
The first one creates the preferences object if needed, then retrieves it to the caller. In the Desktop, this file would be saved under %USER_HOME%/.prefs/tyrian, but just when the second command gets executed. We should always flush the preferences after modifying them. On Android devices, the SharedPreferences class would be used, so all preferences are managed by the built-in preferences mechanism.

Implementing the Options screen

Before implementing the Options screen we could modify our Tyrian class to hold an instance of TyrianPreferences. Now we can finally code our screen. I won't paste the source code here because it is a bit lengthy, so I ask you to see it here. This is what we're doing:
  • Like the other screens we create the scene2d actors and register them in our TableLayout. Now we're handling two new kinds of actors: CheckBox and Slider.
  • We register listeners on these actors so that we do something when their state gets change. Basically we call our TyrianPreferences class, which abstracts the usage of the preferences mechanism.
This is the resulting screen:


We don't need to know how the preferences are saved on the Desktop, but in case you're curious about it, this is the preferences file created by libgdx:
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE properties SYSTEM "http://java.sun.com/dtd/properties.dtd">
<properties>
<entry key="music.enabled">true</entry>
<entry key="volume">0.5</entry>
<entry key="sound.enabled">true</entry>
</properties>

Conclusion

We created the Options screen using new actors (checkbox and slider) and made them save preference values through an abstraction to the preferences mechanism. Note that we don't manage any audio yet, but we'll do it soon. This is the tag for this post on Subversion. Thanks for reading!

16 comments:

  1. I printed all your tuts and i will do my best learning libgdx, thanks you very much for making them!

    ReplyDelete
    Replies
    1. Glad you liked! But watch out.. The new 0.9.6 version of libgdx contains many refactorings. Some of the posts I wrote are already outdated. I added the sentence "Libgdx version used on this post: x.y.z" at the top of each post, to avoid confusions.

      Delete
  2. Hi,

    I'm learning & develop and android games using libgdx.

    Thank your good tutorial.

    How do I using different fnt in menu?

    And you have any tutorial about libgdx(box2d) games tutorial?

    ReplyDelete
    Replies
    1. Thanks!

      If you already have your custom font, you should go to "uiskin.json" and change the line:
      com.badlogic.gdx.graphics.g2d.BitmapFont: { default-font: { file: default.fnt } }

      If not, you can use some tools to generate the files you need. I suggest you read this post: http://www.badlogicgames.com/wordpress/?p=1247

      Good luck!

      Delete
  3. Why this code does not work on Android devices?

    return Gdx.app.getPreferences( PREFS_NAME ) always return null.

    ReplyDelete
    Replies
    1. Please answer to me if you know.

      Delete
    2. This is very strange.. Have a look at the source code for AndroidApplication#getPreferences here:
      http://code.google.com/p/libgdx/source/browse/trunk/backends/gdx-backend-android/src/com/badlogic/gdx/backends/android/AndroidApplication.java?r=2959#285

      It just uses the Android shared preferences mechanism, wrapping it in an AndroidPreferences instance:
      http://code.google.com/p/libgdx/source/browse/trunk/backends/gdx-backend-android/src/com/badlogic/gdx/backends/android/AndroidPreferences.java?r=2959

      I suggest you to post a qustion in the official libgdx forum at: http://badlogicgames.com/forum/

      Sorry I couldn't help!

      Delete
    3. Hi there,

      First of all, thanks for this tutorial, is being a very nice summer school :P

      I'm having similar problem as "spitfire", the profile is working in desktop, but is not working on android. In my case it is not null, but is not saving the data.

      I had to add a permission (WRITE_EXTERNAL_STORAGE) to save the high scores with json (previous post). It is curious, because you don't have it in your manifest file. May it be that I need some other permission to save preferences?

      I'm using a Nexus S with Jelly Beans for testing. I'll try to use the emulator with ICS (just in case).

      Delete
    4. Sorry, when I said profile, I mean preferences.

      Delete
    5. I've tested with ICS as target in ICS emulator with the same result. I'm using the Nightly Builds (27/07/2012), I also tested with the stable build with the same result.

      When this code is executed:
      Gdx.app.getPreferences(PREFS_NAME).putBoolean(PREF_SOUND, soundEnabled);
      Gdx.app.getPreferences(PREFS_NAME).flush();
      Gdx.app.log(OddBlocks.LOG, "PrefMusicSaved (PUT): "+getPrefs().contains(PREF_SOUND));

      I the log says "false".

      Delete
    6. I've found the solution from the libgdx forums, it seems that we should keep the Preferences instance instead of getting a new one each time. In android back-end we obtain a new instance different from the previous one.

      So I added a private Preferences attribute to the MyGamePreferences class, initialized in the constructor. The GetPrefs() is not needed now.

      Delete
    7. Thanks, David! You solved the issue I was having. I made sure to keep the same instance of the preferences and now they are being properly saved.

      Delete
  4. This comment has been removed by the author.

    ReplyDelete
  5. This code:


    public void setSoundEffectsEnabled(
    boolean soundEffectsEnabled )
    {
    getPrefs().putBoolean( PREF_SOUND_ENABLED, soundEffectsEnabled );
    getPrefs().flush();
    }

    causes a problem when used with LibGDX 0.9.7 and Android 4.0.4 (and possibly other combinations). You must change this to

    public void setSoundEffectsEnabled(
    boolean soundEffectsEnabled )
    {
    Preferences preferences = getPrefs();
    preferences.putBoolean( PREF_SOUND_ENABLED, soundEffectsEnabled );
    preferences.flush();
    }

    Thanks for the tutorial, it is very useful.

    Kind Regards,
    Nick

    ReplyDelete
  6. hi. i have this error. is that about my java version or something about it? i have a same error for highscore screen too.

    #
    # A fatal error has been detected by the Java Runtime Environment:
    #
    # EXCEPTION_ACCESS_VIOLATION (0xc0000005) at pc=0x76f83a93, pid=12352, tid=14356
    #
    # JRE version: 6.0_30-b12
    # Java VM: Java HotSpot(TM) Client VM (20.5-b03 mixed mode, sharing windows-x86 )
    # Problematic frame:
    # C [ntdll.dll+0x33a93]
    #
    # An error report file with more information is saved as:
    # F:\java\Android\e-book\GDX\Steigert\tyrian-game\hs_err_pid12352.log
    #
    # If you would like to submit a bug report, please visit:
    # http://java.sun.com/webapps/bugreport/crash.jsp
    # The crash happened outside the Java Virtual Machine in native code.
    # See problematic frame for where to report the bug.
    #

    [error occurred during error reporting , id 0xc0000005]

    ReplyDelete
  7. can you tell me how to change the size of checkbox?

    ReplyDelete