The content covered here builds on the code explored in Creating a Simple Spritesheet Animation with Amethyst. This part will be extending the previous implementation for an “idle” sprite animation by adding a “running” animation in response to user input. In the previous post, we implemented an AnimationSystem
responsible for telling the game what sprite image to draw to the screen during each iteration of the game loop.
Now we will be introducing another system called MovePlayerSystem
, which will implement the logic for moving our sprite image on the screen based on user input.
Overview
Implementing the “running” animation involves the following tasks:
- Add texture data for “running” sprite frames to the RON file.
- Adding a new configuration file for capturing user input.
- Adding an
ActionStatus
component - Extending
AnimationSystem
- Implement
MovePlayerSystem
1. Add new sprite frames to RON file
In Creating a Simple Spritesheet Animation with Amethyst, a configuration file for telling the Loader
resource where to locate the sprite images on the texture was created. For the new animation, we will need to provide the necessary coordinate and size data for the sprite images involved.
// texture.ron
...
// Sprite frame data for "running" animation.
(
x: 192,
y: 164,
width: 16,
height: 28,
),
(
x: 208,
y: 164,
width: 16,
height: 28,
),
(
x: 224,
y: 164,
width: 16,
height: 28,
),
(
x: 240,
y: 164,
width: 16,
height: 28,
),
],
))
For reference, the first sprite image for the “running” animation will be located at the fourth index in the configuration file’s sprites
array. To see what the whole file should look like, see this commit.
2. Configuration for user input
A configuration file is needed to capture user with Amethyst. Like the configuration file containing sprite data, this file will be represented using RON. The contents of the file will look like this:
// bindings.ron
(
axes: {
"player_move_x": Emulated(pos: Key(Right), neg: Key(Left)),
"player_move_y": Emulated(pos: Key(Up), neg: Key(Down)),
},
actions: {},
)
This file allows us to create input mappings to named variables we can reference in our code. In particular, inputs are created to move the player along the horizontal axis, "player_move_x"
, and vertical-axis, "player_move_y"
.
Now that we the RON file containing our input settings, we should tell the game what inputs to capture based on the configuration provided in bindings.ron
. This is done by creating an InputBundle
and adding it to the game object’s data.
// main.rs
use amethyst::{
prelude::*,
input::{InputBundle, StringBindings},
};
let bindings_path = config_dir.join("bindings.ron");
let input_bundle = InputBundle::<StringBindings>::new()
.with_bindings_from_file(bindings_path)?;
let game_data = GameDataBuilder::default()
// ...
.with_bundle(input_bundle)?
For full context on registering the InputBundler
to the game, see this commit.
The InputBundler
also contains a InputHandler
resource, which is responsible for mapping the configured inputs to their axes and capturing user input. The InputHandler
will be read into MovePlayerSystem
, which we will implement later.
3. Define the ActionStatus component
The game needs a way to differentiate between which sequence of sprite images the game should be drawing in response to user input. In Creating a Simple Spritesheet Animation with Amethyst, we implemented an “idle” animation that consisted of four images. The game should be drawing these four images sequentially when there is no user input. Now that the game can take user input, there should be a different sequence of sprite images drawn anytime user input is captured. This new sequence of sprite images was defined in the first task of this post. To determine whether the player’s sprite is “idle” or “running”, we can characterize between the two actions of the player’s sprite with values Idle
or Run
.
enum Action {
Idle,
Run,
}
Next we can add a component called ActionStatus
to the player object. This component describes the type of action the player is performing.
struct ActionStatus {
action_type: Action,
}
impl ActionStatus {
fn set_action_type(&mut self, action: Action) {
self.action_type = action;
}
}
impl Component for ActionStatus {
type Storage = DenseVecStorage<Self>;
}
ActionStatus
also has a method called set_action_type
, which is responsible for setting the action type of the player object in response to user input. This method will be used when we implement MovePlayerSystem
. Another way to think about this component is to think of it as a place on the player object for MovePlayerSystem
to write to, whereas the AnimationSystem
will use ActionStatus
as place to read from.
In the next task, AnimationSystem
will be extended to read from an ActionStatus
component to determine which sequence of sprite images it should be drawing from.
4. Extending AnimationSystem
Now that the game has a way to update the player’s action type, the AnimationSystem
can be extended to read from the player’s ActionStatus
component to update which sprite sequence should be drawn to the screen.
The first thing we need to do is update the SystemData
type to contain the ReadStorage
type for ActionStatus
. This tells the implementation for AnimationSystem
that the game should also read from the ActionStatus
storage.
impl<'s> System<'s> for AnimationSystem {
type SystemData = (
ReadStorage<'s, Animation>,
ReadStorage<'s, ActionStatus>,
WriteStorage<'s, SpriteRender>,
Read<'s, Time>,
);
//...
The system is now reading from the component storage for ActionStatus
. We can update AnimationSystem
to read from the ActionStatus
component associated with the player object.
for (animation, action_status, sprite) in (&animations, &action_statuses, &mut sprite_renders).join() {
let elapsed_time = time.frame_number();
let frame = (elapsed_time / animation.frame_duration) as i32 % animation.frames;
// Read from action_status...
}
The use of join returns an iterator containing all the specific components for that entity, which is the player object in our case. action_status
is an immutable referencee to the player object’s associated ActionStatus
component, which we will use to match
on its action_type
property:
match action_status.action_type {
Action::Idle => {
sprite.sprite_number = animation.first_sprite_index + frame as usize;
},
Action::Run => {
// The first running animation is the fourth indice from the beginning of the
// animation s.
let starting_run_frame = animation.first_sprite_index + 4 as usize;
sprite.sprite_number = starting_run_frame + frame as usize;
},
_ => {},
}
The first matches on the Action::Idle
enum value, which we defined in task three. The code here does not change from our initial implementation for the “idle” animation in Creating a Simple Spritesheet Animation with Amethyst. It will calculate which frame sequence to use starting from the first image’s index defined in the texture.ron
file.
The second match on Action::Run
will get the first sprite image for the player’s running animation sequence. Since the idle animation as four frames in its animation sequence, the starting image for the running animation will be at the fourth index. This index, also the frame number, will be used as the start position for cycling through the sequence of sprite images for the running animation.
Now that AnimationSystem
knows how to draw a specific set of sprite images depending on the player’s action state using the ActionStatus
component, we can implement the MovePlayerSystem
to modify the action_type
field in response to user input.
5. Implement MovePlayerSystem
There are number of things that will go into impementing a system moving the player based on user input. In the end, we want the MovePlayerSystem
to modify the player object’s associated ActionStatus
component’s action_type
field.
The first thing to do is define data types the system will be operating on with SystemData
.
pub struct MovePlayerSystem;
impl <'s> System<'s> for MovePlayerSystem {
type SystemData = (
WriteStorage<'s, ActionStatus>,
WriteStorage<'s, Transform>,
Read<'s, InputHandler<StringBindings>>,
);
In the system’s run
method, iterate over all entites with an ActionStatus
and Transform
component added to it. This will give access to that entity’s associated components for the system to modify.
fn run(&mut self, (mut statuses, mut transforms, input): Self::SystemData) {
for (status, transform) in (&mut statuses, &mut transforms).join() {
// modify `status` and `transform` based on `input`...
}
}
For each entity, read from the InputHandler
resource to capture any input values made by the user. If there is a value for either InputHandler
axes then update the player’s associated Transform
and ActionStatus
components. Otherwise, just set the action_type
value to Action::Idle
.
let movement_x: f32 = input.axis_value("player_move_x").unwrap();
let movement_y: f32 = input.axis_value("player_move_y").unwrap();
if movement_x != 0.0 {
let scaled_amount = 0.65 * movement_x;
let x_pos = transform.translation().x;
// Direction the player is facing. If west, then rotate 180.
let rotation = if movement_x < 0.0 { 3.14 } else { 0.0 };
// Modify the object's transform and status components.
status.set_action_type(Action::Run);
transform.set_rotation_y_axis(rotation);
transform.set_translation_x(
(x_pos + scaled_amount)
.min(CAMERA_WIDTH - PLAYER_WIDTH * 0.5)
.max(PLAYER_WIDTH * 0.5),
);
} else if movement_y != 0.0 {
let scaled_amount = 0.65 * movement_y;
let y_pos = transform.translation().y;
status.set_action_type(Action::Run)
transform.set_translation_y(
(y_pos + scaled_amount)
.min(CAMERA_HEIGHT - PLAYER_HEIGHT * 0.5)
.max(PLAYER_HEIGHT * 0.5),
);
} else {
status.set_action_type(Action::Idle);
}
A few things to note is that these constants CAMERA_WIDTH
/CAMERA_HEIGHT
and PLAYER_WIDTH
/PLAYER_HEIGHT
make it so that the player’s sprite cannot move off the edge of the game’s window. This is a solution also demonstrated in the Pong Clone tutorial provided in the Amethyst documentation.
The full context for MovePlayerSystem
can be found at my GitHub repo for sprite animations here.
Conclusion
I’m happy I was able to get this posted despite the first part being posted back in March. Finding the time and motivation has been difficult this year and it’s a relief to have this finished. Being able to revisit this implementation has helped refresh some concepts I’ve forgotten since then. When I find time, there are a few things I wanted to write about:
- Attack animation. Here we’d add to the user input configuration to capture “actions” that will be interpreted as the player performing an “attack” action.
- CameraFollowSystem. I had this implemented in another project of mine.
Thanks for reading! For full context of my implementation of the running animation, please visit this GitHub commit on my sprite animations project.