03 Sequence Coroutine

Difficulty: Beginner

Location: Assets/Plugins/Animancer/Examples/01 Basics/03 Sequence Coroutine

Namespace: Animancer.Examples.Basics

This example demonstrates how you can use a coroutine to play through an array of animations in a simple sequence as if to showcase them for a professional portfolio or a pack of animations to release on the Asset Store. Or in a game, this approach could be adapted for a melee attack combo system. It uses Transitions to allow the details of each animation to be easily customised in the Inspector.

Pro-Only Features are used in this example: Custom End Times. Animancer Lite allows you to try out these features in the Unity Editor, but they are not available in runtime builds unless you purchase Animancer Pro.

Tutorial

Here's the script we will be building in this tutorial:

using Animancer;
using System.Collections;
using UnityEngine;

public sealed class SequenceCoroutineTutorial : MonoBehaviour
{
    [SerializeField] private AnimancerComponent _Animancer;
    [SerializeField] private TextMesh _Text;
    [SerializeField] private ClipState.Transition[] _Animations;

    private void OnEnable()
    {
        Debug.Assert(_Animations.Length > 1, "Must have more than 1 animation assigned.");
        Debug.Assert(_Animations[0].Clip.isLooping, "The first animation should be looping.");

        Play(0);
    }

    public void PlaySequence()
    {
        StopAllCoroutines();
        StartCoroutine(CoroutineAnimationSequence());
    }

    private IEnumerator CoroutineAnimationSequence()
    {
        for (int i = 1; i < _Animations.Length; i++)
        {
            var state = Play(i);
            yield return state;
        }

        Play(0);
    }

    private AnimancerState Play(int index)
    {
        var animation = _Animations[index];
        _Text.text = animation.Clip.name;
        return _Animancer.Play(animation);
    }
}

And this that it looks like in the Inspector:

Start by following the Basic Scene Setup instructions to create a new scene with the character in it.

Basic Script

  1. Create a new script called SequenceCoroutineTutorial and open it up.
  2. Put using Animancer; at the top of the script.
  3. Give it serialized AnimancerComponent and AnimationClip[] fields for the animations in the sequence.
  4. Use the OnEnable method to play the first clip in the sequence.

The script looks like this so far:

using Animancer;
using UnityEngine;

public sealed class SequenceCoroutineTutorial : MonoBehaviour
{
    [SerializeField] private AnimancerComponent _Animancer;
    [SerializeField] private AnimationClip[] _Animations;

    private void OnEnable()
    {
        _Animancer.Play(_Animations[0]);
    }
}

Basic Coroutine

We could test it out now, but there's not much point because all it does so far is play a single animation. Instead, start making the Coroutine that will play through each animation in the array one by one.

A coroutine is a special type of method with IEnumerator as the return type like so:

private IEnumerator CoroutineAnimationSequence()
{
}

IEnumerator is located in the System.Collections namespace so we need using System.Collections; at the top of the script if it isn't there already. If you are using Visual Studio you can select IEnumerator and press Ctrl + . to open a menu which allows you to easily add the correct using statement.

At this point it's still a regular method that just happens to return an IEnumerator, not actually a coroutine. For example, we could put in return _Animations.GetEnumerator(); to return an object that can be used to iterate through every element of the _Animations array and we could call it like a regular method. This is why if you intend for a method to be used as a coroutine you choose a naming convention to make that clear, such as starting the name with Coroutine as we have done here (or even simply Cr if you prefer something shorter).

What actually makes it a coroutine is the use of yield statements. For example, we could put in yield break; which ends the method like a return; statement in a regular method. That would get it to stop giving us compiler errors, but all we would have is a coroutine that still does nothing.

To implement our sequence, we want to use a loop to iterate through every animation in the _Animations array and make use of Unity's WaitForSeconds class to wait a bit while each one plays:

