From f5bfb0e97e6a4331ff8c572724d635f77938f82e Mon Sep 17 00:00:00 2001 From: rosefarts Date: Mon, 30 Sep 2024 13:07:45 +0200 Subject: [PATCH 01/89] example init --- Cargo.toml | 11 +++++++++++ examples/animation/animation_triggers.rs | 15 +++++++++++++++ 2 files changed, 26 insertions(+) create mode 100644 examples/animation/animation_triggers.rs diff --git a/Cargo.toml b/Cargo.toml index f7420664260fa..9a114f47040ec 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1170,6 +1170,17 @@ doc-scrape-examples = true hidden = true # Animation +[[examples]] +name = "animation_triggers" +path = "examples/animation/animation_triggers.rs" +doc-scrape-examples = true + +[package.metadata.example.animation_triggers] +name = "Animation Triggers" +description = "blablabla" +category = "Animation" +wasm = true + [[example]] name = "animated_fox" path = "examples/animation/animated_fox.rs" diff --git a/examples/animation/animation_triggers.rs b/examples/animation/animation_triggers.rs new file mode 100644 index 0000000000000..e37df7ad0f4e6 --- /dev/null +++ b/examples/animation/animation_triggers.rs @@ -0,0 +1,15 @@ +use bevy::prelude::*; + +fn main() { + App::new() + .add_plugins(DefaultPlugins) + .add_systems(Startup, setup) + .add_systems(Update, update) + .run(); +} + +fn setup() { + println!("HI!"); +} + +fn update() {} From fa01cd15cade1867c727e6cfb9bfad504479a9c2 Mon Sep 17 00:00:00 2001 From: rosefarts Date: Mon, 30 Sep 2024 13:19:51 +0200 Subject: [PATCH 02/89] Update Cargo.toml --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 9a114f47040ec..6c8b6f6394c07 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1170,7 +1170,7 @@ doc-scrape-examples = true hidden = true # Animation -[[examples]] +[[example]] name = "animation_triggers" path = "examples/animation/animation_triggers.rs" doc-scrape-examples = true From f970677c09c75074fdb33b7872b4fc10456d8185 Mon Sep 17 00:00:00 2001 From: rosefarts Date: Mon, 30 Sep 2024 15:04:44 +0200 Subject: [PATCH 03/89] implement animation triggers --- crates/bevy_animation/src/lib.rs | 70 +++++- crates/bevy_animation/src/triggers.rs | 252 ++++++++++++++++++++ crates/bevy_internal/src/default_plugins.rs | 2 +- examples/animation/animated_fox.rs | 7 +- examples/animation/animation_graph.rs | 7 +- examples/animation/animation_triggers.rs | 53 +++- 6 files changed, 376 insertions(+), 15 deletions(-) create mode 100644 crates/bevy_animation/src/triggers.rs diff --git a/crates/bevy_animation/src/lib.rs b/crates/bevy_animation/src/lib.rs index 0b1339d2fb3d9..3b0217f74b4ca 100755 --- a/crates/bevy_animation/src/lib.rs +++ b/crates/bevy_animation/src/lib.rs @@ -13,6 +13,7 @@ pub mod animatable; pub mod graph; pub mod keyframes; pub mod transition; +pub mod triggers; mod util; use alloc::collections::BTreeMap; @@ -23,6 +24,7 @@ use core::{ hash::{Hash, Hasher}, iter, }; +use triggers::{trigger_animation_event, AnimationEvent, AnimationTriggerData}; use bevy_app::{App, Plugin, PostUpdate}; use bevy_asset::{Asset, AssetApp, Assets, Handle}; @@ -30,7 +32,7 @@ use bevy_core::Name; use bevy_ecs::{ entity::MapEntities, prelude::*, reflect::ReflectMapEntities, world::EntityMutExcept, }; -use bevy_math::FloatExt; +use bevy_math::{FloatExt, FloatOrd}; use bevy_reflect::{ prelude::ReflectDefault, utility::NonGenericTypeInfoCell, ApplyError, DynamicStruct, FieldIter, FromReflect, FromType, GetTypeRegistration, NamedField, PartialReflect, Reflect, @@ -468,9 +470,12 @@ pub enum Interpolation { #[derive(Asset, Reflect, Clone, Debug, Default)] pub struct AnimationClip { curves: AnimationCurves, + triggers: AnimationTriggers, duration: f32, } +pub(crate) type AnimationTriggers = HashMap>; + /// A mapping from [`AnimationTargetId`] (e.g. bone in a skinned mesh) to the /// animation curves. pub type AnimationCurves = HashMap, NoOpHash>; @@ -599,6 +604,21 @@ impl AnimationClip { .max(*curve.keyframe_timestamps.last().unwrap_or(&0.0)); self.curves.entry(target_id).or_default().push(curve); } + + /// Add an [`AnimationTrigger`] to an [`AnimationTarget`] named by an [`AnimationTargetId`]. + /// + /// The `event` will trigger on the entity matching the target once the `time` is reached in the animation. + pub fn add_trigger( + &mut self, + target_id: AnimationTargetId, + time: f32, + event: impl AnimationEvent, + ) { + self.duration = self.duration.max(time); + let triggers = self.triggers.entry(target_id).or_default(); + triggers.push((time, AnimationTriggerData::new(event))); + triggers.sort_by_key(|(k, _)| FloatOrd(*k)); + } } /// Repetition behavior of an animation. @@ -660,9 +680,11 @@ pub struct ActiveAnimation { /// /// Note: This will always be in the range [0.0, animation clip duration] seek_time: f32, + last_seek_time: Option, /// Number of times the animation has completed. /// If the animation is playing in reverse, this increments when the animation passes the start. completions: u32, + just_completed: bool, paused: bool, } @@ -676,7 +698,9 @@ impl Default for ActiveAnimation { speed: 1.0, elapsed: 0.0, seek_time: 0.0, + last_seek_time: None, completions: 0, + just_completed: false, paused: false, } } @@ -702,6 +726,9 @@ impl ActiveAnimation { return; } + self.just_completed = false; + self.last_seek_time = Some(self.seek_time); + self.elapsed += delta; self.seek_time += delta * self.speed; @@ -709,6 +736,7 @@ impl ActiveAnimation { let under_time = self.speed < 0.0 && self.seek_time < 0.0; if over_time || under_time { + self.just_completed = true; self.completions += 1; if self.is_finished() { @@ -1107,16 +1135,24 @@ pub type AnimationEntityMut<'w> = EntityMutExcept< /// A system that modifies animation targets (e.g. bones in a skinned mesh) /// according to the currently-playing animations. -pub fn animate_targets( +/// +/// Also triggers animation events. +pub fn animate_targets_and_trigger_events( + par_commands: ParallelCommands, clips: Res>, graphs: Res>, players: Query<(&AnimationPlayer, &Handle)>, - mut targets: Query<(&AnimationTarget, Option<&mut Transform>, AnimationEntityMut)>, + mut targets: Query<( + Entity, + &AnimationTarget, + Option<&mut Transform>, + AnimationEntityMut, + )>, ) { // Evaluate all animation targets in parallel. targets .par_iter_mut() - .for_each(|(target, mut transform, mut entity_mut)| { + .for_each(|(entity, target, mut transform, mut entity_mut)| { let &AnimationTarget { id: target_id, player: player_id, @@ -1184,6 +1220,30 @@ pub fn animate_targets( continue; }; + for (_time, event) in clip + .triggers + .get(&target_id) + .iter() + .flat_map(|t| t.iter()) + .filter(|(t, _)| match active_animation.is_playback_reversed() { + true => { + *t >= active_animation.seek_time + && (active_animation.just_completed + || Some(*t) < active_animation.last_seek_time) + } + false => { + Some(*t) > active_animation.last_seek_time + && (active_animation.just_completed + || *t <= active_animation.seek_time) + } + }) + { + dbg!(active_animation.seek_time); + par_commands.command_scope(|mut commands| { + commands.queue(trigger_animation_event(event.0.clone_value(), entity)); + }) + } + let Some(curves) = clip.curves_for_target(target_id) else { continue; }; @@ -1259,7 +1319,7 @@ impl Plugin for AnimationPlugin { // it to its own system set after `Update` but before // `PostUpdate`. For now, we just disable ambiguity testing // for this system. - animate_targets + animate_targets_and_trigger_events .after(bevy_render::mesh::morph::inherit_weights) .ambiguous_with_all(), expire_completed_transitions, diff --git a/crates/bevy_animation/src/triggers.rs b/crates/bevy_animation/src/triggers.rs new file mode 100644 index 0000000000000..e75ce613df76f --- /dev/null +++ b/crates/bevy_animation/src/triggers.rs @@ -0,0 +1,252 @@ +use std::any::{Any, TypeId}; + +use bevy_ecs::prelude::*; +use bevy_reflect::{ + prelude::*, utility::NonGenericTypeInfoCell, ApplyError, DynamicTupleStruct, FromType, + GetTypeRegistration, ReflectFromPtr, ReflectKind, ReflectMut, ReflectOwned, ReflectRef, + TupleStructFieldIter, TupleStructInfo, TypeInfo, TypeRegistration, Typed, UnnamedField, +}; + +pub(crate) fn trigger_animation_event( + event: Box, + entity: Entity, +) -> impl Command { + move |world: &mut World| { + let (from_reflect, animation_event) = { + let registry = world + .get_resource::() + .expect("Missing resource `AppTypeRegistry`"); + let lock = registry.read(); + let type_info = event.get_represented_type_info().unwrap(); // FIXME: when would this fail? + let registration = lock + .get_with_type_path(type_info.type_path()) + .unwrap_or_else(|| { + panic!( + "Missing type registration for type: `{}`", + type_info.type_path() + ) + }); + ( + registration + .data::() + .cloned() + .unwrap_or_else(|| { + panic!( + "Type `{}` is not registered with data: `ReflectFromReflect`", + type_info.type_path() + ) + }), + registration + .data::() + .cloned() + .unwrap_or_else(|| { + panic!( + "Type `{}` is not registered with data: `ReflectAnimationEvent`", + type_info.type_path() + ) + }), + ) + }; + let event = from_reflect.from_reflect(event.as_ref()).unwrap(); // FIXME: when would this fail? + animation_event.trigger(event.as_ref(), entity, world); + } +} + +pub trait AnimationEvent: Event + Reflect + Clone {} + +#[derive(Clone)] +pub struct ReflectAnimationEvent { + trigger: fn(&dyn Reflect, Entity, &mut World), +} + +impl ReflectAnimationEvent { + /// # Panics + /// + /// Panics if the underlying type of `event` does not match the type this `ReflectAnimationEvent` was constructed for. + pub(crate) fn trigger(&self, event: &dyn Reflect, entity: Entity, world: &mut World) { + (self.trigger)(event, entity, world) + } +} + +impl FromType for ReflectAnimationEvent { + fn from_type() -> Self { + Self { + trigger: |value, entity, world| { + let event = value.downcast_ref::().unwrap().clone(); + world.entity_mut(entity).trigger(event); + }, + } + } +} + +#[derive(TypePath, Debug)] +pub(crate) struct AnimationTriggerData(pub(crate) Box); + +impl AnimationTriggerData { + pub(crate) fn new(event: impl Event + PartialReflect) -> Self { + Self(Box::new(event)) + } +} + +impl Clone for AnimationTriggerData { + fn clone(&self) -> Self { + Self(self.0.clone_value()) + } +} + +impl GetTypeRegistration for AnimationTriggerData { + fn get_type_registration() -> TypeRegistration { + let mut registration = TypeRegistration::of::(); + registration.insert::(FromType::::from_type()); + registration + } +} + +impl Typed for AnimationTriggerData { + fn type_info() -> &'static TypeInfo { + static CELL: NonGenericTypeInfoCell = NonGenericTypeInfoCell::new(); + CELL.get_or_set(|| { + TypeInfo::TupleStruct(TupleStructInfo::new::(&[UnnamedField::new::<()>(0)])) + }) + } +} + +impl TupleStruct for AnimationTriggerData { + fn field(&self, index: usize) -> Option<&dyn PartialReflect> { + match index { + 0 => Some(self.0.as_partial_reflect()), + _ => None, + } + } + + fn field_mut(&mut self, index: usize) -> Option<&mut dyn PartialReflect> { + match index { + 0 => Some(self.0.as_partial_reflect_mut()), + _ => None, + } + } + + fn field_len(&self) -> usize { + 1 + } + + fn iter_fields(&self) -> TupleStructFieldIter { + TupleStructFieldIter::new(self) + } + + fn clone_dynamic(&self) -> DynamicTupleStruct { + DynamicTupleStruct::from_iter([PartialReflect::clone_value(&*self.0)]) + } +} + +impl PartialReflect for AnimationTriggerData { + #[inline] + fn get_represented_type_info(&self) -> Option<&'static TypeInfo> { + Some(::type_info()) + } + + #[inline] + fn into_partial_reflect(self: Box) -> Box { + self + } + + #[inline] + fn as_partial_reflect(&self) -> &dyn PartialReflect { + self + } + + #[inline] + fn as_partial_reflect_mut(&mut self) -> &mut dyn PartialReflect { + self + } + + fn try_into_reflect(self: Box) -> Result, Box> { + Ok(self) + } + + #[inline] + fn try_as_reflect(&self) -> Option<&dyn Reflect> { + Some(self) + } + + #[inline] + fn try_as_reflect_mut(&mut self) -> Option<&mut dyn Reflect> { + Some(self) + } + + fn try_apply(&mut self, value: &dyn PartialReflect) -> Result<(), ApplyError> { + if let ReflectRef::TupleStruct(struct_value) = value.reflect_ref() { + for (i, value) in struct_value.iter_fields().enumerate() { + if let Some(v) = self.field_mut(i) { + v.try_apply(value)?; + } + } + } else { + return Err(ApplyError::MismatchedKinds { + from_kind: value.reflect_kind(), + to_kind: ReflectKind::TupleStruct, + }); + } + Ok(()) + } + + fn reflect_ref(&self) -> ReflectRef { + ReflectRef::TupleStruct(self) + } + + fn reflect_mut(&mut self) -> ReflectMut { + ReflectMut::TupleStruct(self) + } + + fn reflect_owned(self: Box) -> ReflectOwned { + ReflectOwned::TupleStruct(self) + } + + fn clone_value(&self) -> Box { + Box::new(Clone::clone(self)) + } +} + +impl Reflect for AnimationTriggerData { + #[inline] + fn into_any(self: Box) -> Box { + self + } + + #[inline] + fn as_any(&self) -> &dyn Any { + self + } + + #[inline] + fn as_any_mut(&mut self) -> &mut dyn Any { + self + } + + #[inline] + fn into_reflect(self: Box) -> Box { + self + } + + #[inline] + fn as_reflect(&self) -> &dyn Reflect { + self + } + + #[inline] + fn as_reflect_mut(&mut self) -> &mut dyn Reflect { + self + } + + #[inline] + fn set(&mut self, value: Box) -> Result<(), Box> { + *self = value.take()?; + Ok(()) + } +} + +impl FromReflect for AnimationTriggerData { + fn from_reflect(reflect: &dyn PartialReflect) -> Option { + Some(reflect.try_downcast_ref::()?.clone()) + } +} diff --git a/crates/bevy_internal/src/default_plugins.rs b/crates/bevy_internal/src/default_plugins.rs index 74008dafafcc8..84721cfbf38e0 100644 --- a/crates/bevy_internal/src/default_plugins.rs +++ b/crates/bevy_internal/src/default_plugins.rs @@ -141,7 +141,7 @@ impl Plugin for IgnoreAmbiguitiesPlugin { ); app.ignore_ambiguity( bevy_app::PostUpdate, - bevy_animation::animate_targets, + bevy_animation::animate_targets_and_trigger_events, bevy_ui::ui_layout_system, ); } diff --git a/examples/animation/animated_fox.rs b/examples/animation/animated_fox.rs index f10e2b66ee1f3..06fd14ee3c2bc 100644 --- a/examples/animation/animated_fox.rs +++ b/examples/animation/animated_fox.rs @@ -3,7 +3,7 @@ use std::{f32::consts::PI, time::Duration}; use bevy::{ - animation::{animate_targets, RepeatAnimation}, + animation::{animate_targets_and_trigger_events, RepeatAnimation}, pbr::CascadeShadowConfigBuilder, prelude::*, }; @@ -18,7 +18,10 @@ fn main() { }) .add_plugins(DefaultPlugins) .add_systems(Startup, setup) - .add_systems(Update, setup_scene_once_loaded.before(animate_targets)) + .add_systems( + Update, + setup_scene_once_loaded.before(animate_targets_and_trigger_events), + ) .add_systems(Update, keyboard_animation_control) .run(); } diff --git a/examples/animation/animation_graph.rs b/examples/animation/animation_graph.rs index 94b9a2e52295e..2654ad3ac0438 100644 --- a/examples/animation/animation_graph.rs +++ b/examples/animation/animation_graph.rs @@ -4,7 +4,7 @@ //! playing animations by clicking and dragging left or right within the nodes. use bevy::{ - animation::animate_targets, + animation::animate_targets_and_trigger_events, color::palettes::{ basic::WHITE, css::{ANTIQUE_WHITE, DARK_GREEN}, @@ -83,7 +83,10 @@ fn main() { ..default() })) .add_systems(Startup, (setup_assets, setup_scene, setup_ui)) - .add_systems(Update, init_animations.before(animate_targets)) + .add_systems( + Update, + init_animations.before(animate_targets_and_trigger_events), + ) .add_systems( Update, (handle_weight_drag, update_ui, sync_weights).chain(), diff --git a/examples/animation/animation_triggers.rs b/examples/animation/animation_triggers.rs index e37df7ad0f4e6..006e7bdb57873 100644 --- a/examples/animation/animation_triggers.rs +++ b/examples/animation/animation_triggers.rs @@ -1,15 +1,58 @@ -use bevy::prelude::*; +//! blabla + +use bevy::{ + animation::{ + triggers::{AnimationEvent, ReflectAnimationEvent}, + AnimationTarget, AnimationTargetId, + }, + prelude::*, +}; fn main() { App::new() .add_plugins(DefaultPlugins) .add_systems(Startup, setup) - .add_systems(Update, update) + .register_type::() + .observe(Say::observer) .run(); } -fn setup() { - println!("HI!"); +#[derive(Event, Reflect, Clone)] +#[reflect(AnimationEvent)] +enum Say { + Hello, + Bye, +} + +impl Say { + fn observer(trigger: Trigger) { + match trigger.event() { + Say::Hello => println!("HELLO!"), + Say::Bye => println!("BYE!"), + } + } } -fn update() {} +impl AnimationEvent for Say {} + +fn setup( + mut commands: Commands, + mut animations: ResMut>, + mut graphs: ResMut>, +) { + let mut animation = AnimationClip::default(); + + let name = Name::new("abc"); + let id = AnimationTargetId::from(&name); + animation.add_trigger(id, 1.0, Say::Hello); + animation.add_trigger(id, 2.0, Say::Bye); + + let (graph, animation_index) = AnimationGraph::from_clip(animations.add(animation)); + let mut player = AnimationPlayer::default(); + player.play(animation_index).repeat(); + + let player = commands.spawn((name, graphs.add(graph), player)).id(); + commands + .entity(player) + .insert(AnimationTarget { id, player }); +} From 3d067ccce8fe20ac88884e03b4592c525b09670e Mon Sep 17 00:00:00 2001 From: rosefarts Date: Mon, 30 Sep 2024 15:56:32 +0200 Subject: [PATCH 04/89] make target_id optional + rename example --- Cargo.toml | 10 +-- crates/bevy_animation/src/lib.rs | 80 ++++++++++++++----- crates/bevy_animation/src/triggers.rs | 2 +- ...mation_triggers.rs => animation_events.rs} | 6 +- 4 files changed, 67 insertions(+), 31 deletions(-) rename examples/animation/{animation_triggers.rs => animation_events.rs} (89%) diff --git a/Cargo.toml b/Cargo.toml index 6c8b6f6394c07..636305407d88c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1171,13 +1171,13 @@ hidden = true # Animation [[example]] -name = "animation_triggers" -path = "examples/animation/animation_triggers.rs" +name = "animation_events" +path = "examples/animation/animation_events.rs" doc-scrape-examples = true -[package.metadata.example.animation_triggers] -name = "Animation Triggers" -description = "blablabla" +[package.metadata.example.animation_events] +name = "Animation Events" +description = "TODO: add description" category = "Animation" wasm = true diff --git a/crates/bevy_animation/src/lib.rs b/crates/bevy_animation/src/lib.rs index 3b0217f74b4ca..54c8cf2656d3c 100755 --- a/crates/bevy_animation/src/lib.rs +++ b/crates/bevy_animation/src/lib.rs @@ -474,7 +474,8 @@ pub struct AnimationClip { duration: f32, } -pub(crate) type AnimationTriggers = HashMap>; +pub(crate) type AnimationTriggers = + HashMap, Vec<(f32, AnimationTriggerData)>>; /// A mapping from [`AnimationTargetId`] (e.g. bone in a skinned mesh) to the /// animation curves. @@ -610,7 +611,7 @@ impl AnimationClip { /// The `event` will trigger on the entity matching the target once the `time` is reached in the animation. pub fn add_trigger( &mut self, - target_id: AnimationTargetId, + target_id: Option, time: f32, event: impl AnimationEvent, ) { @@ -1133,6 +1134,57 @@ pub type AnimationEntityMut<'w> = EntityMutExcept< ), >; +fn for_each_animation_event<'a, 'b>( + target_id: Option, + clip: &'a AnimationClip, + animation: &ActiveAnimation, + mut f: impl FnMut(f32, Box), +) { + for (t, e) in clip + .triggers + .get(&target_id) + .iter() + .flat_map(|t| t.iter()) + .filter(|(t, _)| match animation.is_playback_reversed() { + true => { + *t >= animation.seek_time + && (animation.just_completed || Some(*t) < animation.last_seek_time) + } + false => { + Some(*t) > animation.last_seek_time + && (animation.just_completed || *t <= animation.seek_time) + } + }) + { + f(*t, e.0.clone_value()) + } +} + +fn trigger_untargeted_animation_events( + mut commands: Commands, + clips: Res>, + graphs: Res>, + players: Query<(Entity, &AnimationPlayer, &Handle)>, +) { + for (entity, player, graph_id) in &players { + // The graph might not have loaded yet. Safely bail. + let Some(graph) = graphs.get(graph_id) else { + return; + }; + + for (index, animation) in player.active_animations.iter() { + let Some(clip_id) = graph.get(*index).unwrap().clip.as_ref() else { + continue; + }; + let clip = clips.get(clip_id).unwrap(); + for_each_animation_event(None, clip, animation, |_, event| { + dbg!(animation.seek_time); + commands.queue(trigger_animation_event(event, entity)); + }); + } + } +} + /// A system that modifies animation targets (e.g. bones in a skinned mesh) /// according to the currently-playing animations. /// @@ -1220,29 +1272,12 @@ pub fn animate_targets_and_trigger_events( continue; }; - for (_time, event) in clip - .triggers - .get(&target_id) - .iter() - .flat_map(|t| t.iter()) - .filter(|(t, _)| match active_animation.is_playback_reversed() { - true => { - *t >= active_animation.seek_time - && (active_animation.just_completed - || Some(*t) < active_animation.last_seek_time) - } - false => { - Some(*t) > active_animation.last_seek_time - && (active_animation.just_completed - || *t <= active_animation.seek_time) - } - }) - { + for_each_animation_event(Some(target_id), clip, active_animation, |_, event| { dbg!(active_animation.seek_time); par_commands.command_scope(|mut commands| { - commands.queue(trigger_animation_event(event.0.clone_value(), entity)); + commands.queue(trigger_animation_event(event, entity)); }) - } + }); let Some(curves) = clip.curves_for_target(target_id) else { continue; @@ -1313,6 +1348,7 @@ impl Plugin for AnimationPlugin { ( advance_transitions, advance_animations, + trigger_untargeted_animation_events, // TODO: `animate_targets` can animate anything, so // ambiguity testing currently considers it ambiguous with // every other system in `PostUpdate`. We may want to move diff --git a/crates/bevy_animation/src/triggers.rs b/crates/bevy_animation/src/triggers.rs index e75ce613df76f..ebca48da32b37 100644 --- a/crates/bevy_animation/src/triggers.rs +++ b/crates/bevy_animation/src/triggers.rs @@ -1,4 +1,4 @@ -use std::any::{Any, TypeId}; +use std::any::Any; use bevy_ecs::prelude::*; use bevy_reflect::{ diff --git a/examples/animation/animation_triggers.rs b/examples/animation/animation_events.rs similarity index 89% rename from examples/animation/animation_triggers.rs rename to examples/animation/animation_events.rs index 006e7bdb57873..559385efc5746 100644 --- a/examples/animation/animation_triggers.rs +++ b/examples/animation/animation_events.rs @@ -1,4 +1,4 @@ -//! blabla +// TODO: rename to animation_events use bevy::{ animation::{ @@ -44,8 +44,8 @@ fn setup( let name = Name::new("abc"); let id = AnimationTargetId::from(&name); - animation.add_trigger(id, 1.0, Say::Hello); - animation.add_trigger(id, 2.0, Say::Bye); + animation.add_trigger(Some(id), 1.0, Say::Hello); + animation.add_trigger(Some(id), 2.0, Say::Bye); let (graph, animation_index) = AnimationGraph::from_clip(animations.add(animation)); let mut player = AnimationPlayer::default(); From a34e47a19cf698fb9220ebd0601fb30f0c4b5dd1 Mon Sep 17 00:00:00 2001 From: rosefarts Date: Mon, 30 Sep 2024 15:57:01 +0200 Subject: [PATCH 05/89] add animation events to animated_fox --- examples/animation/animated_fox.rs | 30 +++++++++++++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/examples/animation/animated_fox.rs b/examples/animation/animated_fox.rs index 06fd14ee3c2bc..81ba89e2b0513 100644 --- a/examples/animation/animated_fox.rs +++ b/examples/animation/animated_fox.rs @@ -3,7 +3,11 @@ use std::{f32::consts::PI, time::Duration}; use bevy::{ - animation::{animate_targets_and_trigger_events, RepeatAnimation}, + animation::{ + animate_targets_and_trigger_events, + triggers::{AnimationEvent, ReflectAnimationEvent}, + AnimationTargetId, RepeatAnimation, + }, pbr::CascadeShadowConfigBuilder, prelude::*, }; @@ -23,6 +27,8 @@ fn main() { setup_scene_once_loaded.before(animate_targets_and_trigger_events), ) .add_systems(Update, keyboard_animation_control) + .observe(FoxStep::observer) + .register_type::() .run(); } @@ -99,14 +105,36 @@ fn setup( println!(" - return: change animation"); } +#[derive(Event, Reflect, Clone)] +#[reflect(AnimationEvent)] +struct FoxStep; + +impl FoxStep { + fn observer(_: Trigger) { + println!("STEP!!!"); + } +} + +impl AnimationEvent for FoxStep {} + // 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, + mut clips: ResMut>, + graphs: Res>, mut players: Query<(Entity, &mut AnimationPlayer), Added>, ) { for (entity, mut player) in &mut players { + let graph = graphs.get(&animations.graph).unwrap(); + let node = graph.get(animations.animations[0]).unwrap(); + let clip = clips.get_mut(node.clip.as_ref().unwrap()).unwrap(); + clip.add_trigger(None, 0.46, FoxStep); + clip.add_trigger(None, 0.64, FoxStep); + clip.add_trigger(None, 0.02, FoxStep); + clip.add_trigger(None, 0.14, FoxStep); + let mut transitions = AnimationTransitions::new(); // Make sure to start the animation via the `AnimationTransitions` From 6df2bf7b95a374b02fb93f74fed073056e0f297b Mon Sep 17 00:00:00 2001 From: rosefarts Date: Mon, 30 Sep 2024 15:59:51 +0200 Subject: [PATCH 06/89] new add_trigger method variant --- crates/bevy_animation/src/lib.rs | 16 +++++++++++++--- examples/animation/animated_fox.rs | 8 ++++---- examples/animation/animation_events.rs | 4 ++-- 3 files changed, 19 insertions(+), 9 deletions(-) diff --git a/crates/bevy_animation/src/lib.rs b/crates/bevy_animation/src/lib.rs index 54c8cf2656d3c..5dea4a5827969 100755 --- a/crates/bevy_animation/src/lib.rs +++ b/crates/bevy_animation/src/lib.rs @@ -609,14 +609,24 @@ impl AnimationClip { /// Add an [`AnimationTrigger`] to an [`AnimationTarget`] named by an [`AnimationTargetId`]. /// /// The `event` will trigger on the entity matching the target once the `time` is reached in the animation. - pub fn add_trigger( + pub fn add_trigger_with_id( &mut self, - target_id: Option, + target_id: AnimationTargetId, time: f32, event: impl AnimationEvent, ) { self.duration = self.duration.max(time); - let triggers = self.triggers.entry(target_id).or_default(); + let triggers = self.triggers.entry(Some(target_id)).or_default(); + triggers.push((time, AnimationTriggerData::new(event))); + triggers.sort_by_key(|(k, _)| FloatOrd(*k)); + } + + /// Add an [`AnimationTrigger`] to an [`AnimationTarget`] named by an [`AnimationTargetId`]. + /// + /// The `event` will trigger on the entity matching the target once the `time` is reached in the animation. + pub fn add_trigger(&mut self, time: f32, event: impl AnimationEvent) { + self.duration = self.duration.max(time); + let triggers = self.triggers.entry(None).or_default(); triggers.push((time, AnimationTriggerData::new(event))); triggers.sort_by_key(|(k, _)| FloatOrd(*k)); } diff --git a/examples/animation/animated_fox.rs b/examples/animation/animated_fox.rs index 81ba89e2b0513..32cfdd2e4f0c5 100644 --- a/examples/animation/animated_fox.rs +++ b/examples/animation/animated_fox.rs @@ -130,10 +130,10 @@ fn setup_scene_once_loaded( let graph = graphs.get(&animations.graph).unwrap(); let node = graph.get(animations.animations[0]).unwrap(); let clip = clips.get_mut(node.clip.as_ref().unwrap()).unwrap(); - clip.add_trigger(None, 0.46, FoxStep); - clip.add_trigger(None, 0.64, FoxStep); - clip.add_trigger(None, 0.02, FoxStep); - clip.add_trigger(None, 0.14, FoxStep); + clip.add_trigger(0.46, FoxStep); + clip.add_trigger(0.64, FoxStep); + clip.add_trigger(0.02, FoxStep); + clip.add_trigger(0.14, FoxStep); let mut transitions = AnimationTransitions::new(); diff --git a/examples/animation/animation_events.rs b/examples/animation/animation_events.rs index 559385efc5746..40f179479c5cd 100644 --- a/examples/animation/animation_events.rs +++ b/examples/animation/animation_events.rs @@ -44,8 +44,8 @@ fn setup( let name = Name::new("abc"); let id = AnimationTargetId::from(&name); - animation.add_trigger(Some(id), 1.0, Say::Hello); - animation.add_trigger(Some(id), 2.0, Say::Bye); + animation.add_trigger_with_id(id, 1.0, Say::Hello); + animation.add_trigger_with_id(id, 2.0, Say::Bye); let (graph, animation_index) = AnimationGraph::from_clip(animations.add(animation)); let mut player = AnimationPlayer::default(); From 44a228fb83ea3d184e66d3383fe4b868886bb205 Mon Sep 17 00:00:00 2001 From: rosefarts Date: Mon, 30 Sep 2024 16:07:01 +0200 Subject: [PATCH 07/89] rename trigger -> event --- .../src/{triggers.rs => events.rs} | 20 +++++++++---------- crates/bevy_animation/src/lib.rs | 14 ++++++------- examples/animation/animated_fox.rs | 10 +++++----- examples/animation/animation_events.rs | 6 +++--- 4 files changed, 25 insertions(+), 25 deletions(-) rename crates/bevy_animation/src/{triggers.rs => events.rs} (93%) diff --git a/crates/bevy_animation/src/triggers.rs b/crates/bevy_animation/src/events.rs similarity index 93% rename from crates/bevy_animation/src/triggers.rs rename to crates/bevy_animation/src/events.rs index ebca48da32b37..66f02a9fc9470 100644 --- a/crates/bevy_animation/src/triggers.rs +++ b/crates/bevy_animation/src/events.rs @@ -80,21 +80,21 @@ impl FromType for ReflectAnimationEvent { } #[derive(TypePath, Debug)] -pub(crate) struct AnimationTriggerData(pub(crate) Box); +pub(crate) struct AnimationEventData(pub(crate) Box); -impl AnimationTriggerData { +impl AnimationEventData { pub(crate) fn new(event: impl Event + PartialReflect) -> Self { Self(Box::new(event)) } } -impl Clone for AnimationTriggerData { +impl Clone for AnimationEventData { fn clone(&self) -> Self { Self(self.0.clone_value()) } } -impl GetTypeRegistration for AnimationTriggerData { +impl GetTypeRegistration for AnimationEventData { fn get_type_registration() -> TypeRegistration { let mut registration = TypeRegistration::of::(); registration.insert::(FromType::::from_type()); @@ -102,7 +102,7 @@ impl GetTypeRegistration for AnimationTriggerData { } } -impl Typed for AnimationTriggerData { +impl Typed for AnimationEventData { fn type_info() -> &'static TypeInfo { static CELL: NonGenericTypeInfoCell = NonGenericTypeInfoCell::new(); CELL.get_or_set(|| { @@ -111,7 +111,7 @@ impl Typed for AnimationTriggerData { } } -impl TupleStruct for AnimationTriggerData { +impl TupleStruct for AnimationEventData { fn field(&self, index: usize) -> Option<&dyn PartialReflect> { match index { 0 => Some(self.0.as_partial_reflect()), @@ -139,7 +139,7 @@ impl TupleStruct for AnimationTriggerData { } } -impl PartialReflect for AnimationTriggerData { +impl PartialReflect for AnimationEventData { #[inline] fn get_represented_type_info(&self) -> Option<&'static TypeInfo> { Some(::type_info()) @@ -207,7 +207,7 @@ impl PartialReflect for AnimationTriggerData { } } -impl Reflect for AnimationTriggerData { +impl Reflect for AnimationEventData { #[inline] fn into_any(self: Box) -> Box { self @@ -245,8 +245,8 @@ impl Reflect for AnimationTriggerData { } } -impl FromReflect for AnimationTriggerData { +impl FromReflect for AnimationEventData { fn from_reflect(reflect: &dyn PartialReflect) -> Option { - Some(reflect.try_downcast_ref::()?.clone()) + Some(reflect.try_downcast_ref::()?.clone()) } } diff --git a/crates/bevy_animation/src/lib.rs b/crates/bevy_animation/src/lib.rs index 5dea4a5827969..79b94b3b99880 100755 --- a/crates/bevy_animation/src/lib.rs +++ b/crates/bevy_animation/src/lib.rs @@ -10,10 +10,10 @@ extern crate alloc; pub mod animatable; +pub mod events; pub mod graph; pub mod keyframes; pub mod transition; -pub mod triggers; mod util; use alloc::collections::BTreeMap; @@ -24,7 +24,7 @@ use core::{ hash::{Hash, Hasher}, iter, }; -use triggers::{trigger_animation_event, AnimationEvent, AnimationTriggerData}; +use events::{trigger_animation_event, AnimationEvent, AnimationEventData}; use bevy_app::{App, Plugin, PostUpdate}; use bevy_asset::{Asset, AssetApp, Assets, Handle}; @@ -475,7 +475,7 @@ pub struct AnimationClip { } pub(crate) type AnimationTriggers = - HashMap, Vec<(f32, AnimationTriggerData)>>; + HashMap, Vec<(f32, AnimationEventData)>>; /// A mapping from [`AnimationTargetId`] (e.g. bone in a skinned mesh) to the /// animation curves. @@ -609,7 +609,7 @@ impl AnimationClip { /// Add an [`AnimationTrigger`] to an [`AnimationTarget`] named by an [`AnimationTargetId`]. /// /// The `event` will trigger on the entity matching the target once the `time` is reached in the animation. - pub fn add_trigger_with_id( + pub fn add_event_with_id( &mut self, target_id: AnimationTargetId, time: f32, @@ -617,17 +617,17 @@ impl AnimationClip { ) { self.duration = self.duration.max(time); let triggers = self.triggers.entry(Some(target_id)).or_default(); - triggers.push((time, AnimationTriggerData::new(event))); + triggers.push((time, AnimationEventData::new(event))); triggers.sort_by_key(|(k, _)| FloatOrd(*k)); } /// Add an [`AnimationTrigger`] to an [`AnimationTarget`] named by an [`AnimationTargetId`]. /// /// The `event` will trigger on the entity matching the target once the `time` is reached in the animation. - pub fn add_trigger(&mut self, time: f32, event: impl AnimationEvent) { + pub fn add_event(&mut self, time: f32, event: impl AnimationEvent) { self.duration = self.duration.max(time); let triggers = self.triggers.entry(None).or_default(); - triggers.push((time, AnimationTriggerData::new(event))); + triggers.push((time, AnimationEventData::new(event))); triggers.sort_by_key(|(k, _)| FloatOrd(*k)); } } diff --git a/examples/animation/animated_fox.rs b/examples/animation/animated_fox.rs index 32cfdd2e4f0c5..8868f0220ac2e 100644 --- a/examples/animation/animated_fox.rs +++ b/examples/animation/animated_fox.rs @@ -5,7 +5,7 @@ use std::{f32::consts::PI, time::Duration}; use bevy::{ animation::{ animate_targets_and_trigger_events, - triggers::{AnimationEvent, ReflectAnimationEvent}, + events::{AnimationEvent, ReflectAnimationEvent}, AnimationTargetId, RepeatAnimation, }, pbr::CascadeShadowConfigBuilder, @@ -130,10 +130,10 @@ fn setup_scene_once_loaded( let graph = graphs.get(&animations.graph).unwrap(); let node = graph.get(animations.animations[0]).unwrap(); let clip = clips.get_mut(node.clip.as_ref().unwrap()).unwrap(); - clip.add_trigger(0.46, FoxStep); - clip.add_trigger(0.64, FoxStep); - clip.add_trigger(0.02, FoxStep); - clip.add_trigger(0.14, FoxStep); + clip.add_event(0.46, FoxStep); + clip.add_event(0.64, FoxStep); + clip.add_event(0.02, FoxStep); + clip.add_event(0.14, FoxStep); let mut transitions = AnimationTransitions::new(); diff --git a/examples/animation/animation_events.rs b/examples/animation/animation_events.rs index 40f179479c5cd..6f85e35390ec6 100644 --- a/examples/animation/animation_events.rs +++ b/examples/animation/animation_events.rs @@ -2,7 +2,7 @@ use bevy::{ animation::{ - triggers::{AnimationEvent, ReflectAnimationEvent}, + events::{AnimationEvent, ReflectAnimationEvent}, AnimationTarget, AnimationTargetId, }, prelude::*, @@ -44,8 +44,8 @@ fn setup( let name = Name::new("abc"); let id = AnimationTargetId::from(&name); - animation.add_trigger_with_id(id, 1.0, Say::Hello); - animation.add_trigger_with_id(id, 2.0, Say::Bye); + animation.add_event_with_id(id, 1.0, Say::Hello); + animation.add_event_with_id(id, 2.0, Say::Bye); let (graph, animation_index) = AnimationGraph::from_clip(animations.add(animation)); let mut player = AnimationPlayer::default(); From 578f5792bf58b4d91b6e21811da47c3ef3915ee0 Mon Sep 17 00:00:00 2001 From: rosefarts Date: Mon, 30 Sep 2024 16:39:47 +0200 Subject: [PATCH 08/89] add some docs --- Cargo.toml | 2 +- crates/bevy_animation/src/events.rs | 3 +++ crates/bevy_animation/src/lib.rs | 8 +++++--- examples/animation/animation_events.rs | 2 +- 4 files changed, 10 insertions(+), 5 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 636305407d88c..dbea9ec30b57b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1177,7 +1177,7 @@ doc-scrape-examples = true [package.metadata.example.animation_events] name = "Animation Events" -description = "TODO: add description" +description = "Demonstrate how to use animation events" category = "Animation" wasm = true diff --git a/crates/bevy_animation/src/events.rs b/crates/bevy_animation/src/events.rs index 66f02a9fc9470..e7a41a9502fe9 100644 --- a/crates/bevy_animation/src/events.rs +++ b/crates/bevy_animation/src/events.rs @@ -7,6 +7,7 @@ use bevy_reflect::{ TupleStructFieldIter, TupleStructInfo, TypeInfo, TypeRegistration, Typed, UnnamedField, }; +// TODO: make a struct pub(crate) fn trigger_animation_event( event: Box, entity: Entity, @@ -52,6 +53,7 @@ pub(crate) fn trigger_animation_event( } } +/// An event that can be used with animations. pub trait AnimationEvent: Event + Reflect + Clone {} #[derive(Clone)] @@ -79,6 +81,7 @@ impl FromType for ReflectAnimationEvent { } } +/// The data that will be used to trigger an animation event. #[derive(TypePath, Debug)] pub(crate) struct AnimationEventData(pub(crate) Box); diff --git a/crates/bevy_animation/src/lib.rs b/crates/bevy_animation/src/lib.rs index 79b94b3b99880..2b30b84820bc3 100755 --- a/crates/bevy_animation/src/lib.rs +++ b/crates/bevy_animation/src/lib.rs @@ -606,7 +606,7 @@ impl AnimationClip { self.curves.entry(target_id).or_default().push(curve); } - /// Add an [`AnimationTrigger`] to an [`AnimationTarget`] named by an [`AnimationTargetId`]. + /// Add an [`AnimationEvent`] to an [`AnimationTarget`] named by an [`AnimationTargetId`]. /// /// The `event` will trigger on the entity matching the target once the `time` is reached in the animation. pub fn add_event_with_id( @@ -621,9 +621,9 @@ impl AnimationClip { triggers.sort_by_key(|(k, _)| FloatOrd(*k)); } - /// Add an [`AnimationTrigger`] to an [`AnimationTarget`] named by an [`AnimationTargetId`]. + /// Add a untargeted [`AnimationEvent`] to this [`AnimationClip`]. /// - /// The `event` will trigger on the entity matching the target once the `time` is reached in the animation. + /// The `event` will trigger on the [`AnimationPlayer`] entity once the `time` is reached in the animation. pub fn add_event(&mut self, time: f32, event: impl AnimationEvent) { self.duration = self.duration.max(time); let triggers = self.triggers.entry(None).or_default(); @@ -1144,6 +1144,7 @@ pub type AnimationEntityMut<'w> = EntityMutExcept< ), >; +/// fn for_each_animation_event<'a, 'b>( target_id: Option, clip: &'a AnimationClip, @@ -1170,6 +1171,7 @@ fn for_each_animation_event<'a, 'b>( } } +/// A system that triggers untargeted animation events for the currently-playing animations. fn trigger_untargeted_animation_events( mut commands: Commands, clips: Res>, diff --git a/examples/animation/animation_events.rs b/examples/animation/animation_events.rs index 6f85e35390ec6..b22eaa9736277 100644 --- a/examples/animation/animation_events.rs +++ b/examples/animation/animation_events.rs @@ -1,4 +1,4 @@ -// TODO: rename to animation_events +//! Demonstrate how to use animation events. use bevy::{ animation::{ From e9c5af1cc174877df660ce0221fed6cc6d49b915 Mon Sep 17 00:00:00 2001 From: rosefarts Date: Mon, 30 Sep 2024 16:54:40 +0200 Subject: [PATCH 09/89] update examples readme --- examples/README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/examples/README.md b/examples/README.md index 96b7c577a4417..83d463b4eca8f 100644 --- a/examples/README.md +++ b/examples/README.md @@ -189,6 +189,7 @@ Example | Description [Animated Fox](../examples/animation/animated_fox.rs) | Plays an animation from a skinned glTF [Animated Transform](../examples/animation/animated_transform.rs) | Create and play an animation defined by code that operates on the `Transform` component [Animated UI](../examples/animation/animated_ui.rs) | Shows how to use animation clips to animate UI properties +[Animation Events](../examples/animation/animation_events.rs) | Demonstrate how to use animation events [Animation Graph](../examples/animation/animation_graph.rs) | Blends multiple animations together with a graph [Animation Masks](../examples/animation/animation_masks.rs) | Demonstrates animation masks [Color animation](../examples/animation/color_animation.rs) | Demonstrates how to animate colors using mixing and splines in different color spaces From 463a3b02169fcf007599359421c2e34ce941e22f Mon Sep 17 00:00:00 2001 From: rosefarts Date: Mon, 30 Sep 2024 17:28:07 +0200 Subject: [PATCH 10/89] edit comments --- crates/bevy_animation/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/bevy_animation/src/lib.rs b/crates/bevy_animation/src/lib.rs index 2b30b84820bc3..c430a51e906d0 100755 --- a/crates/bevy_animation/src/lib.rs +++ b/crates/bevy_animation/src/lib.rs @@ -1144,7 +1144,6 @@ pub type AnimationEntityMut<'w> = EntityMutExcept< ), >; -/// fn for_each_animation_event<'a, 'b>( target_id: Option, clip: &'a AnimationClip, @@ -1156,6 +1155,7 @@ fn for_each_animation_event<'a, 'b>( .get(&target_id) .iter() .flat_map(|t| t.iter()) + // filter out events that did not occur this tick .filter(|(t, _)| match animation.is_playback_reversed() { true => { *t >= animation.seek_time From 841e5171f718768eff0cf8e9f2d61fe27bad93b0 Mon Sep 17 00:00:00 2001 From: rosefarts Date: Mon, 30 Sep 2024 20:36:29 +0200 Subject: [PATCH 11/89] use Box --- crates/bevy_animation/src/events.rs | 89 +++++++------------------- crates/bevy_animation/src/lib.rs | 10 +-- examples/animation/animated_fox.rs | 10 ++- examples/animation/animation_events.rs | 10 ++- 4 files changed, 45 insertions(+), 74 deletions(-) diff --git a/crates/bevy_animation/src/events.rs b/crates/bevy_animation/src/events.rs index e7a41a9502fe9..f164ee9f58425 100644 --- a/crates/bevy_animation/src/events.rs +++ b/crates/bevy_animation/src/events.rs @@ -1,4 +1,4 @@ -use std::any::Any; +use std::{any::Any, fmt::Debug}; use bevy_ecs::prelude::*; use bevy_reflect::{ @@ -7,93 +7,47 @@ use bevy_reflect::{ TupleStructFieldIter, TupleStructInfo, TypeInfo, TypeRegistration, Typed, UnnamedField, }; -// TODO: make a struct pub(crate) fn trigger_animation_event( - event: Box, + event: Box, entity: Entity, ) -> impl Command { move |world: &mut World| { - let (from_reflect, animation_event) = { - let registry = world - .get_resource::() - .expect("Missing resource `AppTypeRegistry`"); - let lock = registry.read(); - let type_info = event.get_represented_type_info().unwrap(); // FIXME: when would this fail? - let registration = lock - .get_with_type_path(type_info.type_path()) - .unwrap_or_else(|| { - panic!( - "Missing type registration for type: `{}`", - type_info.type_path() - ) - }); - ( - registration - .data::() - .cloned() - .unwrap_or_else(|| { - panic!( - "Type `{}` is not registered with data: `ReflectFromReflect`", - type_info.type_path() - ) - }), - registration - .data::() - .cloned() - .unwrap_or_else(|| { - panic!( - "Type `{}` is not registered with data: `ReflectAnimationEvent`", - type_info.type_path() - ) - }), - ) - }; - let event = from_reflect.from_reflect(event.as_ref()).unwrap(); // FIXME: when would this fail? - animation_event.trigger(event.as_ref(), entity, world); + event.trigger(entity, world); } } /// An event that can be used with animations. -pub trait AnimationEvent: Event + Reflect + Clone {} +#[reflect_trait] +pub trait AnimationEvent: Reflect + Send + Sync { + /// Trigger the event, targeting `entity`. + fn trigger(&self, entity: Entity, world: &mut World); -#[derive(Clone)] -pub struct ReflectAnimationEvent { - trigger: fn(&dyn Reflect, Entity, &mut World), + /// Clone this value into a new `Box` + fn clone_value(&self) -> Box; } -impl ReflectAnimationEvent { - /// # Panics - /// - /// Panics if the underlying type of `event` does not match the type this `ReflectAnimationEvent` was constructed for. - pub(crate) fn trigger(&self, event: &dyn Reflect, entity: Entity, world: &mut World) { - (self.trigger)(event, entity, world) - } -} - -impl FromType for ReflectAnimationEvent { - fn from_type() -> Self { - Self { - trigger: |value, entity, world| { - let event = value.downcast_ref::().unwrap().clone(); - world.entity_mut(entity).trigger(event); - }, - } +/// The data that will be used to trigger an animation event. +#[derive(TypePath)] +pub(crate) struct AnimationEventData(pub(crate) Box); + +impl Debug for AnimationEventData { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str("AnimationEventData(")?; + PartialReflect::debug(self.0.as_ref(), f)?; + f.write_str(")")?; + Ok(()) } } -/// The data that will be used to trigger an animation event. -#[derive(TypePath, Debug)] -pub(crate) struct AnimationEventData(pub(crate) Box); - impl AnimationEventData { - pub(crate) fn new(event: impl Event + PartialReflect) -> Self { + pub(crate) fn new(event: impl AnimationEvent) -> Self { Self(Box::new(event)) } } impl Clone for AnimationEventData { fn clone(&self) -> Self { - Self(self.0.clone_value()) + Self(AnimationEvent::clone_value(self.0.as_ref())) } } @@ -101,6 +55,7 @@ impl GetTypeRegistration for AnimationEventData { fn get_type_registration() -> TypeRegistration { let mut registration = TypeRegistration::of::(); registration.insert::(FromType::::from_type()); + registration.insert::(FromType::::from_type()); registration } } diff --git a/crates/bevy_animation/src/lib.rs b/crates/bevy_animation/src/lib.rs index c430a51e906d0..8518deb0ac6f0 100755 --- a/crates/bevy_animation/src/lib.rs +++ b/crates/bevy_animation/src/lib.rs @@ -1144,11 +1144,11 @@ pub type AnimationEntityMut<'w> = EntityMutExcept< ), >; -fn for_each_animation_event<'a, 'b>( +fn for_each_animation_event( target_id: Option, - clip: &'a AnimationClip, + clip: &AnimationClip, animation: &ActiveAnimation, - mut f: impl FnMut(f32, Box), + mut f: impl FnMut(f32, Box), ) { for (t, e) in clip .triggers @@ -1167,7 +1167,7 @@ fn for_each_animation_event<'a, 'b>( } }) { - f(*t, e.0.clone_value()) + f(*t, e.clone().0); } } @@ -1288,7 +1288,7 @@ pub fn animate_targets_and_trigger_events( dbg!(active_animation.seek_time); par_commands.command_scope(|mut commands| { commands.queue(trigger_animation_event(event, entity)); - }) + }); }); let Some(curves) = clip.curves_for_target(target_id) else { diff --git a/examples/animation/animated_fox.rs b/examples/animation/animated_fox.rs index 8868f0220ac2e..5744634007f86 100644 --- a/examples/animation/animated_fox.rs +++ b/examples/animation/animated_fox.rs @@ -115,7 +115,15 @@ impl FoxStep { } } -impl AnimationEvent for FoxStep {} +impl AnimationEvent for FoxStep { + fn trigger(&self, entity: Entity, world: &mut World) { + world.entity_mut(entity).trigger(self.clone()); + } + + fn clone_value(&self) -> Box { + Box::new(self.clone()) + } +} // An `AnimationPlayer` is automatically added to the scene when it's ready. // When the player is added, start the animation. diff --git a/examples/animation/animation_events.rs b/examples/animation/animation_events.rs index b22eaa9736277..82bf4d24f7803 100644 --- a/examples/animation/animation_events.rs +++ b/examples/animation/animation_events.rs @@ -33,7 +33,15 @@ impl Say { } } -impl AnimationEvent for Say {} +impl AnimationEvent for Say { + fn trigger(&self, entity: Entity, world: &mut World) { + world.entity_mut(entity).trigger(self.clone()); + } + + fn clone_value(&self) -> Box { + Box::new(self.clone()) + } +} fn setup( mut commands: Commands, From fce8da260b832b1484c9c6094d49f1f62b998884 Mon Sep 17 00:00:00 2001 From: rosefarts Date: Mon, 30 Sep 2024 20:40:37 +0200 Subject: [PATCH 12/89] module doc --- crates/bevy_animation/src/events.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/bevy_animation/src/events.rs b/crates/bevy_animation/src/events.rs index f164ee9f58425..a01fd360b75ed 100644 --- a/crates/bevy_animation/src/events.rs +++ b/crates/bevy_animation/src/events.rs @@ -1,4 +1,5 @@ use std::{any::Any, fmt::Debug}; +//! Traits and types for triggering events from animations. use bevy_ecs::prelude::*; use bevy_reflect::{ From c29234339a59cc7115237f437216f7e78c02035d Mon Sep 17 00:00:00 2001 From: rosefarts Date: Mon, 30 Sep 2024 20:40:46 +0200 Subject: [PATCH 13/89] use core instead of std --- crates/bevy_animation/src/events.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/crates/bevy_animation/src/events.rs b/crates/bevy_animation/src/events.rs index a01fd360b75ed..873c5c25ed5dd 100644 --- a/crates/bevy_animation/src/events.rs +++ b/crates/bevy_animation/src/events.rs @@ -1,6 +1,7 @@ -use std::{any::Any, fmt::Debug}; //! Traits and types for triggering events from animations. +use core::{any::Any, fmt::Debug}; + use bevy_ecs::prelude::*; use bevy_reflect::{ prelude::*, utility::NonGenericTypeInfoCell, ApplyError, DynamicTupleStruct, FromType, From 6dee939cec2cc6d6656ecdfdb4f8ac474d86083c Mon Sep 17 00:00:00 2001 From: rosefarts Date: Mon, 30 Sep 2024 20:45:47 +0200 Subject: [PATCH 14/89] use core instead of std (again) --- crates/bevy_animation/src/events.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/bevy_animation/src/events.rs b/crates/bevy_animation/src/events.rs index 873c5c25ed5dd..838d622fced56 100644 --- a/crates/bevy_animation/src/events.rs +++ b/crates/bevy_animation/src/events.rs @@ -33,7 +33,7 @@ pub trait AnimationEvent: Reflect + Send + Sync { pub(crate) struct AnimationEventData(pub(crate) Box); impl Debug for AnimationEventData { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { f.write_str("AnimationEventData(")?; PartialReflect::debug(self.0.as_ref(), f)?; f.write_str(")")?; From 8a6aabf9f12f97830eeeb53501be2d26d945389c Mon Sep 17 00:00:00 2001 From: rosefarts Date: Mon, 30 Sep 2024 22:15:11 +0200 Subject: [PATCH 15/89] add `AnimationTargetId::from_str` --- crates/bevy_animation/src/lib.rs | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/crates/bevy_animation/src/lib.rs b/crates/bevy_animation/src/lib.rs index 8518deb0ac6f0..5bf8ff092b601 100755 --- a/crates/bevy_animation/src/lib.rs +++ b/crates/bevy_animation/src/lib.rs @@ -1380,6 +1380,20 @@ impl Plugin for AnimationPlugin { } impl AnimationTargetId { + ///Creates a new [`AnimationTargetId`] by hashing a string seperated by `/`. + /// + /// Typically, this will be the path from the animation root to the + /// animation target (e.g. bone) that is to be animated. + pub fn from_str(path: impl AsRef) -> Self { + let mut blake3 = blake3::Hasher::new(); + blake3.update(ANIMATION_TARGET_NAMESPACE.as_bytes()); + for str in path.as_ref().split('/') { + blake3.update(str.as_bytes()); + } + let hash = blake3.finalize().as_bytes()[0..16].try_into().unwrap(); + Self(*uuid::Builder::from_sha1_bytes(hash).as_uuid()) + } + /// Creates a new [`AnimationTargetId`] by hashing a list of names. /// /// Typically, this will be the path from the animation root to the From c544f7a8b6072859964a6763041b673710275d97 Mon Sep 17 00:00:00 2001 From: rosefarts Date: Mon, 30 Sep 2024 22:15:18 +0200 Subject: [PATCH 16/89] remove dbg --- crates/bevy_animation/src/lib.rs | 2 -- 1 file changed, 2 deletions(-) diff --git a/crates/bevy_animation/src/lib.rs b/crates/bevy_animation/src/lib.rs index 5bf8ff092b601..28188a166aa3e 100755 --- a/crates/bevy_animation/src/lib.rs +++ b/crates/bevy_animation/src/lib.rs @@ -1190,7 +1190,6 @@ fn trigger_untargeted_animation_events( }; let clip = clips.get(clip_id).unwrap(); for_each_animation_event(None, clip, animation, |_, event| { - dbg!(animation.seek_time); commands.queue(trigger_animation_event(event, entity)); }); } @@ -1285,7 +1284,6 @@ pub fn animate_targets_and_trigger_events( }; for_each_animation_event(Some(target_id), clip, active_animation, |_, event| { - dbg!(active_animation.seek_time); par_commands.command_scope(|mut commands| { commands.queue(trigger_animation_event(event, entity)); }); From c0e6d11b58b824b024709eba437dd9c6d8c0d637 Mon Sep 17 00:00:00 2001 From: rosefarts Date: Mon, 30 Sep 2024 22:15:31 +0200 Subject: [PATCH 17/89] update examples --- examples/animation/animated_fox.rs | 122 ++++++++++++++++++++----- examples/animation/animation_events.rs | 16 +--- 2 files changed, 102 insertions(+), 36 deletions(-) diff --git a/examples/animation/animated_fox.rs b/examples/animation/animated_fox.rs index 5744634007f86..9cd171d624bf1 100644 --- a/examples/animation/animated_fox.rs +++ b/examples/animation/animated_fox.rs @@ -8,6 +8,7 @@ use bevy::{ events::{AnimationEvent, ReflectAnimationEvent}, AnimationTargetId, RepeatAnimation, }, + color::palettes::css::WHITE, pbr::CascadeShadowConfigBuilder, prelude::*, }; @@ -21,12 +22,15 @@ fn main() { brightness: 2000., }) .add_plugins(DefaultPlugins) + .init_resource::() + .init_resource::() .add_systems(Startup, setup) .add_systems( Update, setup_scene_once_loaded.before(animate_targets_and_trigger_events), ) .add_systems(Update, keyboard_animation_control) + .add_systems(Update, update_particle) .observe(FoxStep::observer) .register_type::() .run(); @@ -39,6 +43,81 @@ struct Animations { graph: Handle, } +#[derive(Resource)] +struct FoxFeetIds { + forward_right: AnimationTargetId, + forward_left: AnimationTargetId, + back_right: AnimationTargetId, + back_left: AnimationTargetId, +} + +impl Default for FoxFeetIds { + fn default() -> Self { + Self { + forward_right: AnimationTargetId::from_str("root/_rootJoint/b_Root_00/b_Hip_01/b_Spine01_02/b_Spine02_03/b_RightUpperArm_06/b_RightForeArm_07/b_RightHand_08"), + forward_left: AnimationTargetId::from_str("root/_rootJoint/b_Root_00/b_Hip_01/b_Spine01_02/b_Spine02_03/b_LeftUpperArm_09/b_LeftForeArm_010/b_LeftHand_011"), + back_right: AnimationTargetId::from_str("root/_rootJoint/b_Root_00/b_Hip_01/b_RightLeg01_019/b_RightLeg02_020/b_RightFoot01_021/b_RightFoot02_022"), + back_left: AnimationTargetId::from_str("root/_rootJoint/b_Root_00/b_Hip_01/b_LeftLeg01_015/b_LeftLeg02_016/b_LeftFoot01_017/b_LeftFoot02_018"), + } + } +} + +#[derive(Component, Debug)] +struct Particle { + size: f32, + lifetime: Timer, +} + +#[derive(Resource, Event, Reflect, Clone)] +#[reflect(AnimationEvent)] +struct FoxStep { + mesh: Handle, + material: Handle, +} + +impl FromWorld for FoxStep { + fn from_world(world: &mut World) -> Self { + let mesh = world.resource_mut::>().add(Sphere::new(10.0)); + let material = world + .resource_mut::>() + .add(StandardMaterial::from_color(WHITE)); + Self { mesh, material } + } +} + +impl FoxStep { + fn observer(trigger: Trigger, query: Query<&GlobalTransform>, mut commands: Commands) { + let transform = query.get(trigger.entity()).unwrap().compute_transform(); + commands.spawn(( + Particle { + lifetime: Timer::from_seconds(1.0, TimerMode::Once), + size: 1.0, + }, + MaterialMeshBundle { + mesh: trigger.event().mesh.clone(), + material: trigger.event().material.clone(), + transform: Transform { + translation: transform.translation.reject_from(Vec3::Y), + scale: Vec3::splat(1.0), + ..Default::default() + }, + ..Default::default() + }, + )); + // println!("STEP: {}", transform.translation); + } +} + +impl AnimationEvent for FoxStep { + fn trigger(&self, entity: Entity, world: &mut World) { + world.entity_mut(entity).trigger(self.clone()); + } + + fn clone_value(&self) -> Box { + Box::new(self.clone()) + } +} + fn setup( mut commands: Commands, asset_server: Res, @@ -105,31 +184,13 @@ fn setup( println!(" - return: change animation"); } -#[derive(Event, Reflect, Clone)] -#[reflect(AnimationEvent)] -struct FoxStep; - -impl FoxStep { - fn observer(_: Trigger) { - println!("STEP!!!"); - } -} - -impl AnimationEvent for FoxStep { - fn trigger(&self, entity: Entity, world: &mut World) { - world.entity_mut(entity).trigger(self.clone()); - } - - fn clone_value(&self) -> Box { - Box::new(self.clone()) - } -} - // 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, + feet: Res, + step: Res, mut clips: ResMut>, graphs: Res>, mut players: Query<(Entity, &mut AnimationPlayer), Added>, @@ -138,10 +199,10 @@ fn setup_scene_once_loaded( let graph = graphs.get(&animations.graph).unwrap(); let node = graph.get(animations.animations[0]).unwrap(); let clip = clips.get_mut(node.clip.as_ref().unwrap()).unwrap(); - clip.add_event(0.46, FoxStep); - clip.add_event(0.64, FoxStep); - clip.add_event(0.02, FoxStep); - clip.add_event(0.14, FoxStep); + clip.add_event_with_id(feet.forward_right, 0.46, step.clone()); + clip.add_event_with_id(feet.forward_left, 0.64, step.clone()); + clip.add_event_with_id(feet.back_right, 0.14, step.clone()); + clip.add_event_with_id(feet.back_left, 0.02, step.clone()); let mut transitions = AnimationTransitions::new(); @@ -243,3 +304,16 @@ fn keyboard_animation_control( } } } + +fn update_particle( + mut commands: Commands, + mut query: Query<(Entity, &mut Particle, &mut Transform)>, + time: Res