Levelling up in RPGs.

I've previously written about stats in RPGs but I avoided talking about one of the most important RPG stats; the level!

Levels are such a unique stat they need examining all on their own. We'll cover using them to increase stats and how to implement them in a game.

Levels in JRPGs

A level is a number that represents the overall power of an entity in the game. Commonly player characters and enemies will have levels. For example, a level one warrior might be a youth heading out from a small village ready for his first adventure. A level fifty warrior is a veteran of many battles; she's strong, fast and quick witted. The level fifty warrior has grown powerful through experience, she's fought battles, completed quests and these experiences have made her physically and mentally tough. The level is a number that represents this accumulated experience.

The level is a number that represents accumulated experience.

Characters usually start at level 1 and increase their level by gaining experience. Experience points, also known as XP, are used to measure how much a experience a character currently has. Game entities usually have both a level and XP number. The XP number tracks how close the entity is to advancing to the next level. Once an entity's XP reaches a certain threshold, they gain a level and their XP resets to zero. XP is usually gained by defeating enemies but can also be given out for any worthy accomplishment. Each successive level requires more XP to attain; therefore gaining levels gradually becomes tougher. When the player character gains a level, the game acknowledges the achievement with some fanfare and rewards the player by increasing stats and possibly giving out special abilities.

Mass Effect.

Levels aren't just limited to player characters and enemies; special weapons, armor and even spells may have levels. Additionally, the level number is often used directly in the combat simulation code.

Origins

Like almost everything in JRPGs, levels have been passed down from tabletop role-playing games. In D&D levels provide much the same function as in modern JRPGs; they mark out how powerful an entity is and provide a sense of progress for players.

Why levels?

Levels and experience points are a popular mechanic with both players and designers for some of the following reasons:

1. Sense of Achievement

Gaining a level gives the player a sense of achievement and it's an acknowledgement of the player's efforts. When the level number increases it's a way of giving the player a pat on the back and saying "Good job!". The number increasing is a small reward all on it's own but RPG's usually give out additional rewards by increasing the players stats and sometimes unlocking special abilities.

The value in gaining a level is related to how difficult it is to achieve. In order for a level to have value, the player needs to struggle and overcome adversity to reach it. Higher levels require more XP and take longer to reach because the time invested is proportional to the reward. The distance between levels gives them meaning.

2. The Hero's Journey

JRPG characters are literally on a hero’s journey. Their level indicates how far they’ve come from the starting point and hint how far is left to go.

Levels describe the characters growth as they overcome obstacles in the game. Each level bumps the character’s stats, so as the character levels up they become stronger and more powerful in the game world. The flow of the game represents this, the enemies the player is currently battling will suddenly be a little easier, but very quickly the player will discover more powerful enemies and will be battling up hill again. If combat ever gets too easy, it becomes boring.

Level graph.

Difficulty follows a step function, things become briefly easier only to become even harder than before.

3. Dripfeed Complex Mechanics

Most JRPGs have quite complicated battle mechanics with status effects, various types of spell, elements, chain attacks and so on. The battle systems can be so complicated that it's too much to try and show the player all at once. Instead as the player slowly levels up, mechanics are introduced in understandable chunks. This dripfeed helps make the game more accessible and the shallows out the learning curve.

At level 1 only the most basic mechanics are available making combat straight forward for a new player. The choices are very restricted, so the player can quickly try them all out. Gaining levels unlocks new abilities and spells which the player can immediately try out and get comfortable with before the next mechanic is introduced.

This dripfeed also applies to enemies. Early in the game enemies exhibit simple behaviors and few abilities but as the characters level up and progress to new areas, they meet new monsters with special attacks and more advanced tactics. For the player to encounter these more advanced enemies, they must first defeat the simpler enemies by mastering the basic combat techniques.

4. Control the Flow of Content

Levels help drip feed mechanics but they also help control how the player navigates the world.

Is there a cave where the monsters are far too hard? Maybe the player can’t tackle it yet, but they’ll remember the cave, it will become a goal, something to return to with stronger characters.

Levels also control how quickly a character can dispatch monsters, which controls how fast and easily a character can travel from place to place and how long a play-through of the game will take.

Level Formulas

