Animation Engine - Runtime Changes and Fixes

Update 3/22/2022

Update 3/8/2022


Developers,

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 Workspace.AnimationWeightedBlendFix

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:

  1. 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.

  2. 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.

  3. 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 AnimationWeightedBlendFix.

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
Wait(1)
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:

  1. At the same priority (AnimationTrack.Priority, Enum.AnimationPriority)
  2. 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 | Roblox Creator Documentation

Wherever possible/feasible, this is the preferred method of making your animation code compliant.

Option 2

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.

Option 3

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.

267 Likes

This topic was automatically opened after 8 minutes.

Option 1 would be drastically easier to implement if animations weren’t cached after being updated for several days.

48 Likes

Very solid and understandable improvement lmo.

Thanks for keeping us informed while bettering the platform each and every day!

Without you guys we wouldn’t be, where we are today with this amazingly polished platform.

18 Likes

Glad to see some improvements coming to the animation engine! I love to see the performance upgrades, and it’s very nice to see instructions on how we can be prepared for these changes!

15 Likes

To clarify on what bugs are being fixed - are the minor bugs of keyframes with small time values not being sorted properly fixed, for instance? Or how animations use a custom tween-module instead of TweenService? I know this may seem as if I’m just trying to bring the issues to light & get fixed, but I’m asking this because my game calculates the CFrame a limb would be at during a specified TimePosition, which has some very specific calculations - if the aforementioned animation bugs are fixed, my current code’s behavior would break, too.

12 Likes

Okay I see and appreciate the work that is being done to improve animation efficiency, but now we really need more animation priorities. Unless I’m misunderstanding this fix, you cannot stack animations well with just 4 priorities. We’ve asked for more animation priorities for years (Roblox should allow more than 4 priorities for animations) and with this removing the workaround we have been using it could break games.

88 Likes

Thank you for notifying us of some of these big changes.

9 Likes

Does this fix the issue of server animations not replicating themselves unless you’re near the assembley?

9 Likes

I feel like the change to the animation blending will show how limited the current AnimationTrack.Priority is, since there is only 4 options, a lot of animations end up sharing the same priority. I would love to see an update to this to change the priority to maybe be something similar to ZIndex

27 Likes

Will we see more animation priorities moving forward with this? This is by far our preferred method but animation-heavy games such as ours end up pushing the 4-priority limit, which stresses our ability to deploy animations in our projects in a clean and efficient manner.

9 Likes

Yes. We’ll need this for other upcoming features as well. It’s not final yet whether more Enum values will be added, or if Enum.AnimationPriority will be superseded by just an integer priority property.

52 Likes

I like this update and it seems great, but as some people are saying, we really do need more animation priorities to use this effectively! A game i’m developing on already stacks up with emotes, idles, walks etc at 4 priorities and then there are weapon swings and the likes on top of that. A suggestion I would adore in this would be if feasible to make the animation priority deter from Enum’s and have an actual integer as the priority, to allow developers to make many priorities of whatever they want with as many levels as they want.
Looking good, optimizations are great, though more priorities are definitely needed if possible.

6 Likes

This particular update does not change replication behavior, that is a whole separate set of changes that are upcoming as part of larger animation replication, throttling, LoD and sync updates.

7 Likes

im a little confused on what this means what was optimized?

5 Likes

The general memory (RAM) usage of animations was reduced for many devices, allowing the game to store more data for important things.

The performance for animations was also boosted for some devices, this means the game can run at higher FPSs and animations won’t lower FPS as much as before. (Which was not alot to begin with.)

10 Likes

Do all these new performance updates mean the playerscripts are going to be rewritten at some point?

I assume roblox wants to support more players in the server, which is going to be hard when the playerscripts aren’t that great, like how rbxcharacterscripts creates a stepped connection and unnecessary tables for every player, or just the general unnecessariness of most scripts

7 Likes

Correct. Animations are not commonly a bottleneck. Majority of the optimizations that I made to the low-level data structures and evaluators in the animation engine are not so much for immediate benefit, but to allow the system to scale more linearly to much higher player counts, higher part/bone counts in each avatar or NPC, and denser animation data. Mocap data is often fully dense, with a keyframe for every animated joint on every frame.

9 Likes

Awesome to see! I’m really excited to see the LOD feature for animations in the future. Does this system have anything to do with the “ClientAnimatorThrottling” property that was recently added to workspace, or is this completely different? If this is the same thing, do you have a date when would could get more news on that property?

6 Likes

I fully agree with this. We need way more animation priorities or just full control with the use of integer-based priorities (and fixing core having a priority of 1000 yet is the lowest priority, which makes no sense and just exists to complicate things for no reason).

Edit:
Oh, I didn’t see this reply yet…:

6 Likes