Developing Multiplatform Game with LibGDX, part 17: progression and data persistence

Lesson 17: Progression and Data Persistance

So we’ve made a scene transition. But it does not make much sense: the game essentially is restarting. I believe that player needs some sense of progression. Let’s work on this today!

In our logic package, create a new class, called „GameProgress” –  we’re going to store player’s lives and level there. Now I usually store game progress in static variables. If you don’t like it – you may trying making a class and instantiate it accordingly once you start the game. Anyways, here goes:

public class GameProgress {

    public static int playerLives = 3;
    public static int maxPlayerLives = 3;
    public static int playerDamage = 1;
    public static int currentLevel = 0;
}

Not much to say: we’re going to store player lives, the maximum lives/damage our player can have (we might want to increase that value as game progresses!) and the current level.

Let’s start adding functions. We will be determining the health of the enemy according to the player’s progress. Let’s make a static function for that.

public static int getEnemyLives()
{
    return 3 + currentLevel * 2;
}

Nothing fancy. Enemy health will equal to the the current level multiplied by two while adding a constant value.

Now, I did not say data persistence in vain. Let’s implement Load/Save functions. LibGDX offers a really good way to work with simple text / setting files. It’s called Preference Files (https://github.com/libgdx/libgdx/wiki/Preferences). Not really suitable for saving complicated game data (use Binary Files http://www.javapractices.com/topic/TopicAction.do?Id=245 for that)., but will do for a simple game such as ours.

At the top of the GameProgress class, add a static string constants:

private static final String PROGRESS_SAVE_NAME = "progress";
private static final String SAVE_KEY_LIVES = "lives";
private static final String SAVE_KEY_LIVES_MAX = "livesmax";
private static final String SAVE_KEY_CURRENT_LEVEL = "currentlevel";
private static final String SAVE_KEY_PLAYER_DAMAGE = "playerdamage";

It will store the name of our save file, as well as lookup keys. After that, add two simple functions, named “Save()” and “Load()” respectively. Load function is a bit easier, let’s start with that:

public static void Load()
{
    Preferences prefs = Gdx.app.getPreferences(PROGRESS_SAVE_NAME);
    playerLives = prefs.getInteger(SAVE_KEY_LIVES, 3);
    maxPlayerLives = prefs.getInteger(SAVE_KEY_LIVES_MAX, 3);
    currentLevel = prefs.getInteger(SAVE_KEY_CURRENT_LEVEL, 0);
    playerDamage = prefs.getInteger(SAVE_KEY_PLAYER_DAMAGE, 1);
}

Pretty straightforward: open save file, then read integer values with getInteger function. The second parameter is default value (in case we’ve failed to load the value from the file).

Save function is bigger by one line:

public static void Save()
{
    Preferences prefs = Gdx.app.getPreferences(PROGRESS_SAVE_NAME);

    prefs.putInteger(SAVE_KEY_CURRENT_LEVEL, currentLevel);
    prefs.putInteger(SAVE_KEY_PLAYER_DAMAGE, playerDamage);

    prefs.putInteger(SAVE_KEY_LIVES, playerLives);
    prefs.putInteger(SAVE_KEY_LIVES_MAX, maxPlayerLives);

    prefs.flush();
}

We open the preference file, write the game progress there, then we call the “flush” command that actually finalizes the file writing, ensuring that our values are saved. That’s about it! Now let’s make use of our progress. First thing: go to the Enemy class and remove our DEFAULT_ENEMY_LIVES constant. We’re not going to need it anymore. Replace the constructor value with call to our static getEnemyLives function:

super(DEFAULT_ENEMY_LIVES);

now becomes

super(GameProgress.getEnemyLives());

The Player Class will also use some adjustments now. In our constructor, change the max_lives variable assignment to:

max_lives = GameProgress.maxPlayerLives;

Now move to our GameLogic class. Remove DEFAULT_PLAYER_LIVES constant and in our Player initialization, pass GameProgress.playerLives as the life amount.

player = new Player(
        MathUtils.random(MAX_BASE_X),
        MathUtils.random(MAX_BASE_Y),
        game.res,
        GameProgress.playerLives
);

One more thing: Let’s address the damage on bonus pickup.

else if (currentBonus.getBonusType() == Bonus.BONUS_TYPE_ATTACK)
{
    enemy.takeDamage(GameProgress.playerDamage);

And finally, we need to adjust the GameLogic part to account for victory or defeat. First, the victory (same function, after enemy takes damage):

if (enemy.getLives() <= 0)
{
    GameProgress.currentLevel ++;
    GameProgress.playerLives = player.getLives();

Just adjust the level and save the player’s lvies. Now, on defeat, let’s reset all the values. Check our OnEffectOver Method in GameLogic class. We need to add a few lines after player takes the damage:

@Override
public void OnEffectOver(WarningEffect effect) {
    if (effect.getFieldY() == player.getFieldY() &&
        effect.getFieldX() == player.getFieldX())
    {
        player.takeDamage(1);
        if (player.getLives() <= 0)
        {
            GameProgress.Reset();
        }
    }
}

Since we’re planning to reset a lot of functions (player’s lives / max lives / current level), it makes sense to make a function inside our GameProgress class to do those things.

public static void Reset() {
    playerLives = 3;
    maxPlayerLives = 3;
    currentLevel = 0;
    playerDamage = 1;
}

Now, the only thing left to do is to actually implement loading and saving. We load the data once the game is open. Adjust our DodgingHero class, create() function:

@Override
public void create () {
    res = new Resources();
    GameProgress.Load();
    setScreen(new GameScreen(this));
}

And dispose function here should do the save:

@Override
public void dispose () {
    GameProgress.Save();
    res.dispose();
}

Looks good! Now try to run the game. Complete one level and restart the game. You should notice that the data persisted (player lives and enemy has more lives now). Good job!

Relevant git commit: https://github.com/vladimirslav/dodginghero/commit/21dfa37a4f3cf190185e36281e70ffa6a99c20ea

Leave a Reply

Your email address will not be published. Required fields are marked *