mirror of
https://github.com/bevyengine/bevy
synced 2024-11-22 20:53:53 +00:00
8154164f1b
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.
181 lines
6.1 KiB
Rust
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);
|
|
});
|
|
}
|