When In Doubt, Use a LERP! (Let's get technical)
One thing I really haven’t done much of is advocate for specific processes while game creating. Mostly because until recently, I just figured I was a self taught goofball trying to piece things together in the most idiotic ways imaginable and all those professional teams must be doing things WAY better than I am having decades of experience.
But SeaCrit I THINK might be going well, so what the hey, I figure let’s make a blog as if we have any idea WTF we’re doing, so here’s a blog post on LERPs, which I believe to be the absolute best friggin’ function in all of gamedev, and I’m going to try to advocate for them here, and it’s a good time to do so, because just yesterday I did some heavy retooling of the input system for all input types and the absolute lynchpin of my logic are Lerps, and specifically custom lerps that I’ve been putting together here and there.
So if you think about game design, at the very heart of it, is this notion of “I want X to happen based on Y”. Makes sense right?
I want to hit harder when I charge up my attack longer.
I want to gain more and more speed the longer I’m running.
Yesterday I wanted to create some logic along the lines of “I want more agility and speed the more and faster the player moves.” And I was motivated by the fact that when moving from totally still, the keyboard would rocket you forward kinda like a racecar and turning in any direction was way too ABRASIVE.
When we first start developing we’ll create logic along the lines of what we’re taught in school. But the algebra we’re taught in school is not designed for loops and to SLOWLY approach an end point, they output an immediate answer and would reach the end goal in a single frame: startSpeed + maxAccelerationSpeed = maxSpeed. This is not what we want in the discreet math of an action platformer! We want that maxAccelerationSpeed to waver between extremes over time, and like anything in gamedev we want to reach this logic in the most clear, and concise means possible.
The more concise, the more “it just works” the less bugs we create, the less our brain melts refactoring the logic, and the more likely it is to not require constant revision to get up and running and improve down the road.
Put simply, how fun our games are ultimately able to become hinges ENTIRELY on how cleverly simple our systems are.
Great games are not monstrosities of complex edge cases and work arounds, they start that way as crappy games, and through constant revision and practive we learn better, more water tight and dynamic means of creating wondrous logics that can do all manner of neat things.
Getting better at gamedev isn’t about learning a multitude of complex math wizardry, it’s becoming a master of a small handful of logic that runs the motor of your given project.
And most all that boils down to unless you’re knee deep in graphics optimizations is a bit of Trig, some algebra, and knowing key bits of simple logic like lerps. It really is that simple! But it takes a long time to become comfortable with using them in real-time and understanding the complex scopes and growing networks of logic that spiral!
So what makes Lerps so magical? Well, in order to make most logic work properly, you need some bit of logic that defines what it starts at, you need another bit of logic that defines the bounds, and you also need something that increments it in the loop so it can make a change over time. Sometimes you will need another bit of logic that will take it back to that starting value over time, like say a pendulum that swings one direction, then swings back. Or maybe you run faster and faster, and as you let go of the move button, your speed returns quickly back to zero, because if it were to INSTANTLY go to zero, it would look really weird! Things in the real world have momentum and very few things instantaneously go from 0% to 100%, whether that be screaming as loud as you can, blinking as fast as you can, or reaching the fastest you can run, or then returning back to zero.
And what if I told you that there was a single function that could handle ALL of the above!? It could prevent weird outside logic from creating double negatives turning positive, it could prevent outlandish edge case values from sending things out of control. If simply pushed one value towards another value over time, and on top of all that it was totally normalized so you ALWAYS could predict the acceleration and falloff without any headache or mastery of complex mathematics.
AND THAT is exactly why lerps are so friggin’ awesome. They’re compact, they’re useful for just about anything, and the more you use them, the better you get at creating complex logic, and the better your game will be across the board.
Often times, problems are easier to sort out when we start the the end point of where we want to go and walk backwards. You may be confused by everything you want to do, so start with a LERP! What’s the value you want? Where do you want that value to go, and how fast? Then build up the logic from there and feed it into the lerp.
And that’s why in gamedev, for my money I’ll always say:
WHEN IN DOUBT, USE A LERP M*THER FUCKER!
Think of a Lerp as the transportation system of the value you have towards the value you want. You know the value you want to change, you the value you want to go up to or down to, you’re just not sure how to get it there at the right rate or how far along towards that value you want it.
The basic anatomy of a lerp (linear interpolation) is this:
valueWe’reShifting = Lerp(valueWe’reShifting, valueShiftingTowards, whatPercentageOfChangeWeWantClampedFrom0To1);
And we can either adjust higher to something like maxSpeed in a racing game, or we can decelerate to Zero for braking, OR! If we want to go in reverse we can even scale to a negative number! It doesn’t have to increment in a positive direction.
Beyond this, we can also tune the rate of change, like if we want the acceleration to be fast but taper off, can adjust the stepping by a power like so:
var ratio = valueWe’reShifting /valueShiftingTowards;
stifledAcceleration = zz.Pow(ratio, .5f);
valueWe’reShifting = Lerp(valueWe’reShifting, valueShiftingTowards, stifledAcceleration );
This works like a gamma on a TV. A higher value will suppress the scaling until it comes in hard at 1, whereas a lower values will cause it to roll in hard, but taper off towards 1.
Using a power of .5f, we’ll accelerate very fast at low speeds and taper off towards max speed.
OR! We could use another lerp!
var ratio = valueWe’reShifting /valueShiftingTowards;
stifledAcceleration = zz.Pow(ratio, .5f);
acceleratoinBasedOnSpeed = Lerp(accelerationFromStop , maxAcceleration, stifledAcceleration);
valueWe’reShifting = Lerp(valueWe’reShifting, valueShiftingTowards, stifledAcceleration );
While you could use Mathf.lerp (or is it MathF?),it’s a lot easier to just create my one class that’s accessed very quickly from “zz”
public class zz : MonoBehaviour
{
public static void Lerp(ref float value, float b, float t = .03f)
{
value = value + (b - value) * Mathf.Clamp01(t);
}
public static float Lerp(float value, float targetValue, float transitionSpeed = .03f)
{
return Mathf.Lerp(value, targetValue, transitionSpeed);
}
}
When I want to access anything from clamps to lerps, all I have to type is zz.Lerp. And I referenced the core logic from the MathF function.
Let me try to wrap this up because I’m not sure this is going to help anyone, and I actually want to get work done on the project today!
So what the custom logic above does is allows us to cut down on the code bloat EVEN MORE, as using “ref” in the function means we no longer have to assign the value as we did before and anything done to the value in the function is magically applied to the input variable, and if we add “using static zz;” at the top of our scripts we don’t even need to type “zz.Lerp” and can simply use “Lerp”. Our code goes from…
valueWe’reShifting = zz.Lerp(valueWe’reShifting, valueShiftingTowards, .03f);
To…
Lerp(ref valueWe’reShifting, valueShiftingTowards, .03f);
Ok, last cool thing about the lerp, by instantiating what we want it to adjust towards, we can throttle our value towards something, or away from something using the same bit of logic!
In the above scenario, I wanted to gradually change between snappy fast movement where you can dart in any direction in an instant after you have been moving around a bit, to a more slow and fluid speed that would show your fish gracefully turning, and not instantly change direction if you were using a keyboard, and to achieve this, I used a slightly more advanced version of my lerp that interpolates faster or slower based on if the target number is higher or lower than the referenced value. In this instance, I wanted the deceleration to be slower, to avoid a nasty bug where if you turned, from left to right, for a fraction of a second the game would register you as moving slow and cause you to transition to the sluggish movement. So to avoid this, we reduce the lerp towards the slower value:
public static void LerpTowardsWeighted(ref float value, float targetValue, float transitionSpeed = .03f, float transitionSpeedLower = .333f)
{
if (value < targetValue) value = Mathf.Lerp(value, targetValue, transitionSpeed);
else value = Mathf.Lerp(value, targetValue, transitionSpeedLower );
}
And here is the movement logic itself utilizing the above function:
var speedValueCheck = zz.Clamp(MC.speed - .75f) * 1.3f; //zz.Clamp defaults to a 0 - 1 range
speedValueCheck = zz.Clamp(zz.Pow(speedValueCheck, 1.5f), 0f, 1.4f); zz.LerpTowardsWeighted(ref normalSpeedSetInFromSlowMod, speedValueCheck, .1f, .02f);
Because we are now using the LerpTowardsWeighted function, we can input unique lerp strengths per frame depending on if the player is accelerating or decelerating. In this case our lerp of .02 is 5x lower than that of .1, so when moving at high speed we will much faster start converting towards the agile movement types where we use this value as a lerp to adjust between a low mobility value at low speed and a higher one at higher speeds.
It can be a little much to take on at first, but once you start using lerps, it’s actually FAR easier than using large networks of other bits of logic. Again, you will simply automagically restrain all values from getting out of control, you will know at a glance the value you start at, and the value you are ending at, and have a fantastically simple gradient value that dictates the % of change if it’s a one off, or the rate of change if this is fired every fixedUpdate.
By it’s very nature, lerps will set in fast and slow down over time if called every frame. If you start out at 10% from zero towards 1, you will interpolate to .1 on the first frame, and every frame from there will be 10% less recursively, this makes this great for all manner of things like fading UI in and out, for force values, and using the tricks above, you can modify this with powers in the lerp to adjust it to set in slower or faster at the high end.
If you want a purely linear growth over time, I created these countdown/countup functions and calsses that I’ve found to be awesome:
public static void Countdown(ref float value, float rate = 1f, float stopPoint = 0f)
{
if (value > stopPoint) value = LowerLimit(value - Time.deltaTime * rate, stopPoint);
}
public static void Countdown(ref int value, int stopPoint = 0)
{
if (value > stopPoint) value--;
}
public static float Countup(float value, float rate = 1f, float stopPoint = 10000f)
{
if (value < stopPoint) value = Cap(value + Time.deltaTime * rate, stopPoint);
return value;
}
public static void Countup(ref float value, float rate = 1f, float stopPoint = 10000f)
{
if (value < stopPoint) value = Cap(value + Time.deltaTime * rate, stopPoint);
}
These can be called in the same way as the lerps above and will increment towards value at a particular rate with any custom stopping point you want defaulting to counting down to 0, which is super handy.
And this one is REALLY neat that I posted about in a prior blog
public static bool FireAtZero(ref float value, float resetTimeLength = 0f, float rate = 1f)
{
if (value == -1) return false;
if (value == 0) { if (resetTimeLength > 0) value = resetTimeLength; else value = -1; return true; } //Check the single frame where value is 0
if (value > 0) value -= Time.deltaTime * rate;//Tickdown
else value = 0; //If not firing or counting down, defaults to -1 state
return false;
}
If you put this one in an if statement, it will actually fire a function when it reaches zero, and even restart the timer automatically
if(FireAtZero(ref autoFireLaserTimer, autoLaserCooldown)) FireLaser(); //Fires a laser every second
And I’m just throwing tons of crap at the screen and not giving enough explanation of how they work, but I really do gotta get to work! These are some custom countdown and count up classes that are REALLY neat and automatically check the time remaining just by accessing the value, rather than having to do any gnarly steps in your loops, you simply access them at any time and it does simple math against the set time to tell you how long it’s been since they were instantiated, and they have some handy set and get logic within them to make it super easy:
[Serializable]
public class CountUpFloat
{
public float occuranceTime;
private float _timeSince => Time.time - occuranceTime;
public float timeSince { get { return _timeSince; } }
public void SetCountupTimeStamp(float timeOffset = 0)
{
occuranceTime = Time.time + timeOffset;
}
public static implicit operator float(CountUpFloat timer)
{
return timer.timeSince;
}
public static implicit operator CountUpFloat(float countupDuration)
{
return new CountUpFloat { occuranceTime = Time.time + countupDuration };
}
}
[Serializable]
public class CountDownFloat
{
public float destinationTime = 0;
[ShowInInspector] public float timer => destinationTime - Time.time;
public bool reachedZero => destinationTime - Time.time <= 0;
public bool stillCountingDown => timer > 0;
public void SetCountdown(float countdownDuration = 0)
{
destinationTime = countdownDuration + Time.time;
}
public void SetMaxCountdownAgainstCurrentDuration(float countdownDuration = 0)
{
var newCountdownDuration = countdownDuration + Time.time;
destinationTime = zz.ChooseHighest(newCountdownDuration, destinationTime);
}
public static implicit operator float(CountDownFloat timer)
{
return timer.timer;
}
public static implicit operator bool(CountDownFloat timer)
{
return timer.timer > 0;
}
public static implicit operator CountDownFloat(float countdownDuration)
{
return new CountDownFloat { destinationTime = countdownDuration + Time.time };
}
}
Anyhow! Maybe this stuff is kinda boring in light of AI these days, but this is the sorta stuff I’ve been building up that have really helped me work on SeaCrit and push the envelope.
In my view it’s not the great grand picture that we need to trouble ourselves with, it’s every single day getting better at the basics.
Da’Vinci didn’t paint the Mona Lisa by sitting around thinking about the final painting all day long, he painted that because he painted thousands of works prior to that masterpiece, he learned year by year how better to make that singular simple brush stroke, how to get the correct lighting, coloring and style in that localized bit of rendering.
The worth of a work is derived by THOUSANDS and THOUSANDS of tiny tiny strokes, and EACH OF THOSE STROKES are developed over years and years of mastering the absolute basics. All advanced techniques and logics are foundationally built upon more rudimentary bits of logic modularly put together creating more and more advanced bits, which produce ever growing complexities elsewhere in larger and more complex shapes.
The only way we can build a grander, and better future is to dabble in the absolute simple things that were built yesterday, which is why even today I’m far more excited to find a neat little trick to do something easy even easier, than finding some new horribly complex thing I can barely pull off that’s held together by bubblegum and duct tape. The better I can build those base logics and organizations the stronger that bubblegum and duct tape becomes for the cool stuff!
Whew, been a really long time since I attempted to write anything actually valuable in the space of gamedev, I wish I had more time to revise this a few times and make better examples and polish it, but SeaCrit needs doin’!
Oh and I mentioned it some years ago, but once upon a time, many bugs ago, I once got paid to do gamedev tutorials in beer money, back before we were so jaded:
https://code.tutsplus.com/cubes-vs-space-marines-making-a-great-game-in-your-bas...
Arg, wish I could devote more time to stuff like this, but this really does drain more energy than the usual complaining that I could otherwise put into SeaCrit, and we’re so damned close to getting this demo out the door I gotta get to doin’ that!
Oh and there was a great talk on Unity I watched today that put me in a WAY better mood and made me think maybe the future of gamedev isn’t so bleak! And largely inspired this blog. They talk about how flash is dead, but Unity will be easily built on the web and ported over to hundreds of million of people on Meta and elsewhere, which is CRAZY!
SeaCrit is just BEGGING for a platform like that. More and more I’m so friggin’ stoked I pushed this game to play well with web for such low device requirements with so many input options.
With the way the industry is reinventing itself, with how SeaCrit gets better day after day, and the future of web builds is growing exponentially in potential, I could not friggin’ be more optimistic for the future, and not just for SeaCrit. I’m so excited that green devs will have a platform to learn the ropes and share their creations with people and learn how to make games in a great environment like the good old flash days! Maybe some day blogs like this will be more relevant as more and more people take on making web games in Unity.
Oh and health has been kickin’ ass the past couple weeks!
Ok, TOO MUCH DAMN OPTIMISM, GOTTA GET BACK TO WORK!
Get SeaCrit
SeaCrit
Deceptively Deep!
Status | In development |
Author | illtemperedtuna |
Genre | Action, Role Playing, Shooter |
Tags | Beat 'em up, Casual, Indie, Roguelike, Roguelite, Side Scroller, Singleplayer |
More posts
- Distance Makes the Heart Grow Fonder6 days ago
- Enjoy the Ride7 days ago
- We'll Make Our Own Shortcuts! A Step-By-Step Guide To Creating Custom Hotkeys in...8 days ago
- There Are No Shortcuts: Complaining About Hotkeys10 days ago
- Nothin' Worth Doin is Easy12 days ago
- The Incurring Madness13 days ago
Leave a comment
Log in with itch.io to leave a comment.