Skip to content

Commit

Permalink
refactor: Extract animation state into a dedicated component
Browse files Browse the repository at this point in the history
That will make possible to move the animation definition in the assets.

And, it also makes easier (for benimator and the user) to react on
change of the animation component, since it will no longer be updated just because
the state has changed.

BREAKING CHANGE: The system label enum is renamed to
AnimationPostUpdateSystem, as the animation now runs during the
post-update phase
  • Loading branch information
jcornaz committed Jul 11, 2021
1 parent 00164ff commit 7d9c9ac
Show file tree
Hide file tree
Showing 2 changed files with 372 additions and 281 deletions.
289 changes: 8 additions & 281 deletions src/lib.rs
Expand Up @@ -92,24 +92,24 @@
#[macro_use]
extern crate rstest;

use std::ops::{DerefMut, RangeInclusive};
use std::ops::RangeInclusive;
use std::time::Duration;

use bevy_app::prelude::*;
use bevy_core::Time;
use bevy_ecs::prelude::*;
use bevy_reflect::Reflect;
use bevy_sprite::TextureAtlasSprite;

mod state;

/// Plugin to enable sprite-sheet animation
///
/// See crate level documentation for usage
#[derive(Default)]
pub struct AnimationPlugin;

/// Labels of systems that run during the update stage
/// Labels of systems that run during the post-update stage
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, SystemLabel)]
pub enum AnimationUpdateSystem {
pub enum AnimationPostUpdateSystem {
/// System that update the sprite atlas textures
Animate,
}
Expand All @@ -124,10 +124,6 @@ pub struct SpriteSheetAnimation {
pub frames: Vec<Frame>,
/// Animation mode
pub mode: AnimationMode,
#[reflect(ignore)]
current_frame: usize,
#[reflect(ignore)]
elapsed_in_frame: Duration,
}

/// Components that indicates the animation is playing
Expand Down Expand Up @@ -161,7 +157,8 @@ pub struct Frame {
impl Plugin for AnimationPlugin {
fn build(&self, app: &mut AppBuilder) {
app.register_type::<SpriteSheetAnimation>()
.add_system(animate.system().label(AnimationUpdateSystem::Animate));
.add_system_set(state::update_systems())
.add_system_to_stage(CoreStage::PostUpdate, state::post_update_system());
}
}

Expand All @@ -172,8 +169,6 @@ impl SpriteSheetAnimation {
Self {
frames,
mode: AnimationMode::default(),
current_frame: 0,
elapsed_in_frame: Duration::from_nanos(0),
}
}

Expand Down Expand Up @@ -203,42 +198,9 @@ impl SpriteSheetAnimation {
self
}

#[inline]
fn can_update(&self) -> bool {
fn has_frames(&self) -> bool {
!self.frames.is_empty()
}

/// Update the animation and the sprite (if necessary)
///
/// Returns true if the animation has ended
fn update(
&mut self,
mut sprite: impl DerefMut<Target = TextureAtlasSprite>,
delta: Duration,
) -> bool {
debug_assert!(self.can_update());

let mut frame = self.frames[self.current_frame % self.frames.len()];

self.elapsed_in_frame += delta;
if self.elapsed_in_frame >= frame.duration {
if self.current_frame < self.frames.len() - 1 {
self.current_frame += 1;
} else if matches!(self.mode, AnimationMode::Repeat) {
self.current_frame = 0;
} else {
return true;
}

self.elapsed_in_frame -= frame.duration;
frame = self.frames[self.current_frame];
sprite.index = frame.index;
} else if sprite.index != frame.index {
sprite.index = frame.index;
}

false
}
}

impl Default for AnimationMode {
Expand All @@ -256,238 +218,3 @@ impl Frame {
Self { index, duration }
}
}

fn animate(
mut commands: Commands<'_>,
time: Res<'_, Time>,
mut animations: Query<
'_,
(Entity, &mut TextureAtlasSprite, &mut SpriteSheetAnimation),
With<Play>,
>,
) {
for (entity, sprite, mut animation) in animations
.iter_mut()
.filter(|(_, _, anim)| anim.can_update())
{
if animation.update(sprite, time.delta()) {
commands.entity(entity).remove::<Play>();
}
}
}

#[cfg(test)]
mod tests {
use super::*;

#[fixture]
fn sprite() -> TextureAtlasSprite {
TextureAtlasSprite::new(0)
}

#[fixture]
fn sprite_at_second_frame() -> TextureAtlasSprite {
TextureAtlasSprite::new(1)
}

#[fixture]
fn frame_duration() -> Duration {
Duration::from_secs(1)
}

#[fixture]
fn smaller_duration(frame_duration: Duration) -> Duration {
frame_duration - Duration::from_millis(1)
}

mod on_first_frame {
use super::*;

#[fixture]
fn animation(frame_duration: Duration) -> SpriteSheetAnimation {
SpriteSheetAnimation::from_range(0..=1, frame_duration)
}

#[rstest]
fn nothing_happens_if_not_enough_time_has_elapsed_and_index_is_already_set(
mut animation: SpriteSheetAnimation,
mut sprite: TextureAtlasSprite,
smaller_duration: Duration,
) {
animation.update(&mut sprite, smaller_duration);
assert_eq!(sprite.index, 0);
}

#[rstest]
fn updates_index_if_not_on_expected_index(
mut animation: SpriteSheetAnimation,
mut sprite_at_second_frame: TextureAtlasSprite,
smaller_duration: Duration,
) {
animation.update(&mut sprite_at_second_frame, smaller_duration);
assert_eq!(sprite_at_second_frame.index, 0);
}

#[rstest]
fn updates_index_if_enough_time_has_elapsed(
mut animation: SpriteSheetAnimation,
mut sprite: TextureAtlasSprite,
frame_duration: Duration,
) {
animation.update(&mut sprite, frame_duration);
assert_eq!(sprite.index, 1);
}

#[rstest]
fn updates_index_if_enough_time_has_elapsed_after_multiple_updates(
mut animation: SpriteSheetAnimation,
mut sprite: TextureAtlasSprite,
smaller_duration: Duration,
) {
animation.update(&mut sprite, smaller_duration);
animation.update(&mut sprite, smaller_duration);
assert_eq!(sprite.index, 1);
}

#[rstest]
fn elapsed_duration_is_reset(
mut animation: SpriteSheetAnimation,
mut sprite: TextureAtlasSprite,
frame_duration: Duration,
smaller_duration: Duration,
) {
animation.update(&mut sprite, smaller_duration);
animation.update(&mut sprite, smaller_duration);
assert_eq!(
animation.elapsed_in_frame,
(smaller_duration + smaller_duration) - frame_duration
);
}

#[rstest]
fn returns_false(
mut animation: SpriteSheetAnimation,
mut sprite_at_second_frame: TextureAtlasSprite,
frame_duration: Duration,
) {
assert!(!animation.update(&mut sprite_at_second_frame, frame_duration))
}
}

mod repeat {
use super::*;

#[fixture]
fn mode() -> AnimationMode {
AnimationMode::Repeat
}

mod on_last_frame {
use super::*;

#[fixture]
fn animation(frame_duration: Duration, mode: AnimationMode) -> SpriteSheetAnimation {
SpriteSheetAnimation {
frames: vec![Frame::new(0, frame_duration), Frame::new(1, frame_duration)],
mode,
current_frame: 1,
elapsed_in_frame: Duration::from_nanos(0),
}
}

#[rstest]
fn returns_to_first_frame(
mut animation: SpriteSheetAnimation,
mut sprite_at_second_frame: TextureAtlasSprite,
frame_duration: Duration,
) {
animation.update(&mut sprite_at_second_frame, frame_duration);
assert_eq!(sprite_at_second_frame.index, 0);
}

#[rstest]
fn returns_false(
mut animation: SpriteSheetAnimation,
mut sprite_at_second_frame: TextureAtlasSprite,
frame_duration: Duration,
) {
assert!(!animation.update(&mut sprite_at_second_frame, frame_duration))
}
}

mod after_last_frame {
use super::*;

#[fixture]
fn animation(frame_duration: Duration, mode: AnimationMode) -> SpriteSheetAnimation {
SpriteSheetAnimation {
frames: vec![Frame::new(0, frame_duration), Frame::new(1, frame_duration)],
mode,
current_frame: 2,
elapsed_in_frame: Duration::from_nanos(0),
}
}

#[rstest]
fn returns_to_first_frame(
mut animation: SpriteSheetAnimation,
mut sprite_at_second_frame: TextureAtlasSprite,
frame_duration: Duration,
) {
animation.update(&mut sprite_at_second_frame, frame_duration);
assert_eq!(sprite_at_second_frame.index, 0);
}

#[rstest]
fn returns_false(
mut animation: SpriteSheetAnimation,
mut sprite_at_second_frame: TextureAtlasSprite,
frame_duration: Duration,
) {
assert!(!animation.update(&mut sprite_at_second_frame, frame_duration))
}
}
}

mod run_once {
use super::*;

#[fixture]
fn mode() -> AnimationMode {
AnimationMode::Once
}

mod on_last_frame {
use super::*;

#[fixture]
fn animation(frame_duration: Duration, mode: AnimationMode) -> SpriteSheetAnimation {
SpriteSheetAnimation {
frames: vec![Frame::new(0, frame_duration), Frame::new(1, frame_duration)],
mode,
current_frame: 1,
elapsed_in_frame: Duration::from_nanos(500),
}
}

#[rstest]
fn does_nothing(
mut animation: SpriteSheetAnimation,
mut sprite_at_second_frame: TextureAtlasSprite,
frame_duration: Duration,
) {
animation.update(&mut sprite_at_second_frame, frame_duration);
assert_eq!(sprite_at_second_frame.index, 1);
}

#[rstest]
fn returns_true(
mut animation: SpriteSheetAnimation,
mut sprite_at_second_frame: TextureAtlasSprite,
frame_duration: Duration,
) {
assert!(animation.update(&mut sprite_at_second_frame, frame_duration))
}
}
}
}

0 comments on commit 7d9c9ac

Please sign in to comment.