Joule is a highly dynamic player character. Capable of jumping, running, and jetting around with her rocket boots, she requires a very complicated state graph. What’s more she can do all of the above while aiming, firing from the hip, or with her gun at her side.
Given the number of states and transitions necessary to set up this core locomotion suite, duplicating this setup for every weapon stance Joule can be in was not feasible. Fortunately, Mecanim has a solution for this in Sync’d Layers. This checkbox causes Mecanim to duplicate the full state-machine layout on a layer, letting you swap out the Animation Clips as needed without having to maintain multiple states. There is some small overhead for having unused states and transitions in layers where you do not need them. As long as these are the exception, not the rule, the benefits of fewer bugs and faster iteration are well worth the cost.
Joule is joined on her adventures by a set of mechanical companions referred to as Corebots. They follow her through the world, engage in combat, and have a host of special attacks and environmental abilities. They are driven by a complex AI state machine that has to stay synchronized with the Mecanim state machine. Ideally, this complicated structure is created once, and used everywhere, allowing all AI to benefit from work done on any AI.
The three Corebots you find in the game look nothing alike and nothing like a human. Trying to leverage the retargeting built into the Mecanim Humanoid Rig was not an option. While the Humanoid rig is quite powerful, it has very specific setup requirements. A dog, an ape, and a spider do not fit in those requirements. Even Joule doesn’t run on the Humanoid rig due to all the extra bones she has in her spine.
Thankfully, these AI variations all have nearly the same functionality. They all need to move, fight, follow, and idle in a charming fashion. While each of these animations looks completely different, the logic guiding them is the same. This makes the AI a perfect candidate for the Runtime Override Controller. The controller allows you to specify a remapping from one clip to another on a specific Animator. For ReCore, the dev team sets up Dog’s AnimatorController, and then the Ape and the Spider simply override the actual animation data to use something a bit less canine.
While this solution let Armature ship their game, there are some limitations here that they ran into. Any states that are only needed in one of the three Corebot types had to be in the shared Animator Controller, as there is only the one. This has some overhead associated with it, so this technique works best when the vast bulk of state and transition data is shared. For the Corebots, it was well worth the cost.
Armature found that they had to be very careful with Animation Transitions. When you are overriding clips in your Animator, you cannot alter the transition’s settings. Most transitions in ReCore were set to use fixed time to give the animators greater control of the final look. This meant that large changes to the timing of a clip in an Override would cause transitions to happen at the wrong time. When working with Runtime Override Controllers, try to limit the timing changes between the various clips.
Given the sheer number of Animation Clips that are involved in this state machine, the Runtime Override Controller quickly got too complicated to easily edit. ReCore solved this by alphabetizing their Runtime Override Controllers in code. As the number of clips grew even larger, they finally wrote a tool to go in a remap animation clips based on user defined patterns.
Due to the tireless efforts of the teams at Armature and Microsoft, we have been able to track down several performance issues. We are currently looking into improving the way animations allocate memory. As it stands, Unity will fallback to using its slower Default memory allocator if its Temporary Memory Pool is full. This should only happen when many animators are running simultaneously and can be detected by looking for the “TempAlloc.Overflow” label in the profiler. We are also looking at how Mecanim handles having many different animations playing at very low blend weights. Currently, all animations have to be fully processed, even if they will eventually be blended down to almost nothing. You should take care when creating Mecanim Layers or Blend Trees to avoid having lots of animations running with little effect on the final output.
Even with all of the core engine improvements, Armature’s data needed to be set up as efficiently as possible. In order to avoid having to update hundreds of bone Transforms in the UpdateTransform pass on the main thread, Armature needed to ensure all models were imported with the Optimize Game Objects option. This collapses a SkinnedMesh’s bones and allows Mecanim to write directly into graphics memory, removing the need to update every bone’s GameObject every frame while animating. Armature also needed to avoid using OnStateMachineEnter and OnStateMachineExit callbacks. Mecanim detects the presence of those callbacks and prevents threading of the StateMachine evaluation if they are found, as they are not thread safe. This only accounts for ~5% of the total cost of animation evaluation, but every little bit of frame time matters on a big console game.
In an attempt to speed up loading, Armature created pools of all their AIs rather than spawning new instances, Activating and Deactivating the entire Game Object as needed. This causes the Animator component to re-build all of its internal state. The aim is to keep deactivated Animators from taking up runtime memory and to cleanly reset the state of an animator when Deactivated. It is possible to avoid this behavior by setting the Animator Controller disabled without deactivating the entire GameObject. Armature could not afford the additional memory costs or the extra code necessary to handle resetting the Animator’s state.
During Activation, too much of the frame time was spent allocating all the instances of every State Machine Behavior used on every state. This memory is necessary for any State Machine Behavior that could have its own per instance variables or data, and a new copy is needed per Animator, not just per AnimatorController. We added the [SharedBetweenAnimators] attribute to all the behaviors that did not need their own state and got much of that time back. This attribute tells Mecanim that there is no per-instance state that needs to be tracked, and it can simply create one static instance for the relevant class.
Working with Armature to get Joule and her Corebots running, fighting, and jumping provided a perfect case-study. By working with such an ambitious team, we were able to improve the quality of the engine and let Armature get to their vision. Keep checking the blog for more on our partnership with Armature and the development of ReCore.