Case Study: what I learned for Gamedev marketing after running the Game Review Blog

Some backstory: when I published my first game in 2016, nobody wanted to take a look at it; I was getting no replies from anyone. With time, I got a review on two websites (and it felt good, even though one was pretty critical). In retrospective, I can understand them now: the game was not polished enough and I had to remake it 4 times in order to make it look like something that I’m not ashamed of. I’ve recently published another game and it has been better received (and I got a few reviews on the websites too). However, at that time I’ve been quite upset about this. If no one else was there: I wanted to give anyone a chance.

I’ve started my own project in December 2016, a simple site where I started to post short game reviews, http://shortgamereviews.com. So far I have 26 reviews, different games (all on Steam) of different grades of success. After doing this for more than 2 months, I feel like I have enough I can share.

The process:

Send a personalized letter (from my domain email), offer to review the game (either by key or by review copy), if it is received – do three steps:

Some observations:

  • Bigger developers / publishers give out the review versions much easier than smaller ones. (When in fact it’s the smaller one who needs it most). That surprised me greatly.
  • To expand on that: I know how many scam emails you get asking for the keys (hey, I get them too). There are ways to identify the fake ones ( https://www.reddit.com/r/gamedev/comments/2efovt/warning_fake_game_key_requests_and_tips_to_avoid/ ). TL;DR: If someone asks you for more than 1 key – he’s probably trying to scam you.
  • Overall: Saying „No” is fine, but you need to say it. In fact, you get more respect if you answer „No” than if you keep ignoring the letter, as that helps the review planning and actually shows you are considerate of someone’s time.

Advice for developers:

  • Make your email easily available. If someone has to click multiple links to finally get an email – you are risking losing a review. If someone has to google your name to get your email – you are in for the trouble unless you’re making the next “Half-Life”.
  • If you have a separate project email – make sure to check it regularly. (Had some “two weeks later” replies).
  • You get extra points if you have some sort of preview version. I personally never did it (because well, if you the only programmer on the team – you don’t have time for everything), but I was seriously impressed by the dedication of the games that did. They also normally turned out pretty great.
  • That’s perfectly fine not to give a review key; you don’t owe it to anyone
  • When you are approached by a review website, you can request the metrics. Personally, I had no trouble sharing them (and if someone said it’s too low – that’s OK too).
  • If you get a negative review supported by arguments – it’s not assault on you personally. Replying to a review is a good chance to give your opinion, as long as you stay objective.

Some personal realizations, which I keep in mind when talking to fellow game developers:

  • Don’t be afraid of honesty. If you write that the game is good, but in truth is not – that helps no one. Developers see their sales, so the best thing you can do to your fellow devs is to tell what you think (even if their game sucks). If I dislike something about my friends’ games – I tell it. That’s the best help you can give.
  • Even bad reviews add publicity to your game. Maybe it sounds stupid, but there’s a game for everyone. If your game keeps featured somewhere – that’s a higher chance to get noticed. Either by your fans who will give counter-arguments or by people who like trashy stuff (if your game is actually bad).

 

I’ve kept the promise. I obviously don’t get as many request as big game journals do, but so far I’ve been able to look into everything that has been sent in and give a personal feedback to everyone who asked (even if it was a unity game on itch.io).

Developing Multiplatform Game with LibGDX, part 27: Android controls!

Implementing Android Controls

Technically, our game is ready, but in reality, we cannot launch it on Android. This is due to our controls read from Keyboard. Today, we are going to fix that by adding the arrow sprites for Android version.

Here’s 4 buttons that I’ve drawn:

We’re going to place 2 (up-down) buttons on the left side of the screen and remaining two on the right side. Now, let’s declare and load button images:

public TextureRegionDrawable leftArrowBtn;
public TextureRegionDrawable rightArrowBtn;
public TextureRegionDrawable upArrowBtn;
public TextureRegionDrawable downArrowBtn;

Then, at the end of Resources constructor, initialize them:

leftArrowBtn = new TextureRegionDrawable(gameSprites.findRegion("larrow"));
rightArrowBtn = new TextureRegionDrawable(gameSprites.findRegion("rarrow"));
upArrowBtn = new TextureRegionDrawable(gameSprites.findRegion("uarrow"));
downArrowBtn = new TextureRegionDrawable(gameSprites.findRegion("darrow"));

So far so good. Go to our GameScreen, declare new Group:

public Group controlGroup;

We will use it to place the button there (and also disable all of them when the game ends!). At the bottom of our GameScreen constructor, add:

controlGroup = new Group();
gameStage.addActor(controlGroup);
// comment out the next line to test buttons on desktop
if (Gdx.app.getType() == Application.ApplicationType.Android)
{
    prepareDirectionButtons();
}

We create our group as we would create any other actor. After that, we check which device launched our application. If it happened on Android – prepare button! (but if you are testing – comment out the “if” check, because you want to see the buttons on the screen during tests, but players will have no use for them).

Now, we need to define the prepareDirectionButtons function. In advance, I think all of the button functions are going to be somewhat equal (only movement direction, image and button coordinates differ). Therefore, we can make a separate function to create a direction button.

private void prepareDirectionButton(final int dx, final int dy, TextureRegionDrawable img, float x, float y)
{
    ImageButton btn = new ImageButton(img);
    btn.setPosition(x, y);
    btn.addListener(new ClickListener() {
        @Override
        public void touchUp (InputEvent event, float x, float y, int pointer, int button) {
            AttemptMove(dx, dy);
            super.touchUp(event, x, y, pointer, button);
        }
    });
    controlGroup.addActor(btn);
}

As simple as that: create a new button, create a click controller that is similar to our arrow keys (calls AttemtpMove) and then add it to our group. Now all that is left to do is to add 4 direction calls to our prepareDirectionButton:

private void prepareDirectionButtons() {
    // Up-Down
    prepareDirectionButton(0, 1, game.res.upArrowBtn, 2, gameStage.getHeight() / 2 + 2);
    prepareDirectionButton(0, -1, game.res.downArrowBtn, 2, gameStage.getHeight() / 2 - 16);

    // Left-Right
    prepareDirectionButton(-1, 0, game.res.leftArrowBtn, gameStage.getWidth() - 36, gameStage.getHeight() / 2 - 9);
    prepareDirectionButton(1, 0, game.res.rightArrowBtn, gameStage.getWidth() - 18, gameStage.getHeight() / 2 - 9);
}

After that, let’s remove the buttons once the stage is complete. At the beginning of our OnGameEnd call in GameScreen, add the following line:

public void OnGameEnd(final boolean playerWon) {
    controlGroup.remove();

Good! Now we have to make sure that the game launches in landscape mode on Android. Go to our AndroidManifest.xml file and adjust it accordingly (if you have not already):

android:screenOrientation="landscape"

That’s it! The game is going to have the direction buttons now. Try running it on your phone to see if everything’s working as intended.

Multiplatform Gamedev Tutorial

Relevant git commit: https://github.com/vladimirslav/dodginghero/commit/183f5b02b8ddeff08ad71c7681e1ef766ecb50c1

Developing Multiplatform Game with LibGDX, part 26: Music!

Lesson 26 – Background Music

So in the previous lesson, we implemented the game sounds. But there’s no background music and no way to control it (turn it off / make it quieter).

Playing Music is actually pretty simple. Here’s what I added to SoundManager class:

public static Music bMusic = null;
public static void StopBattleMusic()
{
    if (bMusic != null)
    {
        bMusic.stop();
        bMusic = null;
    }
}

public static void PlayBattleMusic()
{
    bMusic = Gdx.audio.newMusic(Gdx.files.internal("music/music" + MathUtils.random(5) + ".mp3"));
    bMusic.setLooping(true);
    bMusic.play();
}

Music does not really work like sounds. We preload sounds, but music is different: since music files can be quite big, they are simply being read in realtime. We’ll simply pick the random tune from music0…music6, set it to looping and then play it. Now, just add a call to start music at the initialization of GameScreen and Stop the music at the disposal.

…
public GameScreen(DodgingHero _game) {
    super(_game);
    batch = new SpriteBatch();
    bg = new Background();
    SoundManager.PlayBattleMusic();
…
@Override
public void dispose()
{
    SoundManager.StopBattleMusic();
    super.dispose();
…

Alright. This part is done. Now let’s make sure we have a button to control the sound. I have four textures with sound button, to indicate different sound/music volume. 0%, 33%, 66%, 100%, called sound0, sound1, sound2 and sound3 respectively.

Adjust our Resources class by adding TextureRegionDrawable array called soundBtn;

public TextureRegionDrawable soundBtn[];

Then, load them:

soundBtn = new TextureRegionDrawable[4];
for (int i = 0; i < soundBtn.length; i++)
{
    soundBtn[i] = new TextureRegionDrawable(gameSprites.findRegion("sound" + i));
}

Now, let’s make a setting for the sound. We could make a Settings file similar to GameProgress file, but since we only have one setting (sound), let’s implement it in GameProgress. If you plan on adding more – I strongly suggest separating into your own settings file.

In our GameProgress file, add a constant to indicate MAX_SOUND_VALUE (that would be 3, 0..3), then add a new static variable called soundVolume; Also, add a save key for it.

public static final int MAX_SOUND_VOLUME = 3;
public static int soundVolume = MAX_SOUND_VOLUME;
private static final String SAVE_KEY_SOUND_VOLUME = "soundvolume";

Make sure to save/load them. In Load():

soundVolume = prefs.getInteger(SAVE_KEY_SOUND_VOLUME, MAX_SOUND_VOLUME);

In Save:

prefs.putInteger(SAVE_KEY_SOUND_VOLUME, soundVolume);

And add a new static function ToggleVolume();

public static void ToggleVolume() {
    soundVolume += 1;
    if (soundVolume > MAX_SOUND_VOLUME)
    {
        soundVolume = 0;
    }
}

Adjust the volume, if it goes over maximum value, just start anew (by disabling it, setting it to 0). Finally, let’s integrate the change to our SoundManager. When we play music/sound, we need to take the value of the soundVolume into account. First, the volume:

private static void playSoundRandomVolume(Sound sound, float min, float max)
{
    if (sound != null)
    {
        sound.play(MathUtils.random(min, max) * GameProgress.soundVolume / GameProgress.MAX_SOUND_VOLUME);
    }
}

Just multiply randomly generated value with soundVolume. Do something similar for music.

public static void PlayBattleMusic()
{
    bMusic = Gdx.audio.newMusic(Gdx.files.internal("music/music" + MathUtils.random(5) + ".mp3"));
    bMusic.setLooping(true);
    bMusic.setVolume((float)GameProgress.soundVolume / GameProgress.MAX_SOUND_VOLUME);
    bMusic.play();
}

Casting to float is important! Otherwise the division happens between two integer values and you end up dividing 1 with 3 and get zero as a result. So you have to be careful with things like these. Taking the current music volume is great. But what if we change the volume during the game? We’ll need to modify the current music volume. Create a new static function, called AdjustVolume. It will call GameProgress.ToggleVolume and then change the volume of the currently playing music to a new value.

public static void AdjustVolume()
{
    GameProgress.ToggleVolume();
    if (bMusic != null)
    {
        bMusic.setVolume((float)GameProgress.soundVolume / GameProgress.MAX_SOUND_VOLUME);
    }
}

That part is done! Now let’s make the sound button. In our GameScreen, add a new variable called

ImageButton sndBtn;

Then initialize it in our GameScreen constructor:

sndBtn = new ImageButton(game.res.soundBtn[GameProgress.soundVolume]);
sndBtn.setPosition(gameStage.getWidth() - sndBtn.getWidth() - 10, 10);
sndBtn.addListener(new ClickListener() {
    public void touchUp (InputEvent event, float x, float y, int pointer, int button) {
        SoundManager.AdjustVolume();
        sndBtn.getStyle().imageUp = game.res.soundBtn[GameProgress.soundVolume];
        super.touchUp(event, x, y, pointer, button);
    }
});

gameStage.addActor(sndBtn);

The idea is simple: change the image after the button has been pressed, indicating the current state of the volume.

Now, if you run the game, you’ll notice that the button is there but it is not clickable. The reason is that we set up our input to GameScreen and not gameStage. We catch events in our gamescreen, but we should be doing it in our stage. Thus, we have to move our keydown function into gamestage. First, remove the InputProcessor implementation from the GameScreen and move keyDown function to stage. Make sure to change the parameters from int keyCode to InputEvent event, int keyCode.

Gdx.input.setInputProcessor(gameStage);
gameStage.addListener(new InputListener(){
      @Override
      public boolean keyDown(InputEvent event, int keyCode) {
          switch (keyCode)
          {
              case Input.Keys.RIGHT:
                  AttemptMove(1, 0);
                  break;
              case Input.Keys.LEFT:
                  AttemptMove(-1, 0);
                  break;
              case Input.Keys.UP:
                  AttemptMove(0, 1);
                  break;
              case Input.Keys.DOWN:
                  AttemptMove(0, -1);
                  break;
              default:
                  break;
          }

          return false;
      }

});

Now, try running the game and pressing the button. You’ll see that both sound/music are adjusted! Awesome! We have a sound button and we can adjust the game volume now!

Disclaimer: I’ve also tampered with game ending functions (not describing them here) to change transitions in GameScreen for html version because it was bugged otherwise. I don’t think that’s relevant to the lesson, but you can check the git commit with full code here (look for endgame boolean):

https://github.com/vladimirslav/dodginghero/commit/223be4e0ecd6518608e0207196b28642b755f8e0

This concludes our second part of lessons, where we polished the game. Next, I’m going to focus on the Android-specific stuff implementation: ads, in-app purchases.

If you want to expand the game idea – think how you can make a special abilities for every character or add more bonuses or stats (gold per bonus picked?). The effects can also be adjusted, floating numbers on damage or when player gains gold. Never stop thinking about the things that can be improved!

Play the game online: http://coldwild.com/dodge2/

Developing Multiplatform Game with LibGDX, part 25: SOUND! And bug fixes.

Minor tweaks

The gameplay is mostly done. One major flaw that remains is the lack of „restart”/”character selection” buttons after the game is over.

Add

eventListener.OnGameEnd(false);

function call in our GameLogic class, exactly once player’s hp reaches zero, right before GameProgress.Reset(true); call. Then modify our GameEventListener and make playerWon Boolean final.


public interface GameEventListener
{
    void OnGameEnd(final boolean playerWon);
}

Modify GameScreen’s OnGameEnd listener function to actually make use of playerWon variable. On our last action (where we get rid of the current screen. Here’s how it looks now:

new Action() {
    @Override
    public boolean act(float delta) {
        dispose();
        if (playerWon)
        {
            game.setScreen(new GameScreen(game));
        }
        else
        {
            game.setScreen(new CharacterSelectionScreen(game));
        }
        return true;
    }
}

After player gets defeated, he gets brought to CharacterSelectionScreen. That’s it. We no longer need to relaunch the game file after loss.

Few bugs:

  • Let’s Tweak our “Start” button and call GameProgress.Reset(false) before the game starts: this will ensure that player gets his max hp (in case we made an upgrade and it raised the hp).
  • After the player wins the stage – make sure to record his hp to currentLives. In our Player.markVictorious function, save the current player’s hp to GameProgress.playerLives:
public void markVictorious()
{
    winTime = timeAlive;
    winning = true;
    GameProgress.playerLives = lives;
}

Sound and Music introduction

Our final (and very important!) part is going to be about music. I’ve got two music/sound packs from opengameart.com:

Converted them to .ogg (smaller size) with http://audio.online-convert.com/convert-to-ogg tool.

I’ve converted the following files from attack sounds:

  • swing.wav -> swing0.ogg
  • swing2.wav -> swing1.ogg
  • swing3.wav -> swing2.ogg
  • coin.wav -> coin.ogg
  • bite-small.wav -> heal.ogg
  • fantozzi-sandl1 -> walk0.ogg
  • fantozzi-sandl2 -> walk1.ogg
  • Fantozzi-SandR1 -> walk2.ogg

Music is left-as is, in mp3, but renamed to music0..music5.ogg. In the directory android/assets/, create music folder and put sounds/music there. Finally, let’s get to coding! In our com.coldwild.dodginghero package, create a new class and call it SoundManager. It’s going to have static functions that controls menu music. Let’s introduce functions that load and unload sounds.

public class SoundManager {

    public static AssetManager assets = new AssetManager();

    public static void LoadSounds() {
        for (int i = 0; i < 3; i++)
        {
            assets.load(Gdx.files.internal("music/swing" + i + ".ogg").path(), Sound.class);
            assets.load(Gdx.files.internal("music/walk" + i + ".ogg").path(), Sound.class);
        }


        assets.load("music/coin.ogg", Sound.class);
        assets.load("music/heal.ogg", Sound.class);
        assets.finishLoading();
    }

    public static void ReleaseSounds()
    {
        assets.dispose();
    }


}

In our DodgingHero class, in our create function, after GameProgress.Load() call, add the call to LoadSounds function:

SoundManager.LoadSounds();

And

SoundManager.ReleaseSounds();

At the end of dispose() function. This will ensure the sounds are loaded at the start of the game and disposed at the end. The easiest ones to implement would be walk / attack sounds. Add those things to our SoundManager:

private static void playSoundRandomVolume(Sound sound, float min, float max)
{
    if (sound != null)
    {
        sound.play(MathUtils.random(min, max));
    }
}

public static void PlaySwingSound()
{
    Sound s = assets.get("music/swing" + MathUtils.random(2) + ".ogg", Sound.class);
    playSoundRandomVolume(s, 0.90f, 1.0f);
}

public static void PlayWalkSound()
{
    Sound s = assets.get("music/walk" + MathUtils.random(2) + ".ogg", Sound.class);
    playSoundRandomVolume(s, 0.45f, 0.55f);
}

Pretty straightforward: pick the necessary (random!) sound, then play it at a reasonably random volume to make it even more randomized. Put the calls to the following functions:

Character.takeDamage()

public void takeDamage(int amnt)
{
    SoundManager.PlaySwingSound();
    timeOfDmgTaken = timeAlive;
    lives -= amnt;
    if (lives < 0)
    {
        lives = 0;
    }
}

And in our GameScreen.AttemptMove

public void AttemptMove(int dx, int dy)
{
    if (player.getLives() > 0 &&
        enemy.getLives() > 0 &&
            logic.CheckMove(player.getFieldX() + dx, player.getFieldY() + dy))
    {
        logic.AssignPlayerPosition(player.getFieldX() + dx, player.getFieldY() + dy);
        SoundManager.PlayWalkSound();
    }
}

Try running the game! Whenever you move or your player gets hit – you’ll hear it. As you remember, we try to split game logic from representation. We won’t be calling soundmanager from our gamelogic, we’ll pass the bonus pickup / attack even to our gamescreen and it will handle it properly. First, add the functions for coin / health pickup:

public static void PlayCoinSound()
{
    Sound coin = assets.get("music/coin.ogg", Sound.class);
    playSoundRandomVolume(coin, 0.9f, 1.0f);
}

public static void PlayHealSound()
{
    Sound heal = assets.get("music/heal.ogg", Sound.class);
    playSoundRandomVolume(heal, 0.9f, 1.0f);
}

In our GameEventListener interface, add a new function declaration:

void OnBonusPickup(byte bonusType);

We’ll call it when player picks a bonus, in our AssignPlayerPosition, right after the check:

if (currentBonus.getFieldX() == fx &&
        currentBonus.getFieldY() == fy)
{
    eventListener.OnBonusPickup(currentBonus.getBonusType());

The only thing left for GameScreen is to implement OnBonusPickup method.

@Override
public void OnBonusPickup(byte bonusType) {
    if (bonusType == Bonus.BONUS_TYPE_COIN)
    {
        SoundManager.PlayCoinSound();
    }
    else if (bonusType == Bonus.BONUS_TYPE_HEALTH)
    {
        SoundManager.PlayHealSound();
    }
}

Run the game, hear more sounds! Awesome. That concludes this lesson. In our next lesson, we’ll work on adding music.

Relevant git commit: https://github.com/vladimirslav/dodginghero/commit/e93739a981ad6da90c2fe69bfce876ae1be045c9

Developing Multiplatform Game with LibGDX, part 24: improving progression and rewards

game progress balancing, replayability

Our stats affect the game, but what happens if you unlock low-level character after you’ve cleared lots of game stages with your high-level one? The difficulty simply won’t be up to par. We have to keep current stage numbers separately for each character.

In our GameProgress class, get rid of currentLevel variable (and also let’s make the naming less confusing). Also, get rid of SAVE_KEY_CURRENT_LEVEL constant. The game levels will be called „game stages” in order to avoid confusion. Add a new save key, called

private static final String SAVE_KEY_PLAYER_STAGE = "playerstage";

Right beside our levels variable, create a new array, called „stages”; Initialize it right beside our levels. Adjust the Load function to set stages to zero by default. Here’s how my Load function looks like now:

public static void Load()
{
    levels = new int[CharacterRecord.CHARACTERS.length];
    stages = new int[CharacterRecord.CHARACTERS.length];

    Preferences prefs = Gdx.app.getPreferences(PROGRESS_SAVE_NAME);

    for (int i = 0; i < CharacterRecord.CHARACTERS.length; i++)
    {
        levels[i] = prefs.getInteger(SAVE_KEY_PLAYER_LEVEL + i, i == 0 ? 1 : 0);
        stages[i] = prefs.getInteger(SAVE_KEY_PLAYER_STAGE + i, 0);
    }
//...

Don’t forget about Save function:

for (int i = 0; i < CharacterRecord.CHARACTERS.length; i++)
{
    prefs.putInteger(SAVE_KEY_PLAYER_LEVEL + i, levels[i]);
    prefs.putInteger(SAVE_KEY_PLAYER_STAGE + i, stages[i]);
}

Now, let’s get rid of errors which we inevitably got after removing currentLevel variable.

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

Becomes

public static int getEnemyLives()
{
    return 3 + stages[currentCharacter] * 2;
}

Let’s also add getEnemyDamage function (it makes sense to increase the damage as player’s hp increases too, right?)

public static int getEnemyDamage()
{
    return 1 + stages[currentCharacter] / 10; // increase damage every 10 stages
}

The Reset function needs to be heavily adjusted.

public static void Reset() {
    currentLevel = 0;
}

 

Forget about it. After player dies, let’s simply reduce his current stage by 5 (so he does not have to do everything from the beginning). We also need to reset player’s hp. Important note: when a new character is chosen in CharacterSelectionScreen, we also must reset player’s hp (since each char can have separate max hp now). Let’s introduce extra Boolean parameter to Reset, called resetProgress. It is only going to be true when player dies, and not simply changes the character.

public static void Reset(boolean resetProgress) {
    if (resetProgress)
    {
        stages[currentCharacter] -= 5;
        if (stages[currentCharacter] < 0)
        {
            stages[currentCharacter] = 0; // don't let it go below zero!
        }
    }

    playerLives = getPlayerMaxHp();
}

Now, let’s arrange the new reset calls. Go to CharacterSelectionScreen and alter the code of our next and prev buttons.

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;
        }
        GameProgress.Reset(false);
        prepareUi();
    }
});
…
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;
        }
        GameProgress.Reset(false);
        prepareUi();
    }
});

