I must say when I set out to do this task, I figured it wouldn't take me a few days of thinking to figure out. What was I trying to do? Something seemingly simple, a blend of mathematics for the leveling system in my new game...
Aside: Now, I used to love math when I was a kid. I was good at it, it made sense, and it let you do a lot of cool stuff. I still realize its importance, but university pretty much killed my love for math by making me memorize formulas I knew how to use to take tests... And if you don't know already, my memory is like a sieve...
Anyway, the world of video games usually involves a lot of these calculations, and I'll laboriously call back my skills and try and figure things out. Usually this is difficult late at night after working all day, so it'll usually take me a couple of days on and off thinking about the problem in different ways to solve it. This is just another one of those stories...
While getting the leveling system up and running was pretty straight-forward and displaying it to the user and making it work was simple too, it just didn't quite feel right. And that's where the trouble set in. Let me give you the background first.
The system I have in place uses two metrics to determine your current level. One is the maximum chain you've been able to achieve and the other is your raw score. The increase of your score will eventually always lead to a new level, but you'll level much faster by continuing a chain throughout.
I used a simple classic tiered system to determine your level based on your maximum chain or current score. This involves an initial number to complete the first level and then a recurring incremental number which determines the next level after that. Thus your current level can be determined like so:
calculatedLevel = startingLevel + ((score >= firstLevelScore) ?
(1.0 + (score - firstLevelScore) / nextLevelScore) :
(score / firstLevelScore));
Note: I have the startingLevel in there as my game never actually starts you off at level one. Depending on your difficulty setting you'll start at higher levels, thus I had to factor that in to my equation. I also did this as a fractional equation so I could retrieve my current progress on the level by simply subtracting my last stored value for the current level from this value.
I repeated this process for the maximum chain value. I would then return the maximum value between the two. Therefore if you were doing well at chains, this would overtake the score level value.
And that's where the problem began. If you were doing well at your chain and then failed to continue it, your score could be a lot lower than the next threshold for the next level and the progress bar would be stuck there for a long time until you continued your chain farther again or your score caught up. But from a user perspective, it was weird to just have a stagnate bar or to have the bar decrease. Therefore I had to combine the two values in a more intelligent manner and still restrict them to be between 0 and 1 to maintain the same intentions of the leveling system.
First off, if you somehow lost points, I had to make sure I didn't loose progress, so I put in a condition for that:
if (scoreLevelProgress < 0 && chainLevelProgress < 0) return currentLevel;
And then came the messy part:
if (scoreLevelProgress > chainLevelProgress)
{
if (chainLevelProgress < 0)
chainLevelProgress = (1.0 - (-chainLevelProgress /
(-chainLevelProgress + 1))) / 2.0;
else
chainLevelProgress = chainLevelProgress / 2.0 + 0.5;
return currentLevel + scoreLevelProgress +
(chainLevelProgress * (1 - scoreLevelProgress));
}
else
{
if (scoreLevelProgress < 0)
scoreLevelProgress = (1.0 - (-scoreLevelProgress /
(-scoreLevelProgress + 1))) / 2.0;
else
scoreLevelProgress = scoreLevelProgress / 2.0 + 0.5;
return currentLevel + chainLevelProgress +
(scoreLevelProgress * (1 - chainLevelProgress));
}
So, what does this do? First scoreLevelProgress and chainLevelProgress are the current progress for those values. Typically they should be between 0 and 1, but they could be negative (e.g. the case I mentioned above where your score lagged behind) based on the previous calculations.
First I had to see which was the dominate value (this should always be falling between 0 and 1). I would use that as a base for my calculations. I would then return the current level plus my progress for that factor (score or chain). This alone gives you behavior equivalent to using Max. But now, I wanted to factor in the progress in the other area. So, I would multiply the progress in that area by the remaining space left in the progress range between 0 and 1. This gives you this line:
return currentLevel + scoreLevelProgress +
(chainLevelProgress * (1 - scoreLevelProgress));
This is fine if your progress in the other area is also between 0 and 1. However, that's not normally the case. So, I had to factor in the cases when that secondary component was negative. And that's what the second level of ifs are for.
What I had to do was normalize the negative value to a scale between 0 and 1 again. Considering this is a moving target, I had to consider something like a limit to model the approach to zero. This is where I factored in dividing the value by itself plus one. This gives you a value which will eventually lead to 0, so I subtracted it from 1 to mimic going from 0 to 1 as the negative value gets closer to 0.
This was great, I could taste victory in my grasp! But what happens when the secondary factor is now positive again and between 0 and 1? This is where I had to divide each case by 2 and for the normal positive case add 0.5. This would then bound the negative secondary factor to [0, 0.5) and the positive to [0.5, 1+]. Then, when it's multiplied by the remaining space between the progress of the primary factor, the whole scale ranges between 0 and 1 still. Success!
The only remaining issue is that this makes the level progress feel a bit logarithmic. However, I'm hoping as the scale of the score increases and level progress decelerates, this doesn't make a huge impact. But I'll look into tweaking it a bit if need be. For now, I have my system which continually provides some feedback on progress to the user and is confined to my original bounds.