diff --git a/CHANGELOG.md b/CHANGELOG.md index d3e2498..63e0356 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## 0.4.0 - 2024-??-?? + +### Added + +- Animation progress can be accessed and mutated directly in the SpritesheetAnimation component + ## 0.3.0 - 2024-08-26 ### Added diff --git a/Cargo.toml b/Cargo.toml index 2ef330a..5ec3fc8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "bevy_spritesheet_animation" -version = "0.3.0" +version = "0.4.0" description = "A Bevy plugin for animating sprites" repository = "https://github.com/merwaaan/bevy_spritesheet_animation" readme="README.md" diff --git a/examples/composition.rs b/examples/composition.rs index be99d5e..44987bf 100644 --- a/examples/composition.rs +++ b/examples/composition.rs @@ -4,7 +4,9 @@ pub mod common; use bevy::prelude::*; -use bevy_spritesheet_animation::prelude::*; +use bevy_spritesheet_animation::{ + components::spritesheet_animation::AnimationProgress, prelude::*, +}; fn main() { App::new() diff --git a/examples/control.rs b/examples/control.rs new file mode 100644 index 0000000..dbbd1b5 --- /dev/null +++ b/examples/control.rs @@ -0,0 +1,91 @@ +// This example illustrates how to control the state of an animated sprite. + +#[path = "./common/mod.rs"] +pub mod common; + +use bevy::prelude::*; +use bevy_spritesheet_animation::{ + components::spritesheet_animation::AnimationProgress, prelude::*, +}; + +fn main() { + App::new() + .add_plugins(DefaultPlugins) + .add_plugins(SpritesheetAnimationPlugin) + .add_systems(Startup, setup) + .add_systems(Update, update) + .run(); +} + +fn setup( + mut commands: Commands, + mut library: ResMut, + mut atlas_layouts: ResMut>, + assets: Res, +) { + commands.spawn(Camera2dBundle::default()); + + let texture = assets.load("character.png"); + + let layout = atlas_layouts.add(TextureAtlasLayout::from_grid( + UVec2::new(96, 96), + 8, + 8, + None, + None, + )); + + let clip_id = library.new_clip(|clip| { + clip.push_frame_indices(Spritesheet::new(8, 8).positions([(0, 1), (0, 3), (0, 5), (0, 7)])) + .set_default_duration(AnimationDuration::PerFrame(3_000)); + }); + + let animation_id = library.new_animation(|animation| { + animation + .add_stage(clip_id.into()) + .set_repeat(AnimationRepeat::Cycles(1)); + }); + + commands.spawn(( + SpriteBundle { + texture, + ..default() + }, + TextureAtlas { + layout, + ..default() + }, + SpritesheetAnimation::from_id(animation_id), + )); +} + +fn update(keyboard: Res>, mut sprites: Query<&mut SpritesheetAnimation>) { + for mut sprite in &mut sprites { + // Play/Pause + + if keyboard.just_pressed(KeyCode::KeyP) { + sprite.playing = !sprite.playing; + } + + // Reset + + if keyboard.just_pressed(KeyCode::KeyR) { + sprite.reset(); + } + + // Switch to specific frames + + static KEYS: [KeyCode; 4] = [ + KeyCode::Numpad0, + KeyCode::Numpad1, + KeyCode::Numpad2, + KeyCode::Numpad3, + ]; + + for (frame, key) in KEYS.iter().enumerate() { + if keyboard.just_pressed(*key) { + sprite.progress.frame = frame; + } + } + } +} diff --git a/src/animator.rs b/src/animator.rs index cee1d3e..51b71f7 100644 --- a/src/animator.rs +++ b/src/animator.rs @@ -12,8 +12,10 @@ use bevy::{ use std::{collections::HashMap, sync::Arc}; use crate::{ - animation::AnimationId, components::spritesheet_animation::SpritesheetAnimation, - events::AnimationEvent, library::SpritesheetLibrary, + animation::AnimationId, + components::spritesheet_animation::{AnimationProgress, SpritesheetAnimation}, + events::AnimationEvent, + library::SpritesheetLibrary, systems::spritesheet_animation::ActualTime, }; @@ -30,6 +32,10 @@ struct AnimationInstance { current_stage_index: usize, accumulated_time: u32, + /// The previous animation progress. + /// To detect when users manually changed it. + last_progress: AnimationProgress, + /// Marks when the animation has ended to emit end events only once ended: bool, } @@ -73,23 +79,19 @@ impl SpritesheetAnimator { // Create a new animation instance if: // - the entity is new OR // - it switched animation OR - // - a reset has been requested + // - progress has been manually controlled let needs_new_animation_instance = match self.animation_instances.get(&entity) { - // The entity has an animation instance already but it switched animation + // The entity has an animation instance already but it switched animation/was manually controlled Some(instance) => { instance.animation_id != entity_animation.animation_id - || entity_animation.reset_requested + || instance.last_progress != entity_animation.progress } // The entity has no animation instance yet None => true, }; if needs_new_animation_instance { - // Clear any reset request - - entity_animation.reset_requested = false; - // Retrieve the cached animation data (create it if needed) let cache = self @@ -101,8 +103,11 @@ impl SpritesheetAnimator { // Create a new iterator for this animation - let mut iterator = - AnimationIterator::new(entity_animation.animation_id, cache.clone()); + let mut iterator = AnimationIterator::new( + entity_animation.animation_id, + entity_animation.progress, + cache.clone() + ); // Immediatly assign the first frame to kicktart the animation @@ -111,6 +116,10 @@ impl SpritesheetAnimator { if let Some(first_frame) = &maybe_first_frame { entity_atlas.index = first_frame.atlas_index; + // Update the animation progress + + entity_animation.progress = ; + // Emit events for the first frame let events = SpritesheetAnimator::promote_events(&first_frame.events, &entity); @@ -132,6 +141,7 @@ impl SpritesheetAnimator { current_frame_duration: first_frame_duration, current_stage_index: first_stage_index, accumulated_time: 0, + last_progress: AnimationProgress::default(), ended: false, }, ); @@ -141,7 +151,7 @@ impl SpritesheetAnimator { // Skip the update if the animation is paused // - // (skipped AFTER the setup above so that the first frame is assigned, even if paused) + // (skipped AFTER the previous steps so that the first/manual frame is assigned, even if paused) if !entity_animation.playing { continue; @@ -164,6 +174,10 @@ impl SpritesheetAnimator { entity_atlas.index = next_frame.atlas_index; + // Update the animation progress + + entity_animation.progress = ; + // Store this frame's data animation_instance.current_frame_duration = next_frame.duration; diff --git a/src/animator/cache.rs b/src/animator/cache.rs index 59c1358..e901688 100644 --- a/src/animator/cache.rs +++ b/src/animator/cache.rs @@ -1,19 +1,23 @@ use std::collections::HashMap; +use bevy::prelude::IntoSystem; use itertools::Itertools; use crate::{ animation::{AnimationDirection, AnimationDuration, AnimationId, AnimationRepeat}, + clip::{AnimationClip, AnimationClipId}, + components::spritesheet_animation::AnimationProgress, easing::Easing, events::AnimationMarkerId, library::SpritesheetLibrary, + stage::AnimationStage, }; /// A partial version of AnimationEvent. /// /// The SpritesheetAnimator will promote them to regular AnimationEvents, -/// adding the information available at its level (entity). -#[derive(Debug, Clone, PartialEq, Eq, Hash)] +/// adding the information available at its level (the entity). +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub(super) enum AnimationFrameEvent { MarkerHit { marker_id: AnimationMarkerId, @@ -39,12 +43,12 @@ pub(super) struct AnimationFrame { pub atlas_index: usize, pub duration: u32, pub events: Vec, - pub stage_index: usize, + pub progress: AnimationProgress, } /// The [AnimationCache] contains pre-computed frames for an animation. /// -/// The idea is to cache for each frame its atlas index, stage index, duration and emitted events +/// The idea is to cache for each frame its atlas index, progress, duration and emitted events /// so that playing an animation becomes just a matter of iterating over this cache without the need /// to re-evaluate its parameters. pub(super) struct AnimationCache { @@ -56,7 +60,7 @@ pub(super) struct AnimationCache { // The total number of cycles to play. // None if infinite. - pub cycle_count: Option, + pub repetitions: Option, // The direction of the animation to handle a special case for PingPong pub animation_direction: AnimationDirection, @@ -72,121 +76,45 @@ impl AnimationCache { // In practice, this cannot fail as the library is the sole creator of animation/animation IDs .unwrap(); + let animation_repeat = animation.repeat().unwrap_or_default(); let animation_direction = animation.direction().unwrap_or_default(); // If the animation repeats 0 times, just create an empty cache that will play no frames - let animation_repeat = animation.repeat().unwrap_or_default(); - if matches!(animation_repeat, AnimationRepeat::Cycles(0)) { return Self { frames: Vec::new(), frames_pong: None, - cycle_count: None, + repetitions: None, animation_direction, }; } // Gather the data for all the stages - let stages_data = animation + let stages = animation .stages() .iter() - .enumerate() - .map(|(stage_index, stage)| { - let stage_clip = library - .clips() - .get(stage.clip_id()) - // In practice, this cannot fail as the library is the sole creator of clip/clip IDs - .unwrap(); - - let stage_duration = stage - // Use the stage's duration first - .duration() - // Fallback to the clip's default duration - .or(*stage_clip.default_duration()) - // Fallback to the global default value - .unwrap_or(AnimationDuration::default()); - - let stage_repeat = stage - // Use the stage's repetitions first - .repeat() - // Fallback to the clip's default repetitions - .or(*stage_clip.default_repeat()) - // Fallback to a default value - .unwrap_or(1); - - let stage_direction = stage - // Use the stage's direction first - .direction() - // Fallback to the clip's default direction - .or(*stage_clip.default_direction()) - // Fallback to the global default value - .unwrap_or(AnimationDirection::default()); - - let stage_easing = stage - // Use the stage's easing first - .easing() - // Fallback to the clip's default easing - .or(*stage_clip.default_easing()) - // Fallback to the global default value - .unwrap_or(Easing::default()); - - // Compute the stage's duration in milliseconds, taking repetitions into account - - let frame_count_with_repetitions = match stage_direction { - AnimationDirection::Forwards | AnimationDirection::Backwards => { - stage_clip.frame_count() as u32 * stage_repeat - } - AnimationDirection::PingPong => { - stage_clip.frame_count().saturating_sub(1) as u32 * stage_repeat + 1 - } - }; - - let stage_duration_with_repetitions_ms = match stage_duration { - AnimationDuration::PerFrame(frame_duration) => { - frame_duration * frame_count_with_repetitions as u32 - } - AnimationDuration::PerCycle(cycle_duration) => cycle_duration, - }; - - ( - stage_index, - stage_clip, - stage_duration, - stage_repeat, - stage_direction, - stage_easing, - stage_duration_with_repetitions_ms, - ) - }); - - // Filter out stages with 0 frames / durations of 0 - // - // Doing so at this point will simplify what follows as well as the playback code as we won't have to handle those special cases - - let stages_data = stages_data.filter( - |(_, stage_clip, _, _, _, _, stage_duration_with_repetitions_ms)| { - stage_clip.frame_count() > 0 && *stage_duration_with_repetitions_ms > 0 - }, - ); + .map(|stage| Stage::new(stage, library)) + // Filter out stages with 0 frames / durations of 0 + // + // Doing so at this point will simplify what follows as well as the playback code as we won't have to handle those special cases + .filter(|stage| stage.clip.frame_count() > 0 && stage.duration_with_repetitions > 0); // Compute the total duration of one cycle of the animation in milliseconds - let animation_cycle_duration_ms = stages_data + let animation_cycle_duration = stages .clone() - .map(|(_, _, _, _, _, _, stage_duration_with_repetitions_ms)| { - stage_duration_with_repetitions_ms - }) + .map(|stage| stage.duration_with_repetitions) .sum::(); // If the animation lasts 0 ms, just create an empty cache that will play no frames - if animation_cycle_duration_ms == 0 { + if animation_cycle_duration == 0 { return Self { frames: Vec::new(), frames_pong: None, - cycle_count: None, + repetitions: None, animation_direction, }; } @@ -200,25 +128,15 @@ impl AnimationCache { // This nested structure is not ideal to work with but it's convenient as it preserves the clip boundaries // that we need to inject events at the appropriate frames - let mut all_cycles: Vec>> = Vec::new(); + let mut animation_frames = AnimationFrames::empty(); + let mut animation_frames_pong = None; - let mut all_cycles_pong = None; - - for ( - stage_index, - stage_clip, - stage_duration, - stage_repeat, - stage_direction, - _stage_easing, - stage_duration_with_repetitions_ms, - ) in stages_data.clone() - { + for (stage_index, stage) in stages.enumerate() { // Adjust the actual duration of the current stage if the animation specifies its own duration let stage_corrected_duration = match animation.duration() { // No duration is defined for the animation: keep the stage's duration - None => stage_duration, + None => stage.duration, // The per-frame duration is defined for the animation: override the stage's duration with it Some(AnimationDuration::PerFrame(animation_frame_duration)) => { @@ -228,11 +146,11 @@ impl AnimationCache { // The per-cycle duration of the animation is defined: // assign a duration to the stage that stays proportional to its base duration with respect to the total animation duration Some(AnimationDuration::PerCycle(animation_cycle_duration)) => { - let stage_ratio = stage_duration_with_repetitions_ms as f32 - / animation_cycle_duration_ms as f32; + let stage_ratio = + stage.duration_with_repetitions as f32 / *animation_cycle_duration as f32; AnimationDuration::PerCycle( - (*animation_cycle_duration as f32 * stage_ratio / stage_repeat as f32) + (*animation_cycle_duration as f32 * stage_ratio / stage.repetitions as f32) as u32, ) } @@ -240,69 +158,62 @@ impl AnimationCache { // Compute the duration of a single frame - let stage_frame_duration_ms = match stage_corrected_duration { - AnimationDuration::PerFrame(frame_duration_ms) => frame_duration_ms, - AnimationDuration::PerCycle(cycle_duration_ms) => { - cycle_duration_ms / stage_clip.frame_count() as u32 + let stage_frame_duration = match stage_corrected_duration { + AnimationDuration::PerFrame(frame_duration) => frame_duration, + AnimationDuration::PerCycle(cycle_duration) => { + cycle_duration / stage.clip.frame_count() as u32 } }; // Generate all the frames for a single cycle of the current stage - let one_cycle = stage_clip.frame_indices().iter().enumerate().map( - move |(frame_index, atlas_index)| { - // Convert this frame's markers into events to emit when reaching it - - let events = stage_clip - .markers() - .get(&frame_index) - .map(|frame_markers| { - frame_markers - .iter() - .map(|marker| AnimationFrameEvent::MarkerHit { - marker_id: *marker, - stage_index, - animation_id, - }) - .collect() - }) - .unwrap_or(Vec::new()); - - AnimationFrame { - atlas_index: *atlas_index, - duration: stage_frame_duration_ms, - events, - stage_index, - } - }, - ); - - // Repeat/reverse the cycle for all the cycles of the current stage + let stage_one_cycle_frames = + StageFrames::from(stage.clip.frame_indices().iter().enumerate().map( + move |(frame_index, frame_atlas_index)| { + // Convert this frame's markers into events to emit when reaching it + + let frame_events = stage + .clip + .markers() + .get(&frame_index) + .map(|frame_markers| { + frame_markers + .iter() + .map(|marker| AnimationFrameEvent::MarkerHit { + marker_id: *marker, + stage_index, + animation_id, + }) + .collect() + }) + .unwrap_or(Vec::new()); + + StageFrame { + atlas_index: *frame_atlas_index, + duration: stage_frame_duration, + events: frame_events, + } + }, + )); - let mut stage_cycles = Vec::new(); + // Repeat/reverse the cycle for all the repetitions of the current stage - for cycle_index in 0..stage_repeat { - stage_cycles.push(match stage_direction { - AnimationDirection::Forwards => one_cycle.clone().collect_vec(), - AnimationDirection::Backwards => one_cycle.clone().rev().collect_vec(), + for stage_cycle_index in 0..stage.repetitions { + animation_frames.push(match stage.direction { + AnimationDirection::Forwards => stage_one_cycle_frames.clone(), + AnimationDirection::Backwards => stage_one_cycle_frames.backwards(), AnimationDirection::PingPong => { - // First cycle: use all the frames - if cycle_index == 0 { - one_cycle.clone().collect_vec() - } - // Following odd cycles, use all the frames but the first one, and reversed - else if cycle_index % 2 == 1 { - one_cycle.clone().rev().skip(1).collect_vec() - } - // Even cycles: use all the frames but the first one - else { - one_cycle.clone().skip(1).collect_vec() + if stage_cycle_index == 0 { + // First cycle: use all the frames (pong() would remove the first one) + stage_one_cycle_frames.clone() + } else if stage_cycle_index % 2 == 1 { + stage_one_cycle_frames.pong() + } else { + stage_one_cycle_frames.ping() } } }); } - - all_cycles.push(stage_cycles); } // Filter out empty stages/cycles/frames @@ -324,7 +235,7 @@ impl AnimationCache { // Order/reverse the cycles to match the animation direction if needed - let reverse = |all_cycles: &mut Vec>>| { + let reverse = |all_cycles: &mut Vec>>| { for stage in &mut *all_cycles { for cycle in &mut *stage { cycle.reverse(); @@ -338,10 +249,11 @@ impl AnimationCache { match animation_direction { // Backwards: reverse all the frames - AnimationDirection::Backwards => reverse(&mut all_cycles), + AnimationDirection::Backwards => animation_frames = animation_frames.backwards(), - // PingPong: reverse all the frame in the alternate "pong" collection + // PingPong: reverse ALL the frame in the alternate "pong" collection AnimationDirection::PingPong => { + // TODO animation_frames_pong = Some(animation_frames.backwards()) all_cycles_pong = Some(all_cycles.clone()); reverse(all_cycles_pong.as_mut().unwrap()) } @@ -373,7 +285,7 @@ impl AnimationCache { }); } - previous_cycle_stage_index = Some(cycle[0].stage_index); + previous_cycle_stage_index = Some(cycle[0].progress.stage); } // Inject a ClipEnd event on the first frame of each stage after the first one @@ -388,13 +300,13 @@ impl AnimationCache { }); } - previous_stage_stage_index = Some(stage[0][0].stage_index); + previous_stage_stage_index = Some(stage[0][0].progress.stage); } // Build a (stage index, easing) record let stages_easing: HashMap = HashMap::from_iter( - stages_data + stages .clone() .map(|(stage_index, _, _, _, _, stage_easing, _)| (stage_index, stage_easing)), ); @@ -410,7 +322,7 @@ impl AnimationCache { // Apply easing on the stage - let stage_index = stage_frames[0].stage_index; + let stage_index = stage_frames[0].progress.stage; let easing = stages_easing[&stage_index]; apply_easing(&mut stage_frames, easing); @@ -435,7 +347,7 @@ impl AnimationCache { None }; - // Compute the total number of stages (taking repetitions into account) + // Compute the total number of cycles (taking repetitions into account) let animation_repeat = animation.repeat().unwrap_or_default(); @@ -449,12 +361,140 @@ impl AnimationCache { Self { frames: all_frames, frames_pong: all_frames_pong, - cycle_count, + repetitions: cycle_count, animation_direction, } } } +struct Stage { + clip: AnimationClip, + duration: AnimationDuration, + repetitions: u32, + direction: AnimationDirection, + easing: Easing, + duration_with_repetitions: u32, +} + +impl Stage { + fn new(stage: &AnimationStage, library: &SpritesheetLibrary) -> Self { + let clip = library + .clips() + .get(stage.clip_id()) + // In practice, this cannot fail as the library is the sole creator of clip/clip IDs + .unwrap(); + + let duration = stage + // Use the stage's duration first + .duration() + // Fallback to the clip's default duration + .or(*clip.default_duration()) + // Fallback to the global default value + .unwrap_or(AnimationDuration::default()); + + let repetitions = stage + // Use the stage's repetitions first + .repeat() + // Fallback to the clip's default repetitions + .or(*clip.default_repeat()) + // Fallback to a default value + .unwrap_or(1); + + let direction = stage + // Use the stage's direction first + .direction() + // Fallback to the clip's default direction + .or(*clip.default_direction()) + // Fallback to the global default value + .unwrap_or(AnimationDirection::default()); + + let easing = stage + // Use the stage's easing first + .easing() + // Fallback to the clip's default easing + .or(*clip.default_easing()) + // Fallback to the global default value + .unwrap_or(Easing::default()); + + // Compute the stage's duration in milliseconds, taking repetitions into account + + let frame_count_with_repetitions = match direction { + AnimationDirection::Forwards | AnimationDirection::Backwards => { + clip.frame_count() as u32 * repetitions + } + AnimationDirection::PingPong => { + clip.frame_count().saturating_sub(1) as u32 * repetitions + 1 + } + }; + + let duration_with_repetitions = match duration { + AnimationDuration::PerFrame(frame_duration) => { + frame_duration * frame_count_with_repetitions as u32 + } + AnimationDuration::PerCycle(cycle_duration) => cycle_duration, + }; + + Self { + clip: clip.clone(), + duration, + repetitions, + direction, + easing, + duration_with_repetitions, + } + } +} + +#[derive(Clone)] +struct StageFrame { + atlas_index: usize, + duration: u32, + events: Vec, +} + +#[derive(Clone)] +struct StageFrames { + frames: Vec, +} + +impl StageFrames { + fn from>(frames: I) -> Self { + Self { + frames: frames.collect(), + } + } + + fn backwards(&self) -> Self { + Self { + frames: self.frames.iter().cloned().rev().collect_vec(), + } + } + + fn ping(&self) -> Self { + Self { + frames: self.frames.iter().cloned().rev().skip(1).collect_vec(), + } + } + + fn pong(&self) -> Self { + Self { + frames: self.frames.iter().cloned().skip(1).collect_vec(), + } + } +} + +struct AnimationFrames { + stages: Vec, +} + +impl AnimationFrames { + fn empty() -> Self { + Self { stages: vec![] } + } + + fn push(&mut self, frames: StageFrames) {} +} + fn apply_easing(frames: &mut Vec, easing: Easing) { // Linear easing: exit early, there's nothing to do @@ -464,11 +504,11 @@ fn apply_easing(frames: &mut Vec, easing: Easing) { // Compute the total duration of the sequence - let total_duration_ms: u32 = frames.iter().map(|frame| frame.duration).sum(); + let total_duration: u32 = frames.iter().map(|frame| frame.duration).sum(); // If the total duration is zero, exit early to prevent arithmetic errors - if total_duration_ms == 0 { + if total_duration == 0 { return; } @@ -480,7 +520,7 @@ fn apply_easing(frames: &mut Vec, easing: Easing) { for frame in frames { // Convert the duration to a normalized time - let normalized_time = accumulated_time as f32 / total_duration_ms as f32; + let normalized_time = accumulated_time as f32 / total_duration as f32; // Apply the easing @@ -488,7 +528,7 @@ fn apply_easing(frames: &mut Vec, easing: Easing) { // Convert back to a duration - let eased_time = normalized_eased_time * total_duration_ms as f32; + let eased_time = normalized_eased_time * total_duration as f32; let eased_duration = (eased_time - previous_eased_time) as u32; diff --git a/src/animator/iterator.rs b/src/animator/iterator.rs index 1418e31..aa8f397 100644 --- a/src/animator/iterator.rs +++ b/src/animator/iterator.rs @@ -1,6 +1,9 @@ use std::sync::Arc; -use crate::animation::{AnimationDirection, AnimationId}; +use crate::{ + animation::{AnimationDirection, AnimationId}, + components::spritesheet_animation::AnimationProgress, +}; use super::cache::{AnimationCache, AnimationFrame, AnimationFrameEvent}; @@ -26,14 +29,24 @@ pub(super) struct AnimationIterator { } impl AnimationIterator { - pub fn new(animation_id: AnimationId, cache: Arc) -> Self { - Self { + pub fn new( + animation_id: AnimationId, + start_at: AnimationProgress, + cache: Arc, + ) -> Self { + // Fast-forward the iterator to the starting point + + let start_animation_cycle_index = start_at.repetition; + + let iterator = Self { animation_id, cache, current_frame_index: 0, - current_animation_cycle_index: 0, + current_animation_cycle_index: start_animation_cycle_index, animation_cycle_just_ended: None, - } + }; + + iterator } } @@ -94,7 +107,7 @@ impl Iterator for AnimationIterator { if self .cache - .cycle_count + .repetitions .map(|cycle_count| self.current_animation_cycle_index < cycle_count as usize) .unwrap_or(true) { diff --git a/src/components/spritesheet_animation.rs b/src/components/spritesheet_animation.rs index 55cf294..24107fb 100644 --- a/src/components/spritesheet_animation.rs +++ b/src/components/spritesheet_animation.rs @@ -2,6 +2,36 @@ use bevy::ecs::component::Component; use crate::animation::AnimationId; +/// The progress of an animation +#[derive(Component, Debug, Clone, Default, PartialEq)] +pub struct AnimationProgress { + /// The relative index of the current frame within the current stage repetition + /// + /// For instance, if a 3-frame animation stage repeats two times, the values will be: + /// 0 → 1 → 2 → 0 → 1 → 2 + pub frame: usize, + + /// The relative index of the current stage within the current animation repetition + /// + /// For instance, if a 2-stage animation stage repeats three times, the values will be: + /// 0 → 1 → 0 → 1 → 0 → 1 + pub stage: usize, + + /// The current stage repetition + /// + /// Will wrap around for each animation repetition. + /// + /// For instance, if a 2-stage animation stage repeats three times, the values will be: + /// stage = 0 → 1 → 0 → 1 → 0 → 1 + /// stage_repetition = 0 → 0 → 1 → 1 → 2 → 2 + pub stage_repetition: usize, + + /// The current animation repetition + /// + /// Will increase indefinitely if the animation is configured with [AnimationRepeat::Loop]. TODO link + pub repetition: usize, +} + /// A Bevy component that enables spritesheet animations. /// /// It contains an [AnimationId] that references an [Animation](crate::prelude::Animation) created with [SpritesheetLibrary::new_animation](crate::prelude::SpritesheetLibrary::new_animation). @@ -51,6 +81,9 @@ pub struct SpritesheetAnimation { /// The ID of the animation to play pub animation_id: AnimationId, + /// The current progress of the animation + pub progress: AnimationProgress, + /// Is the animation currently playing? /// /// The animation can alternatively be stopped by removing the [SpritesheetAnimation] component from its entity entirely. @@ -59,9 +92,6 @@ pub struct SpritesheetAnimation { /// A speed multiplier for the animation, defaults to 1 pub speed_factor: f32, - - /// Marks the animation to be reset by the animator on the next update - pub(crate) reset_requested: bool, } impl SpritesheetAnimation { @@ -73,14 +103,20 @@ impl SpritesheetAnimation { pub fn from_id(animation_id: AnimationId) -> Self { Self { animation_id, + progress: AnimationProgress::default(), playing: true, speed_factor: 1.0, - reset_requested: false, } } - /// Resets the animation to its initial state. + /// TODO + pub fn switch(&mut self, animation_id: AnimationId) { + self.animation_id = animation_id; + self.progress = AnimationProgress::default(); + } + + /// TODO pub fn reset(&mut self) { - self.reset_requested = true; + self.progress = AnimationProgress::default(); } } diff --git a/src/events.rs b/src/events.rs index f765e36..c1c8ac8 100644 --- a/src/events.rs +++ b/src/events.rs @@ -97,19 +97,19 @@ pub enum AnimationEvent { entity: Entity, marker_id: AnimationMarkerId, animation_id: AnimationId, - stage_index: usize, + //stage_index: usize, }, /// A cycle of a clip has ended ClipCycleEnd { entity: Entity, animation_id: AnimationId, - stage_index: usize, + //stage_index: usize, }, /// An clip ended ClipEnd { entity: Entity, animation_id: AnimationId, - stage_index: usize, + //stage_index: usize, }, /// A cycle of an animation has ended AnimationCycleEnd {