And fix the GameLogic a bit: firstly, we cannot simply increase current level when the enemy has less than 0 lives (we removed the variable, remember?). Let’s make a separate function inside the GameProgress, named increaseStage(). Let’s also grant player some money on level completion. Replace:

GameProgress.currentLevel++; 

With

GameProgress.increaseStage(); 

increaseStage implementation inside our GameProgress:

public static void increaseStage() {
    currentGold += 1 + stages[currentCharacter] / 4; // increase gold gain every 4 levels
    stages[currentCharacter]++;
}

The values are taken somewhat randomly, so only testing will show if those values are good. Always be on lookout to see what’s going on with the game balance. Two last changes:

player.takeDamage(1);
if (player.getLives() <= 0)
{
    GameProgress.Reset();
}

Pass true to our Reset function (we want to reduce levels when player has lost the game) and also use the new enemy damage vs player.


player.takeDamage(GameProgress.getEnemyDamage());
if (player.getLives() <= 0)
{
    GameProgress.Reset(true);
}

That should do it. Not much to see, but the gameplay-wise this will change a lot.

Relevant git commit: https://github.com/vladimirslav/dodginghero/commit/49e8b0fe0fa156e17acd6d4ab01d798296f70f03

Developing Multiplatform Game with LibGDX, part 23: stat logic and display

