March 19, 2020

Creating a Simple Spritesheet Animation with Amethyst

I’ve been doing a lot of reading on the Amethyst game engine recently, mostly to satiate my curiosity on the game development space for the Rust programming language, but also to find an interesting open-sourced project to (hopefully!) get involved with. I worked through the first three chapters of the Amethyst documentation and then a pong tutorial to get a better feel of how to use it. By the end of it, I’m slightly more confident with the concepts used by Amethyst, though I still feel I need to create my own small example to solidify the bit of knowledge I have.

So I decided to write my own “How to” post on creating a simple spritesheet animation with Amethyst. For this example, I’m borrowing 0x72’s dungeon spritesheet texture and using the “wizard_m_idle_anim” frames for the animation. As the frame name suggests, we’ll be focusing on creating an idle animation for the wizard sprite, which is 4 frames long.

Setup the RON file

We need to be able to tell the Loader resource where to find the sprite on the spritesheet texture. Luckily, the texture provided at the link above also includes a file called “tiles_list_v1.1” which have all the coordinates and sizes for characters, tiles, items, etc.

// texture.ron

List((
    texture_width: 512,
    texture_height: 512,
    sprites: [
        (
            x: 128,
            y: 164,
            width: 16,
            height: 28,
        ),
        (
            x: 144,
            y: 164,
            width: 16,
            height: 28,
        ),
        (
            x: 160,
            y: 164,
            width: 16,
            height: 28,
        ),
        (
            x: 176,
            y: 164,
            width: 16,
            height: 28,
        ),
    ],
))

The coordinates of the first frame is located at (128, 164) on the spritesheet. Since the frames are all on the same row, we only need to increment the x-coordinate by 16 to get the next frame in the animation.

Define an Animation Component

Amethyst is designed using the Entity-component-system (ECS). This means components can be grouped together by the entity they’re connected to. Entities by themselves are merely identifiers of objects created in the world and don’t hold any data or methods of their components. This allows object creation to be as generic as possible.

So the goal here is to define a component that can describe the animation of an entity.

// state.rs

// Describes the "animation" of an entity
pub struct Animation {
  // The number of frames in the animation
  pub frames: i32,
  // How long to show a single frame
  pub frame_duration: u64,
  // Starting sprite index
  pub first_sprite_index: usize,
}

impl Component for Animation {
  // The storage type of the Animation component
  type Storage = DenseVecStorage<Self>;
}

This component’s data is pretty simple, but it’s enough to do what we need. Here we are describing one animation cycle for an entity where frames is the number of sprite images to flip through and frame_duration describes how long to show each image. We will tie this data together when we implement its System later on, but for now know the Animation component describes the data needed to animate an entity.

Loading the SpriteSheet

Now we have a component to describe the “animation” of an entity. We’re still missing something though. Right now our Animation component is just a container of data. It has no knowledge of the sprite images used in the animation, nor does it need to!

First let’s load the spritesheet and its associated RON file.

// state.rs

fn load_sprite_sheet(world: &mut World) -> Handle<SpriteSheet> {
  // Create a handle referencing the texture we are using.
  let texture_handle = {
    let loader = world.read_resource::<Loader>();
    let texture_storage = world.read_resource::<AssetStorage<Texture>>();
    loader.load(
      "texture/sprite_sheet.png",
      ImageFormat::default(),
      (),
      &texture_storage,
    )
  };

  let loader = world.read_resource::<Loader>();
  let sprite_sheet_store = world.read_resource::<AssetStorage<SpriteSheet>>();
  loader.load(
    "texture/sprite_sheet.ron", // Here we load the associated ron file
    SpriteSheetFormat(texture_handle),
    (),
    &sprite_sheet_store,
  )
}

Note to self: there’s a quite a bit of code here so let’s take some notes.

The first thing we do is create a texture_handle for the spritesheet we want to use. This texture_handle contains data describing how the spritesheet should be formatted. To do this, we use the Loader resource. It’s responsible for loading files stored in the assets directory. By calling Loader::load the texture will be stored in AssetStorage<Texture> and a reference (or Handle) to the asset is returned and stored in texture_handle.

