02-02 Speed and Time

Location: Assets/Plugins/Animancer/Examples/02 Fine Control/02 Speed and Time

Recommended After: Transitions

Learning Outcomes: in this example you will learn:

How to control the speed and time of an animation.

How to play an animation backwards.

This example demonstrates how you can control the speed and time of an animation to make a robot that can Wake Up to move then go back to sleep by playing its Wake Up animation backwards. The Directional Blending example expands upon this concept to implement proper locomotion for the bot to actually move around the scene in any direction.

Pro-Only Features are used in this example: modifying AnimancerState.Speed and AnimancerState.NormalizedTime. Animancer Lite allows you to try out these features in the Unity Editor, but they're not available in runtime builds unless you purchase Animancer Pro.

Summary

  • To play an animation backwards, set its Speed to -1.
  • To play an animation backwards from the end, also set its NormalizedTime to 1.
  • To pause all animations and save processing time, call animancerComponent.Playable.PauseGraph();.

Overview

The spider bot character we want to implement has two main states that it can be in:

Asleep Moving

This example explains how to implement those states and the transitions between them in a way that allows the target state to be seamlessly changed at any time.

The SpiderBot script doesn't act on its own. Instead, it exposes a public IsMoving property which can be set by other scripts:

using Animancer;
using UnityEngine;

public sealed class SpiderBot : MonoBehaviour
{
    [SerializeField] private AnimancerComponent _Animancer;
    [SerializeField] private ClipTransition _WakeUp;

    [SerializeReference] private ITransition _Move;

    private bool _IsMoving;

    public AnimancerComponent Animancer => _Animancer;

    public ITransition Move => _Move;

    private void Awake()
    {
        _WakeUp.Events.OnEnd = OnWakeUpEnd;

        _Animancer.Play(_WakeUp);
        _Animancer.Playable.PauseGraph();
        _Animancer.Evaluate();
    }

    private void OnWakeUpEnd()
    {
        if (_WakeUp.State.Speed > 0)
            _Animancer.Play(_Move);
        else
            _Animancer.Playable.PauseGraph();
    }

    public bool IsMoving
    {
        get => _IsMoving;
        set
        {
            if (value)
                WakeUp();
            else
                GoToSleep();
        }
    }

    private void WakeUp()
    {
        if (_IsMoving)
            return;

        _IsMoving = true;

        var state = _Animancer.Play(_WakeUp);
        state.Speed = 1;

        _Animancer.Playable.UnpauseGraph();
    }

    private void GoToSleep()
    {
        if (!_IsMoving)
            return;

        _IsMoving = false;

        var state = _Animancer.Play(_WakeUp);
        state.Speed = -1;

        if (state.Weight == 0 || state.NormalizedTime > 1)
        {
            state.NormalizedTime = 1;
        }
    }
}

The code structure is similar to the Transitions example, except that it's controlled by a UI Toggle instead of Player Input and the Transitions and Animation Clips have been combined into one block to simplify this diagram (and future ones):

UI Toggle

It is often a good idea to separate the way you control something from the thing being controlled. This makes your scripts a bit more complex because you can no longer simply read one block of code to see all your logic, but in exchange it also makes your scripts much more flexible and easier to maintain. Writing the SpiderBot script with the intention of being controlled by something else means that the Directional Blending example can reuse this SpiderBot script but control it in a completely different way.

We could write another script to control it with keyboard input for this example, but there wouldn't be anything particularly interesting about that script (click here to see how simple it would be) and the videos on this page will more clearly show what's happening if you can see what's controlling it so instead we're going to set up a UI Toggle for it.

using UnityEngine;

public sealed class SpiderBotInput : MonoBehaviour
{
    [SerializeField] private SpiderBot _SpiderBot;

    private void Update()
    {
        _SpiderBot.IsMoving = Input.GetKey(KeyCode.W);
    }
}

Before we set up the toggle, we need a basic script with the IsMoving property we want to control:

using Animancer;
using UnityEngine;

public sealed class SpiderBot : MonoBehaviour
{
    private bool _IsMoving;

    public bool IsMoving
    {
        get => _IsMoving;
        set
        {
            _IsMoving = value;
            Debug.Log("Set IsMoving to " + value);
        }
    }
}

If you are unfamiliar with Unity's UI system, the official Creating UI Buttons tutorial explains how to create a Button.

Making a UI Toggle is very similar to making a Button, but instead of the On Click event that Buttons have, a Toggle has an On Value Changed event with a bool parameter to indicate what value it was set to. This means that when selecting which method you want it to call, the IsMoving property will appear twice:

Dynamic Bool Static Parameters
IsMoving in the Dynamic bool group. bool IsMoving in the Static Parameters group.
This is the one we want because it will pass the current value of the Toggle component into the method. This one shows a toggle in the event itself and will pass that value into the method. It gives the same value every time, regardless of the state of the Toggle component.

