This is a continuation of Level Systems (part 1). In part 2 we will be getting into some heavy math with advanced experience functions, modifier systems, and sanity checks.
I’ve decided to take out the section dealing with choosing constants for this part due to the length. If you’d like that section, I can add it later on.
Modifier Systems
I will only be approaching two methods of modifier systems, they are not necessarily the most efficient or “best”.
The first style of modifier system is simply a boolean flag with a preset modification rate.
Assuming you had our previous version of the AddExp function, this is a pretty simple addition. We will start with our variables:
local ModifierFlag,ExpMultiplier = false,2
These, likely, should be variables with the rest and be global within your environment so you can implement ways to change them. I’ll leave that part to you.
The changes to the AddExp function can be broken into steps:
- Before the rest of the AddExp function runs we need to first check if the modifier is set.
- Then we must multiply our experience
- Finally continue with the rest of the function
In code this would look like this:
Hidden so you can try implementing it yourself first
local function AddExp(amount)
if (ModifierFlag) then amount = amount * ExpMultiplier end
if (currentExperience+amount) > EF(Level) then
--Step 0: Since we overflowed, calculate leftover Exp
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
Now this is no where near an ideal setup, in many games for example you may want to sell a time limited experience booster, this rapidly becomes more complex, and for simplicity I’m not going to handle the datastore part of this system, other guides cover datastore better than I ever could.
So what’s the solution? Data structures!
Let’s first map out the type of information we’d need for an implementation which: Has a time limited booster, the booster has a specific multiplier, and a single booster cannot be duplicated.
(I’ll let you handle the player specific part of this, remember this is a theory guide not an implementation guide.)
So now we have:
- An identifier for the booster
- A timestamp for the booster
- A duration for the booster
- The multiplier that the booster provides
In code we’d assume that this structure might look a bit like:
local SomeRandomPlayersBoosters = {identifier,timestamp,duration,multiplier}
Your own implementation might be more readable if you include this with dictionary definitions as such:
local SomeRandomPlayersBoosters = {
{
Identifier = "SomeBooster"; --This doesn't have to be a string, just unique to the booster type
TimeStamp = 0; --in reality this would be tick() of when it was created
Duration = 259200; --I don't really care what your duration is, but it'll need to be in seconds. For now I set it to three days, handle your datastore appropriately
Multiplier = 2; --Ideally some number greater than 1, otherwise you will hurt your players not help them
},
{
--This would be another booster, same properties, I left this here as an example.
--In reality this would break the next couple steps if left here.
}
}
Cool! We now know what our data looks like, so let’s figure out how to make a booster.
The function which creates the booster will stamp the time itself, that means the only things the function needs are the duration, multiplier, and identifier.
Hidden so you may try it yourself first
function CreateModifier(identifier, duration, multiplier)
--tick() is the provided function we will use. It returns the current time in seconds since 1970 started.
return {Identifier = identifier, TimeStamp = tick(), Duration = duration, Multiplier = multiplier}
end
Awesome, we now have a way to create a modifier, I’ll (again) leave it to you to make this player specific, each player will need their own table of modifiers, so the function above will likely need another argument to insert the modifier into the appropriate table, or return it to a function that already has that information.
With this setup you’ll be able to do awesome things! You can for example, create a double experience weekend, and then players can still buy experience boosters! They feel good about what they earn, you still make money, etc.
Now we have to get into the less satisfying part of actually reimplementing the AddExp function to use this. I’ll post my own implementation below, but if you’d like to try it yourself, this is the algorithm broken into steps:
- Before we run the core of the function we’ve already made, we need to find a way to remove any expired modifiers.
- After we remove the expired modifiers we need to apply all of the modifiers to the experience
- Finally we need to finish our function like we did before.
The following implementation will work for that same single player like above, I have faith you all can figure out how to make this player specific.
AddExp with Modifiers implementation
function AddExp(amount,recursed) --recursed is added for an efficiency check
if recursed ~= true then
local removals = {}
for i,v in next, SomeRandomPlayersModifiers do
if v.Duration + v.TimeStamp < tick() then
table.insert(removals,i)
else
--do the multiplication on the amount
amount = amount * v.Multiplier
end
end
for i,v in next, removals do
table.remove(SomeRandomPlayersModifiers,(v-(i-1)))
end
else
--We don't do the multiplier because the experience is multiplied. We also already checked for expired boosters.
end
if (currentExperience+amount) > EF(Level) then
--Step 0: Since we overflowed, calculate leftover Exp
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,true)
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
At this point you basically have a working modifier system if you’ve been following along and making the appropriate changes for having multiple players.
Advanced Experience Functions
This part is going to get math heavy. I will not go into much of the implementation of these functions, just cover the math behind a couple common techniques. I will be including graphs, although you can graph them yourself as well, I highly recommend desmos for this.
Disclaimer: All of the graphs below are unlabeled. Assume X axis is level and Y axis is experience needed. You may have to change your constants to get something satisfying.
When we first made an experience function in part 1, we used a linearly increasing function, specifically,
constant + Level*ExperienceScale
But wait! I hear you say, that’s not a line like it should be! That’s because we’re only using whole numbers, which is why it looks staggered. Specifically this actual function would look like:
constant + math.floor(Level)*ExperienceScale
Now, this may be fine for a game that stops gaining levels at say level 15 or 20, but if you have infinite levels then eventually you need to collect infinite experience. Which becomes more of a punishment to the player than a reward, or milestone.
For that reason, many games often use a logarithmic curve, initially levels increase in experience needed kind of rapidly and then taper off.
This of course is not totally usable, and you’d need to like the linear version add a constant so that you have some amount of experience under the curve, also don’t forget that this is a floor function on our system. Finally, that’s not a very steep curve at the beginning so we should probably add some constant multiplier on the outside. I used this:
3*math.log(math.floor(Level)) + 1
Again, this is nice, and can be used in many implementations, while it still increases infinitely, it does so much slower, your players will likely be happier with a curve like this.
But, again there is another form of experience function, in my opinion, the most rewarding for the player. And that is a milestone experience function. Many more modern games will use functions similar to this. Essentially you want to create a repeating function that slowly increases. You reach your milestone, then the experience goes back down by quite a bit, etc. This also lets you build in other types of reward mechanics.
The math behind this specific function utilizes a modulus to create the milestone levels, and a linear function to create the slow rise.
(math.floor(Level)%5) + (math.floor(Level)/5)
The last type of experience function I will cover is an advanced experience function which is a hybrid between a logarithmic function and a milestone function. This is basically what League of Legends uses currently.
The math of this is a bit tedious and you’d have to play around with it for your own implementations, but it looks like this:
function EF()
if Level<50 then
return 13.85*math.log(math.floor(Level))
else
return (math.floor(Level)%5) + math.log(math.floor(Level))+(math.floor(Level)%10)+(math.floor(Level)/30) + 50
end
end
And, to reference part 1, if you hate your players you can do something like this:
2^(math.floor(Level)/5)
There are more types of experience functions available, however these are rather common examples, and hopefully these examples will help you craft your own with your own constants.
Sanity Checks
Perhaps the most important part of this entire guide is sanity checks. It’d be unfortunate for example if your players overflowed their levels or experience.
As part of this, I’m going to address one concern from a previous part.
Thanks to @Quoteory for reminding me of this.
What if your experience gain is so high that it overflows multiple levels? At extremely high levels this could cause the AddExp function to recurse to an almost infinite extent and crash. The solution is a sanity check. For instance in a single call it might be a bit odd for a player to overflow their experience bar more than 3 times, that should raise flags and how you handle it is up to you, for this I’ll just reduce the experience call to 3 calls.
Example:
function ExperienceOverflowCheck(Exp)
if Exp > EF(Level) + EF(Level+1) + EF(Level+2) then
--Could be a hacker depending on your implementation, handle it appropriately.
return EF(Level) + EF(Level+1) + EF(Level+2)
else
return Exp
end
end
This is a simple check that can be done before or after your multiplier code, I recommend after, so that stacking multipliers has some upper limit in how many levels you can gain with one call.
There is of course one more problem, ideally your implementation wont allow this anyways, but what if there was some way the client could force AddExp(someRandomAmount) to be called? What if it did that a thousand times a second? That seems a bit ridiculous to me, so we can handle it with a log structure of some kind.
All we really need over a given period of time is the average call rate, and we can put a limit on that average rate. We could for instance, poll this every 3-5 seconds. In 3 seconds you may say only want them to be able to call this 15 times, maybe they had some sort of AOE multikill, let’s give them the benefit of the doubt.
A simple check could look like this:
RecentCalls = {}
function AddExp()
table.insert(RecentCalls, tick())
local removals = {}
local avg,num = 0,0
for i,v in next, RecentCalls do
if tick()-v > 3 then --Our time check
table.insert(removals,i) --add it to the removal queue
else
avg = avg + (v-RecentCalls[1]) --normalize against t0
num = num + 1
end
end
for i,v in next, removals do
table.remove(RecentCalls,(v-(i-1)))
end
num = num == 0 and 1 or num --don't divide by 0
avg = avg/num
if avg > 15 then --Our sanity check finally
return --exit, maybe handle this as a hacker, up to you. But it's a high number.
end
--body that we've been working on
end
This is where I will wrap up this part. If there is significant interest I can do more parts, but I have not decided to as of now. I’m happy to answer questions in the comments, and if there are concerns with any of the code posted please let me know so I can fix it.
As stated a couple times, this code is meant to be a guide not an absolute, make your own. I’ve tried to glitch proof everything that I’ve written but something probably slipped through, and it’s obviously not highly-efficient in multiple places.
That said, I hope this helps.
Good luck, Vathriel
Edit: If you have happened to be following along, all of your level values will be whole numbers, so your experience functions will not need the math.floor calls, I was mainly using those for graphical purposes. You may be able to gain a marginal efficiency boost by removing those calls and instead using the raw Level value.