Next, we create another loader but this time our asset will be stored in AssetStorage<SpriteSheet>. The SpriteSheet struct is what contains the information needed to draw the graphics to the screen. Again, Loader::load returns a Handle to the asset, so Handle<SpriteSheet> is returned by load_sprite_sheet.

Attaching the Animation and SpriteRender Components

Now that we’ve loaded the SpriteSheet with load_sprite_sheet, it’s almost time to connect our animation and sprite components to an entity! We just need to create a SpriteRender component first.

let sprite_render = SpriteRender {
  // the SpriteSheet returned from load_sprite_sheet
  sprite_sheet: sprite_sheet_handle,
  // the first frame of the animation is the first sprite defined in the RON file
  sprite_number: 0,
};

Now that we’ve created a SpriteRender component, let’s attach it to an entity we create inside a function called initialize_sprite:

// state.rs

fn initialize_sprite(world: &mut World, sprite_sheet_handle: Handle<SpriteSheet>) {
  // Create the translation
  let mut local_transform = Transform::default();
  local_transform.set_translation_xyz(CAMERA_WIDTH / 2.0, CAMERA_HEIGHT / 2.0, 0.0);

  // Create the Animation component
  let animation = Animation {
    frames: 4,
    frame_duration: 10,
    first_sprite_index: 0, // the first frame for this example is the first sprite.
  };

  // Create the SpriteRender component
  let sprite_render = SpriteRender {
    sprite_sheet: sprite_sheet_handle,
    sprite_number: 0,
  };

  // Create the entity and attach the SpriteRender and Animation components.
  world
    .create_entity()
    .with(sprite_render)
    .with(animation)
    .with(local_transform)
    .build();
}

Note: a Transform component is also being attached to the entity so that it can be centered in the middle of the window. For more context, see the entire state.rs file

The AnimationSystem

It’s time to implement the logic that will animate entities with an associated Animation and SpriteRender component.

Note: In Amethyst, a system is simply a struct that implements the amethyst::ecs::System trait. The run method is executed every game loop iteration.

// import the Animation component we defined in state.rs
use crate::state::Animation;

pub struct AnimationSystem;

impl<'s> System<'s> for AnimationSystem {
  type SystemData = (
    ReadStorage<'s, Animation>,
    WriteStorage<'s, SpriteRender>,
    Read<'s, Time>,
  );

  fn run(&mut self, (animations, mut sprite_renders, time): Self::SystemData) {
    // Get every animation with its associated SpriteRender component.
    for (animation, sprite) in (&animations, &mut sprite_renders).join() {
      let elapsed_time = time.frame_number();
      let frame = (elapsed_time / animation.frame_duration) as i32 % animation.frames;

      sprite.sprite_number = animation.first_sprite_index + frame as usize;
    }
  }
}

What’s happening here? The AnimationSystem needs access to the game data involving entities with associated the Animation and SpriteRender components. Both have their own storages. Specifically, we want to read data from an Animation component and use it to write new data to SpriteRender. Amethyst requires all systems to define a SystemData type, which tells the engine what data will be provided to the run method every time it’s executed.

We also want to draw a new image for every animation.frame_duration. Earlier this value was set to 10, meaning that a new sprite image in the animation is drawn every 10 frames. To set the animation to the passage of time we need to use the core::timing::Time resource to get the elapsed time since the first game loop iteration. Using some math, we can get the next frame to draw by doing (elapsed_time / animation.frame_duration) as i32 % animation.frames. This will give the index of the sprite image that is next in the animation.

Now that we have the sprite frame to draw, we can overwrite the sprite_number with the new one.

Conclusion

The code shown above are snippets of this sprite animation exercise. To see the whole thing, I have posted it on GitHub at https://github.com/tigleym/simple_sprite_animations.

We now have an idle animation going for the wizard’s sprite images. But what about a “running” animation? In the next post we’ll look at how we can draw the running images based on user inputs.

Resources

© Micah Tigley 2020

Powered by Hugo & Kiss.