Animation system in Bullet Waste

September 16, 2024 · 7 minutes read
Author: Andrzej "Pojemnik" Gauza

Hi! After a little summer break we are back to technical posts. As you may have guessed from the title, today I will show you around the animation system in Bullet Waste. I will tell a bit of backstory, explain the design goals and how we managed to fulfill them.

strafe
Animations don't always work as expected

Handling animations in Unity is one of these things that are easy to begin with, but tend to get harder as your project develops. The AnimationController provides a nice out of the box experience. It is beginner-friendly and has a lot of useful features. Problems begin when you have many different animations to play and your logic starts to get more and more complicated. If you are not careful, you may get entangled in a web of transitions, states, substates and parameters, wondering how on earth is that supposed to work.

controller
  How on earth is that supposed to work?

Animations in Bullet Waste

For a long time, Bullet Waste handled animations the same way as it did back in its gamejam days. Without a clear separation, every part of the code that needed to interact with the animations messed around with parameters of the AnimationController. As usual in a spaghetti like that, changing something in one place was breaking a functionality somewhere else. It was high time to redesign it from scratch.

Redesigning it from scratch

It’s good to think for a moment before you start writing code  

~Napoleon Bonaparte

Our goal was to create a proxy layer that will hide the dirty details of the AnimationController and provide a simple interface for the rest of the game to interact with. Other scripts have nothing to do with state parameters, layer masking and other animation related stuff. It means they should know about it as little as possible.

The question was whether or not to use transitions in the AnimationController in the same way as before. They do provide a simple, graphical way of designing a state machine, but they quickly get messy. I decided to stop using them and handle transitions by myself. Not using Unity’s systems is something that we tend to do a lot in Bullet Waste. For example, our projectile handling system ditched creating objects in the hierarchy in a quest for maximum performance. You can read more about this solution here.

Handling transitions and state

Ok, but how to avoid using default parameter-driven transitions? We can start every animation manually by calling Animator.Play or other similar methods. This way we gain full control over what is happening. Of course, we have to implement our own logic to handle the states. The initial idea was to use a pushdown automaton or some other state machine. I created a prototype that made use of such solution, but at the end of the day I didn’t go this way. I wanted to keep at least some visual debugging and configuration, without having to design my own editor windows. I also wanted to be able to play more than one animation at once, but more on that later.

The final system uses layers of the AnimationController for it’s core functionality. Every layer is a separate state machine with an assigned priority. The system chooses which layer to activate and which state to play. These two processes are independent. When we change layer to a one with higher priority, we don’t stop animations playing on the lower layers. Instead, the state from a higher layer covers up states from lower ones. This way, we can go back to an animation that is playing on a lower layer. If the newly started animation is on the same layer, it interrupts the current one. Only one layer at a time is “active” - it has weight 1, while others have 0. An exception from this is the process of changing an active layer - it’s done by gradually increasing the weight of the new layer while decreasing the weight of the old one.

layer

An example of a layer. Notice the lack of defined transitions

We didn’t actually get rid of all transitions in the AnimationController. Unconditional ones were left, as they provide a simple way of playing sequences of animations without having to write any code to handle them.

Example

The explanation might have been a little complicated, so let me guide you through an example.

example part 1

We have a simple AnimationController with two layers and three animations. At first, the Run animation is playing on Layer 0. Layer 0 has weight 1 and Layer 1 has weight 0 - only the Run is visible.

example part 2

Now, we start the Jump animation. It is located on a layer with higher index than the currently visible one, which means it has higher priority. Animation starts and layers’ weights change (they do it gradually, so we get a smooth transition, but here we omit that for simplicity). Now only the Jump animation is visible, even though Run is still playing. Notice that Jump has no arrow indicating that it is looped.

example part 3

As the Jump animation is not looped, after it finishes weights change back to how they were at the start.

Handling other cases is left as an exercise for the reader.

BJ

The system also supports animation masks, blend trees, substates and animation events - every feature of the Animator that we use in Bullet Waste.

The interface

One of the goals of this redesign was to provide a simple and consistent interface for the rest of the game to work with. With the new solution, only a few methods are needed to use the system. We start a state with Set. For looped animations we have a complementary Reset method. There is a way to modify blend tree parameters and an event handler for Unity’s animation events. Every state has a corresponding enum value used for identification. That’s all.

In the end, components using the new system don’t know if the animation they wanted to play is actually visible or not. There is a guarantee that the animation is playing on some layer, but not necessarily on the active one.

Technical details and quirks

State detection

The main class of the system needs to know what is happening inside the Animator. Unfortunately, Unity doesn’t provide any default events on start and end of a state. In theory, we could query current states on every layer of the Animator every frame, but it doesn’t seem very efficient. To solve this, we used a StateMachineBaviour to send events when states begin and end. States had to be identified by their hashes. There is an interesting issue with this approach - OnStateExit in StateMachineBaviour is called only when a new state on the same layer is entered. It means that after every not looping state we had to add an unconditional transition to a dummy state which is only used to detect the completion of the state before it.

empty state

A dummy state added to a layer

Configuration

As we are not using the default transition system, I had to find a way of configuring the state changes. Thankfully, our solution doesn’t forbid any transitions. We also assume that every transition to a state takes the same amount of time. This considerably reduces complexity of the configuration, as we don’t have to create any matrices of transition durations.

configuration example

Here is an example of how to configure a few animations and their transitions. There is also a graphical bug in the Unity editor. One of many graphical bugs actually.

Summary

I hope this short description will inspire you to play a bit with handling animations in Unity. By careful design you can avoid many common problems and create something that is both useful and reusable in future projects.

We plan to write some more technical posts in coming weeks, so stay tuned for more content.

Andrzej “Pojemnik” Gauza


Close Terminal E-Mail Discord Download GitHub Alternate GitHub itch.io Menu Check Circle Google Play Space Shuttle angle-right Warning YouTube