Now we come to the meat of a leveling system; how to implement it. We need a formula that given a level tells us the amount of experience points required to reach next level. An example is shown below.

xp_for_level_2 = next_lvl(1)

The formula should return an increasing amount of XP per level and be easy to tweak for balancing purposes.

Example Systems

Nearly all RPGs have some leveling function hidden inside them. Let's take a look at some functions taken from existing games. (These have been reverse engineered and therefore may not be totally accurate!)

Lua doesn't have a 'round' function, to round a number to the nearest whole number, but we're going to need it so we'll define our own here.

-- round(0.7) -> 1
-- round(5.32) -> 5
function round(n)
    if n < 0 then
        return math.ceil(n - 0.5)
    else
        return math.floor(n + 0.5)
    end
end

To get an better understanding of these systems, open excel or a programming editor and tinker with some of the examples.

Original D&D

Dungeons and Dragons is the game many early JRPGs used for their inspiration, so it seems only fair to include it's level function.

function nextLevel(level)
    return 500 * (level ^ 2) - (500 * level)
end

DND Level graph.

Generation 1 Pokemon

Pokemon actually has two leveling systems, this is the faster of the two.

function nextLevel(level)
    return round((4 * (level ^ 3)) / 5)
end

Pokemon Level graph.

Disgea

Disgea applies this formula to the first 99 levels after that it changes things up.

function nextLevel(level)
     return round( 0.04 * (level ^ 3) + 0.8 * (level ^ 2) + 2 * level)
end

Disgea Level graph.

Common Themes in Leveling Functions

All the examples use exponential functions to get a steep leveling curve. The steeper the curve the greater the amount of XP required for each successive level and probably the more time the player has to invest to reach it. Each of the above formulas is tailored for each game. For instance in Dungeons and Dragons, games progress for weeks and high level characters are rare, so the level curve is extremely steep. Pokemon has many monsters each of which can be leveled up, so it has a more modest curve. The shallowest curve of all is Disgea, as nearly every item in the game has some type of leveling applied to it.

Levels are Only One Part of the Eco-System

Levels are only one small part of a JRPG and can't stand alone. The speed to progress through levels depends not only on the XP required but also on how often and what amounts of XP are given out in the game. This usually means how many monsters a player can expect to encounter and how much XP will be rewarded each time one is defeated.

Designing Our Own Formula

Now we'll design our own formula, loosely based on Final Fantasy. We can revise it when creating the game content, deciding what xp enemies have and how quickly we want the player to be able to progress.

Here's the basic formula.

function nextLevel(level)
    local exponent = 1.5
    local baseXP = 1000
    return math.floor(baseXP * (level ^ exponent))
end

This formula is pretty simple but let's go through it.

To begin with let's assume the exponent is 1, this means each level the amount of XP needed increases by baseXP. If baseXP is 1000, level 1 requires 1000xp, level 2 2000xp, level 3 3000xp and so on. This is a simple step increase, the levels aren't getting increasingly hard. If you kill 10 wolves to get level 1, to get to level 2 you'd only need to kill 20 wolves; we want each level to be more challenging; that's where the exponent comes in.

The exponent represents the difficulty between levels and how that difficulty increases, let's set it 1.5. Level 1 now requires 1000xp, level 2 requires 2828xp, level 3 5196xp an ever increasing difficulty which better suited for our needs.

When tweaking our formula we can alter the baseXP for how much XP we generally want a level to cost and the exponent for how increasingly difficult we want it to become to attain those levels.

Level Up!

Now we have a function that tells us how much XP is needed to gain a level, it's time to look at what happens when we actually level up. In the most simple case, some of the characters stats will increase but this doesn't happen in a uniform way.

The personality of a JRPG character is tied quite closely to how a character's stats are distributed. A thief like character will have better speed increases from a level than a hulking warrior. We need a way to define which stats are emphasized for a given character. Therefore we'll write the code to make it simple to give different characters different stat growth strategies.

Final Fantasy games usually have a slightly random stat growth from 1 to 3 points for the base stats and a larger increase for HP and MP. Our system will follow this convention.

Implementation

Each stat the character has is given a growth rate. We'll write a level up function that uses these growth rates to increase the stats. (Later we might want to extend the level up function to unlock abilities or spells at certain levels.)

