bevy/examples/animation/animated_ui.rs
Patrick Walton 8154164f1b
Allow animation clips to animate arbitrary properties. (#15282)
Currently, Bevy restricts animation clips to animating
`Transform::translation`, `Transform::rotation`, `Transform::scale`, or
`MorphWeights`, which correspond to the properties that glTF can
animate. This is insufficient for many use cases such as animating UI,
as the UI layout systems expect to have exclusive control over UI
elements' `Transform`s and therefore the `Style` properties must be
animated instead.

This commit fixes this, allowing for `AnimationClip`s to animate
arbitrary properties. The `Keyframes` structure has been turned into a
low-level trait that can be implemented to achieve arbitrary animation
behavior. Along with `Keyframes`, this patch adds a higher-level trait,
`AnimatableProperty`, that simplifies the task of animating single
interpolable properties. Built-in `Keyframes` implementations exist for
translation, rotation, scale, and morph weights. For the most part, you
can migrate by simply changing your code from
`Keyframes::Translation(...)` to `TranslationKeyframes(...)`, and
likewise for rotation, scale, and morph weights.

An example `AnimatableProperty` implementation for the font size of a
text section follows:

     #[derive(Reflect)]
     struct FontSizeProperty;

     impl AnimatableProperty for FontSizeProperty {
         type Component = Text;
         type Property = f32;
fn get_mut(component: &mut Self::Component) -> Option<&mut
Self::Property> {
             Some(&mut component.sections.get_mut(0)?.style.font_size)
         }
     }

In order to keep this patch relatively small, this patch doesn't include
an implementation of `AnimatableProperty` on top of the reflection
system. That can be a follow-up.

This patch builds on top of the new `EntityMutExcept<>` type in order to
widen the `AnimationTarget` query to include write access to all
components. Because `EntityMutExcept<>` has some performance overhead
over an explicit query, we continue to explicitly query `Transform` in
order to avoid regressing the performance of skeletal animation, such as
the `many_foxes` benchmark. I've measured the performance of that
benchmark and have found no significant regressions.

A new example, `animated_ui`, has been added. This example shows how to
use Bevy's built-in animation infrastructure to animate font size and
color, which wasn't possible before this patch.

## Showcase


https://github.com/user-attachments/assets/1fa73492-a9ce-405a-a8f2-4aacd7f6dc97

## Migration Guide

* Animation keyframes are now an extensible trait, not an enum. Replace
`Keyframes::Translation(...)`, `Keyframes::Scale(...)`,
`Keyframes::Rotation(...)`, and `Keyframes::Weights(...)` with
`Box::new(TranslationKeyframes(...))`, `Box::new(ScaleKeyframes(...))`,
`Box::new(RotationKeyframes(...))`, and
`Box::new(MorphWeightsKeyframes(...))` respectively.
2024-09-23 17:14:12 +00:00

181 lines
6.1 KiB
Rust

//! Shows how to use animation clips to animate UI properties.
use bevy::{
animation::{AnimationTarget, AnimationTargetId},
prelude::*,
};
// A type that represents the font size of the first text section.
//
// We implement `AnimatableProperty` on this.
#[derive(Reflect)]
struct FontSizeProperty;
// A type that represents the color of the first text section.
//
// We implement `AnimatableProperty` on this.
#[derive(Reflect)]
struct TextColorProperty;
// Holds information about the animation we programmatically create.
struct AnimationInfo {
// The name of the animation target (in this case, the text).
target_name: Name,
// The ID of the animation target, derived from the name.
target_id: AnimationTargetId,
// The animation graph asset.
graph: Handle<AnimationGraph>,
// The index of the node within that graph.
node_index: AnimationNodeIndex,
}
// The entry point.
fn main() {
App::new()
.add_plugins(DefaultPlugins)
// Note that we don't need any systems other than the setup system,
// because Bevy automatically updates animations every frame.
.add_systems(Startup, setup)
.run();
}
impl AnimatableProperty for FontSizeProperty {
type Component = Text;
type Property = f32;
fn get_mut(component: &mut Self::Component) -> Option<&mut Self::Property> {
Some(&mut component.sections.get_mut(0)?.style.font_size)
}
}
impl AnimatableProperty for TextColorProperty {
type Component = Text;
type Property = Srgba;
fn get_mut(component: &mut Self::Component) -> Option<&mut Self::Property> {
match component.sections.get_mut(0)?.style.color {
Color::Srgba(ref mut color) => Some(color),
_ => None,
}
}
}
impl AnimationInfo {
// Programmatically creates the UI animation.
fn create(
animation_graphs: &mut Assets<AnimationGraph>,
animation_clips: &mut Assets<AnimationClip>,
) -> AnimationInfo {
// Create an ID that identifies the text node we're going to animate.
let animation_target_name = Name::new("Text");
let animation_target_id = AnimationTargetId::from_name(&animation_target_name);
// Allocate an animation clip.
let mut animation_clip = AnimationClip::default();
// Create a curve that animates font size.
//
// `VariableCurve::linear` is just a convenience constructor; it's also
// possible to initialize the structure manually.
animation_clip.add_curve_to_target(
animation_target_id,
VariableCurve::linear::<AnimatablePropertyKeyframes<FontSizeProperty>>(
[0.0, 0.5, 1.0, 1.5, 2.0, 2.5, 3.0],
[24.0, 80.0, 24.0, 80.0, 24.0, 80.0, 24.0],
),
);
// Create a curve that animates font color. Note that this should have
// the same time duration as the previous curve.
animation_clip.add_curve_to_target(
animation_target_id,
VariableCurve::linear::<AnimatablePropertyKeyframes<TextColorProperty>>(
[0.0, 1.0, 2.0, 3.0],
[Srgba::RED, Srgba::GREEN, Srgba::BLUE, Srgba::RED],
),
);
// Save our animation clip as an asset.
let animation_clip_handle = animation_clips.add(animation_clip);
// Create an animation graph with that clip.
let (animation_graph, animation_node_index) =
AnimationGraph::from_clip(animation_clip_handle);
let animation_graph_handle = animation_graphs.add(animation_graph);
AnimationInfo {
target_name: animation_target_name,
target_id: animation_target_id,
graph: animation_graph_handle,
node_index: animation_node_index,
}
}
}
// Creates all the entities in the scene.
fn setup(
mut commands: Commands,
asset_server: Res<AssetServer>,
mut animation_graphs: ResMut<Assets<AnimationGraph>>,
mut animation_clips: ResMut<Assets<AnimationClip>>,
) {
// Create the animation.
let AnimationInfo {
target_name: animation_target_name,
target_id: animation_target_id,
graph: animation_graph,
node_index: animation_node_index,
} = AnimationInfo::create(&mut animation_graphs, &mut animation_clips);
// Build an animation player that automatically plays the UI animation.
let mut animation_player = AnimationPlayer::default();
animation_player.play(animation_node_index).repeat();
// Add a camera.
commands.spawn(Camera2dBundle::default());
// Build the UI. We have a parent node that covers the whole screen and
// contains the `AnimationPlayer`, as well as a child node that contains the
// text to be animated.
commands
.spawn(NodeBundle {
// Cover the whole screen, and center contents.
style: Style {
position_type: PositionType::Absolute,
top: Val::Px(0.0),
left: Val::Px(0.0),
right: Val::Px(0.0),
bottom: Val::Px(0.0),
justify_content: JustifyContent::Center,
align_items: AlignItems::Center,
..default()
},
..default()
})
.insert(animation_player)
.insert(animation_graph)
.with_children(|builder| {
// Build the text node.
let player = builder.parent_entity();
builder
.spawn(
TextBundle::from_section(
"Bevy",
TextStyle {
font: asset_server.load("fonts/FiraSans-Bold.ttf"),
font_size: 24.0,
color: Color::Srgba(Srgba::RED),
},
)
.with_text_justify(JustifyText::Center),
)
// Mark as an animation target.
.insert(AnimationTarget {
id: animation_target_id,
player,
})
.insert(animation_target_name);
});
}