API updates to the AnimationPlayer (#9002)

# Objective

Added `AnimationPlayer` API UX improvements. 

- Succestor to https://github.com/bevyengine/bevy/pull/5912
- Fixes https://github.com/bevyengine/bevy/issues/5848

_(Credits to @asafigan for filing #5848, creating the initial pull
request, and the discussion in #5912)_
## Solution

- Created `RepeatAnimation` enum to describe an animation repetition
behavior.
- Added `is_finished()`, `set_repeat()`, and `is_playback_reversed()`
methods to the animation player.
- ~~Made the animation clip optional as per the comment from #5912~~
> ~~My problem is that the default handle [used the initialize a
`PlayingAnimation`] could actually refer to an actual animation if an
AnimationClip is set for the default handle, which leads me to ask,
"Should animation_clip should be an Option?"~~
- Added an accessor for the animation clip `animation_clip()` to the
animation player.

To determine if an animation is finished, we use the number of times the
animation has completed and the repetition behavior. If the animation is
playing in reverse then `elapsed < 0.0` counts as a completion.
Otherwise, `elapsed > animation.duration` counts as a completion. This
is what I would expect, personally. If there's any ambiguity, perhaps we
could add some `AnimationCompletionBehavior`, to specify that kind of
completion behavior to use.

Update: Previously `PlayingAnimation::elapsed` was being used as the
seek time into the animation clip. This was misleading because if you
increased the speed of the animation it would also increase (or
decrease) the elapsed time. In other words, the elapsed time was not
actually the elapsed time. To solve this, we introduce
`PlayingAnimation::seek_time` to serve as the value we manipulate the
move between keyframes. Consequently, `elapsed()` now returns the actual
elapsed time, and is not effected by the animation speed. Because
`set_elapsed` was being used to manipulate the displayed keyframe, we
introduce `AnimationPlayer::seek_to` and `AnimationPlayer::replay` to
provide this functionality.

## Migration Guide

- Removed `set_elapsed`.
- Removed `stop_repeating` in favour of
`AnimationPlayer::set_repeat(RepeatAnimation::Never)`.
- Introduced `seek_to` to seek to a given timestamp inside of the
animation.
- Introduced `seek_time` accessor for the `PlayingAnimation::seek_to`.
- Introduced `AnimationPlayer::replay` to reset the `PlayingAnimation`
to a state where no time has elapsed.

---------

Co-authored-by: Hennadii Chernyshchyk <genaloner@gmail.com>
Co-authored-by: François <mockersf@gmail.com>
This commit is contained in:
DevinLeamy 2023-08-28 12:43:04 -04:00 committed by GitHub
parent 3cf94e7c9d
commit db5f80b2be
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 162 additions and 40 deletions

View file

@ -102,12 +102,6 @@ impl AnimationClip {
self.duration
}
/// The bone ids mapped by their [`EntityPath`].
#[inline]
pub fn paths(&self) -> &HashMap<EntityPath, usize> {
&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<AnimationClip>,
path_cache: Vec<Vec<Option<Entity>>>,
/// 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<AnimationClip>) -> &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<AnimationClip>,
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<AnimationClip> {
&self.animation.animation_clip
}
/// Check if the given animation clip is being played.
pub fn is_playing_clip(&self, handle: &Handle<AnimationClip>) -> 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::<AnimationClip>()
.register_asset_reflect::<AnimationClip>()
.register_type::<AnimationPlayer>()
.register_type::<PlayingAnimation>()
.add_systems(
PostUpdate,
animation_player.before(TransformSystem::TransformPropagate),

View file

@ -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 <digit> 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);
}
}
}

View file

@ -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) {

View file

@ -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();