To define the rate of growth we're going to borrow from tabletop role-playing games and use dice. Our stat growth rates will be defined by different dice, one roll for a fast rate, one roll for a slow rate. Dice are intuitive way to think about different distributions when increasing stats. Rolling 1 die with 6 sides returns a number from 1 to 6, all equally likely. Roll 2 die with 3 sides, add the results, and then the range is from 2 to 6 and the distribution is weighted towards the middle. The middle heavy distribution arises because there are more possible combinations for rolls to make a 4 than for a 2 or 6. (If you want to get a good understanding of this see Amit's Article here)

It's common to write dice throws out as 1D6, which means one roll of a six sided die, 2D6 would be two rolls of a six die and so on. As this notation is pretty easy to read and write that's the notation we'll use to create our dice class. This means writing a small parser but it will make our game data much easier to read down the line.

The parser needs to handle something of the form [number]D[number] which can be optionally followed by +[number] for a modifier. Then if we want to group several dice together we'll separate each definition by space. Here are some examples:

"1D6"           one roll of a six sided die
"2D6+3"         two rolls of a six sided die and add three
"1D6 2D8+10"    one roll of a six sided die plus two rolls of an eight sided die plus ten

Our class will parse these strings into the Roll class that we can use in code. Here's the constructor and parser code.

Roll = {}
Roll.__index = Roll

function Roll:Create(diceStr)
    local this =
    {
        dice = {}
    }
    setmetatable(this, self)
    this:Parse(diceStr)
    return this
end

function Roll:Parse(diceStr)
    local len = string.len(diceStr)
    local index = 1
    local allDice = {}

    while index <= len do
        local die
        die, index = self:ParseDie(diceStr, index)
        table.insert(self.dice, die)
        index = index + 1 -- eat ' '
    end
end

This code allows a Roll object to be created with a string (such as "1d6"). The constructor sets up the class and passes the string to a method called Parse. Parse has an index variable for the position in the string and repeatedly passes this to the function ParseDie until the index is at or past the end of the string.

Parse, in plain English, just says keep parsing dice from the string until we reach the end. ParseDice returns a die table, describing a die, and a new index. The new die is then added to the Roll's list of dice.

Let's add the final parsing functions and then move on to the rest of the class.

function Roll:ParseDie(diceStr, i)
    local rolls
    rolls, i = self:ParseNumber(diceStr, i)

    i = i + 1 -- Move past the 'D'

    local sides
    sides, i = self:ParseNumber(diceStr, i)

    if i == string.len(diceStr) or
        string.sub(diceStr, i, i) == ' ' then
        return { rolls, sides, 0 }, i
    end

    if string.sub(diceStr, i, i) == '+' then
        i = i + 1 -- move past the '+'
        local plus
        plus, i = self:ParseNumber(diceStr, i)
        return { rolls, sides, plus }, i
    end
end

function Roll:ParseNumber(str, index)

    local isNum =
    {
        ['0'] = true,
        ['1'] = true,
        ['2'] = true,
        ['3'] = true,
        ['4'] = true,
        ['5'] = true,
        ['6'] = true,
        ['7'] = true,
        ['8'] = true,
        ['9'] = true
    }

    local len = string.len(str)
    local subStr = {}

    for i = index, len do

        local char = string.sub(str, i, i)

        if not isNum[char] then
            return tonumber(table.concat(subStr)), i
        end

        table.insert(subStr, char)
    end

    return tonumber(table.concat(subStr)), len
end

These two methods are a little longer but they're both quite simple. ParseDie parses a number, the rolls, then advances the index past the letter D, it then parses another number, the sides. Then it checks if it's at the end of the string or reached a space, otherwise it advances the index, past the + and reads a final number. It then returns the die as a table of 3 parts; the rolls, the sides and the modifier.

ParseNumber reads all characters that are digits and returns the new number and index. It's not as important to go through this step by step but if you're interested then please check it out!

Now the parsing is done, we can add some functionality to the Roll class.

-- Notice this uses a '.' not a ':' meaning the function can be called
-- without having a class instance
function Roll.Die(rolls, faces, modifier)
    local total = 0

    for i = 1, rolls do
        total = total + math.random(1, faces)
    end
    return total + (modifier or 0)
