Location: Assets/Plugins/Animancer/Examples/02 Fine Control/04 Update Rate
Recommended After: Quick Play
Learning Outcomes: in this example you will learn:
How to control how often animations are updated.
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.
Summary
- To prevent Unity from updating animations normally, call
animancerComponent.Playable.PauseGraph();
. - To update animations manually, call
animancerComponent.Evaluate(deltaTime);
.
Overview
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.IsPlayableInitialized)
_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 Animancer.Units;
using UnityEngine;
public sealed class DynamicUpdateRate : MonoBehaviour
{
[SerializeField] private LowUpdateRate _LowUpdateRate;
[SerializeField] private TextMesh _TextMesh;
[SerializeField, Meters] 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 this example is just using NamedAnimancerComponent
s for each of the characters with an animation set to Play Automatically
in the Inspector:
The code structure is fairly straightforward:
Controlling Updates
Controlling when Animancer updates is very easy:
- Get the
AnimancerComponent
you want to control:
public sealed class LowUpdateRate : MonoBehaviour
{
[SerializeField] private AnimancerComponent _Animancer;
- Call
AnimancerPlayable.PauseGraph
top prevent it from updating on its own:
private void OnEnable()
{
_Animancer.Playable.PauseGraph();
}
- 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 initialized before doing so (otherwise accessing _Animancer.Playable
would initialize it again):
private void OnDisable()
{
if (_Animancer != null && _Animancer.IsPlayableInitialized)
_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, Meters] private float _SlowUpdateDistance = 5;
[Meters]
is a Units Attribute which causes the field to display an " m"
suffix in the Inspector to indicate that the value is measured in meters.
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're relying on their precise timing. The same applies if you're 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 or even adapt based on the current frame rate.
- 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 theAnimator.cullingMode
also has some options to control what gets animated when it is off-screen.