Assigning meaning to stats

So far in our game, we can upgrade the character and unlock new ones. But the upgrades do nothing. Let’s actually make them affect the game.

Player’s hp, hp regen, damage from attack and bonus spawn time is going to depend on character level. Go to our CharacterRecord class. Introduce four functions to get each value. We are going to pass the level of the character, the function is going to return the value.

For HP, damage and regen – let’s make linear functions. First, get rid of maxPlayerLives and playerDamage in GameProgress. While you’re there, also get rid of playerLives = 3 in our „Reset” function. Then, go back to CharacterRecord and implement the three functions.

public int getMaxHp(int level)
{
    return 3 + level / levelsForHpUpgrade;
}

public int getDmg(int level)
{
    return 1 + level / levelsForAttackUpgrade;
}

public int getHpRestored(int level)
{
    return 1 + level / levelsForHpRegenUpgrade;
}

Pretty straightforward. Take the base value, add level divided by the levels necessary for upgrade, get result. Looks good. What about bonus spawning? We don’t want the bonus time to go down drastically, so it makes sense to introduce diminishing returns.

public float getBonusSpawnReduction(int level)
{
    int bonusSpawnLvl = level / levelsForBonusSpawnUpgrade;
    return bonusSpawnLvl / (30 + bonusSpawnLvl); // 30 enables diminishing returns, x / ( x + 30)
}