end

function Roll:Roll()
    local total = 0

    for die in ipairs(dice) do
        total = total + Roll.Die(unpack(die))
    end

    return total
end

The class has a function call Roll.Die which can be called directly

Roll.Die(1, 6) -- some num 1..6 (1 die, 6 faces)

Or the class can be instantiated so it can be called multiple times

local r1d6 = Roll:Create("1d6")

print(r1d6:Roll())

print(r1d6:Roll())

The class can also handle more complicated rolls.

local roll = Roll:Create("1d6 1d8+10")

print(roll:Roll())

With the dice implemented it's simple enough to add different stats growths.

local Growth =
{
    fast = Roll:Create("3d2"),
    med = Roll:Create("1d3"),
    slow = Roll:Create("1d2")
}

Leveling up can be complicated, for instance carrying a certain item, or have certain abilities selected may effect or even control entirely how the stats grow, but we're going to keep things simple. The stat growth rates are set in character definition tables. Let's say our RPG contains only 3 characters, the main hero, a thief and a mage. We might define their stats improvements like so.

heroDef =
{
    stats =
    {
        ... -- starting stats
    },
    statGrowth =
    {
        ["hp_max"] = Roll:Create("4d50+100"),
        ["mp_max"] = Roll:Create("2d50+100"),
        ["str"] = Growth.fast,
        ["spd"] = Growth.fast,
        ["int"] = Growth.med,
    },
    -- additional character definition info
}

thiefDef =
{
    stats
    {
        ... -- starting stats
    },
    statGrowth =
    {
        ["hp_max"] = Roll:Create("4d40+100"),
        ["mp_max"] = Roll:Create("2d25+100"),
        ["str"] = Growth.fast,
        ["spd"] = Growth.fast,
        ["int"] = Growth.slow,
    },
    -- additional character definition info
}

mageDef =
{
    stats =
    {
        ... -- starting stats
    },
    statGrowth =
    {
        ["hp_max"] = Roll:Create("3d40+100"),
        ["mp_max"] = Roll:Create("4d50+100"),
        ["str"] = Growth.med,
        ["spd"] = Growth.med,
        ["int"] = Growth.fast,
    },
    -- additional character definition info
}

The Hero character's stat growth is the best. The hero's strength, speed both grow fast and his intelligence grows at a medium rate. His HP increases by 4d50+100 and MP by 2d50+10. The thief is similar but weaker in both HP and MP and not as quick to increases intelligence. The mage growth in strength and speed is medium but fast in intelligence and has lower HP gains than the thief but higher MP gains than anyone. With all this information laid out as above, it's very easy to tweak as the game develops and easier to compare different characters and monsters.

These definition tables are used to create character class instances which will be used by the game to store information about the character and display it on screen. Here's a bare-bones character class, showing only the fields needed for leveling up and stats.

Character = {}
Character.__index = Character

-- In this case we're only interested in the character def but
-- in the final game, the character will take more parameters.
function Character:Create(def, ...)

    local this =
    {
        def = def,
        stats = Stats:Create(def.stats),
        statGrowth = def.statGrowth,
        xp = 0,
        level = 1,
    }

    this.nextLevelXP = nextLevel(this.level)

    setmetatable(this, self)
    return this
end

Our character class needs a function for adding XP and the ability to detect when a level is gained. Gaining a level is an important event and needs to be communicated to the player visually. In order to make it easier to write this presentation code, the AddXP function will not be responsible for actually applying the level up, it just reports that the player can level up. Levels and experience are usually awarded only at the end of a battle, when the player is shown a Battle Summary screen. This summary screen usually shows the player's xp, stats and levels as they were at the start of the battle and then counts up the XP gained for each character. If a level is achieved then the stat increase is shown. When we make our RPG we'll be programming this screen and by carefully structuring the code now, we can make it easier on ourselves later!

function Character:ReadyToLevelUp()
    return self.xp >= self.nextLevelXP
end

function Character:AddXP(xp)
    self.xp = self.xp + xp
    return ReadyToLevelUp()
end

This function returns true if a level has been gained when the player has been given new XP. The LevelUp function actually works out the stat growth, increments the level and handles