Fields

We are going to use two animations in this example:

  • SpiderBot-WakeUp will be used for several things:
    • When the bot is asleep, it will be paused at the start of this animation.
    • To wake up, this animation will be played forwards.
    • To go back to sleep, this animation will be played backwards.
  • SpiderBot-MoveRight will be used once the bot is finished waking up and starts actually moving.

As usual, we need a reference to the AnimancerComponent we want to control and a Transition for each of the animations.

public sealed class SpiderBot : MonoBehaviour
{
    [SerializeField] private AnimancerComponent _Animancer;
    [SerializeField] private ClipTransition _WakeUp;
    [SerializeField] private ClipTransition _Move;

We could have used AnimationClip fields for the animations, but Transitions allow us to define details such as the fade duration in the Inspector where they can be easily accessed by non-programmers without needing to modify the script.

Planning Ahead

We want to be able to reuse the SpiderBot script in the Directional Blending example where the bot needs to use a Mixer for its movement animations to allow movement in any direction instead of just the one SpiderBot-MoveRight animation we are using in this example.

This can be easily supported by changing the _Move field to be a Serialized Reference to hold any ITransition (all Transition Types implement that interface):

[SerializeReference] private ITransition _Move;

For this example, we will set this field as a ClipTransition just like it was in code before, and in the Directional Blending example we will give it a different type.

The Serialized Reference section explains those concepts in more detail.

Other References

The Directional Blending example will also need to access the AnimancerComponent and the movement animation so we public read-only properties for them using Expression Bodied Syntax.

public AnimancerComponent Animancer => _Animancer;

public ITransition Move => _Move;

Without Expression Bodied Syntax, those properties would look like this.

public AnimancerComponent Animancer { get { return _Animancer; } }

public ITransition Move { get { return _Move; } }

Inspector Setup

With the fields defined, we can now set up their values in the Inspector:

