Skip to content

Commit

Permalink
feat!: add ping pong animation mode (#25)
Browse files Browse the repository at this point in the history
BREAKING CHANGE: add a new variant (`PingPong`) to the `AnimationMode`

BREAKING CHANGE: the `mode` field of `SpriteSheetAnimation` is no longer public
  • Loading branch information
aaneto authored Jan 19, 2022
1 parent b782ce9 commit 76a6306
Show file tree
Hide file tree
Showing 4 changed files with 139 additions and 11 deletions.
43 changes: 43 additions & 0 deletions examples/ping_pong.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
use std::time::Duration;

use bevy::prelude::*;

use benimator::*;

fn main() {
App::new()
.add_plugins(DefaultPlugins)
.add_plugin(AnimationPlugin)
.add_startup_system(spawn.system())
.run();
}

fn spawn(
mut commands: Commands,
asset_server: Res<AssetServer>,
mut textures: ResMut<Assets<TextureAtlas>>,
mut animations: ResMut<Assets<SpriteSheetAnimation>>,
) {
// Don't forget the camera ;-)
commands.spawn_bundle(OrthographicCameraBundle::new_2d());

// Create an animation
// Here we use an index-range (from 0 to 4) where each frame has the same duration
let animation_handle = animations
.add(SpriteSheetAnimation::from_range(0..=4, Duration::from_millis(100)).ping_pong());

commands
// Spawn a bevy sprite-sheet
.spawn_bundle(SpriteSheetBundle {
texture_atlas: textures.add(TextureAtlas::from_grid(
asset_server.load("coin.png"),
Vec2::new(16.0, 16.0),
5,
1,
)),
transform: Transform::from_scale(Vec3::splat(10.0)),
..Default::default()
})
.insert(animation_handle)
.insert(Play);
}
18 changes: 14 additions & 4 deletions src/animation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,17 +12,21 @@ pub struct SpriteSheetAnimation {
/// Frames
pub frames: Vec<Frame>,
/// Animation mode
pub mode: AnimationMode,
pub(crate) mode: AnimationMode,
}

/// Animation mode (run once or repeat)
/// Animation mode (run once, repeat or ping-pong)
#[derive(Debug, Copy, Clone, Eq, PartialEq)]
pub enum AnimationMode {
/// Runs the animation once and then stop playing
Once,

/// Repeat the animation forever
Repeat,

/// Repeat the animation forever, going back and forth between
/// the first and last frame.
PingPong,
}

/// A single animation frame
Expand Down Expand Up @@ -57,14 +61,13 @@ impl SpriteSheetAnimation {
/// # Example
///
/// ```
/// # use benimator::{AnimationMode, SpriteSheetAnimation};
/// # use benimator::SpriteSheetAnimation;
/// # use std::time::Duration;
/// // Easily create a reversed animation
/// let animation = SpriteSheetAnimation::from_iter((0..5).rev(), Duration::from_millis(100));
///
/// assert_eq!(animation.frames.iter().map(|frame| frame.index).collect::<Vec<_>>(), vec![4, 3, 2, 1, 0]);
/// assert!(animation.frames.iter().all(|frame| frame.duration.as_millis() == 100));
/// assert_eq!(animation.mode, AnimationMode::Repeat);
/// ```
///
/// For more granular configuration, see [`from_frames`](SpriteSheetAnimation::from_frames)
Expand All @@ -89,6 +92,13 @@ impl SpriteSheetAnimation {
self
}

/// Set the animation mode to [`AnimationMode::PingPong`]
#[must_use]
pub fn ping_pong(mut self) -> Self {
self.mode = AnimationMode::PingPong;
self
}

pub(crate) fn has_frames(&self) -> bool {
!self.frames.is_empty()
}
Expand Down
44 changes: 37 additions & 7 deletions src/state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ pub(crate) fn post_update_system() -> impl System<In = (), Out = ()> {
pub struct SpriteSheetAnimationState {
current_frame: usize,
elapsed_in_frame: Duration,
// Control ping_pong backward frame navigation.
going_backward: bool,
}

impl SpriteSheetAnimationState {
Expand All @@ -63,13 +65,37 @@ impl SpriteSheetAnimationState {

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

self.elapsed_in_frame -= frame.duration;
Expand Down Expand Up @@ -177,6 +203,7 @@ mod tests {
SpriteSheetAnimationState {
current_frame: 1,
elapsed_in_frame: Duration::from_secs(1),
going_backward: false,
}
}

Expand Down Expand Up @@ -304,6 +331,7 @@ mod tests {
SpriteSheetAnimationState {
current_frame: 1,
elapsed_in_frame: Duration::from_nanos(0),
going_backward: false,
}
}

Expand Down Expand Up @@ -345,6 +373,7 @@ mod tests {
SpriteSheetAnimationState {
current_frame: 2,
elapsed_in_frame: Duration::from_nanos(0),
going_backward: false,
}
}

Expand Down Expand Up @@ -397,6 +426,7 @@ mod tests {
SpriteSheetAnimationState {
current_frame: 1,
elapsed_in_frame: Duration::from_nanos(500),
going_backward: false,
}
}

Expand Down
45 changes: 45 additions & 0 deletions tests/spec.rs
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,51 @@ fn run_once(mut app: App) {
);
}

#[rstest]
fn run_ping_pong(mut app: App) {
let animation = app
.world
.get_resource_mut::<Assets<SpriteSheetAnimation>>()
.unwrap()
.add(SpriteSheetAnimation::from_range(0..=2, Duration::from_nanos(0)).ping_pong());

let entity = app
.world
.spawn()
.insert_bundle((TextureAtlasSprite::new(0), animation, Play))
.id();

app.update();
assert_eq!(
app.world.get::<TextureAtlasSprite>(entity).unwrap().index,
1
);

app.update();
assert_eq!(
app.world.get::<TextureAtlasSprite>(entity).unwrap().index,
2
);

app.update();
assert_eq!(
app.world.get::<TextureAtlasSprite>(entity).unwrap().index,
1
);

app.update();
assert_eq!(
app.world.get::<TextureAtlasSprite>(entity).unwrap().index,
0
);

app.update();
assert_eq!(
app.world.get::<TextureAtlasSprite>(entity).unwrap().index,
1
);
}

#[fixture]
fn app() -> App {
let mut app = App::new();
Expand Down

0 comments on commit 76a6306

Please sign in to comment.