 dfdf2b9ea4
			
		
	
	
		dfdf2b9ea4
		
			
		
	
	
	
	
		
			
			This is an implementation of RFC #51: https://github.com/bevyengine/rfcs/blob/main/rfcs/51-animation-composition.md Note that the implementation strategy is different from the one outlined in that RFC, because two-phase animation has now landed. # Objective Bevy needs animation blending. The RFC for this is [RFC 51]. ## Solution This is an implementation of the RFC. Note that the implementation strategy is different from the one outlined there, because two-phase animation has now landed. This is just a draft to get the conversation started. Currently we're missing a few things: - [x] A fully-fleshed-out mechanism for transitions - [x] A serialization format for `AnimationGraph`s - [x] Examples are broken, other than `animated_fox` - [x] Documentation --- ## Changelog ### Added * The `AnimationPlayer` has been reworked to support blending multiple animations together through an `AnimationGraph`, and as such will no longer function unless a `Handle<AnimationGraph>` has been added to the entity containing the player. See [RFC 51] for more details. * Transition functionality has moved from the `AnimationPlayer` to a new component, `AnimationTransitions`, which works in tandem with the `AnimationGraph`. ## Migration Guide * `AnimationPlayer`s can no longer play animations by themselves and need to be paired with a `Handle<AnimationGraph>`. Code that was using `AnimationPlayer` to play animations will need to create an `AnimationGraph` asset first, add a node for the clip (or clips) you want to play, and then supply the index of that node to the `AnimationPlayer`'s `play` method. * The `AnimationPlayer::play_with_transition()` method has been removed and replaced with the `AnimationTransitions` component. If you were previously using `AnimationPlayer::play_with_transition()`, add all animations that you were playing to the `AnimationGraph`, and create an `AnimationTransitions` component to manage the blending between them. [RFC 51]: https://github.com/bevyengine/rfcs/blob/main/rfcs/51-animation-composition.md --------- Co-authored-by: Rob Parrett <robparrett@gmail.com>
		
			
				
	
	
		
			133 lines
		
	
	
		
			5.1 KiB
		
	
	
	
		
			Rust
		
	
	
	
	
	
			
		
		
	
	
			133 lines
		
	
	
		
			5.1 KiB
		
	
	
	
		
			Rust
		
	
	
	
	
	
| //! Animation transitions.
 | |
| //!
 | |
| //! Please note that this is an unstable temporary API. It may be replaced by a
 | |
| //! state machine in the future.
 | |
| 
 | |
| use bevy_ecs::{
 | |
|     component::Component,
 | |
|     system::{Query, Res},
 | |
| };
 | |
| use bevy_reflect::Reflect;
 | |
| use bevy_time::Time;
 | |
| use bevy_utils::Duration;
 | |
| 
 | |
| use crate::{graph::AnimationNodeIndex, ActiveAnimation, AnimationPlayer};
 | |
| 
 | |
| /// Manages fade-out of animation blend factors, allowing for smooth transitions
 | |
| /// between animations.
 | |
| ///
 | |
| /// To use this component, place it on the same entity as the
 | |
| /// [`AnimationPlayer`] and [`bevy_asset::Handle<AnimationGraph>`]. It'll take
 | |
| /// responsibility for adjusting the weight on the [`ActiveAnimation`] in order
 | |
| /// to fade out animations smoothly.
 | |
| ///
 | |
| /// When using an [`AnimationTransitions`] component, you should play all
 | |
| /// animations through the [`AnimationTransitions::play`] method, rather than by
 | |
| /// directly manipulating the [`AnimationPlayer`]. Playing animations through
 | |
| /// the [`AnimationPlayer`] directly will cause the [`AnimationTransitions`]
 | |
| /// component to get confused about which animation is the "main" animation, and
 | |
| /// transitions will usually be incorrect as a result.
 | |
| #[derive(Component, Default, Reflect)]
 | |
| pub struct AnimationTransitions {
 | |
|     main_animation: Option<AnimationNodeIndex>,
 | |
|     transitions: Vec<AnimationTransition>,
 | |
| }
 | |
| 
 | |
| /// An animation that is being faded out as part of a transition
 | |
| #[derive(Debug, Reflect)]
 | |
