Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
3 changed files
with
364 additions
and
3 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,3 +1,304 @@ | ||
#![deny(future_incompatible)] | ||
#![warn(nonstandard_style, rust_2018_idioms)] | ||
#![warn(clippy::pedantic)] | ||
#![warn(nonstandard_style, rust_2018_idioms, missing_docs)] | ||
#![warn(clippy::pedantic)] | ||
#![allow(clippy::needless_pass_by_value)] | ||
|
||
//! A sprite-sheet animation plugin for [bevy](https://bevyengine.org) | ||
//! | ||
//! ## Usage | ||
//! | ||
//! 1. Add the [`AnimationPlugin`] plugin | ||
//! | ||
//! ```no_run | ||
//! use std::time::Duration; | ||
//! use bevy::prelude::*; | ||
//! use animism::*; | ||
//! | ||
//! fn main() { | ||
//! App::build() | ||
//! .add_plugins(DefaultPlugins) | ||
//! .add_plugin(AnimationPlugin) // <-- Enable sprite-sheet animations | ||
//! .add_startup_system(spawn.system()) | ||
//! // ... | ||
//! .run() | ||
//! } | ||
//! | ||
//! fn spawn() { /* ... */ } | ||
//! ``` | ||
//! | ||
//! 2. Insert the [`SpriteSheetAnimation`] component to the sprite sheets you want to animate | ||
//! | ||
//! ```rust | ||
//! # use std::time::Duration; | ||
//! # use bevy::prelude::*; | ||
//! # use animism::*; | ||
//! | ||
//! fn spawn(mut commands: Commands) { | ||
//! // For this example, we'll use a frame-rate of 12 sprites per second (aka animating on twos) | ||
//! let frame_duration = Duration::from_secs_f64(1.0 / 12.0); | ||
//! | ||
//! commands | ||
//! .spawn_bundle(SpriteSheetBundle { | ||
//! // TODO: Configure your sprite sheet | ||
//! ..Default::default() | ||
//! }) | ||
//! // Insert the animation component | ||
//! // Each frame take an index in the TextureAtlasSprite and a duration | ||
//! .insert(SpriteSheetAnimation::from_frames(vec![ | ||
//! Frame::new(0, frame_duration), | ||
//! Frame::new(1, frame_duration), | ||
//! Frame::new(2, frame_duration), | ||
//! ])); | ||
//! } | ||
//! ``` | ||
//! | ||
#[cfg(test)] | ||
#[macro_use] | ||
extern crate rstest; | ||
|
||
use std::ops::DerefMut; | ||
use std::time::Duration; | ||
|
||
use bevy_app::prelude::*; | ||
use bevy_core::Time; | ||
use bevy_ecs::prelude::*; | ||
use bevy_reflect::Reflect; | ||
use bevy_sprite::TextureAtlasSprite; | ||
|
||
/// 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 | ||
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, SystemLabel)] | ||
pub enum AnimationUpdateSystem { | ||
/// System that update the sprite atlas textures | ||
Animate, | ||
} | ||
|
||
/// Component to animate the `TextureAtlasSprite` of the same entity | ||
/// | ||
/// See crate level documentation for usage | ||
#[derive(Debug, Clone, Default, Reflect)] | ||
#[reflect(Component)] | ||
pub struct SpriteSheetAnimation { | ||
/// Frames | ||
pub frames: Vec<Frame>, | ||
#[reflect(ignore)] | ||
current_frame: usize, | ||
#[reflect(ignore)] | ||
elapsed_in_frame: Duration, | ||
} | ||
|
||
/// A single animation frame | ||
#[derive(Debug, Copy, Clone, Default, Reflect)] | ||
pub struct Frame { | ||
/// Index in the sprite atlas | ||
pub index: u32, | ||
/// How long should the frame be displayed | ||
pub duration: Duration, | ||
} | ||
|
||
impl Plugin for AnimationPlugin { | ||
fn build(&self, app: &mut AppBuilder) { | ||
app.register_type::<SpriteSheetAnimation>() | ||
.add_system(animate.system().label(AnimationUpdateSystem::Animate)); | ||
} | ||
} | ||
|
||
impl SpriteSheetAnimation { | ||
/// Create a new animation from frames | ||
#[must_use] | ||
pub fn from_frames(frames: Vec<Frame>) -> Self { | ||
Self { | ||
frames, | ||
current_frame: 0, | ||
elapsed_in_frame: Duration::from_nanos(0), | ||
} | ||
} | ||
|
||
#[inline] | ||
fn can_update(&self) -> bool { | ||
!self.frames.is_empty() | ||
} | ||
|
||
fn update(&mut self, mut sprite: impl DerefMut<Target = TextureAtlasSprite>, delta: Duration) { | ||
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 { | ||
self.elapsed_in_frame -= frame.duration; | ||
|
||
self.current_frame += 1; | ||
if self.current_frame >= self.frames.len() { | ||
self.current_frame = 0; | ||
} | ||
|
||
frame = self.frames[self.current_frame]; | ||
sprite.index = frame.index; | ||
} else if sprite.index != frame.index { | ||
sprite.index = frame.index; | ||
} | ||
} | ||
} | ||
|
||
impl Frame { | ||
/// Create a new animation frame | ||
#[inline] | ||
#[must_use] | ||
pub fn new(index: u32, duration: Duration) -> Self { | ||
Self { index, duration } | ||
} | ||
} | ||
|
||
fn animate( | ||
time: Res<'_, Time>, | ||
mut animations: Query<'_, (&mut TextureAtlasSprite, &mut SpriteSheetAnimation)>, | ||
) { | ||
for (sprite, mut animation) in animations.iter_mut().filter(|(_, anim)| anim.can_update()) { | ||
animation.update(sprite, time.delta()); | ||
} | ||
} | ||
|
||
#[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_frames(vec![ | ||
Frame::new(0, frame_duration), | ||
Frame::new(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 | ||
); | ||
} | ||
} | ||
|
||
mod on_last_frame { | ||
use super::*; | ||
|
||
#[fixture] | ||
fn animation(frame_duration: Duration) -> SpriteSheetAnimation { | ||
SpriteSheetAnimation { | ||
frames: vec![Frame::new(0, frame_duration), Frame::new(1, frame_duration)], | ||
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); | ||
} | ||
} | ||
|
||
mod after_last_frame { | ||
use super::*; | ||
|
||
#[fixture] | ||
fn animation(frame_duration: Duration) -> SpriteSheetAnimation { | ||
SpriteSheetAnimation { | ||
frames: vec![Frame::new(0, frame_duration), Frame::new(1, frame_duration)], | ||
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); | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,52 @@ | ||
#[macro_use] | ||
extern crate rstest; | ||
|
||
use std::time::Duration; | ||
|
||
use bevy::prelude::*; | ||
|
||
use animism::*; | ||
use bevy_core::CorePlugin; | ||
|
||
#[rstest] | ||
fn repeated_animation(mut app: App) { | ||
let entity = app | ||
.world | ||
.spawn() | ||
.insert_bundle(( | ||
TextureAtlasSprite::new(0), | ||
SpriteSheetAnimation::from_frames(vec![ | ||
Frame::new(0, Duration::from_nanos(0)), | ||
Frame::new(1, Duration::from_nanos(0)), | ||
Frame::new(2, Duration::from_nanos(0)), | ||
]), | ||
)) | ||
.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, | ||
0 | ||
); | ||
} | ||
|
||
#[fixture] | ||
fn app() -> App { | ||
let mut builder = App::build(); | ||
|
||
builder.add_plugin(CorePlugin).add_plugin(AnimationPlugin); | ||
|
||
builder.app | ||
} |