04 Update Rate

Difficulty: Beginner

Location: Assets/Plugins/Animancer/Examples/02 Fine Control/04 Update Rate

Namespace: Animancer.Examples.FineControl

This example demonstrates how you can potentially improve performance by not updating Animancer every frame. The following video shows 3 characters:

  • Normal Rate: updates every frame as usual.
  • Low Rate: updates a limited mumber of times per second.
  • Dynamic Rate: updates every frame if the character is close to the camera, but starts limiting the update rate if it gets further away.

There are two scripts used in this example:

The Low Rate and Dynamic Rate characters both use the LowUpdateRate script which prevents Animancer from updating normally so that it can tell Animancer when to update:

using Animancer;
using UnityEngine;

public sealed class LowUpdateRate : MonoBehaviour
{
    [SerializeField] private AnimancerComponent _Animancer;
    [SerializeField] private float _UpdatesPerSecond = 10;

    private float _LastUpdateTime;

    private void OnEnable()
    {
        _Animancer.Playable.PauseGraph();
        _LastUpdateTime = Time.time;
    }

    private void OnDisable()
    {
        if (_Animancer != null && _Animancer.IsPlayableInitialised)
            _Animancer.Playable.UnpauseGraph();
    }

    private void Update()
    {
        var time = Time.time;
        var timeSinceLastUpdate = time - _LastUpdateTime;
        if (timeSinceLastUpdate > 1 / _UpdatesPerSecond)
        {
            _Animancer.Evaluate(timeSinceLastUpdate);
            _LastUpdateTime = time;
        }
    }
}

The Dynamic Rate character also has a DynamicUpdateRate script which enables or disables its LowUpdateRate script depending on how far the character is away from the camera (and displays the current details using a TextMesh):

using Animancer;
using UnityEngine;

public sealed class DynamicUpdateRate : MonoBehaviour
{
    [SerializeField] private LowUpdateRate _LowUpdateRate;
    [SerializeField] private TextMesh _TextMesh;
    [SerializeField] private float _SlowUpdateDistance = 5;

    private Transform _Camera;

    private void Awake()
    {
        _Camera = Camera.main.transform;
    }

    private void Update()
    {
        var offset = _Camera.position - transform.position;
        var squaredDistance = offset.sqrMagnitude;

        _LowUpdateRate.enabled = squaredDistance > _SlowUpdateDistance * _SlowUpdateDistance;

        var distance = Mathf.Sqrt(squaredDistance);
        var updating = _LowUpdateRate.enabled ? "Slowly" : "Normally";
        _TextMesh.text = $"Distance {distance}\nUpdating {updating}\n\nDynamic Rate";
    }
}

Neither of those scripts actually plays any animations. In a real game you would have other scripts doing that separately from the system controlling the update rate, but for this example we are just using NamedAnimancerComponents for each of the characters so that we can assign an animation to Play Automatically using the Inspector:

Controlling Updates

Controlling when Animancer updates is very easy:

  1. Get the AnimancerComponent you want to control:
public sealed class LowUpdateRate : MonoBehaviour
{
    [SerializeField] private AnimancerComponent _Animancer;
  1. Call AnimancerPlayable.PauseGraph top prevent it from updating on its own:
    private void OnEnable()
    {
        _Animancer.Playable.PauseGraph();
    }
  1. Call AnimancerComponent.Evaluate when you want to update it:
    private void Update()
    {
        ...
        _Animancer.Evaluate(...);
    }
}

Low Update Rate

The LowUpdateRate script wants to update Animancer at a specific frequency, so it has an Inspector field to set how often it updates and it also needs to keep track of how much time has passed since the last update:

public sealed class LowUpdateRate : MonoBehaviour
{
    [SerializeField] private AnimancerComponent _Animancer;
    [SerializeField] private float _UpdatesPerSecond = 10;

    private float _LastUpdateTime;

Every Update it can simply calculate how much time has passed since it last updated Animancer:

    private void Update()
    {
        var time = Time.time;
        var timeSinceLastUpdate = time - _LastUpdateTime;

If enough time has passed based on the desired number of _UpdatesPerSecond then it tells Animancer to Evaluate its animations, using the timeSinceLastUpdate as the deltaTime parameter so that Animancer knows how much to advance the time of the animation:

        if (timeSinceLastUpdate > 1 / _UpdatesPerSecond)
        {
            _Animancer.Evaluate(timeSinceLastUpdate);
            _LastUpdateTime = time;
        }
    }

LowUpdateRate is also designed to be enabled and disabled by DynamicUpdateRate so it grabs the current time when enabled to store as the _LastUpdateTime:

    private void OnEnable()
    {
        _Animancer.Playable.PauseGraph();
        _LastUpdateTime = Time.time;
    }

And when disabled it makes sure to call AnimancerPlayable.UnpauseGraph to let Animancer update normally again. Since Unity also calls OnDisable when destroying the object as well (such as when loading a different scene) it needs to make sure the AnimancerComponent still exists and is still initialised before doing so (otherwise accessing _Animancer.Playable would initialise it again):

    private void OnDisable()
    {
        if (_Animancer != null && _Animancer.IsPlayableInitialised)
            _Animancer.Playable.UnpauseGraph();
    }

Dynamic Update Rate

Simply updating all animations less often would usually look terrible, so many games only decrease their animation update rates for characters that are far away from the camera because that means they are smaller on screen and the player is less likely to be looking closely at them. That's what the DynamicUpdateRate script does by simply enabling and disabling the LowUpdateRate script as necessary:

public sealed class DynamicUpdateRate : MonoBehaviour
{
    [SerializeField] private LowUpdateRate _LowUpdateRate;
    [SerializeField] private float _SlowUpdateDistance = 5;

Finding the Camera.main is a slow operation so we cache its Transform to avoid needing to find it again every update:

    private Transform _Camera;

    private void Awake()
    {
        _Camera = Camera.main.transform;
    }

Then it just checks the distance between itself and the camera to decide whether or not the LowUpdateRate script should be enabled. As mentioned in the Vector Magnitude section, calculating the squared distance is faster than calculating the actual distance so in this case we can do that and compare it to the squared distance threshold:

    private void Update()
    {
        var offset = _Camera.position - transform.position;
        var squaredDistance = offset.sqrMagnitude;

        // enabled = true if the distance is larger.
        // enabled = false if the distance is smaller.
        _LowUpdateRate.enabled = squaredDistance > _SlowUpdateDistance * _SlowUpdateDistance;

        ...

For the sake of this example, we also want to show what it's currently doing internally so we just display those details using a TextMesh:

    [SerializeField] private TextMesh _TextMesh;

    private void Update()
    {
        ...

        var distance = Mathf.Sqrt(squaredDistance);
        var updating = _LowUpdateRate.enabled ? "Slowly" : "Normally";
        _TextMesh.text = $"Distance {distance}\nUpdating {updating}\n\nDynamic Rate";
    }
}

Further Development

The scripts used in this example could be used in a real game (after you remove the TextMesh), but there are a few more ways the idea could be improved upon to get even better performance and fine tune the visual appearance.

Events

Keep in mind that updating animations at a lower rate will also affect Animation Events and Animancer Events so it might not be an option if you are relying on their precise timing. The same applies if you are using physics hitboxes based on the character's bone positions. In these cases you might need indicate which animations are important so that the system doesn't lower the character's animation update rate while the timing is critical or maybe when characters are close to each other.

Singleton

Every MonoBehaviour event method that Unity calls (such as OnEnable or Update) has a larger performance cost than calling a method normally in C#. This means that it is actually more efficient (but more complex) to have a single script in the scene for Unity to Update which has a list of all the other things you want it to update.

Staggered Updates

  • Controlling the animation updates of your characters using individual scripts like in this example would allow their low update rates to get enabled and disabled at different times depending on the exact frame when they each go in or out of the camera range. Multiple characters might update in the same frame or they might not.
  • Having all your characters in a list managed by a singleton script would allow it to update them all at once, which might look a bit more consistent but would mean that you have a couple of quick frames with few animations updating followed by one slow one where everything gets updated.
  • But you could get even better performance by only updating some of the characters in the list each frame.

For example, if you have 100 characters and want 5 updates per second while the game is running at 50 frames per second, that means you only need one animation update every 10 frames so you could either:

  • Do nothing for the first 9 frames then update them all at once.
  • Or update 10 of them every frame to keep the performance cost more consistent.

Other Factors

  • Variable Rates: there is no reason why the system would need to be limited to only "update every frame" or "update 10 times per second". You could have multiple different rates at different distances (possibly corresponding to LOD distances) or you could calculate a variable rate to be proportional to the distance without using explicit thresholds.
  • Size: you could account for the size of a character (possibly based on its Renderer.bounds) when determining how far away it needs to be before you start dropping its update rate. A large character could still be clearly visible on screen at a distance where a smaller character might be less distinct.
  • Priority: less important characters like background critters could have their update rate decreased more aggressively than key characters like players and bosses.
  • Visibility: characters that aren't even on-screen (easily checked using Renderer.isVisible) might not need to update very often. Note that the Animator.cullingMode also has some options to control what gets animated when it is off-screen.