The next function CreateLevelUp returns a table with the player's levelled up stats, new abilities and so forth. We'll call this function CreateLeveUp the code is shown below.

function Character:CreateLeveUp()

    local levelup =
    {
        xp = - self.nextLevelXP,
        level = 1,
        stats = {},
    }


    for id, dice in pairs(self.statGrowth) do
        levelup.stats[id] = dice:Roll()
    end

    -- Additional level up code
    -- e.g. if you want to apply
    -- a bonus every 4 levels
    -- or restore the players MP/HP

    return levelup
end

This newLevel table represents the character's next level, we can inspect this and display the values on screen before applying it to the player. Say the player has unlocked a new ability, this is very easy to learn by inspecting the newLevel table. If we just had a function that immediately leveled up the player it would be harder to discovered the player had unlocked this ability.

ApplyLevel is the function that actually increases the player level and stats (and decreases the XP).

function Character:ApplyLevel(levelup)
    self.xp = self.xp + levelup.xp
    self.level = self.level + levelup.level
    self.nextLevelXP = nextLevel(self.level)

    assert(self.xp >= 0)

    for k, v in pairs(levelup.stats) do
        self.stats.mBase[k] = self.stats.mBase[k] + v
    end

    -- Unlock any special abilities etc.
end

That's all the leveling up code needed for now. Time to write a small program to demonstrate it all working. The program uses a simple function ApplyXP which takes in an xp point amount, then for each level gained prints out the new level number and the stats that have been gained.

function PrintLevelUp(levelup)

    local stats = levelup.stats

    print(string.format("HP:+%d MP:+%d",
          stats["hp_max"],
          stats["mp_max"]))

    print(string.format("str:+%d spd:+%d int:+%d",
          stats["str"],
          stats["spd"],
          stats["int"]))
    print("")
end

function ApplyXP(char, xp)
    char:AddXP(xp)

    while(char:ReadyToLevelUp()) do

        local levelup = char:CreateLevelUp()
        local levelNumber = char.level + levelup.level
        print(string.format("Level Up! (Level %d)", levelNumber))
        PrintLevelUp(levelup)
        char:ApplyLevel(levelup)
    end

end

PrintLevelUp is a helper function that prints out the levelup table in a nicely formatted way. The ApplyXP function demonstrates our level up code so far. It takes two parameters; char and xp. The char is a Character table and the xp is the amount of XP to give this character. The function itself adds all the xp to the player, then while the player can gain levels, levelup tables are created, printed out and applied to the character. In this way the player can gain as many levels as a increase in XP requires.

A small script can now be written to ApplyXP to an instance of the hero character and print out the final result.

hero = Character:Create(heroDef)
ApplyXP(hero, 10001)

-- Print out the final stats
print("==XP applied==")
print("Level:", hero.level)
print("XP:", hero.xp)
print("Next Level XP:", hero.nextLevelXP)

local stats = hero.stats

print(string.format("HP:%d MP:%d",
      stats:Get("hp_max"),
      stats:Get("mp_max")))

print(string.format("str:%d spd:%d int:%d",
      stats:Get("str"),
      stats:Get("spd"),
      stats:Get("int")))

In the above example the player has been given 10,001 XP points which is enough to level the hero up to level 4, with a little XP left over. This is a good script to experiment with, changing the stat growth strategies, the amount of XP awarded and so on, to get a feel for how the leveling system we've built hangs together.

Wrapping Things Up

This article has managed to cover a lot of ground relating to leveling systems from their composition; XP, Level number and formulas, their origins, how they can grow a characters stats and how they can be implemented in code. There's a lot of code dotted about, I've gathered up everything that's used in the last example and put it in a gist here. It includes the stats class from the previous article as well as fragments of classes that would be needed in a larger game.

Only the very most standard level up system for JRPGs has been covered here. This is a great starting point for changing it to meet the needs of your game. For instance instead of XP you might have souls that are sucked from an enemy, or some kind of reverse XP system where you removed disadvantages from your player to restore him to super demigod like state! Everything is free for you to change.

Perks and the unlocks of special abilities haven't been covered here. Unlocking special powers is a simple enough addition - a level check and a boolean flag but without the code to actually use the ability it seems premature to add.

Have fun creating your own level system, please share how it goes.