
# Objective Currently, the observer API looks like this: ```rust app.add_observer(|trigger: Trigger<Explode>| { info!("Entity {} exploded!", trigger.target()); }); ``` Future plans for observers also include "multi-event observers" with a trigger that looks like this (see [Cart's example](https://github.com/bevyengine/bevy/issues/14649#issuecomment-2960402508)): ```rust trigger: Trigger<( OnAdd<Pressed>, OnRemove<Pressed>, OnAdd<InteractionDisabled>, OnRemove<InteractionDisabled>, OnInsert<Hovered>, )>, ``` In scenarios like this, there is a lot of repetition of `On`. These are expected to be very high-traffic APIs especially in UI contexts, so ergonomics and readability are critical. By renaming `Trigger` to `On`, we can make these APIs read more cleanly and get rid of the repetition: ```rust app.add_observer(|trigger: On<Explode>| { info!("Entity {} exploded!", trigger.target()); }); ``` ```rust trigger: On<( Add<Pressed>, Remove<Pressed>, Add<InteractionDisabled>, Remove<InteractionDisabled>, Insert<Hovered>, )>, ``` Names like `On<Add<Pressed>>` emphasize the actual event listener nature more than `Trigger<OnAdd<Pressed>>`, and look cleaner. This *also* frees up the `Trigger` name if we want to use it for the observer event type, splitting them out from buffered events (bikeshedding this is out of scope for this PR though). For prior art: [`bevy_eventlistener`](https://github.com/aevyrie/bevy_eventlistener) used [`On`](https://docs.rs/bevy_eventlistener/latest/bevy_eventlistener/event_listener/struct.On.html) for its event listener type. Though in our case, the observer is the event listener, and `On` is just a type containing information about the triggered event. ## Solution Steal from `bevy_event_listener` by @aevyrie and use `On`. - Rename `Trigger` to `On` - Rename `OnAdd` to `Add` - Rename `OnInsert` to `Insert` - Rename `OnReplace` to `Replace` - Rename `OnRemove` to `Remove` - Rename `OnDespawn` to `Despawn` ## Discussion ### Naming Conflicts?? Using a name like `Add` might initially feel like a very bad idea, since it risks conflict with `core::ops::Add`. However, I don't expect this to be a big problem in practice. - You rarely need to actually implement the `Add` trait, especially in modules that would use the Bevy ECS. - In the rare cases where you *do* get a conflict, it is very easy to fix by just disambiguating, for example using `ops::Add`. - The `Add` event is a struct while the `Add` trait is a trait (duh), so the compiler error should be very obvious. For the record, renaming `OnAdd` to `Add`, I got exactly *zero* errors or conflicts within Bevy itself. But this is of course not entirely representative of actual projects *using* Bevy. You might then wonder, why not use `Added`? This would conflict with the `Added` query filter, so it wouldn't work. Additionally, the current naming convention for observer events does not use past tense. ### Documentation This does make documentation slightly more awkward when referring to `On` or its methods. Previous docs often referred to `Trigger::target` or "sends a `Trigger`" (which is... a bit strange anyway), which would now be `On::target` and "sends an observer `Event`". You can see the diff in this PR to see some of the effects. I think it should be fine though, we may just need to reword more documentation to read better.
296 lines
9.1 KiB
Rust
296 lines
9.1 KiB
Rust
//! Plays animations from a skinned glTF.
|
|
|
|
use std::{f32::consts::PI, time::Duration};
|
|
|
|
use bevy::{
|
|
animation::AnimationTargetId, color::palettes::css::WHITE, pbr::CascadeShadowConfigBuilder,
|
|
prelude::*,
|
|
};
|
|
use rand::{Rng, SeedableRng};
|
|
use rand_chacha::ChaCha8Rng;
|
|
|
|
const FOX_PATH: &str = "models/animated/Fox.glb";
|
|
|
|
fn main() {
|
|
App::new()
|
|
.insert_resource(AmbientLight {
|
|
color: Color::WHITE,
|
|
brightness: 2000.,
|
|
..default()
|
|
})
|
|
.add_plugins(DefaultPlugins)
|
|
.init_resource::<ParticleAssets>()
|
|
.init_resource::<FoxFeetTargets>()
|
|
.add_systems(Startup, setup)
|
|
.add_systems(Update, setup_scene_once_loaded)
|
|
.add_systems(Update, simulate_particles)
|
|
.add_observer(observe_on_step)
|
|
.run();
|
|
}
|
|
|
|
#[derive(Resource)]
|
|
struct SeededRng(ChaCha8Rng);
|
|
|
|
#[derive(Resource)]
|
|
struct Animations {
|
|
index: AnimationNodeIndex,
|
|
graph_handle: Handle<AnimationGraph>,
|
|
}
|
|
|
|
#[derive(Event, Reflect, Clone)]
|
|
struct OnStep;
|
|
|
|
fn observe_on_step(
|
|
trigger: On<OnStep>,
|
|
particle: Res<ParticleAssets>,
|
|
mut commands: Commands,
|
|
transforms: Query<&GlobalTransform>,
|
|
mut seeded_rng: ResMut<SeededRng>,
|
|
) {
|
|
let translation = transforms
|
|
.get(trigger.target().unwrap())
|
|
.unwrap()
|
|
.translation();
|
|
// Spawn a bunch of particles.
|
|
for _ in 0..14 {
|
|
let horizontal = seeded_rng.0.r#gen::<Dir2>() * seeded_rng.0.gen_range(8.0..12.0);
|
|
let vertical = seeded_rng.0.gen_range(0.0..4.0);
|
|
let size = seeded_rng.0.gen_range(0.2..1.0);
|
|
|
|
commands.spawn((
|
|
Particle {
|
|
lifetime_timer: Timer::from_seconds(
|
|
seeded_rng.0.gen_range(0.2..0.6),
|
|
TimerMode::Once,
|
|
),
|
|
size,
|
|
velocity: Vec3::new(horizontal.x, vertical, horizontal.y) * 10.0,
|
|
},
|
|
Mesh3d(particle.mesh.clone()),
|
|
MeshMaterial3d(particle.material.clone()),
|
|
Transform {
|
|
translation,
|
|
scale: Vec3::splat(size),
|
|
..Default::default()
|
|
},
|
|
));
|
|
}
|
|
}
|
|
|
|
fn setup(
|
|
mut commands: Commands,
|
|
asset_server: Res<AssetServer>,
|
|
mut meshes: ResMut<Assets<Mesh>>,
|
|
mut materials: ResMut<Assets<StandardMaterial>>,
|
|
mut graphs: ResMut<Assets<AnimationGraph>>,
|
|
) {
|
|
// Build the animation graph
|
|
let (graph, index) = AnimationGraph::from_clip(
|
|
// We specifically want the "run" animation, which is the third one.
|
|
asset_server.load(GltfAssetLabel::Animation(2).from_asset(FOX_PATH)),
|
|
);
|
|
|
|
// Insert a resource with the current scene information
|
|
let graph_handle = graphs.add(graph);
|
|
commands.insert_resource(Animations {
|
|
index,
|
|
graph_handle,
|
|
});
|
|
|
|
// 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(),
|
|
));
|
|
|
|
// Fox
|
|
commands.spawn(SceneRoot(
|
|
asset_server.load(GltfAssetLabel::Scene(0).from_asset(FOX_PATH)),
|
|
));
|
|
|
|
// We're seeding the PRNG here to make this example deterministic for testing purposes.
|
|
// This isn't strictly required in practical use unless you need your app to be deterministic.
|
|
let seeded_rng = ChaCha8Rng::seed_from_u64(19878367467712);
|
|
commands.insert_resource(SeededRng(seeded_rng));
|
|
}
|
|
|
|
// An `AnimationPlayer` is automatically added to the scene when it's ready.
|
|
// When the player is added, start the animation.
|
|
fn setup_scene_once_loaded(
|
|
mut commands: Commands,
|
|
animations: Res<Animations>,
|
|
feet: Res<FoxFeetTargets>,
|
|
graphs: Res<Assets<AnimationGraph>>,
|
|
mut clips: ResMut<Assets<AnimationClip>>,
|
|
mut players: Query<(Entity, &mut AnimationPlayer), Added<AnimationPlayer>>,
|
|
) {
|
|
fn get_clip<'a>(
|
|
node: AnimationNodeIndex,
|
|
graph: &AnimationGraph,
|
|
clips: &'a mut Assets<AnimationClip>,
|
|
) -> &'a mut AnimationClip {
|
|
let node = graph.get(node).unwrap();
|
|
let clip = match &node.node_type {
|
|
AnimationNodeType::Clip(handle) => clips.get_mut(handle),
|
|
_ => unreachable!(),
|
|
};
|
|
clip.unwrap()
|
|
}
|
|
|
|
for (entity, mut player) in &mut players {
|
|
// Send `OnStep` events once the fox feet hits the ground in the running animation.
|
|
|
|
let graph = graphs.get(&animations.graph_handle).unwrap();
|
|
let running_animation = get_clip(animations.index, graph, &mut clips);
|
|
|
|
// You can determine the time an event should trigger if you know witch frame it occurs and
|
|
// the frame rate of the animation. Let's say we want to trigger an event at frame 15,
|
|
// and the animation has a frame rate of 24 fps, then time = 15 / 24 = 0.625.
|
|
running_animation.add_event_to_target(feet.front_left, 0.625, OnStep);
|
|
running_animation.add_event_to_target(feet.front_right, 0.5, OnStep);
|
|
running_animation.add_event_to_target(feet.back_left, 0.0, OnStep);
|
|
running_animation.add_event_to_target(feet.back_right, 0.125, OnStep);
|
|
|
|
// Start the animation
|
|
|
|
let mut transitions = AnimationTransitions::new();
|
|
|
|
// Make sure to start the animation via the `AnimationTransitions`
|
|
// component. The `AnimationTransitions` component wants to manage all
|
|
// the animations and will get confused if the animations are started
|
|
// directly via the `AnimationPlayer`.
|
|
transitions
|
|
.play(&mut player, animations.index, Duration::ZERO)
|
|
.repeat();
|
|
|
|
commands
|
|
.entity(entity)
|
|
.insert(AnimationGraphHandle(animations.graph_handle.clone()))
|
|
.insert(transitions);
|
|
}
|
|
}
|
|
|
|
fn simulate_particles(
|
|
mut commands: Commands,
|
|
mut query: Query<(Entity, &mut Transform, &mut Particle)>,
|
|
time: Res<Time>,
|
|
) {
|
|
for (entity, mut transform, mut particle) in &mut query {
|
|
if particle.lifetime_timer.tick(time.delta()).just_finished() {
|
|
commands.entity(entity).despawn();
|
|
return;
|
|
}
|
|
|
|
transform.translation += particle.velocity * time.delta_secs();
|
|
transform.scale = Vec3::splat(particle.size.lerp(0.0, particle.lifetime_timer.fraction()));
|
|
particle
|
|
.velocity
|
|
.smooth_nudge(&Vec3::ZERO, 4.0, time.delta_secs());
|
|
}
|
|
}
|
|
|
|
#[derive(Component)]
|
|
struct Particle {
|
|
lifetime_timer: Timer,
|
|
size: f32,
|
|
velocity: Vec3,
|
|
}
|
|
|
|
#[derive(Resource)]
|
|
struct ParticleAssets {
|
|
mesh: Handle<Mesh>,
|
|
material: Handle<StandardMaterial>,
|
|
}
|
|
|
|
impl FromWorld for ParticleAssets {
|
|
fn from_world(world: &mut World) -> Self {
|
|
Self {
|
|
mesh: world.add_asset::<Mesh>(Sphere::new(10.0)),
|
|
material: world.add_asset::<StandardMaterial>(StandardMaterial {
|
|
base_color: WHITE.into(),
|
|
..Default::default()
|
|
}),
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Stores the `AnimationTargetId`s of the fox's feet
|
|
#[derive(Resource)]
|
|
struct FoxFeetTargets {
|
|
front_right: AnimationTargetId,
|
|
front_left: AnimationTargetId,
|
|
back_left: AnimationTargetId,
|
|
back_right: AnimationTargetId,
|
|
}
|
|
|
|
impl Default for FoxFeetTargets {
|
|
fn default() -> Self {
|
|
let hip_node = ["root", "_rootJoint", "b_Root_00", "b_Hip_01"];
|
|
let front_left_foot = hip_node.iter().chain(
|
|
[
|
|
"b_Spine01_02",
|
|
"b_Spine02_03",
|
|
"b_LeftUpperArm_09",
|
|
"b_LeftForeArm_010",
|
|
"b_LeftHand_011",
|
|
]
|
|
.iter(),
|
|
);
|
|
let front_right_foot = hip_node.iter().chain(
|
|
[
|
|
"b_Spine01_02",
|
|
"b_Spine02_03",
|
|
"b_RightUpperArm_06",
|
|
"b_RightForeArm_07",
|
|
"b_RightHand_08",
|
|
]
|
|
.iter(),
|
|
);
|
|
let back_left_foot = hip_node.iter().chain(
|
|
[
|
|
"b_LeftLeg01_015",
|
|
"b_LeftLeg02_016",
|
|
"b_LeftFoot01_017",
|
|
"b_LeftFoot02_018",
|
|
]
|
|
.iter(),
|
|
);
|
|
let back_right_foot = hip_node.iter().chain(
|
|
[
|
|
"b_RightLeg01_019",
|
|
"b_RightLeg02_020",
|
|
"b_RightFoot01_021",
|
|
"b_RightFoot02_022",
|
|
]
|
|
.iter(),
|
|
);
|
|
Self {
|
|
front_left: AnimationTargetId::from_iter(front_left_foot),
|
|
front_right: AnimationTargetId::from_iter(front_right_foot),
|
|
back_left: AnimationTargetId::from_iter(back_left_foot),
|
|
back_right: AnimationTargetId::from_iter(back_right_foot),
|
|
}
|
|
}
|
|
}
|