bevy/examples/animation/animated_mesh.rs
Greeble b2f3248432
Make the animated_mesh example more intuitive (#17421)
# Objective

Make the `animated_mesh` example more intuitive and easier for the user
to extend.

# Solution

The `animated_mesh` example shows how to spawn a single mesh and play a
single animation. The original code is roughly:

1. In `setup_mesh_and_animation`, spawn an entity with a SceneRoot that
will load and spawn the mesh. Also record the animation to play as a
resource.
2. Use `play_animation_once_loaded` to detect when any animation players
are spawned, then play the animation from the resource.

When I used this example as a starting point for my own app, I hit a
wall when trying to spawn multiple meshes with different animations.
`play_animation_once_loaded` tells me an animation player spawned
somewhere, but how do I get from there to the right animation? The
entity it runs on is spawned by the scene so I can't attach any data to
it?

The new code takes a different approach. Instead of a global resource,
the animation is recorded as a component on the entity with the
SceneRoot. Instead of detecting animation players spawning wherever, an
observer is attached to that specific entity.

This feels more intuitive and localised, and I think most users will
work out how to get from there to different animations and meshes. The
downside is more lines of code, and the "find the animation players"
part still feels a bit magical and inefficient.

# Side Notes

- The solution was mostly stolen from
https://github.com/bevyengine/bevy/issues/14852#issuecomment-2481401769.
- The example still feels too complicated.
    - "Why do I have to make this graph to play one animation?"
- "Why can't I choose and play the animation in one step and avoid this
temporary component?"
    - I think this requires engine changes.
- I originally started on a separate example of multiple meshes
([branch](https://github.com/bevyengine/bevy/compare/main...greeble-dev:bevy:animated-mesh-multiple)).
- I decided that the user could probably work this out themselves from
the single animation example.
    - But maybe still worth following through.

# Testing

`cargo run --example animated_mesh`

---------

Co-authored-by: Rob Parrett <robparrett@gmail.com>
2025-01-20 21:12:06 +00:00

130 lines
4.6 KiB
Rust

//! Plays an animation on a skinned glTF model of a fox.
use std::f32::consts::PI;
use bevy::{pbr::CascadeShadowConfigBuilder, prelude::*, scene::SceneInstanceReady};
// An example asset that contains a mesh and animation.
const GLTF_PATH: &str = "models/animated/Fox.glb";
fn main() {
App::new()
.insert_resource(AmbientLight {
color: Color::WHITE,
brightness: 2000.,
..default()
})
.add_plugins(DefaultPlugins)
.add_systems(Startup, setup_mesh_and_animation)
.add_systems(Startup, setup_camera_and_environment)
.run();
}
// A component that stores a reference to an animation we want to play. This is
// created when we start loading the mesh (see `setup_mesh_and_animation`) and
// read when the mesh has spawned (see `play_animation_once_loaded`).
#[derive(Component)]
struct AnimationToPlay {
graph_handle: Handle<AnimationGraph>,
index: AnimationNodeIndex,
}
fn setup_mesh_and_animation(
mut commands: Commands,
asset_server: Res<AssetServer>,
mut graphs: ResMut<Assets<AnimationGraph>>,
) {
// Create an animation graph containing a single animation. We want the "run"
// animation from our example asset, which has an index of two.
let (graph, index) = AnimationGraph::from_clip(
asset_server.load(GltfAssetLabel::Animation(2).from_asset(GLTF_PATH)),
);
// Store the animation graph as an asset.
let graph_handle = graphs.add(graph);
// Create a component that stores a reference to our animation.
let animation_to_play = AnimationToPlay {
graph_handle,
index,
};
// Start loading the asset as a scene and store a reference to it in a
// SceneRoot component. This component will automatically spawn a scene
// containing our mesh once it has loaded.
let mesh_scene = SceneRoot(asset_server.load(GltfAssetLabel::Scene(0).from_asset(GLTF_PATH)));
// Spawn an entity with our components, and connect it to an observer that
// will trigger when the scene is loaded and spawned.
commands
.spawn((animation_to_play, mesh_scene))
.observe(play_animation_when_ready);
}
fn play_animation_when_ready(
trigger: Trigger<SceneInstanceReady>,
mut commands: Commands,
children: Query<&Children>,
animations_to_play: Query<&AnimationToPlay>,
mut players: Query<&mut AnimationPlayer>,
) {
// The entity we spawned in `setup_mesh_and_animation` is the trigger's target.
// Start by finding the AnimationToPlay component we added to that entity.
if let Ok(animation_to_play) = animations_to_play.get(trigger.target()) {
// The SceneRoot component will have spawned the scene as a hierarchy
// of entities parented to our entity. Since the asset contained a skinned
// mesh and animations, it will also have spawned an animation player
// component. Search our entity's descendants to find the animation player.
for child in children.iter_descendants(trigger.target()) {
if let Ok(mut player) = players.get_mut(child) {
// Tell the animation player to start the animation and keep
// repeating it.
//
// If you want to try stopping and switching animations, see the
// `animated_mesh_control.rs` example.
player.play(animation_to_play.index).repeat();
// Add the animation graph. This only needs to be done once to
// connect the animation player to the mesh.
commands
.entity(child)
.insert(AnimationGraphHandle(animation_to_play.graph_handle.clone()));
}
}
}
}
// Spawn a camera and a simple environment with a ground plane and light.
fn setup_camera_and_environment(
mut commands: Commands,
mut meshes: ResMut<Assets<Mesh>>,
mut materials: ResMut<Assets<StandardMaterial>>,
) {
// Camera
commands.spawn((
Camera3d::default(),
Transform::from_xyz(100.0, 100.0, 150.0).looking_at(Vec3::new(0.0, 20.0, 0.0), Vec3::Y),
));
// Plane
commands.spawn((
Mesh3d(meshes.add(Plane3d::default().mesh().size(500000.0, 500000.0))),
MeshMaterial3d(materials.add(Color::srgb(0.3, 0.5, 0.3))),
));
// Light
commands.spawn((
Transform::from_rotation(Quat::from_euler(EulerRot::ZYX, 0.0, 1.0, -PI / 4.)),
DirectionalLight {
shadows_enabled: true,
..default()
},
CascadeShadowConfigBuilder {
first_cascade_far_bound: 200.0,
maximum_distance: 400.0,
..default()
}
.build(),
));
}