30 is just a magical number. You can make it any value you want. All we have to do is put it into necessary places now.

Go to our Player class. Change the lines

max_lives = GameProgress.maxPlayerLives;
set(res.playerSprites.get(CharacterRecord.CHARACTERS[GameProgress.currentCharacter].name));

To

max_lives = GameProgress.getPlayerMaxHp();
set(res.playerSprites.get(CharacterRecord.CHARACTERS[GameProgress.currentCharacter].name));

In our GameProgress class, define the following function:

public static int getPlayerMaxHp() {
    CharacterRecord currentChar = CharacterRecord.CHARACTERS[currentCharacter];
    return currentChar.getMaxHp(levels[currentCharacter]);
}

We just lookup the current character record, then try to get max hp depending on the level (we’ve also made the sprite setting line a bit more readable. Now, let’s modify GameLogic for our new character stats.

First, go to our AssignPlayerPosition function. There are two places that we should change. First,

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

Let’s replace GameProgress.playerDamage with static function of the similar name.

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

In our GameProgress, create a function getPlayerDamage():

public static int getPlayerDamage() {
    CharacterRecord currentChar = CharacterRecord.CHARACTERS[currentCharacter];
    return currentChar.getDmg(levels[currentCharacter]);
}

While you’re still there, do the same for hp restored per bonus:

public static int getPlayerHealthRestored() {
    CharacterRecord currentChar = CharacterRecord.CHARACTERS[currentCharacter];
    return currentChar.getHpRestored(levels[currentCharacter]);
}

Go back to logic. In BONUS_TYPE_HEALTH check, replace player.addLives(1)  with

player.addLives(GameProgress.getPlayerHealthRestored());

Now, the only thing left to modify is our BONUS_SPAWN_INTERVAL. We’re going to set it’s value during GameLogic construction (since player cannot levelup during the battles).

Remove the default value of 2.0f from BONUS_SPAWN_INTERVAL and remove keyword static.

private final float BONUS_SPAWN_INTERVAL;

Instead, initialize BONUS_SPAWN_INTERVAL in GameLogic constructor.

BONUS_SPAWN_INTERVAL = 2.0f * (1 - GameProgress.getPlayerBonusReduction());

The function itself:

public static float getPlayerBonusReduction() {
    CharacterRecord currentChar = CharacterRecord.CHARACTERS[currentCharacter];
    return currentChar.getBonusSpawnReduction(levels[currentCharacter]);
}

 

Displaying Stat Information

 

That’s it. The changes should affect the game now. But we cannot really see them in character selection scree. Let’s fix that! First, move the hero sprite to the left by adjusting its X position:

heroSprite.setPosition((uiStage.getWidth() - heroSprite.getWidth()) / 4,
                       (uiStage.getHeight() - heroSprite.getHeight()) / 2);

Then, move the arrow button to the top of the screen (otherwise they just start getting in the way).

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

Then, let’s actually work on adding the labels with stats. Since we have 4 stats which will involve repetitive label creation, make a function to do that:

private Label prepareStatLabel(String text, float x, float y, Label.LabelStyle textStyle)
{
    Label lbl = new Label(text, textStyle);
    lbl.setAlignment(Align.left);
    lbl.setPosition(x, y);
    uiStage.addActor(lbl);
    return lbl;
}

It will return the newly created label, so that it can be used to properly figure out the coordinates of the next one below it. I add the stats after I add heroSprite to uiStage:

uiStage.addActor(heroSprite);

Label stat = prepareStatLabel("DMG:" + GameProgress.getPlayerDamage(),
        uiStage.getWidth() / 2,
        heroSprite.getY() + heroSprite.getHeight(),
        textStyle);

stat = prepareStatLabel("HP:" + GameProgress.getPlayerMaxHp(),
        uiStage.getWidth() / 2,
        stat.getY() - 10,
        textStyle);

stat = prepareStatLabel("HEAL:" + GameProgress.getPlayerHealthRestored(),
        uiStage.getWidth() / 2,
        stat.getY() - 10,
        textStyle);

prepareStatLabel("BNS:" + GameProgress.getBonusReductionValue(),
        uiStage.getWidth() / 2,
        stat.getY() - 10,
        textStyle);
;

//Bonus function for reference in GameProgress:
public static int getBonusReductionValue() {
    CharacterRecord currentChar = CharacterRecord.CHARACTERS[currentCharacter];
    return levels[currentCharacter] / currentChar.levelsForBonusSpawnUpgrade;
}

This should do it! Not only stats are working, but they are also displayed properly. Here’s what I have now:

Relevant git commit: https://github.com/vladimirslav/dodginghero/commit/c867d7ca47e0ea721353d03107ee7f9e160d32aa

Developing Multiplatform Game with LibGDX, part 22: unlocking characters / levelling up

Lesson 22: Ways to Spend Gold: Unlocking / Upgrading characters

So in the previous lesson we’ve implemented a way to collect gold, an in-game resource. But the trouble is: there’s no way to spend it yet. Let’s fix it today!

I am planning to do two things today: make characters unlockable and enable „upgrade” button for unlocked characters. All characters (except for human) are going to be locked by default. As soon as player collects 1000 gold – he can unlock a character. The amount of gold is up to you. I think 500 is a good amount, because we’ll be able to introduce In-App-Purchases later which would grant enough gold, but also player can gather enough once he played some time (without paying a dime).

Alright, so let’s modify our progress file first. We’re going to adjust it gradually, according to the changes what we are making. First thing that we are going to do, let’s make a separate array of levels of each character. 0 will mean that the character is locked, any value above that will simply mean character level (1+). Introduce the following constants / variables:


public static final int CHARACTER_PRICE = 1000; // price to unlock a character

public static int levels[]; // level of each character, 0 = locked

private static final String SAVE_KEY_PLAYER_LEVEL = "playerlevel";

We’re going to init the array in our Load function. Since we are saving/loading multiple values, we’re simply going to add an index to the key.

public static void Load()
{
    levels = new int[CharacterRecord.CHARACTERS.length];

    Preferences prefs = Gdx.app.getPreferences(PROGRESS_SAVE_NAME);

    for (int i = 0; i < CharacterRecord.CHARACTERS.length; i++)
    {
        levels[i] = prefs.getInteger(SAVE_KEY_PLAYER_LEVEL + i, i == 0 ? 1 : 0);
    }

Something similar goes to saving. In our Save() function, add the following code:

for (int i = 0; i < CharacterRecord.CHARACTERS.length; i++)
{
    prefs.putInteger(SAVE_KEY_PLAYER_LEVEL + i, levels[i]);
}

This should do it for saving/loading. Now that the backend is (somewhat) handled – let’s adjust the menu to allow the magic to happen!

So, first, let’s modify our “Start” button to be replaced with “Unlock” button if character is locked. The code is pretty simple:

if (GameProgress.levels[GameProgress.currentCharacter] == 0)
{
    TextButton startBtn = new TextButton("Unlock(1000 Gold)", buttonStyle);
    startBtn.setPosition((uiStage.getWidth() - startBtn.getWidth()) / 2, uiStage.getHeight() / 6);
    startBtn.addListener(new ClickListener() {
        public void touchUp (InputEvent event, float x, float y, int pointer, int button) {
            if (GameProgress.currentGold >= GameProgress.CHARACTER_PRICE)
            {
                GameProgress.currentGold -= GameProgress.CHARACTER_PRICE;
                GameProgress.levels[GameProgress.currentCharacter] = 1;
                prepareUi();
            }
        }
    });
    uiStage.addActor(startBtn);
}
else
{
    // start / upgrade button code goes here
    TextButton startBtn = new TextButton("START", buttonStyle);
    startBtn.setPosition((uiStage.getWidth() - startBtn.getWidth()) / 2, uiStage.getHeight() / 6);
    startBtn.addListener(new ClickListener() {
        public void touchUp (InputEvent event, float x, float y, int pointer, int button) {
            dispose();
            game.setScreen(new GameScreen(game));
        }
    });
    uiStage.addActor(startBtn);
}

We just add a different button in case the character is locked.

You might have noticed that we have to call uiStage.clear() before every prepareUi call. So, this is redundant. Let’s simply move uiStage.clear() to the beginning of prepareUi() and remove all other uiStage.clear() calls on this screen.

Run the game and try switching characters. If you set the CHARACTER_PRICE to lower value for debugging purposes and try unlocking a character – you’ll see that it becomes available.

After that, let’s get to upgrade button. But first, I think it makes sense if we add a label with character level or just a warning (“char locked!”). First, move textStyle declaration/initialization on top of our prepareUi function, right above the buttonStyle declaration.

Now, let’s make a new label, after our heroSprite definition. (Because we’re going to use that as an orientation / position settings).

uiStage.addActor(heroSprite);

// char level
int lvl = GameProgress.levels[GameProgress.currentCharacter];
Label statusText = new Label(lvl > 0 ? "LVL: " + lvl : "LOCKED", textStyle);
statusText.setPosition(heroSprite.getX() + (heroSprite.getWidth() - statusText.getWidth()) / 2,
        heroSprite.getY() - statusText.getHeight() - 5);
uiStage.addActor(statusText);

Now we can finally get to upgrade button. You might have noticed, that the interface is getting a bit overcrowded. Let’s move the start button to the top and levelup button to the bottom (where there’s Unlock button for locked chars).

Here’s how the code looks like:

else
{
    // start / upgrade button code goes here
    TextButton startBtn = new TextButton("START", buttonStyle);
    startBtn.setPosition((uiStage.getWidth() - startBtn.getWidth()) / 2, uiStage.getHeight() * 5 / 6);
    startBtn.addListener(new ClickListener() {
        public void touchUp (InputEvent event, float x, float y, int pointer, int button) {
            dispose();
            game.setScreen(new GameScreen(game));
        }
    });
    uiStage.addActor(startBtn);


    TextButton upgradeBtn = new TextButton(
            "LvlUp(" + GameProgress.getNextUpgradeCost(GameProgress.currentCharacter) + ")",
            buttonStyle);
    upgradeBtn.setPosition((uiStage.getWidth() - upgradeBtn.getWidth()) / 2, uiStage.getHeight() / 6);
    upgradeBtn.addListener(new ClickListener() {
        public void touchUp (InputEvent event, float x, float y, int pointer, int button) {
            if (GameProgress.currentGold >= GameProgress.getNextUpgradeCost(GameProgress.currentCharacter))
            {
                GameProgress.currentGold -= GameProgress.getNextUpgradeCost(GameProgress.currentCharacter);
                GameProgress.levels[GameProgress.currentCharacter] += 1;
                prepareUi();
            }
        }
    });
    uiStage.addActor(upgradeBtn);
}

The start button code pretty much remains unchanged, except for Y position, which has moved to top of the screen.

UpgradeBtn is similar to start button, but it invokes getNextUpgradeCost function (will get to it later). The position is the same as the old “start” button position. The click function acts in a similar way, checks the next level upgrade cost and if it’s fine – takes the money and increases the character level. Looks good? Let’s go to GameProgress and define the function to get next level cost. As an experiment, I think it’s fine if we multiply the current level value by 2.

public static int getNextUpgradeCost(int currentCharacter) {
    return levels[currentCharacter] * 2;
}

I’ve also noticed a bug, CharacterSelectionScreen does not have resize handling, so resizing gets ugly. Let’s fix that!

@Override
public void resize(int width, int height)
{
    super.resize(width, height);
    uiStage.getViewport().update(width, height, true);
}

Run the game! If you have some gold already – upgrade your character. For now, the upgrade is only visual, but don’t get upset – we’re going to fix this in the next lesson! See you then.

Git commit: https://github.com/vladimirslav/dodginghero/commit/564990aeed19a887e568666bf85d9b52865c7466

Developing Multiplatform Game with LibGDX, part 21: Gold Gathering

Lesson 21: Gold Gathering

Last lesson, we’ve added multiple characters and made it possible to pick them. We’ve also introduced  the levelling stats. Unfortunately, since there are no levels, there’s no point of stats.

First thing’s first, how are we going to level up our characters? Well, this is quite easy! They’ll have to push the “Upgrade” button! What’s that? We cannot simply allow player to mash the upgrade button? Fine, we’ll introduce in-game currency to solve the issue of infinite upgrading.

First thing: we’ll add a coin that can be picked on the battlefield. Yes, similar to health / attack. Here’s how it looks (drawn by https://twitter.com/ElenaNazaire):

coin

First, go to our Bonus.java file. Add a new static byte to mark our coin bonus.

public static byte BONUS_TYPE_COIN = 2;

In our “setup” function, add a case for bType == BONUS_TYPE_COIN:

else if (bType == BONUS_TYPE_HEALTH)
{
    set(res.healthBonus);
}
else if (bType == BONUS_TYPE_COIN)
{
    set(res.coinBonus);
}

You’ll notice that coinBonus is not defined yet. That’s allright. Go to Resources class and right under our healthBonus sprite, define a new sprite:

public Sprite healthBonus;
public Sprite coinBonus;

Then, down below, load it the same way you would load healthBonus.

healthBonus = gameSprites.createSprite("health");
coinBonus = gameSprites.createSprite("coin");

Now, make sure we create the bonus in-game. In GameLogic, let’s adjust oru SpawnBonus function. Now if you’ve played the game as much as I did, you can already notice that hearts spawn a bit too much. Let’s reduce their rate and increase the attack spawn rate a bit. Say, 5 out of 8 times we want attacks, 2 out of 8 times we want coins and 1 time we want health bonus.

Replace this part:

bonuses.add(Bonus.Create(fx,
        fy,
        MathUtils.random(3) < 1 ? Bonus.BONUS_TYPE_HEALTH : Bonus.BONUS_TYPE_ATTACK,
        game.res));

With:

byte activeBonus = Bonus.BONUS_TYPE_ATTACK; // use it by default
int rnd = MathUtils.random(7); // 0 .. 7
if (rnd > 6)
{
    activeBonus = Bonus.BONUS_TYPE_HEALTH;
}
else if (rnd > 4)
{
    activeBonus = Bonus.BONUS_TYPE_COIN;
}

bonuses.add(Bonus.Create(fx,
        fy,
        activeBonus,
        game.res));

Use attack bonus by default, then in a few special cases switch it to health and coins. Let’s run the game! You now see that the coins are spawning, but picking them does nothing. Let’s fix that!

Handling the logic behind coins

Great, so what do we do now? How do we make coins work? We need to introduce a separate variable that would store the coins. Let’s handle it in GameProgress.

public static int currentGold = 0;

Add saving/loading at once.

private static final String SAVE_KEY_PLAYER_GOLD = "playergold";

In our Load() function, let’s handle the loading of our gold:

currentGold = prefs.getInteger(SAVE_KEY_PLAYER_GOLD, 0);

And a bit further, in our Save() function, let’s handle the proper saving of the value!

prefs.putInteger(SAVE_KEY_PLAYER_GOLD, currentGold);

Good, the gold is saved, but it is not gathered properly. Time to implement that. Remember, in our GameLogic class, we have the AssignPlayerPosition function? In it, we process the bonus pickups. Let’s alter it to actually handle the coin pickups. Here’s how my full check looks now:

if (currentBonus.getBonusType() == Bonus.BONUS_TYPE_HEALTH)
{
    player.addLives(1);
}
else if (currentBonus.getBonusType() == Bonus.BONUS_TYPE_ATTACK)
{
    enemy.takeDamage(GameProgress.playerDamage);
    if (enemy.getLives() <= 0)
    {
        GameProgress.currentLevel++;
        player.markVictorious();
        eventListener.OnGameEnd(true);
    }
}
else if (currentBonus.getBonusType() == Bonus.BONUS_TYPE_COIN)
{
    GameProgress.currentGold += 1;
}

That should do it.

Displaying the coin amount to the player

Now let’s ensure we display our coins both in GameScreen and CharacterSelectionScreen.

Start with CharacterSelectionScreen. We want to display coins on the bottom left side of the screen. To do this, we’ll show the coin image and write amount of coins we have beside it. Open our CharacterSelectionScreen class. Go to prepareUi() function, and add the following lines to the end of it:

// coin image
Image coinImage = new Image(game.res.coinBonus);
coinImage.setPosition(1, 1);
uiStage.addActor(coinImage);

// amount of coins
Label.LabelStyle textStyle = new Label.LabelStyle(game.res.gamefont, Color.WHITE);
Label coinAmntLbl = new Label("" + GameProgress.currentGold, textStyle);

// set X position to the right of our coin and Y to be exactly in the middle of it
coinAmntLbl.setPosition(coinImage.getX() + coinImage.getWidth() + 3,
        coinImage.getY() + (coinImage.getHeight() - coinAmntLbl.getHeight())/ 2);
uiStage.addActor(coinAmntLbl);

Now let’s add it in our GameScreen now. Since we don’t use Stage in gamescreen for ui elements, let’s just draw it “roughly”. Go to our DrawUi function and add the following code:

batch.draw(game.res.coinBonus,
        gameStage.getViewport().getScreenX() + 2,
        gameStage.getViewport().getScreenY() + 5
);
DrawShadowed("" + GameProgress.currentGold,
        gameStage.getViewport().getScreenX() + game.res.coinBonus.getWidth() + 4,
        gameStage.getViewport().getScreenY() + 10 + game.res.coinBonus.getHeight() / 2,
        gameStage.getWidth() - 5,
        Align.left,
        Color.WHITE);

Pretty self-explanatory, first we draw a coin, then: the amount of gold we actually have. Run the game! You’re going to see the coins on our main screen and in the game, as well as notice how the amount increases after you pick them.

final

This took a bit more time and changes that I’ve expected, so I’ll try to cover levelling in our next tutorial.

Relevant git commit: https://github.com/vladimirslav/dodginghero/commit/6eb79301a6ce8727379edc00b431c6a17b6ff506

Tags:

Android game tutorial, game development, multiplatform game development, beginning game development, gamedev tutorial, libgdx, android gamedev

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 Resources.java 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(
        game.res.playerSprites.get(CharacterRecord.CHARACTERS[GameProgress.currentCharacter].name)
);

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;
        }
        uiStage.clear();
        prepareUi();
    }
});

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

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;
        }
        uiStage.clear();
        prepareUi();
    }
});
prevBtn.setPosition(uiStage.getWidth() / 6 - prevBtn.getWidth() / 2, uiStage.getHeight() / 2);
uiStage.addActor(prevBtn);

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 Player.java file, and replace

