diff --git a/crates/bevy_animation/src/lib.rs b/crates/bevy_animation/src/lib.rs index b84e871f04..59eab3f92b 100644 --- a/crates/bevy_animation/src/lib.rs +++ b/crates/bevy_animation/src/lib.rs @@ -102,12 +102,6 @@ impl AnimationClip { self.duration } - /// The bone ids mapped by their [`EntityPath`]. - #[inline] - pub fn paths(&self) -> &HashMap { - &self.paths - } - /// Add a [`VariableCurve`] to an [`EntityPath`]. pub fn add_curve_to_path(&mut self, path: EntityPath, curve: VariableCurve) { // Update the duration of the animation by this curve duration if it's longer @@ -129,27 +123,96 @@ impl AnimationClip { } } +/// Repetition behavior of an animation. +#[derive(Reflect, Copy, Clone, Default)] +pub enum RepeatAnimation { + /// The animation will finish after running once. + #[default] + Never, + /// The animation will finish after running "n" times. + Count(u32), + /// The animation will never finish. + Forever, +} + #[derive(Reflect)] struct PlayingAnimation { - repeat: bool, + repeat: RepeatAnimation, speed: f32, + /// Total time the animation has been played. + /// + /// Note: Time does not increase when the animation is paused or after it has completed. elapsed: f32, + /// The timestamp inside of the animation clip. + /// + /// Note: This will always be in the range [0.0, animation clip duration] + seek_time: f32, animation_clip: Handle, path_cache: Vec>>, + /// Number of times the animation has completed. + /// If the animation is playing in reverse, this increments when the animation passes the start. + completions: u32, } impl Default for PlayingAnimation { fn default() -> Self { Self { - repeat: false, + repeat: RepeatAnimation::default(), speed: 1.0, elapsed: 0.0, + seek_time: 0.0, animation_clip: Default::default(), path_cache: Vec::new(), + completions: 0, } } } +impl PlayingAnimation { + /// Check if the animation has finished, based on its repetition behavior and the number of times it has repeated. + /// + /// Note: An animation with `RepeatAnimation::Forever` will never finish. + #[inline] + pub fn is_finished(&self) -> bool { + match self.repeat { + RepeatAnimation::Forever => false, + RepeatAnimation::Never => self.completions >= 1, + RepeatAnimation::Count(n) => self.completions >= n, + } + } + + /// Update the animation given the delta time and the duration of the clip being played. + #[inline] + fn update(&mut self, delta: f32, clip_duration: f32) { + if self.is_finished() { + return; + } + + self.elapsed += delta; + self.seek_time += delta * self.speed; + + if (self.seek_time > clip_duration && self.speed > 0.0) + || (self.seek_time < 0.0 && self.speed < 0.0) + { + self.completions += 1; + } + + if self.seek_time >= clip_duration { + self.seek_time %= clip_duration; + } + if self.seek_time < 0.0 { + self.seek_time += clip_duration; + } + } + + /// Reset back to the initial state as if no time has elapsed. + fn replay(&mut self) { + self.completions = 0; + self.elapsed = 0.0; + self.seek_time = 0.0; + } +} + /// An animation that is being faded out as part of a transition struct AnimationTransition { /// The current weight. Starts at 1.0 and goes to 0.0 during the fade-out. @@ -222,7 +285,7 @@ impl AnimationPlayer { /// If `transition_duration` is set, this will use a linear blending /// between the previous and the new animation to make a smooth transition pub fn play(&mut self, handle: Handle) -> &mut Self { - if self.animation.animation_clip != handle || self.is_paused() { + if !self.is_playing_clip(&handle) || self.is_paused() { self.start(handle); } self @@ -235,24 +298,56 @@ impl AnimationPlayer { handle: Handle, transition_duration: Duration, ) -> &mut Self { - if self.animation.animation_clip != handle || self.is_paused() { + if !self.is_playing_clip(&handle) || self.is_paused() { self.start_with_transition(handle, transition_duration); } self } - /// Set the animation to repeat + /// Handle to the animation clip being played. + pub fn animation_clip(&self) -> &Handle { + &self.animation.animation_clip + } + + /// Check if the given animation clip is being played. + pub fn is_playing_clip(&self, handle: &Handle) -> bool { + self.animation_clip() == handle + } + + /// Check if the playing animation has finished, according to the repetition behavior. + pub fn is_finished(&self) -> bool { + self.animation.is_finished() + } + + /// Sets repeat to [`RepeatAnimation::Forever`]. + /// + /// See also [`Self::set_repeat`]. pub fn repeat(&mut self) -> &mut Self { - self.animation.repeat = true; + self.animation.repeat = RepeatAnimation::Forever; self } - /// Stop the animation from repeating - pub fn stop_repeating(&mut self) -> &mut Self { - self.animation.repeat = false; + /// Set the repetition behaviour of the animation. + pub fn set_repeat(&mut self, repeat: RepeatAnimation) -> &mut Self { + self.animation.repeat = repeat; self } + /// Repetition behavior of the animation. + pub fn repeat_mode(&self) -> RepeatAnimation { + self.animation.repeat + } + + /// Number of times the animation has completed. + pub fn completions(&self) -> u32 { + self.animation.completions + } + + /// Check if the animation is playing in reverse. + pub fn is_playback_reversed(&self) -> bool { + self.animation.speed < 0.0 + } + /// Pause the animation pub fn pause(&mut self) { self.paused = true; @@ -284,11 +379,21 @@ impl AnimationPlayer { self.animation.elapsed } - /// Seek to a specific time in the animation - pub fn set_elapsed(&mut self, elapsed: f32) -> &mut Self { - self.animation.elapsed = elapsed; + /// Seek time inside of the animation. Always within the range [0.0, clip duration]. + pub fn seek_time(&self) -> f32 { + self.animation.seek_time + } + + /// Seek to a specific time in the animation. + pub fn seek_to(&mut self, seek_time: f32) -> &mut Self { + self.animation.seek_time = seek_time; self } + + /// Reset the animation to its initial state, as if no time has elapsed. + pub fn replay(&mut self) { + self.animation.replay(); + } } fn entity_from_path( @@ -489,16 +594,12 @@ fn apply_animation( children: &Query<&Children>, ) { if let Some(animation_clip) = animations.get(&animation.animation_clip) { - if !paused { - animation.elapsed += time.delta_seconds() * animation.speed; - } - let mut elapsed = animation.elapsed; - if animation.repeat { - elapsed %= animation_clip.duration; - } - if elapsed < 0.0 { - elapsed += animation_clip.duration; - } + // We don't return early because seek_to() may have been called on the animation player. + animation.update( + if paused { 0.0 } else { time.delta_seconds() }, + animation_clip.duration, + ); + if animation.path_cache.len() != animation_clip.paths.len() { animation.path_cache = vec![Vec::new(); animation_clip.paths.len()]; } @@ -556,7 +657,7 @@ fn apply_animation( // PERF: finding the current keyframe can be optimised let step_start = match curve .keyframe_timestamps - .binary_search_by(|probe| probe.partial_cmp(&elapsed).unwrap()) + .binary_search_by(|probe| probe.partial_cmp(&animation.seek_time).unwrap()) { Ok(n) if n >= curve.keyframe_timestamps.len() - 1 => continue, // this curve is finished Ok(i) => i, @@ -566,7 +667,7 @@ fn apply_animation( }; let ts_start = curve.keyframe_timestamps[step_start]; let ts_end = curve.keyframe_timestamps[step_start + 1]; - let lerp = (elapsed - ts_start) / (ts_end - ts_start); + let lerp = (animation.seek_time - ts_start) / (ts_end - ts_start); // Apply the keyframe match &curve.keyframes { @@ -620,7 +721,6 @@ impl Plugin for AnimationPlugin { app.add_asset::() .register_asset_reflect::() .register_type::() - .register_type::() .add_systems( PostUpdate, animation_player.before(TransformSystem::TransformPropagate), diff --git a/examples/animation/animated_fox.rs b/examples/animation/animated_fox.rs index 0052cbce75..808d04adf1 100644 --- a/examples/animation/animated_fox.rs +++ b/examples/animation/animated_fox.rs @@ -5,6 +5,7 @@ use std::time::Duration; use bevy::pbr::CascadeShadowConfigBuilder; use bevy::prelude::*; +use bevy_internal::animation::RepeatAnimation; fn main() { App::new() @@ -77,6 +78,8 @@ fn setup( println!(" - spacebar: play / pause"); println!(" - arrow up / down: speed up / slow down animation playback"); println!(" - arrow left / right: seek backward / forward"); + println!(" - digit 1 / 3 / 5: play the animation times"); + println!(" - L: loop the animation forever"); println!(" - return: change animation"); } @@ -116,13 +119,13 @@ fn keyboard_animation_control( } if keyboard_input.just_pressed(KeyCode::Left) { - let elapsed = player.elapsed(); - player.set_elapsed(elapsed - 0.1); + let elapsed = player.seek_time(); + player.seek_to(elapsed - 0.1); } if keyboard_input.just_pressed(KeyCode::Right) { - let elapsed = player.elapsed(); - player.set_elapsed(elapsed + 0.1); + let elapsed = player.seek_time(); + player.seek_to(elapsed + 0.1); } if keyboard_input.just_pressed(KeyCode::Return) { @@ -134,5 +137,24 @@ fn keyboard_animation_control( ) .repeat(); } + + if keyboard_input.just_pressed(KeyCode::Key1) { + player.set_repeat(RepeatAnimation::Count(1)); + player.replay(); + } + + if keyboard_input.just_pressed(KeyCode::Key3) { + player.set_repeat(RepeatAnimation::Count(3)); + player.replay(); + } + + if keyboard_input.just_pressed(KeyCode::Key5) { + player.set_repeat(RepeatAnimation::Count(5)); + player.replay(); + } + + if keyboard_input.just_pressed(KeyCode::L) { + player.set_repeat(RepeatAnimation::Forever); + } } } diff --git a/examples/stress_tests/many_foxes.rs b/examples/stress_tests/many_foxes.rs index 18dee869bc..194100658b 100644 --- a/examples/stress_tests/many_foxes.rs +++ b/examples/stress_tests/many_foxes.rs @@ -270,13 +270,13 @@ fn keyboard_animation_control( } if keyboard_input.just_pressed(KeyCode::Left) { - let elapsed = player.elapsed(); - player.set_elapsed(elapsed - 0.1); + let elapsed = player.seek_time(); + player.seek_to(elapsed - 0.1); } if keyboard_input.just_pressed(KeyCode::Right) { - let elapsed = player.elapsed(); - player.set_elapsed(elapsed + 0.1); + let elapsed = player.seek_time(); + player.seek_to(elapsed + 0.1); } if keyboard_input.just_pressed(KeyCode::Return) { diff --git a/examples/tools/scene_viewer/animation_plugin.rs b/examples/tools/scene_viewer/animation_plugin.rs index 4c4e032a38..e1f2970547 100644 --- a/examples/tools/scene_viewer/animation_plugin.rs +++ b/examples/tools/scene_viewer/animation_plugin.rs @@ -91,7 +91,7 @@ fn handle_inputs( let resume = !player.is_paused(); // set the current animation to its start and pause it to reset to its starting state - player.set_elapsed(0.0).pause(); + player.seek_to(0.0).pause(); clips.advance_to_next(); let current_clip = clips.current();