Level Systems (part 1)

Hello there! It would seem there is a need for a guide about leveling systems, based almost purely on the number of questions received. I’m going to dump a bunch of theory here on the math and ideas behind leveling systems.

In this part I will explain:

  • The theory behind basic leveling systems
  • The theory behind experience functions
  • The theory behind add experience functions
  • Basic level bar scaling

In the next part I will explain:

  • Modifier systems and their implementations
  • Advanced experience functions
  • Sanity checks
  • Choices, Choices, finding good constants

Small disclaimer, this is by no means a definitive guide, the language I use is based on my own experience with crafting such features.

The Therory Behind Basic Leveling Systems
A basic leveling system has a couple parts but can essentially be broken into this:

  • A function which dictates the amount of experience to the next level

  • A function which adds some experience

  • A way to sanity check the amount of experience being set

Basic Experience Functions
Mathematically a basic experience function is an increasing function that scales with level, simply that is:

Level * ExperienceScale

This however, does create a couple problems, for instance, level 0 needs 0 experience to get to level 1. To get around this the most basic experience function that is super usable has one addition, a second constant (besides ExperienceScale).

constant + Level * ExperienceScale

This is a rather nice approach to a rather simple problem, and it is what we will be using for the rest of this guide, in code I would have it look a little like this:

local Level = 0
local constant, ExperienceScale = 100,10
local EF = (function(level) return constant + (level*ExperienceScale) end)

It doesn’t actually matter that the function looks exactly like this, for example you could make it exponential if you hate your players.

You may also be asking, why I dedicated a function to the experience instead of a single calculation later on, this has to due with reusability in multiple functions (including the experience bar scaling) and further the ability to override the function in part 2 with more advanced experience functions where this would be our default. (Say if we were implementing this as a library for multiple games.)

And the constants are up to you, I think the values of 100 and 10 are decent but this would also depend on how much experience you give your players later on.

Add Experience Functions
Simply defined, this is the function that adds experience, and increments your level as a result.
The algorithm is rather simple, and should look something like this:

  • Take in an amount of experience

  • Check if the incoming experience overflows the level, if so increment the level add the overflow

  • Otherwise, just add the experience

A simple implementation might look like this:

local function AddExp(amount)
    if (currentExperience+amount) > EF(Level) then
        --Step 0: Since we overflowed calculate the exp leftover
        local LeftOverExp = (currentExperience+amount)-EF(Level)
        --Step 1: increment the level
        Level = Level + 1
        --Step 2: Since we overflowed, set the exp to 0 and recurse
        currentExperience = 0
        AddExp(LeftOverExp)
    elseif (currentExperience+amount) == EF(Level) then
        --Step 1: increment the level
        Level = Level + 1
        --Step 2: reset experience
        currentExperience = 0
    else
        currentExperience = currentExperience + amount
   end
end

This is a simple algorithm, although it includes no sanity checks, which I will introduce in Part 2, should you stay tuned for that.

Basic Level Bar Scaling
Because your level bar may be horizontal or vertical, I’m not actually going to explain which dimension to scale your bar, I’m also not going to explain a full GUI implementation incase you wish to implement this on other platforms. Instead, I am going to assume you either know the maximum width, or height of your level bar.

With that said, the math for this is simple and yet all too often I see it overcomplicated.

We have a few variables from earlier which we can use, specifically currentExperience and EF(Level) (or our total experience for this level.) This can be used to create a simple ratio to be multiplied by our bar height or width. The calculation would look like such:

(currentExperience/EF(Level)) * HeightOrWidth

That, would be the size that your bar should be in the current moment. I’ll leave the animation implementation to you, whether it be instant or tween in some manner.

I’ve left some things out for simplicity, however if there is significant interest I can cover those topics in part 2 or (if I do a part 3).

At the end of this I will also open source my own module for leveling systems.

Should this be a repeat post, or somewhat inadequate please let me know so I may update it.

Good luck, Vathriel

EDIT: Part 2 is out here

133 Likes

it looks like the AddExp function could use a wait(), if a large amount of Exp was added it would probably crash (because it would keep calling the function to add more Exp for a while if the Exp was a high amount)

Looking forward to Part 2

3 Likes

I’m glad you caught this, I’ll cover it in part two under “Sanity checks” <3

2 Likes

I may be wrong but I think I found another slight issue

    if (currentExperience+amount) > EF(Level) then
        --Step 1: increment the level
        Level = Level + 1
        --Step 2: Since we overflowed, set the exp to 0 and recurse
        local LeftOverExp = (currentExperience+amount)-EF(Level)
        currentExperience = 0
        AddExp(LeftOverExp)

the EF(Level) function gets updated with the new level on line 3(of the code snippet) which ends up increasing the max exp by a bit more than what it’s suppose to be

example

you have 1400 exp and the max exp is 1200 you would do 1400 - 1200 to get 200, but if you update the EF(Level) after you gained that level the max exp would end up being more than 1200 like 1500

1400 - 1500 is -100 when you should be getting 200

Simple fix would be to move the Level = Level + 1 below the LeftOverExp variable

I’m really tired so there is a chance I could be wrong

4 Likes

Glitch report checks out, I’ll fix it now. Thanks :heart:

2 Likes

Hello!
This seems to be really small issue, but seems that you did = instead of ==. In an if condition.

Single = is used to assign a value to variable.
Two = are used to compare the values.

This is in the AddExp function. I’m on phone so its hard for me to describe but.

elseif (currentExperience+amount) = EF(Level) then

Should be

elseif (currentExperience+amount) == EF(Level) then

Edit: you could easily shorten up the script by removing the elseif and changing > to >= in the if condition.

1 Like

Thanks for noticing that typo.

As for > vs >=, the intent is to shorten the actual processed instructions, as it removes the need to create a local variable on the stack through a function call and arithmetic, as well as avoiding a pointless recursive call. That’s why I had the final else statement.

I would honestly use something like this.

local function AddExp(amount)
    local newExp = currentExperience+amount
    while newExp >= EF(Level) do -- If EXP is high for multiple levels this is useful (repeat as long as newExp is greater than required EXP.
        -- Step 0: Calculate the new EXP.
        newExp = newExp - EF(Level)
        -- Step 1: Increase the level
        Level = Level + 1
    end
   currentExperience = newExp
end
3 Likes

That would work fine as well, and would be decently efficient, but when you get to part 2 you see that I use the number of recursions as an upper limit for sanity checking. A similar thing could be implemented with an if/break system with a counter in your code.

I do like this refactor decently enough, it’s clean and would likely be more efficient given how Lua scopes actually look in byte code. I may in the next couple days update both parts of the guide to include examples with this looping method.

Appreciated.

1 Like

This is more of a curiosity as a (self-proclaimed) programmer. I’ve seen multiple scripts using

local functionName = (function(parameters) end)

I normally use

local function functionName(parameters) end

I noticed that you used the first over the later, what would be the difference between the two? Calling the function seems to be the same both ways, just with one, the function is being assigned to a variable, does this affect the speed of the script? Or is it just done due to personal preferences rather than specific scripting (optimisation etc) reasons.

That happens in both cases actually. In the second case there is also a variable functionName that contains a function value.

One is syntactic sugar for the other. Most people would probably prefer the second form since it is more standard and is easier to read over the first one. It should not make a significant (if any) difference in performance which one you use.

3 Likes

Much appreciated. This really algorithm really helps as I was going to overcomplicate it myself, but looked here to double check. I like it cause you can add a player parameter too.