Lerping with Coroutines and Animation Curves
Lerping (Linear Interpolation) provides the intermediary steps of a transition. Calculated over time, it can convey the visual movement of objects.
For example, imagine a coin on a ruler. At the start of our animation, the coin is at the 0cm mark. After 3 seconds, it is at the 30cm mark. Knowing our start position, end position, and duration, interpolation can tell us where the coin should appear at any time in-between. At 1 second, the coin will be at the 10cm mark, and at 1.5 seconds, the 15cm mark. We use these calculations to know where to place the coin throughout an animation.
Therefore, to move objects along multiple axes, rotate around axes, or scale along axes, we simply interpolation multiple properties. Unity provides methods to achieve this. Mathf.Lerp()
for any single floating point number or axis interpolation, Vector2.Lerp()/Vector3.Lerp()
for two- and three-axis interpolation, and Quaternion.Lerp()
for rotational interpolation.
Robert Utter wrote a highly recommended explanation of the proper way to use these methods in Lerp Like a Pro. This article intends to extend on this knowledge
Isolating animation within a coroutine
The simplest implementation of coroutines within Unity allows components to trigger a function that will run every subsequent frame, similar to the Update()
method found in all MonoBehaviour scripts. Let’s prepare an IEnumerator that would do just that.
Here, yield return null
simply tells Unity to continue from this line on the next frame. It is common to place yield
methods within loops to benefit from continuous iteration. Unfortunately in our current script we aren’t doing anything in the loop, plus, true
is always true
, so there is never a condition that would prevent the loop from running forever!
If we add a duration
value, we now know the destination for this animation. At every frame, we increment a journey
value with delta time to represent the current progress. Printing a division of the two provides us with the percentage through the animation’s journey each frame.
Currently, this coroutine would still run infinitely, printing endless output of 1
once the clamped journey reaches and exceeds the total duration.
We can assign our clamped value to percent
, and observe that the comparison of our two source values (the static duration
and the incrementing journey
variables) can be used for evaluating if the loop has additional frames remaining.
This condition, journey <= duration
, can replace our existing while(true)
placeholder and our loop will now exit on time.
But, the coroutine still doesn’t do anything.
Now we’re cookin’! Two Vector3
parameters are defined, which we provide to Vector3.Lerp with percent
to interpolate between the two points. Naturally we need to be able to run this as a coroutine, and luckily our flexible parameters can provide a handy example such as:
And there we have it. A simple, flexible and isolated animation method for an object’s position. The reusable flow of journey
and percent
make duration-based animation consistent and precise for coroutines.
Non-linear interpolation (Nlerp!?)
We have animation, excellent! Our position can move 10 units in 10 seconds, moving 1 unit per second…. uhhhh, kinda boring.
Linear interpolation, by definition, is straight forward, but games and interfaces demand to be juicy. Everybody wants to see realistic and satisfying animations.
There are excellent libraries, such as DOTween and Mathfx, that can assist with common animation curves. Alternatively, we can take advantage of an Inspector-friendly AnimationCurve
to visually define our animation flow throughout the duration:
By defining curvePercent
, against a public AnimationCurve
in our component, we can now use the curve to represent the variation of the interpolation for faster take off, intermittent jitters, tapered slow down or anything else we can imagine! Simply ensure that the curve begins at (0,0)
(time and value), and ends at (1,1)
.
Tip: By also changing
Lerp
toLerpUnclamped
, our curves can go beyond the bounds of the origin and target positions. This can create elaborate “bouncy” effects.
Best of all, all curves will adapt to the duration provided by your coroutine argument, adding even more flexibility and re-usability.
Serialised curves
Unity’s visual editor is sufficient to design curves, along with saving libraries of curves for reuse or to package with a project, but our scripts can also define curve defaults or presents directly.
Curves can be declared with any number of keyframes to represent the desired path. The examples above match two core presets from the curve editor defaults.
Tip: It can be useful to declare one of the above default curves in scripts to avoid confusion. If a script’s curve is never drawn in the visual editor, no animation will occur on that object