| pub struct AnimationTransition {
 | |
|     /// The current weight. Starts at 1.0 and goes to 0.0 during the fade-out.
 | |
|     current_weight: f32,
 | |
|     /// How much to decrease `current_weight` per second
 | |
|     weight_decline_per_sec: f32,
 | |
|     /// The animation that is being faded out
 | |
|     animation: AnimationNodeIndex,
 | |
| }
 | |
| 
 | |
| impl AnimationTransitions {
 | |
|     /// Creates a new [`AnimationTransitions`] component, ready to be added to
 | |
|     /// an entity with an [`AnimationPlayer`].
 | |
|     pub fn new() -> AnimationTransitions {
 | |
|         AnimationTransitions::default()
 | |
|     }
 | |
| 
 | |
|     /// Plays a new animation on the given [`AnimationPlayer`], fading out any
 | |
|     /// existing animations that were already playing over the
 | |
|     /// `transition_duration`.
 | |
|     ///
 | |
|     /// Pass [`Duration::ZERO`] to instantly switch to a new animation, avoiding
 | |
|     /// any transition.
 | |
|     pub fn play<'p>(
 | |
|         &mut self,
 | |
|         player: &'p mut AnimationPlayer,
 | |
|         new_animation: AnimationNodeIndex,
 | |
|         transition_duration: Duration,
 | |
|     ) -> &'p mut ActiveAnimation {
 | |
|         if let Some(old_animation_index) = self.main_animation.replace(new_animation) {
 | |
|             if let Some(old_animation) = player.animation_mut(old_animation_index) {
 | |
|                 if !old_animation.is_paused() {
 | |
|                     self.transitions.push(AnimationTransition {
 | |
|                         current_weight: old_animation.weight,
 | |
|                         weight_decline_per_sec: 1.0 / transition_duration.as_secs_f32(),
 | |
|                         animation: old_animation_index,
 | |
|                     });
 | |
|                 }
 | |
|             }
 | |
|         }
 | |
| 
 | |
|         self.main_animation = Some(new_animation);
 | |
|         player.start(new_animation)
 | |
|     }
 | |
| }
 | |
| 
 | |
| /// A system that alters the weight of currently-playing transitions based on
 | |
| /// the current time and decline amount.
 | |
| pub fn advance_transitions(
 | |
|     mut query: Query<(&mut AnimationTransitions, &mut AnimationPlayer)>,
 | |
|     time: Res<Time>,
 | |
| ) {
 | |
|     // We use a "greedy layer" system here. The top layer (most recent
 | |
|     // transition) gets as much as weight as it wants, and the remaining amount
 | |
|     // is divided between all the other layers, eventually culminating in the
 | |
|     // currently-playing animation receiving whatever's left. This results in a
 | |
|     // nicely normalized weight.
 | |
|     let mut remaining_weight = 1.0;
 | |
|     for (mut animation_transitions, mut player) in query.iter_mut() {
 | |
|         for transition in &mut animation_transitions.transitions.iter_mut().rev() {
 | |
|             // Decrease weight.
 | |
|             transition.current_weight = (transition.current_weight
 | |
|                 - transition.weight_decline_per_sec * time.delta_seconds())
 | |
|             .max(0.0);
 | |
| 
 | |
|             // Update weight.
 | |
|             let Some(ref mut animation) = player.animation_mut(transition.animation) else {
 | |
|                 continue;
 | |
|             };
 | |
|             animation.weight = transition.current_weight * remaining_weight;
 | |
|             remaining_weight -= animation.weight;
 | |
|         }
 | |
| 
 | |
|         if let Some(main_animation_index) = animation_transitions.main_animation {
 | |
|             if let Some(ref mut animation) = player.animation_mut(main_animation_index) {
 | |
|                 animation.weight = remaining_weight;
 | |
|             }
 | |
|         }
 | |
|     }
 | |
| }
 | |
| 
 | |
| /// A system that removed transitions that have completed from the
 | |
| /// [`AnimationTransitions`] object.
 | |
| pub fn expire_completed_transitions(
 | |
|     mut query: Query<(&mut AnimationTransitions, &mut AnimationPlayer)>,
 | |
| ) {
 | |
|     for (mut animation_transitions, mut player) in query.iter_mut() {
 | |
|         animation_transitions.transitions.retain(|transition| {
 | |
|             let expire = transition.current_weight <= 0.0;
 | |
|             if expire {
 | |
|                 player.stop(transition.animation);
 | |
|             }
 | |
|             !expire
 | |
|         });
 | |
|     }
 | |
| }
 |