Developing Multiplatform Game with LibGDX, part 20: Introducing Character Variety

Lesson 20: Introducing Character Variety and Upgrade system

So as you’ve seen in our previous lesson, we had added the character select screen. Unfortunately, there were no characters to select 🙂 Let’s fix that!

What do we need? We’ll need a data structure to describe each character’s stats. We can do the following: introduce multiple characters and make them upgradable for game currency. The trick is: each character has its own strongpoints. I.e. one character will be getting +1 health after every upgrade, the other: after every two, another one: after every three. Same goes for attack strength from picking relevant bonus and for hp regeneration (how much hp you restore from picking hearts). That way we can create a character that restores lots of hp, but has small healthpool. Or the character that does lots of damage, but has trouble with hp regeneration.

In our logic/objects package, create a new class and name it “CharacterRecord.” I suggest we make four different stats:

  • Upgrade levels needed for hp upgrade (player buys one upgrade level, the character’s hp upgrades after every N levels). Similar to that:
  • Upgrade levels needed for hp regen upgrade (how much hp player regenerates per heart picked?)
  • Upgrade levels needed for attack upgrade (how much damage player deals per attack?)
  • Upgrade levels needed for bonus time upgrade (let’s make bonus spawn time dependant on player’s character!)

Obviously, each character will also need a name (at least for display purposes on the menu). CharacterRecord will store the base stats, but not the level by itself. So, it should be simple, really. We can create a simple constructor and pre-define the characters this way:

public class CharacterRecord {

    public final int levelsForHpUpgrade;
    public final int levelsForHpRegenUpgrade;
    public final int levelsForAttackUpgrade;
    public final int levelsForBonusSpawnUpgrade;

    public final String name;

    public CharacterRecord(int lvlHp, int lvlRegen, int lvlAttack, int lvlBonus, String _name)
        levelsForHpUpgrade = lvlHp;
        levelsForHpRegenUpgrade = lvlRegen;
        levelsForAttackUpgrade = lvlAttack;
        levelsForBonusSpawnUpgrade = lvlBonus;
        name = _name;

    public static String CHAR_NAME_HUMAN = "Human";
    public static String CHAR_NAME_SPIDER = "Spider";
    public static String CHAR_NAME_SKELETON = "Mr.Skeletal";
    public static String CHAR_NAME_GHOST = "Ghost";
    public static String CHAR_NAME_SLIME = "Slimey";

    public static CharacterRecord CHARACTERS[] =
        new CharacterRecord(2, 2, 4, 4, CHAR_NAME_HUMAN),
        new CharacterRecord(3, 6, 3, 3, CHAR_NAME_SPIDER),
        new CharacterRecord(6, 12, 1, 3, CHAR_NAME_SKELETON),
        new CharacterRecord(4, 4, 2, 4, CHAR_NAME_GHOST),
        new CharacterRecord(3, 3, 4, 1, CHAR_NAME_SLIME),

My general character idea is:

  • Human has good health, but not so good attack / bonus spawn rate.
  • Spider gets good health, but bad regen. Better attack than human, but bonus spawn times are getting upgraded slower.
  • Skeleton is terrible at health/healing, but does good damage at good intervals.
  • Ghost is mostly average. Good stats, but nothing exceptional.
  • Slime has amazingly fast bonus spawns and a bit wors stats otherwise.

Adjusting the character selection screen

Before we actually make stats relevant, we’ll have to create different character selection at first. So we have some stats pre-defined, but what do we do now? Let’s actually start by allowing to select different characters. Before we even talk about swapping active character index, let’s discuss how we get the relevant character sprite and name to be displayed in our CharacterSelectionScreen. I suggest we start working on resources class. Similar to the way we return enemySprites, we should make a hashmap that stores player sprites. This time, I’m making it with <String,Sprite> pair (as I am going to use it sparcely), but I must let you know that using strings as lookup won’t do if you do some real-time rendering and not single sporadic lookups. It just takes a big hit on performance (alright, it probably depends on the implementation of hashmap, but I won’t go that deep in this tutorial).

Open up our class. Right beside our enemySprites declaration, declare another Hashmap:

public HashMap<String, Sprite> playerSprites;

This will store various player sprites. Initialize it at the same place where you initialize the enemySprites. Then fill it with relevant content.

playerSprites = new HashMap<String, Sprite>();
playerSprites.put(CharacterRecord.CHAR_NAME_HUMAN, gameSprites.createSprite("player"));
playerSprites.put(CharacterRecord.CHAR_NAME_SPIDER, gameSprites.createSprite("spider"));
playerSprites.put(CharacterRecord.CHAR_NAME_SKELETON, gameSprites.createSprite("skeleton"));
playerSprites.put(CharacterRecord.CHAR_NAME_GHOST, gameSprites.createSprite("ghost"));
playerSprites.put(CharacterRecord.CHAR_NAME_SLIME, gameSprites.createSprite("slime"));

That should do it.

Making character selection work

OK, we got player sprite list. What do we do? Go to CharacterSelectionScreen. We’ll need to adjust a few things. First, move currentCharacter variable into GameProgress class:

public class GameProgress {

    public static int currentCharacter = 0;

This is necessary, because we’re going to save the selected character (a bit later). Go back to our CharacterSelectionScreen. See our hero image creation in prepareUi()? Let’s make heroSprite be initialized with the current selected character’s sprite.

Image heroSprite = new Image(

Also, remove the line

currentCharacter = 0;

From constructor. Try running the game. You should see the good old human sprite. The difference is that we’re looking it up from our hashmap instead of directly providing it to our heroSprite Image. With that, the preparations are mostly done. Let’s finally get to switching the active character sprite! All we need to do is to add listeners to nextBtn and prevBtn (which are somewhat similar).

TextButton nextBtn = new TextButton(">>>", buttonStyle);
nextBtn.addListener(new ClickListener() {
    public void touchUp (InputEvent event, float x, float y, int pointer, int button) {
        GameProgress.currentCharacter += 1;
        if (GameProgress.currentCharacter == CharacterRecord.CHARACTERS.length)
            GameProgress.currentCharacter = 0;

nextBtn.setPosition(uiStage.getWidth() * 5 / 6 - nextBtn.getWidth() / 2, uiStage.getHeight() / 2);

TextButton prevBtn = new TextButton("<<<", buttonStyle);
prevBtn.addListener(new ClickListener() {
    public void touchUp (InputEvent event, float x, float y, int pointer, int button) {
        GameProgress.currentCharacter -= 1;
        if (GameProgress.currentCharacter < 0)
            GameProgress.currentCharacter = CharacterRecord.CHARACTERS.length - 1;
prevBtn.setPosition(uiStage.getWidth() / 6 - prevBtn.getWidth() / 2, uiStage.getHeight() / 2);

Just to explain a bit: we assign click listeners to next/prev buttons. What we do is increase/decrease current character index. If it is out of bounds – go to the beginning/end of the list (to allow player scrolling through the characters without limitation). After that, we clear all Ui elements from the stage and reinitialize them. This might seem like an overkill (we just have to switch the heroSprite, right? But in truth this won’t be the only thing later on (when we’ll be writing stats on the main screen). So we clear all the ui elements from the screen and then repopulate it again.

Now as the final thing for this tutorial part, let’s just make out GameScreen to show the new selected sprite (but not be affected by the stats in any way).

Go to our file, and replace


In player’s constructor with:


Run the game and try picking any character! You should see that our player character sprite has successfully changed. That concludes lesson 20! Next time, we’ll actually try to make those stats affect the game and introduce the upgrade/unlock system.

Relevant git commit:

Leave a Reply

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