In the last post, we established what “lerping” is (not to be confused with larping) and what the best method is for using a lerp to create framerate-independent animations in Unity. This post will introduce you to a helper class designed to make coding these lerp animations easy.

Enter AnimationHelper

The design I went with involves a singleton-like object that I can call upon from any script to start (and stop) animations. Before getting into the entire implementation, I’ll begin with the core methods:

public static Coroutine Animate(float startTime, float duration, Action<float> onUpdate, Action onFinish = null)
{
    return inst.StartCoroutine(AnimateCoroutine(startTime, duration, onUpdate, onFinish));
}

static IEnumerator AnimateCoroutine(float startTime, float duration, Action<float> onUpdate, Action onFinish)
{
    float endTime = startTime + duration;

    while (Time.time < endTime)
    {
        // Update for frame
        onUpdate(Mathf.Clamp01((Time.time - startTime) / duration));

        yield return null;
    }

    // Update for final frame
    onUpdate(1f);

    yield return null;

    // Run callback
    if (onFinish != null)
    {
        onFinish();
    }

    yield return null;
}

Using it looks like this:

using UnityEngine;
using System.Collections;

public class Test : MonoBehaviour
{
    void Start()
    {
        // Do this in Start(), so AnimationHelper is initialized

        AnimationHelper.Animate(Time.time, 3.0f, (t) => {
            transform.localScale = Vector3.Lerp(Vector3.one, Vector3.zero, t);
        },
        () => print("Done animating!"));
    }
}

With the AnimationHelper script attached to an empty object and the Test script attached to another object (say, a cube), this will start an animation to scale the test object from 1 to 0 over a period of 3 seconds. One of the first things you’ll notice is that the coroutine, or at least the first part of it, looks similar to that of our previous post. We simply generalized it to allow the insertion of any update function, supplied as a System.Action (one of .NET’s implementations of a function object). An onFinish callback was also added for performing actions at the end of an animation (useful also for chaining animations).

You’ll notice an explicit call to onUpdate(1f). Because there’s no guarantee that Time.time will ever be exactly equal to endTime, the while loop uses an explicit less than operator, <, in its condition. We then ensure the animation finishes at the exact endpoint with a final call to onUpdate with a t value of 1.

Another notable piece is the use of Mathf.Clamp01. This was added to allow start times in the future. Without it, t could go negative and cause undesired behavior. This way, t is guaranteed to be between 0 and 1 at all times.

The full implementation (thus far)

With missing pieces now included, the full implementation (thus far) of AnimationHelper looks like this:

using UnityEngine;
using System;
using System.Collections;

public class AnimationHelper : MonoBehaviour
{   
    // Fields
    private static AnimationHelper inst;

    // Methods

    void Awake()
    {
        EnforceSingleton();
    }

    void EnforceSingleton()
    {
        if (inst == null)
        {
            inst = this;
        }
        else if (inst != this)
        {
            Destroy(gameObject);
        }

        DontDestroyOnLoad(gameObject);
    }

    public static Coroutine Animate(float startTime, float duration, Action<float> onUpdate, Action onFinish = null)
    {
        return inst.StartCoroutine(AnimateCoroutine(startTime, duration, onUpdate, onFinish));
    }

    static IEnumerator AnimateCoroutine(float startTime, float duration, Action<float> onUpdate, Action onFinish)
    {
        float endTime = startTime + duration;

        while (Time.time < endTime)
        {
            // Update for frame
            onUpdate(Mathf.Clamp01((Time.time - startTime) / duration));

            yield return null;
        }

        // Update for final frame
        onUpdate(1f);

        yield return null;

        // Run callback
        if (onFinish != null)
        {
            onFinish();
        }

        yield return null;
    }

    static void Cancel(ref Coroutine coroutine)
    {
        if (coroutine != null)
        {
            inst.StopCoroutine(coroutine);
            coroutine = null;
        }
    }
}

You’ll notice the inclusion of a couple extra methods, EnforceSingleton and Cancel. EnforceSingleton does as the name sounds – it enforces that there is only one instance of an AnimationHelper alive at one time. This pattern is commonly seen in Unity tutorials and is useful for accessing singleton-like objects without the need to assign references in the Inspector. The above implementation eliminates the need for calling AnimationHelper.inst by making all methods static, though this is purely for convenience. Also note that because AnimationHelper is enforced to be like a singleton, you can put one in each scene without any side effects. If your application loads a new scene with its own AnimationHelper, the old one will hang around (because of DontDestroyOnLoad) and the new one will be destroyed.

The Cancel method allows you to prematurely end an animation. You’ll notice that the Animate method returns a coroutine, which can be used as a “handle” to the animation. Cancel is useful because it’s very easy to end up running conflicting coroutines (which can be difficult to debug). There are essentially two ways to avoid conflicting animations. Which one you choose depends on your scenario.

  1. Call Cancel(ref coroutineHandle) just before executing any potentially-conflicting animation. coroutineHandle can be acquired by storing the result of Animate.
  2. Avoid starting an animation until the potentially-conflicting one completes. You can use a bool value to keep track of whether or not an animation is playing, setting it to true just before calling Animate and having it set to false in the animation’s onFinish action.

Up next

While the above implementation of AnimationHelper is super useful, there are still some adjustments we can make. The next posts will cover how to support easing functions in your animations and also how to avoid conflicting animations with animation “tokens.”


unity animation coroutine lerp
PREV NEXT