  • We are going to be controlling the Speed of the Wake Up animation in code, so we might as well untick the Speed toggle in the Inspector. This won't change the behaviour, but it will save a bit of performance since the transition won't apply its Speed every time it is played right before we replace it anyway.
  • As mentioned in the Planning Ahead section, we want the Move field set as a ClipTransition.

All the other default values are fine.

Sleeping Pose

Later on we will be making the bot Go To Sleep by playing its Wake Up animation backwards. That animation is not set to be looping (its Loop Time toggle is unticked) so when its Time reaches 0, it will continue showing the first frame of the animation. If we just set its Speed to -1 to play it backwards:

private void Awake()
{
    var state = _Animancer.Play(_WakeUp);
    state.Speed = -1;

That would give us the pose we want (pictured to the right), but the animation would actually still be playing, meaning that:

  • Its Time would continue into negative numbers.
  • The animation would still get evaluated every frame which is a waste of performance since nothing is actually being animated.

So there are a few other ways we could handle it:

  • Setting the state.Speed = 0; would prevent the Time from continuing into the negatives, but wouldn't help with performance.
  • Setting the state.IsPlaying = false; would do the same and have very very slightly better performance, but wouldn't make a worthwhile difference.
    • Note that IsPlaying simply determines whether the state's Time is advancing and is completely separate from Weight which determines how much of an effect that state has on the character's pose.

A better approach which would actually have a notable performance benefit is to pause the entire Playable Graph so that Unity won't waste any time updating it every frame when we know the output pose won't change:

private void Awake()
{
    _Animancer.Play(_WakeUp);
    _Animancer.Playable.PauseGraph();

We just told Unity not to update this character's animations on its own, so we also need to tell it to apply the current values for the desired pose to actually take effect:

    _Animancer.Evaluate();
}

Is Moving

We're now ready to turn the IsMoving property we made in the UI Toggle section into something useful.

We have a field to store the last value the property was given (so that if we are already waking up we can ignore subsequent commands to wake up and vice versa):

private bool _IsMoving;

The property's getter simply returns that value:

public bool IsMoving
{
    get => _IsMoving;

We won't actually be using the getter for anything, so this could actually just be a SetIsMoving method (or a property without a getter), but there's no harm in adding that one line of code in case someone might want to use this script a bit differently later on.

The setter simple checks the value that the property is being set to in order to decide whether to Wake Up or Go To Sleep:

    set
    {
        if (value)
            WakeUp();
        else
            GoToSleep();
    }
}

Here's what that looks like all together:

private bool _IsMoving;

public bool IsMoving
{
    get => _IsMoving;
    set
    {
        if (value)
            WakeUp();
        else
            GoToSleep();
    }
}

Wake Up

When told to WakeUp, if we were already waking up, we can just return immediately to not bother executing the rest of the method:

private void WakeUp()
{
    if (_IsMoving)
        return;

Otherwise, we need to set the _IsMoving field to true so we know that we are now waking up:

    _IsMoving = true;

Then we play the _WakeUp transition and set its Speed to 1 (it would be 1 by default, but Go To Sleep will set it to -1 to play backwards):

    var state = _Animancer.Play(_WakeUp);
    state.Speed = 1;
}

We also need to make sure the graph is unpaused since we paused it to set the Sleeping Pose and will do so again when we Go To Sleep:

    _Animancer.Playable.UnpauseGraph();

Here's what that looks like all together:

private void WakeUp()
{
    if (_IsMoving)
        return;

    _IsMoving = true;

    var state = _Animancer.Play(_WakeUp);
    state.Speed = 1;

    _Animancer.Playable.UnpauseGraph();
}

After Wake Up

The above code would allow the bot to Wake Up, but then it would just freeze in place.

So we want to give it an End Event. Since _WakeUp is a Transition, that means we need to initialize its events on startup rather than every time we play it:

private void Awake()
{
    _WakeUp.Events.OnEnd = OnWakeUpEnd;

    // The rest of the Awake method we wrote in the Sleeping Pose section.
}

private void OnWakeUpEnd()
{
    // To Do.
}

Because we are using the _WakeUp animation for both waking up and going back to sleep, this event will actually need to do different things depending on whether it is currently playing forwards or backwards:

  • Playing forwards (positive Speed) means the bot is waking up, so when it ends we want to play the _Move animation.
  • Playing backwards (negative Speed) means the bot is going to sleep, so when it ends we want to pause the graph just like we did to set the initial Sleeping Pose.
private void OnWakeUpEnd()
{
    if (_WakeUp.State.Speed > 0)
        _Animancer.Play(_Move);
    else
        _Animancer.Playable.PauseGraph();
}

Go To Sleep

GoToSleep starts as the opposite of Wake Up:

  • If _IsMoving was already false, return to exit the method immediately because we are already going to sleep. Otherwise;
  • Set the _IsMoving field to false.
  • Play the _WakeUp animation.
  • And make it play backwards.
private void GoToSleep()
{
    if (!_IsMoving)
        return;

    _IsMoving = false;

    var state = _Animancer.Play(_WakeUp);
    state.Speed = -1;

As mentioned when setting up the Sleeping Pose, playing backwards from the start of a non-looping animation would just leave it frozen on the first frame, which obviously isn't what we want here. We want to play backwards from the end of the animation so we could simply set state.NormalizedTime = 1;, but then if the bot is in the middle of waking up and tries to go back to sleep it would snap to the end of the animation before playing back through it which would look bad.

So we actually only want to snap to the end of the animation if it wasn't already playing. We could check state.IsPlaying for that, but more specifically we actually want to check if the animation was affecting the model's pose because it could potentially be paused (IsPlaying == false) but still affecting the pose (Weight > 0). That can't happen in this example, but in more complex situations it is important to be clear about exactly which conditions you are intending to check for.

    if (state.Weight == 0)
    {
        state.NormalizedTime = 1;
    }
}

Just like how playing backwards from 0 would allow the Time to continue into negative values, playing forwards will also allow the Time to continue increasing past the end of the animation even though the output pose is being clamped (because the animation is not looping).

This means that when the _WakeUp animation ends (at state.NormalizedTime == 1) and begins Fading to the _Move animation over 0.25 seconds, the NormalizedTime will continue increasing above 1. Then if we reverse the animation it would spend that same amount of time going back down towards 1 during which the model would still be frozen at the end of the animation. That would look bad, so we can avoid it by also clamping the NormalizedTime down to 1 if it was higher:

    if (state.Weight == 0 || state.NormalizedTime > 1)
    {
        state.NormalizedTime = 1;
    }
}

Here's what that looks like all together:

private void GoToSleep()
{
    if (!_IsMoving)
        return;

    _IsMoving = false;

    var state = _Animancer.Play(_WakeUp);
    state.Speed = -1;

    if (state.Weight == 0 || state.NormalizedTime > 1)
    {
        state.NormalizedTime = 1;
    }
}

Also, remember that when the animation finishes it will trigger the End Event we set up in the After Wake Up section which will call _Animancer.Playable.PauseGraph(); because the Speed is now negative.

Conclusion

We now have a bot that can:

  • Be asleep by default and won't waste processing power on animations because the graph is paused.
  • Wake up and then move.
  • Stop moving and go back to sleep.
  • Immediately swap between waking up and going back to sleep at any point.

That's all for this example, but the Directional Blending example expands upon this concept to implement proper locomotion for the bot to move around in any direction.