We’re releasing performance improvements and bug fixes to the animation engine this week.
Because these changes include a bug fix which will change the behavior of animation blending in some games, this is being done as a 3-phase rollout that is initially going to be opt-in via a Workspace property
What’s Changing and Why
This rollout enables a new, optimized animation runtime that has couple of huge performance benefits, especially on mobile devices that majority of Roblox players are using:
Animations that consist of dense, uniformly-sampled, linear keyframes, which includes all Roblox catalog animations, are now stored in memory using quaternions. With the initial release, memory usage is reduced to 40% of previous requirements, and allows for future improvements to bring this down closer to an average of 22% of original size.
The in-memory structure for keyframes is now optimized for cache coherency during normal, forward playback. This change alone can improve animation performance by 30% or more on mobile devices.
Several other areas of the animation engine have been optimized to enable the system to scale to support higher-complexity characters (more joints/bones), higher numbers of concurrently-animating characters, and longer, denser animation sequences.
The rollout also includes one major bug fix to weighted blending which is needed for future development of the animation engine, but is also going to break some games that are relying on the order of AnimationTrack:Play() calls to cause animations to override each other. It is because of this fix that the feature is being presented as a 3-phase rollout that is initially opt-in.
The nature of the coding pattern that will break, and recommending solutions, are presented in the following sections.
The 3-Phase Rollout Plan
Phase 1 - Happening this week
Involves the fixes being enabled on production by Roblox, but without any immediate changes to existing games. Developers will be able to preview changes, or enable them live, via the Workspace property
This phase is expected to last a minimum of 4 weeks, in order to give you time to test your code against the changes and make updates if necessary. It also gives us time to respond to any additional unforeseen issues that may come up during testing.
Phase 2 - 4 or more weeks from Phase 1
Involves changing the feature from being opt-in, to being opt-out. Developers who have not been able to make their games compatible with the changes will be able to keep using the legacy code, but all new place files will use the latest code by default.
Phase 3 - TBD
This phase is the removal of the legacy code, at which point all production games will be running on the latest version of the animation engine with all of the improvements and fixes mentioned below.
The Animation Blending Pattern That Will Not Survive
We are aware of a particular animation usage pattern that relies on a behavior of an AnimationTrack being able to override an already-playing track of the same priority, if played with a weight of 1.0. This behavior was never by design, it is a consequence of two implementation details:1) How AnimationTracks are iterated over by the current Animator, and 2) A long-standing math bug in our AnimationTrack weight-blending code.
This behavior is not going to survive as the Animator evolves to support parallelism, throttling/LoD, and more complex blending arrangements, and it will be necessary to replace code that relies on this call-order behavior with one of the options outlined in the section “How to Make Your Animation Code Compliant”, below.
Consider the following sequence of AnimationTrack:Play() calls, involving two full-body Animations that are published and played at the same Enum.AnimationPriority:
TrackA:Play( 0, 1, 1) – Track A is a looped animation, such as Idle
TrackB:Play( 0, 1, 1) – Track B is a non-looped 1 second emote, weapon swing, or similar
Under the existing animation runtime, the behavior is this:
0:00 - Track A starts playing an Idle loop at weight 1.0
1:00 - Track B plays, completely overriding Track A by playing at weight 1.0
2:00 - Track B ends, character resumes displaying Track A Idle animation playing at weight 1.0
With the bug fix, the behavior of this sequence will be:
0:00 - Track A starts playing an Idle loop at weight 1.0
1:00 - Track B starts, resulting in an animation that is a 50/50 blend of Tracks A and B
2:00 - Track B ends, returning to Track A idle animation playing at weight 1.0
The unsupported pattern occurs when two or more AnimationTracks are played:
- At the same priority (AnimationTrack.Priority, Enum.AnimationPriority)
- With track blend weights that sum to greater than 1.0
In the above example, the existing Animator behavior will predictably result in Track B overriding Track A, because the two Play() statements are played from the same script in a guaranteed order.
There are worse scenarios, however, where the end result is not so well defined and can differ from client to client. If AnimationTrack():Play() calls are bound to different events or property changes which do not necessarily get processed in the same order on all clients, or if Play() calls are made from both client and server scripts on the same Animator, a race condition can occur.
For example, if the majority of a character’s animations are being started by their Animate LocalScript, but a specific reaction or animation is then played on the character from a server Script, the race condition may resolve to a different outcome on different clients if there is a priority and blending weight conflict. This is an additional reason for strongly preferring use of AnimationTrack.Priority’s explicit guarantee of ordering over reliance on implementation-defined behavior.
Why This Fix is Necessary
Even when tracks are started in a reliable order, there is a bug in the current Animator’s blend math which results in unexpected blending if the sum of same-priority tracks’ weights exceeds 1.0. Consider the case of playing 4 tracks, all with the same priority and weight of 0.5. The outcome with the current system will be that the 4 tracks will be blended with weights of 0.125, 0.125, 0.25, and 0.5.
With the bug fixed, all tracks will blend with weight 0.25, which is arguably a far more sensible result. It is because of this behavior that this bug absolutely must be fixed, so that observed behavior matches API documentation behavior, and so that things like 8-way motion controllers can be made that blend walk, run, sprint, and strafe animations predictably and without loss of developer sanity.
How to Make Your Animation Code Compliant
There are 3 ways to correct the unsupported usage pattern, so that animations will continue to override as expected. Which option is best will likely depend on your current usage of AnimationPriority and how many total animations are involved.
Option 1 - Preferred
Changing Animations to Use AnimationPriority
The intended and preferred way to make Track B temporarily override an already-playing Track A is to publish Track B from Studio’s Animation Editor with a higher priority than is used by Track A. The full list of available priority values and their ordering can be found here: AnimationTrack.Priority
Wherever possible/feasible, this is the preferred method of making your animation code compliant.
Managing Blend Weight Sum from Lua
This option involves using Lua to manage the weights of the AnimationTracks playing through an Animator so that the weights or currently-playing tracks reflect the actual, intended playback behavior. This is the method used by the default Roblox Animate LocalScript to blend walk and run, as well as ensure that emotes override already-playing idle animations. Because all Roblox catalog animations are published at Core priority, the Animate script computes weights for the walk and run animations which sum to 1.0. When emotes are played, any currently-running Idle animation is first stopped so that the emote plays at full strength.
It’s worth noting here that completely stopping an animation with AnimationTrack:Stop() or by setting the weight to 0 can result in a visual discontinuity, where the character may render in a default stance briefly, especially if the second animation is being played for the first time or when fade-out and fade-in times are 0. This can be avoided if instead of stopping the playing animation, you adjust its weight to a very small value such as 0.0001 using AnimationTrack:AdjustWeight(), but allow it to continue playing. The tradeoff is that the minimized track will still incur the full per-frame cost of animation evaluation, which in cases where the animation is only being momentarily suppressed is usually negligibly different from using Stop() and Play() to stop and restart the track with non-zero fade-out and fade-in times.
Using Lua Code to Change AnimationTrack.Priority
AnimationTrack.Priority is normally established at publish time, but it can also be changed at runtime using Lua code. There is a big gotcha with this option, however, which needs to be taken into account: The AnimationTrack.Priority property does not replicate from client to server or server to client! The consequence of this is that setting AnimationTrack.Priority from Lua from a server Script will not change how the animation plays on any client, and likewise, changes made from a client LocalScript will not affect server playback. Because this is cumbersome to manage at runtime, this option should be considered last.