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.
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.
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
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
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
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.
Enjoyed this article? Share or link to it, to support the site and help fellow developers.