We wrapped up the last post with supporting easing functions in our Unity lerp animations. In this final post, we’ll add a finishing touch to our AnimationHelper which will help deal with the problem of conflicting animations.

Conflicting animations

Imagine you had a panel in your UI that you wanted to animate open and closed. It’s not out of the realm of possibilities that the coroutines that run these animations could end up running at the same time, causing a mysterious half-open, jittering panel.

In dealing with conflicting coroutine animations, you’ve got two options:

  1. Prematurely cancel the conflicting animation, or
  2. Wait for the conflicting animation to complete

The end of the second post in this series offered a suggestion for each of those scenarios, namely calling the AnimationHelper.Cancel method for option 1 and maintaining a bool flag for option 2. We’ll refine the implementation of these options by introducing an animation “token.”

AnimationToken

At its simplest, a token is an object that refers to some action or event that is currently in effect. The token can then serve as a “handle” to that action or event, allowing some form of control or inspection of it. Currently, the AnimationHelper.Animate method returns a Coroutine object, which can later be used to cancel the animation. You cannot, however, use the coroutine to query the current state of the animation (as previously mentioned, you’d have to use a separately-maintained variable for that). Though you could think of a coroutine as a sort of token to an animation, we’d ultimately like to use our token to help with both cancelling and querying an animation. The interface for it would look like this:

public interface IAnimationToken
{
    bool isPlaying { get; }

    bool isStopped { get; }

    void Cancel();
}

For convenience, let’s add some extension methods to allow calling these methods on null tokens:

public static class AnimationTokenExtensionMethods
{
    public static bool IsValidAndPlaying(this IAnimationToken token)
    {
        return token != null && token.isPlaying;
    }

    public static bool IsNullOrStopped(this IAnimationToken token)
    {
        return token == null || token.isStopped;
    }

    public static void SafeCancel(this IAnimationToken token)
    {
        if (token != null)
        {
            token.Cancel();
        }
    }
}

The reason that the animation token is designed using an interface is so that we can implement it as a private class inside AnimationHelper. This keeps other users of our framework from accidentally instantiating their own mangled tokens. The implementation of our animation token below will essentially be a wrapper for a coroutine. When the coroutine is null, the animation that the token refers to is considered to be stopped.

public class AnimationHelper : MonoBehaviour
{
    // Inner Classes

    private class AnimationToken : IAnimationToken
    {
        public Coroutine coroutine;

        public bool isPlaying { get { return coroutine != null; } }

        public bool isStopped { get { return coroutine == null; } }

        public void Cancel()
        {
            if (isPlaying)
            {
                inst.StopCoroutine(coroutine);
                coroutine = null;
            }
        }
    }
    ...
}

That’s it. All that’s left to do now is modify AnimatonHelper.Animate and its dependent code to work with tokens instead of coroutines. Notice that AnimationHelper.Cancel has been removed since that logic was moved into the animation token. Also notice that the state of the token will be updated automatically when the animation reaches its end – no more bool variables floating around!

public static IAnimationToken Animate(float startTime, float duration, Action<float> onUpdate, Action onFinish = null)
{
    AnimationToken token = new AnimationToken();
    token.coroutine = inst.StartCoroutine(AnimateCoroutine(token, startTime, duration, onUpdate, onFinish));

    return token;
}

static IEnumerator AnimateCoroutine(AnimationToken token, 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;

    // Mark animation as done
    token.coroutine = null;

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

    yield return null;
}

A short example

Going back to our opening and closing panel example, let’s see how the code would look for methods that open and close the panel while avoiding animation conflicts:

using UnityEngine;

public class ExamplePanel : MonoBehaviour
{
    // Fields
    private IAnimationToken animToken;

    // Methods

    public void Open()
    {
        animToken.SafeCancel();

        Vector2 startPos = transform.localPosition;

        animToken = AnimationHelper.Animate(Time.time, 1f, (t) => {
            transform.localPosition = Vector2.Lerp(startPos, new Vector2(100f, 0f), EasingFunctions.easeInOut(t));
        });
    }

    public void Close()
    {
        animToken.SafeCancel();

        Vector2 startPos = transform.localPosition;

        animToken = AnimationHelper.Animate(Time.time, 1f, (t) => {
            transform.localPosition = Vector2.Lerp(startPos, Vector2.zero, EasingFunctions.easeInOut(t));
        });
    }
}

Note that choosing not to cancel the animation and instead checking animToken.IsNullOrStopped() is also an option, depending on the desired effect.

The complete implementation

I hope you’ve enjoyed this series and that this mini-framework will be helpful to you in your Unity endeavors. For completeness, I’ve provided the entire final implementation below. Simply put the AnimationHelper script on an empty game object in one or more scenes, and you’re good to go!

AnimationHelper.cs

using UnityEngine;
using System;
using System.Collections;

public interface IAnimationToken
{
    bool isPlaying { get; }

    bool isStopped { get; }

    void Cancel();
}

public static class AnimationTokenExtensionMethods
{
    public static bool IsValidAndPlaying(this IAnimationToken token)
    {
        return token != null && token.isPlaying;
    }

    public static bool IsNullOrStopped(this IAnimationToken token)
    {
        return token == null || token.isStopped;
    }

    public static void SafeCancel(this IAnimationToken token)
    {
        if (token != null)
        {
            token.Cancel();
        }
    }
}

public class AnimationHelper : MonoBehaviour
{
    // Inner Classes

    private class AnimationToken : IAnimationToken
    {
        public Coroutine coroutine;

        public bool isPlaying { get { return coroutine != null; } }

        public bool isStopped { get { return coroutine == null; } }

        public void Cancel()
        {
            if (isPlaying)
            {
                inst.StopCoroutine(coroutine);
                coroutine = null;
            }
        }
    }

    // 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 IAnimationToken Animate(float startTime, float duration, Action<float> onUpdate, Action onFinish = null)
    {
        AnimationToken token = new AnimationToken();
        token.coroutine = inst.StartCoroutine(AnimateCoroutine(token, startTime, duration, onUpdate, onFinish));

        return token;
    }

    static IEnumerator AnimateCoroutine(AnimationToken token, 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;

        // Mark animation as done
        token.coroutine = null;

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

        yield return null;
    }
}

EasingFunctions.cs

(This does not need to be attached to a game object, since it’s not a Monobehaviour)

using UnityEngine;
using UnityEngine.Assertions;

public static class EasingFunctions
{
    public static float easeIn(float t, float e = 2f)
    {
        Assert.IsTrue(e >= 1f);
        return 1f - Mathf.Pow(1f - t, e);
    }

    public static float easeOut(float t, float e = 2f)
    {
        Assert.IsTrue(e >= 1f);
        return Mathf.Pow(t, e);
    }

    public static float easeInOut(float t, float e = 2f)
    {
        Assert.IsTrue(e >= 1f);
        return Mathf.Pow(t, e) / (Mathf.Pow(t, e) + Mathf.Pow(1f - t, e));
    }
}

unity animation coroutine lerp
PREV NEXT