private IEnumerator CoroutineAnimationSequence()
{
    // Start at the second animation (index [1]) since we are already playing the first.
    for (int i = 1; i < _Animations.Length; i++)
    {
        // Play it.
        _Animancer.Play(_Animations[i]);

        // Wait for 1 second.
        yield return new WaitForSeconds(1);

        // Then go to the next animation.
    }

    // After the last animation plays and waits, go back to the first animation.
    _Animancer.Play(_Animations[0]);
}

Obviously playing each animation for the same 1 second duration isn't going to be what we want because they have different lengths, so we will improve that soon (in Animation Durations).

The last thing we need before we can go back to Unity and try it out is a method to actually run the coroutine which we will make public so it can be called by a UI Button. To do that, we need to call it and pass the IEnumerator it creates into the StartCoroutine method so Unity knows it needs to continue checking when the next part of the coroutine should be executed (according to the yield statement we put in.

We also want to call StopAllCoroutines first, just in case the button gets clicked again before the previous sequence ends. It wouldn't be very useful to have two sequences running at once (it can be in other situations, but not for this). StartCoroutine actually returns a Coroutine object which you can store in a field in case you want to stop that specific one later on, but since we only have one we can just stop everything.

public void PlaySequence()
{
    StopAllCoroutines();
    StartCoroutine(CoroutineAnimationSequence());
}

The full script now looks like this:

using Animancer;
using System.Collections;
using UnityEngine;

public sealed class SequenceCoroutineTutorial : MonoBehaviour
{
    [SerializeField] private AnimancerComponent _Animancer;
    [SerializeField] private AnimationClip[] _Animations;

    private void OnEnable()
    {
        _Animancer.Play(_Animations[0]);
    }

    public void PlaySequence()
    {
        StopAllCoroutines();
        StartCoroutine(CoroutineAnimationSequence());
    }

    private IEnumerator CoroutineAnimationSequence()
    {
        for (int i = 1; i < _Animations.Length; i++)
        {
            _Animancer.Play(_Animations[i]);

            yield return new WaitForSeconds(1);
        }

        _Animancer.Play(_Animations[0]);
    }
}

Using the Sequence

Save the script and go back to the Unity scene so we can see it in action.

  1. Add the SequenceCoroutineTutorial script we just created to the LowPolyMan in the scene. It would work regardless of which object we put it on, but there is the most convenient.
  2. Assign the LowPolyMan's AnimancerComponent to the Animancer field on the component we just added.
  3. Assign the animations you want in your sequence to the Animations array, starting with an Idle animation.

Those foldout arrows to the left of each field come from Inspector Gadgets Pro and allow you to view the Inspector of the referenced object without needing to change your selection. In this case it allows us to view the import settings of the AnimationClips, but we will not be using it here since all their settings are already configured correctly.

Unfortunately, due to limitations in Unity's GUI system, when this feature is used on the elements of an array the object's Inspector will be drawn below the entire array rather than directly below the object's field.

Now we need a UI Button to call our PlaySequence method:

  1. Right click in the Hierarchy window and create a new UI/Button. This creates a Canvas with a Button as its child and an Event System.
  2. Click on the Anchor Presets button in the top left of the Button's RectTransform component.
  3. Hold both Shift and Alt then click on the Bottom Center preset to move the anchors, pivot, and position.
  4. Go to the On Click event of the Button component and use the + button to add a new function to call.
  5. Drag the LowPolyMan from the Hierarchy window into the object reference field of the new function.
  6. Use the dropdown menu to select the SequenceCoroutineTutorial -> PlaySequence method we created earlier.
  7. Select the Text object under the Button and change its text to "Play Sequence".

Now if we enter Play Mode we can see the sequence in action.

Remember to exit Play Mode after you are done looking.

Animation Names

That's not the best sequence ever, but before we start fixing it up it would be nice to have a clear indication of which animation is playing, so let's add some text to show the name of the current animation.

Add a TextMesh field:

[SerializeField] private TextMesh _Text;

Now we need to set the text whenever we play an animation so let's make a new method to do both at once:

private void Play(AnimationClip animation)
{
    _Text.text = animation.name;
    _Animancer.Play(animation);
}

That would allow us to call Play(_Animations[0]) and Play(_Animations[i]), but for the sake of demonstration we can go one step further by changing the parameter to an int index:

private void Play(int index)
{
    var animation = _Animations[index];
    _Text.text = animation.name;
    _Animancer.Play(animation);
}

Now we can call Play(0) and Play(i) instead. In such a simple self-contained script the difference doesn't really matter, but if you were to expose this method to other scripts you would consider whether you want them to be playing animations managed in this script by index or passing in their own animations which this script does not need to know anything about.

Once we change all _Animancer.Play calls to use our Play method the full script looks like this:

using Animancer;
using System.Collections;
using UnityEngine;

public sealed class SequenceCoroutineTutorial : MonoBehaviour
{
    [SerializeField] private AnimancerComponent _Animancer;
    [SerializeField] private TextMesh _Text;
    [SerializeField] private AnimationClip[] _Animations;

    private void OnEnable()
    {
        Play(0);
    }

    public void PlaySequence()
    {
        StopAllCoroutines();
        StartCoroutine(CoroutineAnimationSequence());
    }

    private IEnumerator CoroutineAnimationSequence()
    {
        for (int i = 1; i < _Animations.Length; i++)
        {
            Play(i);

            yield return new WaitForSeconds(1);
        }

        Play(0);
    }

    private void Play(int index)
    {
        var animation = _Animations[index];
        _Text.text = animation.name;
        _Animancer.Play(animation);
    }
}

Save the script and return to Unity to create and connect the text object:

  1. Right click in the Hierarchy window and create a new 3D Object/3D Text and call it Animation Name.
  2. Set its Position to 0, 2, 0. Inspector Gadgets allows you to Middle Click any field in the Transform component to reset its value, but otherwise you can simply reset the X and Z values manually.
  3. Set its Rotation to 0, 180, 0.
  4. Set its Scale to 0.1, 0.1, 0.1. Inspector Gadgets shows Scale as a single field when all values are the same (the = button separates the fields), but otherwise you can just type the same value in each field.
  5. Configure the TextMesh: Text = "Animation Name", Anchor = Lower Center, Font Size = 25.
  6. Go to our SequenceCoroutineTutorial component on the LowPolyMan and drag the Animation Name object into the Text field we just added to the script.

Now in Play Mode we will be able to see the name of the animation being played.

Remember to exit Play Mode after you are done looking.

Animation Durations

Giving every animation the same arbitrary duration is clearly not the best way to do things because each animation can have a different length. Fortunately, since we have the AnimationClips, we have direct access to their lengths so we could change the WaitForSeconds instruction to use that instead of a constant 1:

Play(i);
yield return new WaitForSeconds(_Animations[i].length);

That would definitely get the job done for this situation, but it is not necessarily the best way to do it. For example, if something else started a different animation or changed the speed of this one, the coroutine would still be waiting for the original fixed amount of time.

Instead, we can change our Play method to return the AnimancerState it gets from _Animancer.Play and use that state as the yield instruction. This will wait until that state either reaches the end or stops playing for any reason, including any interruptions, speed changes, or even direct manipulation of its Time property:

private AnimancerState Play(int index)
{
    var animation = _Animations[index];
    _Text.text = animation.name;
    return _Animancer.Play(animation);
}

...

var state = Play(i);
yield return state;

The full script now looks like this:

using Animancer;
using System.Collections;
using UnityEngine;
using UnityEngine.UI;

public sealed class SequenceCoroutineTutorial : MonoBehaviour
{
    [SerializeField] private AnimancerComponent _Animancer;
    [SerializeField] private TextMesh _Text;
    [SerializeField] private AnimationClip[] _Animations;

    private void OnEnable()
    {
        Play(0);
    }

    public void PlaySequence()
    {
        StopAllCoroutines();
        StartCoroutine(CoroutineAnimationSequence());
    }

    private IEnumerator CoroutineAnimationSequence()
    {
        for (int i = 1; i < _Animations.Length; i++)
        {
            var state = Play(i);
            yield return state;
        }

        Play(0);
    }

    private AnimancerState Play(int index)
    {
        var animation = _Animations[index];
        _Text.text = animation.name;
        return _Animancer.Play(animation);
    }
}

And if we try it in Play Mode, this time it will play through each animation fully.

Remember to exit Play Mode after you are done looking.

Alternate Loop Duration

Playing through each animation once works well for non-looping animations like GolfSwing, but not so well for looping ones. Walk and Run only play one step with each foot and GolfSwingReady only shows for a brief moment since it is not much more than a stationary pose.

So let's add another field to specify the duration to show looping animations.

[SerializeField] private float _LoopDuration = 3;

3 seconds seems like a reasonable default value, but since it's a serializable field we can use the Inspector to adjust it if we want to.

Now we can use that field in our coroutine depending on whether each animation is looping or not:

var state = Play(i);

if (state.IsLooping)
    yield return new WaitForSeconds(_LoopDuration);
else
    yield return state;

The full script now looks like this:

using Animancer;
using System.Collections;
using UnityEngine;

public sealed class SequenceCoroutineTutorial : MonoBehaviour
{
    [SerializeField] private AnimancerComponent _Animancer;
    [SerializeField] private TextMesh _Text;
    [SerializeField] private float _LoopDuration = 3;
    [SerializeField] private AnimationClip[] _Animations;

    private void OnEnable()
    {
        Play(0);
    }

    public void PlaySequence()
    {
        StopAllCoroutines();
        StartCoroutine(CoroutineAnimationSequence());
    }

    private IEnumerator CoroutineAnimationSequence()
    {
        for (int i = 1; i < _Animations.Length; i++)
        {
            var state = Play(i);

            if (state.IsLooping)
                yield return new WaitForSeconds(_LoopDuration);
            else
                yield return state;
        }

        Play(0);
    }

    private AnimancerState Play(int index)
    {
        var animation = _Animations[index];
        _Text.text = animation.name;
        return _Animancer.Play(animation);
    }
}

And if we try it in Play Mode, we get to see the looping animations for 3 seconds each while the GolfSwing just plays its full length once.

Remember to exit Play Mode after you are done looking.

Transitions

What we have made so far works pretty well, but doesn't give us any convenient way to adjust the details of the individual animations. For example, the GolfSwingReady animation is just a stationary pose so it really doesn't need the same 3 seconds that we give Walk and Run.

We could turn _LoopDuration into a float[] with each value corresponding to one of the _Animations, but that wouldn't be particularly convenient to work with in the Inspector and would rely on the user to always make sure both arrays have the same size. This would be even worse if we wanted to specify transition durations and play speeds as well.

This is where Transitions come in. They are serializable classes that package additional fields along with each AnimationClip.

So let's make a few changes to the script:

  1. Change the _Animations field type to ClipState.Transition[]. A ClipState is a type of AnimancerState that is used to play a single AnimationClip so we want transitions that will create ClipStates when we play them.
  2. Remove the _LoopDuration field and change the coroutine back to just yield return state;.
  3. Change the Play method to get the animation name from animation.Clip.name instead of just animation.name. Since it already uses var animation = ... instead of specifying the exact type (AnimationClip animation = ...) we do not need to modify that part after changing the _Animations field.

The full script now looks like this:

using Animancer;
using System.Collections;
using UnityEngine;

public sealed class SequenceCoroutineTutorial : MonoBehaviour
{
    [SerializeField] private AnimancerComponent _Animancer;
    [SerializeField] private TextMesh _Text;
    [SerializeField] private ClipState.Transition[] _Animations;

    private void OnEnable()
    {
        Play(0);
    }

    public void PlaySequence()
    {
        StopAllCoroutines();
        StartCoroutine(CoroutineAnimationSequence());
    }

    private IEnumerator CoroutineAnimationSequence()
    {
        for (int i = 1; i < _Animations.Length; i++)
        {
            var state = Play(i);
            yield return state;
        }

        Play(0);
    }

    private AnimancerState Play(int index)
    {
        var animation = _Animations[index];
        _Text.text = animation.Clip.name;
        return _Animancer.Play(animation);
    }
}

Unfortunately, changing the field from direct AnimationClip references to another class that happens to contain an AnimationClip means that Unity will lose the references we assigned earlier, so you need to select the LowPolyMan and assign all the animations again.

As you can see, the fields now have foldout arrows and an "Eye" icon on the right (which allows you to preview each transition). Unlike the Inspector Gadgets foldout arrows, these ones expand to show the details of each transition. See the Transitions page for a full explanation of all the details, but the only one we care about right now is the End Time which will determine how long the yield lasts:

  1. For the Idle animation it does not matter because we are not waiting for it to finish. Changing the End Time will not do anything on its own if your code is not waiting for the animation to end.
  2. For the Walk and Run animations we want them to each show a few walk cycles. Set them using the normalized time field (the left one with the "x"). 3 loops of the Walk takes about 3 seconds but since the Run is shorter it can do 4 loops in a similar amount of time.
  3. The GolfSwingReady animation is basically a stationary pose so we do not really care about the way it loops and can just set it to take 1 second (this time using the right field with the "s").
  4. Unlike the others, the GolfSwing animation is not looping so we just want it to play through once and therefore we do not need to change the End Time.

As noted at the start of this example, the ability to set Custom End Times like this is a Pro-Only feature. Animancer Lite allows you to try it out in the Unity Editor, but it will always use the default end time (the actual end of the animation) in runtime builds. See the Feature Comparison for more information.

Assertions

That is all we are going to do with the sequence itself, but there is one more advantage to the approach used here which would not be possible when using an Animator Controller: your scripts can easily check and validate their expectations.

The script we have written would give a generic IndexOutOfRangeException if we did not set up any of the animations in the Inspector and if we only added one animation it would actually work fine but simply do nothing more than play that animation, which defeats its purpose. So we can use Debug.Assert in our OnEnable method to ensure that there are at least two animations and give a more helpful error message if there are not:

Debug.Assert(_Animations.Length > 1, "Must have more than 1 animation assigned.");

Debug.Assert is like saying "(condition) must be true, so give error (message) if it is not". If you check its definition, you can see that it has a [Conditional("UNITY_ASSERTIONS")] attribute which means that any calls to that method will be automatically removed if that conditional compilation symbol is not defined, as if we had put #if UNITY_ASSERTIONS before it and #endif after.

In the case of UNITY_ASSERTIONS, it is defined in the Unity Editor and in Development Builds, but not in regular release builds. This allows us to validate our data to catch issues during development without affecting the performance of the released game.

Since we have access to the animation details, we can enforce specific restrictions on them as well. The script would still work fine with a non-looping animation, but since we are using it as an Idle animation, it would probably not be appropriate to just play it once and freeze on the last frame. So we can also check that:

Debug.Assert(_Animations[0].Clip.isLooping, "The first animation should be looping.");

This is what the final script looks like:

using Animancer;
using System.Collections;
using UnityEngine;

public sealed class SequenceCoroutineTutorial : MonoBehaviour
{
    [SerializeField] private AnimancerComponent _Animancer;
    [SerializeField] private TextMesh _Text;
    [SerializeField] private ClipState.Transition[] _Animations;

    private void OnEnable()
    {
        Debug.Assert(_Animations.Length > 1, "Must have more than 1 animation assigned.");
        Debug.Assert(_Animations[0].Clip.isLooping, "The first animation should be looping.");

        Play(0);
    }

    public void PlaySequence()
    {
        StopAllCoroutines();
        StartCoroutine(CoroutineAnimationSequence());
    }

    private IEnumerator CoroutineAnimationSequence()
    {
        for (int i = 1; i < _Animations.Length; i++)
        {
            var state = Play(i);
            yield return state;
        }

        Play(0);
    }

    private AnimancerState Play(int index)
    {
        var animation = _Animations[index];
        _Text.text = animation.Clip.name;
        return _Animancer.Play(animation);
    }
}