set(res.player);

In player’s constructor with:

set(res.playerSprites.get(CharacterRecord.CHARACTERS[GameProgress.currentCharacter].name));

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: https://github.com/vladimirslav/dodginghero/commit/62d13eecf8b7d3a016d973276b7bf6af0c2515b0

Developing Multiplatform Game with LibGDX, part 19: basic character selection screen

Adding Different Playable Heroes

Allright, our enemies attack differently. But how can we diversify the gameplay of our player? Let’s introduce different playable characters.

First, let us ask ourselves, what do we want to display here? We want to make a screen where player chooses (and later – unlocks/upgrades) the character he wants to play with. Each character will have its strengths: it is important to give choice when it comes to game decisionmaking.

We’ll need to make a separate screen for that. In our screens package, create a new class, name it “CharacterSelectionScreen.” Make it extend DefaultScreen.

Similar to our GameScreen, we need a stage that is going to process player input / display UI elements. Declare it:

Stage uiStage;

We also need to track the currently selected character. Let’s make an integer index specially for that.

int currentCharacter;

The constructor is simple:

public CharacterSelectionScreen(DodgingHero _game) {
    super(_game);
    currentCharacter = 0; //TODO: Load it from settings later
    FitViewport viewport = new FitViewport(160, 120);
    uiStage = new Stage(viewport);
    Gdx.input.setInputProcessor(uiStage);
}

