Tuesday, October 4, 2011

roguelike tutorial 14: experience and leveling up

It's great that we've got some caves, equipment, and monsters but what about character development? Levels, feats, attributes, classes, and skill trees can be a complex subject worth studying but, and maybe you saw this coming, we'll do something simple to start with. For now, when a creature is killed, it's killer gains xp, when enough xp is gained, a new level is gained and some benefit is chosen. One thing I'd like to have is other creatures can gain levels too. Why? Just because I haven't seen it done before (at least not that I've noticed). It also means that positioning weak monsters between you and the big bad guy may just make the big bad guy gain levels. Maybe you should start rethinking the use of meat shields....

Our creatures need xp, a level, and a way to gain xp and levels. When the xp passes the next level threshold, the level should be incremented, the ai should get notified, and the creature should heal a bit.

private int xp;
  public int xp() { return xp; }
  public void modifyXp(int amount) {
      xp += amount;

      notify("You %s %d xp.", amount < 0 ? "lose" : "gain", amount);

      while (xp > (int)(Math.pow(level, 1.5) * 20)) {
          level++;
          doAction("advance to level %d", level);
          ai.onGainLevel();
          modifyHp(level * 2);
      }
  }

  private int level;
  public int level() { return level; }

The starting level is initialized to 1 in the constructor. I'm using a formula to determine how much experience is needed for the next level but you can use a lookup table or some other formula. Many interesting things have been said about leveling and power curves so read up, try different things, and do what works for you.

We need to update the attack method to grant experience when a creature is killed. Add the following to the end of the attack method:

if (other.hp < 1)
      gainXp(other);

Now we can grant experience based on some experience value the creature has, or on it's level, or on the killers level, or by some combination. I'm using a simple formula for now.

public void gainXp(Creature other){
    int amount = other.maxHp
      + other.attackValue()
      + other.defenseValue()
      - level * 2;

    if (amount > 0)
      modifyXp(amount);
  }

This ensures that tougher creatures are worth more and by subtracting the killer's level, easy creatures will soon be worth nothing. It's not perfect but it's simple to explain, understand, and code.

If you play it now (after creating an empty onGainLevel in CreatureAi) you should see notices about gaining levels. That's a good sign.


For this simple roguelike, when something gains a level it get's some stat bonus; increased hp, increased attack, etc. The player will be shown a list to chose from but other creatures will get one at random.

Let's create a class to represent a level up option's name and actual effect.

package rltut;

public abstract class LevelUpOption {
  private String name;
  public String name() { return name; }

  public LevelUpOption(String name){
    this.name = name;
  }

  public abstract void invoke(Creature creature);
}

We need something to track all the possible options and enforce some of our level-up logic. We'll call it a LevelUpController — even though classes with Manager or Controller in the name are usually vague and messy and not the best way to do things.

package rltut;

import java.util.ArrayList;
import java.util.List;

public class LevelUpController {

  private static LevelUpOption[] options = new LevelUpOption[]{
    new LevelUpOption("Increased hit points"){
      public void invoke(Creature creature) { creature.gainMaxHp(); }
    },
    new LevelUpOption("Increased attack value"){
      public void invoke(Creature creature) { creature.gainAttackValue(); }
    },
    new LevelUpOption("Increased defense value"){
      public void invoke(Creature creature) { creature.gainDefenseValue(); }
    },
    new LevelUpOption("Increased vision"){
      public void invoke(Creature creature) { creature.gainVision(); }
    }
  };
}
I created a few simple options based on the stats we already have but I'm sure you can come up with more. These are anonymous classes - if you're not familiar with them you should check them out. Anonymous classes can make some things very clear and succinct - other things are best left to regular classes.

This LevelUpController should be able to select one option at random and apply it to a given creature.
public void autoLevelUp(Creature creature){
    options[(int)(Math.random() * options.length)].invoke(creature);
  }

Now the CreatureAi can call this to automatically gain some benefit when a creature gains a level.
public void onGainLevel() {
    new LevelUpController().autoLevelUp(creature);
  }

Of course the Creature class needs to support these new options too. Here's what I came up with:
public void gainMaxHp() {
    maxHp += 10;
    hp += 10;
    doAction("look healthier");
  }

  public void gainAttackValue() {
    attackValue += 2;
    doAction("look stronger");
  }

  public void gainDefenseValue() {
    defenseValue += 2;
    doAction("look tougher");
  }

  public void gainVision() {
    visionRadius += 1;
    doAction("look more aware");
  }

If you try it now you should see that when you gain a level you look tougher or stronger etc. You may even notice something else looking stronger or more aware. That's why I think it's so cool to have a method like doAction; you can, if you're lucky, see the rare and subtle events like these.

Try it.


What about when the player gains a level? Shouldn't we show a list of options for the user to choose from? Let's start by making sure the player doesn't automatically get free bonuses.

Override the onGainLevel method in the PlayerAi class.
public void onGainLevel(){
  }

Now we update the PlayScreen's respondToUserInput method. When we first enter the method, before the user does anything, we need to record the player's level.
int level = player.level();
After responding to the player's input, we need to see if that resulted in a level up. If so, we jump into a LevelUpScreen and tell it how many bonuses the player get's to pick.
if (player.level() > level)
      subscreen = new LevelUpScreen(player, player.level() - level);

Now all we need is a LevelUpScreen that uses a LevelUpController to show what can be picked and applies that choice.
package rltut.screens;

import java.awt.event.KeyEvent;
import java.util.List;

import rltut.Creature;
import rltut.LevelUpController;
import asciiPanel.AsciiPanel;

public class LevelUpScreen implements Screen {
  private LevelUpController controller;
  private Creature player;
  private int picks;

  public LevelUpScreen(Creature player, int picks){
    this.controller = new LevelUpController();
    this.player = player;
    this.picks = picks;
  }

  @Override
  public void displayOutput(AsciiPanel terminal) {
    List<String> options = controller.getLevelUpOptions();

    int y = 5;
    terminal.clear(' ', 5, y, 30, options.size() + 2);
    terminal.write("     Choose a level up bonus       ", 5, y++);
    terminal.write("------------------------------", 5, y++);

    for (int i = 0; i < options.size(); i++){
      terminal.write(String.format("[%d] %s", i+1, options.get(i)), 5, y++);
    }
  }

  @Override
  public Screen respondToUserInput(KeyEvent key) {
    List<String> options = controller.getLevelUpOptions();
    String chars = "";

    for (int i = 0; i < options.size(); i++){
      chars = chars + Integer.toString(i+1);
    }

    int i = chars.indexOf(key.getKeyChar());

    if (i < 0)
      return this;

    controller.getLevelUpOption(options.get(i)).invoke(player);

    if (--picks < 1)
      return null;
    else
      return this;
  }
}

And there you go; a simple leveling system that lets the player decide how they want to progress their character. You could add different bonuses like special moves, critical hits, extra xp per kill, or special abilities. You could even make it so the player could chose a new item after leveling up.

download the code

4 comments:

  1. public void gainDefenseValue() {
    attackValue += 2; //This should say defenseValue += 2;
    doAction("look tougher");
    }

    ReplyDelete
  2. We would further amount to better understanding of the various objects and reliable opinions as narrated here and hopefully for the future these would almost help students to bring every possible guides herewith.

    ReplyDelete
  3. You didn't write getLevelUpOptions method, neither you wrote getLevelUpOption. They are easy to code yourself, so whatever.

    ReplyDelete