Dead Simple Spritesheet Animation
Live | Repository
Motivation
I’ve been using Bevy for a video game project and it’s been a delightful experience. As the project has grown, I’ve turned into a level designer, animator, illustrator, along with being a programmer.
Sometimes I hit walls, and rabbit holes around those walls are too compelling to pass up. One such rabbit hole was building an animation state machine that could create different looping animations from a single sprite sheet. The only reason for using a single sprite-sheet is that being the dev and the illustrator is very time consuming and I wanted my workflow to remain as simple as possible. But as I've built a few different prototypes the single spritesheet model has proven to be quite useful.
!Sprite sheet
!Animated
I created an animation-transition crate that abstracts most of the details away and I've used it on multiple prototypes so I think that it is fairly straight forward to use in 2D games. The following is a sort of how-to guide to create an animation state machine from scratch.
Animation Pages
I first create an enum with variants with self explanatory names. For the above example three variants suffice: Idle, Rising, and Falling. The enum variants are then mapped to animation pages. Pages are a way to encode the information needed to build looping animations. They are made up of two parts: an offset, and a page size. The offset stores the index of the first sprite in a single animation loop, and page size stores the number of sprites in the same animation.
Implementation
Let's take a look at some code.
Animation variants enum
The enum needs to implement PartialEq so that the variants can be compared to each other when making decisions during animation state transitions.
Creating an AnimationLoop trait
The trait is straightforward, defines a function that would return the offset and size of the animation page.
Implementing the AnimationLoop trait for the enum
This is also straightforward, I match the enum variant to the appropriate animation page tuple i.e. frame offset and page size.
State Transitions
Playing a single looping animation can be accomplished by flipping through a contiguous array of indices, that are then used to fetch the appropriate sprite from a sprite atlas. Moving between different animations can be accomplished by simply updating the animation variant.
Implementation
I encapsulate the information needed for the two actions (looping animation, and variant transition) in a struct called PlayerAnimationState.
Animation state manager
The struct stores the animation variant to play, and the current index of the frame.
Implementing transition functions
wrapping_next_idx increments the current index and wraps around at the page boundary. It makes looping animations trivial to implement.
transition_variant updates the animation state manager so that I can play a different animation based on in-game events. This one accepts an argument of type PlayerAnimationVariant, so different in-game events can trigger appropriate animations in response. For example when I press spacebar on Toad, the animation state is first updated to Rising, once the toad reaches it's maximum jump height the animation is updated to Falling, and finally on touching the ground it can be transitioned back to Idle.
All of this can be somewhat tedious, and repetitive if you're working on multiple games. I created a Rust crate that simplifies the process by providing:
- the traits AnimationLoop and AnimationTransition<T: AnimationLoop>
- a handy proc macro AnimationTransitionMacro that implements the necessary index/variant manipulation functions for the animation state manager
Discussion in the ATmosphere