We create a stage using fit viewport to always maintain the resolution. We use our stage as inputProcessor, because it would allow stage to capture the inputs. Now we need to define dispose and render functions. Let’s make them real simple first.

@Override
public void render(float delta)
{
    Gdx.gl.glClearColor(0, 0, 0, 1);
    Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT);
    uiStage.act(delta);
    uiStage.draw();
}

@Override
public void dispose()
{
    Gdx.input.setInputProcessor(null);
    uiStage.dispose();
    super.dispose();
}

Render simply clears the screen, updates the stage and draws it. Dispose resets the input, then frees resources taken up by stage.

Finally, go to our DodgingHero.java class, and change

setScreen(new GameScreen(this));

to:

setScreen(new CharacterSelectionScreen(this));

This will make the game show us Character Selection Screen instead of GameScreen (at once) where we’ll be able to pick necessary characters. Run the game. You should see the black blank screen. Great! That means we could set our new screen.

Now, to progress further, let’s add a simple “Start” button. Well, not really a button since it’s going to be a text caption that you need to press to start the game. Still, it’s going to be great.

In our CharacterSelectClass, let’s prepare a new function, name it prepareUi(). We’re going to call it at the bottom of our constructor (right after Gdx.input.setInputProcessor(uiStage);).

The first version of the function looks like this:

