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.

                                                                       
                    DECLARING THE ENUM                                 
                                                                       
                                                                       
       ╔═══════╦═══════╦═══════╦═══════╦═══════╦═══════╗               
       ║       ║       ║       ║       ║       ║       ║▐▌             
       ║   0   ║   1   ║   2   ║   3   ║   4   ║   5   ║▐▌             
       ╚═══════╩═══════╩═══════╩═══════╩═══════╩═══════╝▐▌             
        ▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▘             
                           [usize; 6]                                  
                                                                       
                                                                       
     pub enum Variant {                pub trait AnimationLoop {       
         Idle,           implements                                    
         Rising, ━━━━━━━━━━━━━━━━━━━━━▶    fn page() -> (usize, usize);
         Falling,                                                      
     }                                 }                               
                                                                       
                                                                       
                    MAPPING TO idx/offset                              
                                                                       
       ╔═══════╦═══════╦═══════╦═══════╦═══════╦═══════╗               
       ║       ║       ║       ║       ║       ║       ║▐▌             
       ║   0   ║   1   ║   2   ║   3   ║   4   ║   5   ║▐▌             
       ╚═══════╩═══════╩═══════╩═══════╩═══════╩═══════╝▐▌             
        ▀▀▀▀▀▀▀▀▀▀▀│▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀│▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▘             
                   └───────┬───────┘                                   
              Idle::pages(); // (offset: 1, size: 3)                   
                                                                       
                                                                       
                                                                       
       ╔═══════╦═══════╦═══════╦═══════╦═══════╦═══════╗               
       ║       ║       ║       ║       ║       ║       ║▐▌             
       ║   0   ║   1   ║   2   ║   3   ║   4   ║   5   ║▐▌             
       ╚═══════╩═══════╩═══════╩═══════╩═══════╩═══════╝▐▌             
        ▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀│▀▀▀▀▀▀▀│▀▀▀▀▀▀▀▀▀▀▀▀▀▘             
                                   └───┬───┘                           
              Rising::pages(); // (offset: 3, size: 2)                 
                                                                       
                                                                       

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.

#[derive(Clone, PartialEq)]
pub enum PlayerAnimationVariant {
    Idle,
    Rising,
    Falling,
}

Creating an AnimationLoop trait

The trait is straightforward, defines a function that would return the offset and size of the animation page.

pub trait AnimationLoop {
	fn page(&self) -> (usize, usize);
}

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.

impl AnimationLoop for PlayerAnimationVariant {
    fn page(&self) -> (usize, usize) {
        match self {
            // (idx_offset, loop_size) describe the animation loop
            PlayerAnimationVariant::Idle => (0, 3),
            PlayerAnimationVariant::Rising => (2, 2),
            PlayerAnimationVariant::Falling => (4, 4),
        }
    }
}

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.

                                                                       
                                                                       
                   FLIPPING THROUGH SPRITES                            
                                                                       
                                                                       
                                   ┌───────┐                           
                                   │       │                           
       ╔═══════╦═══════╦═══════╦═══════╦═══▼═══╦═══════╗               
       ║       ║       ║       ║       ║       ║       ║▐▌             
       ║   0   ║   1   ║   2   ║   3   ║   4   ║   5   ║▐▌             
       ╚═══════╩═══════╩═══════╩═══════╩═══════╩═══════╝▐▌             
        ▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▘             
                                                                       
              animation_state.variant; // Variant::Rising              
              animation_state.idx; // 3                                
              animation_state.wrapping_next_idx(); //  4               
                                                                       
                                                                       
                       VARIANT TRANSITION                              
                                                                       
                                                                       
                                       ┏━━━━━━━━━━━┓                   
                                       ┃           ┃                   
                                   ┌───────┐       ┃                   
       ╔═══════╦═══════╦═══════╦═══════╦═══════╦═══▼═══╗               
       ║       ║       ║       ║       ║       ║       ║▐▌             
       ║   0   ║   1   ║   2   ║   3   ║   4   ║   5   ║▐▌             
       ╚═══════╩═══════╩═══════╩═══════╩═══════╩═══════╝▐▌             
        ▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▘             
                                                                       
              animation_state.variant;              // Variant::Rising 
              animation_state.transition_variant(); // ()              
              animation_state.variant;              // Variant::Falling
              animation_state.idx;                  // 5               
                                                                       
                                                                       

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.

pub struct PlayerAnimationState {
    pub variant: PlayerAnimationVariant,
    pub idx: usize,
}

Implementing transition functions

wrapping_next_idx increments the current index and wraps around at the page boundary. It makes looping animations trivial to implement.

impl PlayerAnimationState {
    fn wrapping_next_idx(&mut self) -> usize {
        let current_idx = self.idx;
        let (offset, size) = self.variant.page();

        self.idx = offset + (current_idx + 1) % size;

        self.idx
    }
}

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 Puddle Jumpr, 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.

impl PlayerAnimationState {
    ...

    fn transition_variant(&mut self, to: PlayerAnimationVariant) {
        let (offset, _) = to.page();
        self.variant = to;
        self.idx = offset;
    }
}

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