void prepareUi()
{
    TextButton.TextButtonStyle buttonStyle = new TextButton.TextButtonStyle();
    buttonStyle.font = game.res.gamefont;
    buttonStyle.fontColor = Color.WHITE;
    TextButton startBtn = new TextButton("START", buttonStyle);
    startBtn.setPosition((uiStage.getWidth() - startBtn.getWidth()) / 2, uiStage.getHeight() / 6);
    startBtn.addListener(new ClickListener() {
        public void touchUp (InputEvent event, float x, float y, int pointer, int button) {
            dispose();
            game.setScreen(new GameScreen(game));
        }
    });
    uiStage.addActor(startBtn);
}

We’re creating a TextButton that has “Start” writte on it. First, we make the button style where we define the font and color of the font. If you have graphics – it’s also possible to define up/hover/down states of the button (however, I – don’t).

Then, the button is being initialized by passing the text and the style and setting the position. The trickiest part is handling button clicks: in this case we’re calling addListener and defining our own version of ClickListener, by overriding the touchup function. At this point, as soon as player clicks our “Start” button, the screen is going to switch for our GameScreen.

Finally, don’t forget to add out new button to the stage by calling addActor method! After that, run the game.

Good, we have a start button. Now let’s display our hero and make arrow-like buttons (that are going to be used later). Again, in this lesson, let’s just do a simple display, without functionality. At the end of our prepareUi function, add these lines:

Image heroSprite = new Image(game.res.player);
heroSprite.setPosition((uiStage.getWidth() - heroSprite.getWidth()) / 2,
                       (uiStage.getHeight() - heroSprite.getHeight()) / 2);
uiStage.addActor(heroSprite);

TextButton nextBtn = new TextButton(">>>", buttonStyle);
nextBtn.setPosition(uiStage.getWidth() * 5 / 6 - nextBtn.getWidth() / 2, uiStage.getHeight() / 2);
uiStage.addActor(nextBtn);

TextButton prevBtn = new TextButton("<<<", buttonStyle);
prevBtn.setPosition(uiStage.getWidth() / 6 - prevBtn.getWidth() / 2, uiStage.getHeight() / 2);
uiStage.addActor(prevBtn);

That’s it! After you run the game, you should see something like this now:

final_screen

That concludes lesson 19! In the next lesson, we’re actually going to work on implementing the character choice, but for now the screen should be enough. Relevant git commit: https://github.com/vladimirslav/dodginghero/commit/e46d43eac5dcba21b3e3df502a0706d1436885c3