From d9190e4ff63eb1dbc6cc29122d0f84149ee7d4b6 Mon Sep 17 00:00:00 2001 From: poopy Date: Sun, 6 Oct 2024 12:03:05 +0200 Subject: [PATCH] Add Support for Triggering Events via `AnimationEvent`s (#15538) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # Objective Add support for events that can be triggered from animation clips. This is useful when you need something to happen at a specific time in an animation. For example, playing a sound every time a characters feet hits the ground when walking. Closes #15494 ## Solution Added a new field to `AnimationClip`: `events`, which contains a list of `AnimationEvent`s. These are automatically triggered in `animate_targets` and `trigger_untargeted_animation_events`. ## Testing Added a couple of tests and example (`animation_events.rs`) to make sure events are triggered when expected. --- ## Showcase `Events` need to also implement `AnimationEvent` and `Reflect` to be used with animations. ```rust #[derive(Event, AnimationEvent, Reflect)] struct SomeEvent; ``` Events can be added to an `AnimationClip` by specifying a time and event. ```rust // trigger an event after 1.0 second animation_clip.add_event(1.0, SomeEvent); ``` And optionally, providing a target id. ```rust let id = AnimationTargetId::from_iter(["shoulder", "arm", "hand"]); animation_clip.add_event_to_target(id, 1.0, HandEvent); ``` I modified the `animated_fox` example to show off the feature. ![CleanShot 2024-10-05 at 02 41 57](https://github.com/user-attachments/assets/0bb47db7-24f9-4504-88f1-40e375b89b1b) --------- Co-authored-by: Matty Co-authored-by: Chris Biscardi Co-authored-by: François Mockers --- Cargo.toml | 11 + crates/bevy_animation/Cargo.toml | 1 + crates/bevy_animation/derive/Cargo.toml | 25 + crates/bevy_animation/derive/src/lib.rs | 29 ++ crates/bevy_animation/src/animation_event.rs | 281 +++++++++++ crates/bevy_animation/src/lib.rs | 486 ++++++++++++++++++- examples/README.md | 1 + examples/animation/animated_fox.rs | 197 +++++++- examples/animation/animation_events.rs | 121 +++++ 9 files changed, 1144 insertions(+), 8 deletions(-) create mode 100644 crates/bevy_animation/derive/Cargo.toml create mode 100644 crates/bevy_animation/derive/src/lib.rs create mode 100644 crates/bevy_animation/src/animation_event.rs create mode 100644 examples/animation/animation_events.rs diff --git a/Cargo.toml b/Cargo.toml index bd4efd8337..320ab09977 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1188,6 +1188,17 @@ doc-scrape-examples = true hidden = true # Animation +[[example]] +name = "animation_events" +path = "examples/animation/animation_events.rs" +doc-scrape-examples = true + +[package.metadata.example.animation_events] +name = "Animation Events" +description = "Demonstrate how to use animation events" +category = "Animation" +wasm = true + [[example]] name = "animated_fox" path = "examples/animation/animated_fox.rs" diff --git a/crates/bevy_animation/Cargo.toml b/crates/bevy_animation/Cargo.toml index 33cec87758..faef0cdbb2 100644 --- a/crates/bevy_animation/Cargo.toml +++ b/crates/bevy_animation/Cargo.toml @@ -10,6 +10,7 @@ keywords = ["bevy"] [dependencies] # bevy +bevy_animation_derive = { path = "derive", version = "0.15.0-dev" } bevy_app = { path = "../bevy_app", version = "0.15.0-dev" } bevy_asset = { path = "../bevy_asset", version = "0.15.0-dev" } bevy_color = { path = "../bevy_color", version = "0.15.0-dev" } diff --git a/crates/bevy_animation/derive/Cargo.toml b/crates/bevy_animation/derive/Cargo.toml new file mode 100644 index 0000000000..ad44a82b00 --- /dev/null +++ b/crates/bevy_animation/derive/Cargo.toml @@ -0,0 +1,25 @@ +[package] +name = "bevy_animation_derive" +version = "0.15.0-dev" +edition = "2021" +description = "Derive implementations for bevy_animation" +homepage = "https://bevyengine.org" +repository = "https://github.com/bevyengine/bevy" +license = "MIT OR Apache-2.0" +keywords = ["bevy"] + +[lib] +proc-macro = true + +[dependencies] +bevy_macro_utils = { path = "../../bevy_macro_utils", version = "0.15.0-dev" } +proc-macro2 = "1.0" +quote = "1.0" +syn = { version = "2.0", features = ["full"] } + +[lints] +workspace = true + +[package.metadata.docs.rs] +rustdoc-args = ["-Zunstable-options", "--generate-link-to-definition"] +all-features = true diff --git a/crates/bevy_animation/derive/src/lib.rs b/crates/bevy_animation/derive/src/lib.rs new file mode 100644 index 0000000000..48bce1163e --- /dev/null +++ b/crates/bevy_animation/derive/src/lib.rs @@ -0,0 +1,29 @@ +//! Derive macros for `bevy_animation`. + +extern crate proc_macro; + +use bevy_macro_utils::BevyManifest; +use proc_macro::TokenStream; +use quote::quote; +use syn::{parse_macro_input, DeriveInput}; + +/// Used to derive `AnimationEvent` for a type. +#[proc_macro_derive(AnimationEvent)] +pub fn derive_animation_event(input: TokenStream) -> TokenStream { + let ast = parse_macro_input!(input as DeriveInput); + let name = ast.ident; + let manifest = BevyManifest::default(); + let bevy_animation_path = manifest.get_path("bevy_animation"); + let bevy_ecs_path = manifest.get_path("bevy_ecs"); + let animation_event_path = quote! { #bevy_animation_path::animation_event }; + let (impl_generics, ty_generics, where_clause) = ast.generics.split_for_impl(); + // TODO: This could derive Event as well. + quote! { + impl #impl_generics #animation_event_path::AnimationEvent for #name #ty_generics #where_clause { + fn trigger(&self, _time: f32, _weight: f32, entity: #bevy_ecs_path::entity::Entity, world: &mut #bevy_ecs_path::world::World) { + world.entity_mut(entity).trigger(Clone::clone(self)); + } + } + } + .into() +} diff --git a/crates/bevy_animation/src/animation_event.rs b/crates/bevy_animation/src/animation_event.rs new file mode 100644 index 0000000000..825ad8e362 --- /dev/null +++ b/crates/bevy_animation/src/animation_event.rs @@ -0,0 +1,281 @@ +//! 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, + GetTypeRegistration, ReflectFromPtr, ReflectKind, ReflectMut, ReflectOwned, ReflectRef, + TupleStructFieldIter, TupleStructInfo, TypeInfo, TypeRegistration, Typed, UnnamedField, +}; + +pub use bevy_animation_derive::AnimationEvent; + +pub(crate) fn trigger_animation_event( + entity: Entity, + time: f32, + weight: f32, + event: Box, +) -> impl Command { + move |world: &mut World| { + event.trigger(time, weight, entity, world); + } +} + +/// An event that can be used with animations. +/// It can be derived to trigger as an observer event, +/// if you need more complex behaviour, consider +/// a manual implementation. +/// +/// # Example +/// +/// ```rust +/// # use bevy_animation::prelude::*; +/// # use bevy_ecs::prelude::*; +/// # use bevy_reflect::prelude::*; +/// # use bevy_asset::prelude::*; +/// # +/// #[derive(Event, AnimationEvent, Reflect, Clone)] +/// struct Say(String); +/// +/// fn on_say(trigger: Trigger) { +/// println!("{}", trigger.event().0); +/// } +/// +/// fn setup_animation( +/// mut commands: Commands, +/// mut animations: ResMut>, +/// mut graphs: ResMut>, +/// ) { +/// // Create a new animation and add an event at 1.0s. +/// let mut animation = AnimationClip::default(); +/// animation.add_event(1.0, Say("Hello".into())); +/// +/// // Create an animation graph. +/// let (graph, animation_index) = AnimationGraph::from_clip(animations.add(animation)); +/// +/// // Start playing the animation. +/// let mut player = AnimationPlayer::default(); +/// player.play(animation_index).repeat(); +/// +/// commands.spawn((graphs.add(graph), player)); +/// } +/// # +/// # bevy_ecs::system::assert_is_system(setup_animation); +/// ``` +#[reflect_trait] +pub trait AnimationEvent: CloneableAnimationEvent + Reflect + Send + Sync { + /// Trigger the event, targeting `entity`. + fn trigger(&self, time: f32, weight: f32, entity: Entity, world: &mut World); +} + +/// This trait exist so that manual implementors of [`AnimationEvent`] +/// do not have to implement `clone_value`. +#[diagnostic::on_unimplemented( + message = "`{Self}` does not implement `Clone`", + note = "consider annotating `{Self}` with `#[derive(Clone)]`" +)] +pub trait CloneableAnimationEvent { + /// Clone this value into a new `Box` + fn clone_value(&self) -> Box; +} + +impl CloneableAnimationEvent for T { + fn clone_value(&self) -> Box { + Box::new(self.clone()) + } +} + +/// The data that will be used to trigger an animation event. +#[derive(TypePath)] +pub(crate) struct AnimationEventData(pub(crate) Box); + +impl AnimationEventData { + pub(crate) fn new(event: impl AnimationEvent) -> Self { + Self(Box::new(event)) + } +} + +impl Debug for AnimationEventData { + 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(")")?; + Ok(()) + } +} + +impl Clone for AnimationEventData { + fn clone(&self) -> Self { + Self(CloneableAnimationEvent::clone_value(self.0.as_ref())) + } +} + +// We have to implement `GetTypeRegistration` manually because of the embedded +// `Box`, which can't be automatically derived yet. +impl GetTypeRegistration for AnimationEventData { + fn get_type_registration() -> TypeRegistration { + let mut registration = TypeRegistration::of::(); + registration.insert::(FromType::::from_type()); + registration + } +} + +// We have to implement `Typed` manually because of the embedded +// `Box`, which can't be automatically derived yet. +impl Typed for AnimationEventData { + fn type_info() -> &'static TypeInfo { + static CELL: NonGenericTypeInfoCell = NonGenericTypeInfoCell::new(); + CELL.get_or_set(|| { + TypeInfo::TupleStruct(TupleStructInfo::new::(&[UnnamedField::new::<()>(0)])) + }) + } +} + +// We have to implement `TupleStruct` manually because of the embedded +// `Box`, which can't be automatically derived yet. +impl TupleStruct for AnimationEventData { + 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)]) + } +} + +// We have to implement `PartialReflect` manually because of the embedded +// `Box`, which can't be automatically derived yet. +impl PartialReflect for AnimationEventData { + #[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)) + } +} + +// We have to implement `Reflect` manually because of the embedded +// `Box`, which can't be automatically derived yet. +impl Reflect for AnimationEventData { + #[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(()) + } +} + +// We have to implement `FromReflect` manually because of the embedded +// `Box`, which can't be automatically derived yet. +impl FromReflect for AnimationEventData { + fn from_reflect(reflect: &dyn PartialReflect) -> Option { + Some(reflect.try_downcast_ref::()?.clone()) + } +} diff --git a/crates/bevy_animation/src/lib.rs b/crates/bevy_animation/src/lib.rs index f4b24807c1..ce849d0689 100755 --- a/crates/bevy_animation/src/lib.rs +++ b/crates/bevy_animation/src/lib.rs @@ -11,17 +11,19 @@ extern crate alloc; pub mod animatable; pub mod animation_curves; +pub mod animation_event; pub mod gltf_curves; pub mod graph; pub mod transition; mod util; +use animation_event::{trigger_animation_event, AnimationEvent, AnimationEventData}; use core::{ any::{Any, TypeId}, cell::RefCell, fmt::Debug, hash::{Hash, Hasher}, - iter, + iter, slice, }; use graph::AnimationNodeType; use prelude::AnimationCurveEvaluator; @@ -37,6 +39,7 @@ use bevy_ecs::{ reflect::{ReflectMapEntities, ReflectVisitEntities, ReflectVisitEntitiesMut}, world::EntityMutExcept, }; +use bevy_math::FloatOrd; use bevy_reflect::{ prelude::ReflectDefault, utility::NonGenericTypeInfoCell, ApplyError, DynamicTupleStruct, FromReflect, FromType, GetTypeRegistration, PartialReflect, Reflect, ReflectFromPtr, @@ -61,8 +64,12 @@ use uuid::Uuid; pub mod prelude { #[doc(hidden)] pub use crate::{ - animatable::*, animation_curves::*, graph::*, transition::*, AnimationClip, - AnimationPlayer, AnimationPlugin, VariableCurve, + animatable::*, + animation_curves::*, + animation_event::{AnimationEvent, ReflectAnimationEvent}, + graph::*, + transition::*, + AnimationClip, AnimationPlayer, AnimationPlugin, VariableCurve, }; } @@ -275,9 +282,24 @@ impl Typed for VariableCurve { #[derive(Asset, Reflect, Clone, Debug, Default)] pub struct AnimationClip { curves: AnimationCurves, + events: AnimationEvents, duration: f32, } +#[derive(Reflect, Debug, Clone)] +struct TimedAnimationEvent { + time: f32, + event: AnimationEventData, +} + +#[derive(Reflect, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone)] +enum AnimationEventTarget { + Root, + Node(AnimationTargetId), +} + +type AnimationEvents = HashMap>; + /// A mapping from [`AnimationTargetId`] (e.g. bone in a skinned mesh) to the /// animation curves. pub type AnimationCurves = HashMap, NoOpHash>; @@ -435,6 +457,50 @@ impl AnimationClip { .or_default() .push(variable_curve); } + + /// Add an [`AnimationEvent`] to an [`AnimationTarget`] named by an [`AnimationTargetId`]. + /// + /// The `event` will trigger on the entity matching the target once the `time` (in seconds) + /// is reached in the animation. + /// + /// Use [`add_event`](Self::add_event) instead if you don't have a specific target. + pub fn add_event_to_target( + &mut self, + target_id: AnimationTargetId, + time: f32, + event: impl AnimationEvent, + ) { + self.add_event_to_target_inner(AnimationEventTarget::Node(target_id), time, event); + } + + /// Add a untargeted [`AnimationEvent`] to this [`AnimationClip`]. + /// + /// The `event` will trigger on the [`AnimationPlayer`] entity once the `time` (in seconds) + /// is reached in the animation. + /// + /// See also [`add_event_to_target`](Self::add_event_to_target). + pub fn add_event(&mut self, time: f32, event: impl AnimationEvent) { + self.add_event_to_target_inner(AnimationEventTarget::Root, time, event); + } + + fn add_event_to_target_inner( + &mut self, + target: AnimationEventTarget, + time: f32, + event: impl AnimationEvent, + ) { + self.duration = self.duration.max(time); + let triggers = self.events.entry(target).or_default(); + match triggers.binary_search_by_key(&FloatOrd(time), |e| FloatOrd(e.time)) { + Ok(index) | Err(index) => triggers.insert( + index, + TimedAnimationEvent { + time, + event: AnimationEventData::new(event), + }, + ), + } + } } /// Repetition behavior of an animation. @@ -489,9 +555,13 @@ pub struct ActiveAnimation { /// /// Note: This will always be in the range [0.0, animation clip duration] seek_time: f32, + /// The `seek_time` of the previous tick, if any. + 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, + /// `true` if the animation was completed at least once this tick. + just_completed: bool, paused: bool, } @@ -503,7 +573,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, } } @@ -525,6 +597,9 @@ impl ActiveAnimation { /// Update the animation given the delta time and the duration of the clip being played. #[inline] fn update(&mut self, delta: f32, clip_duration: f32) { + self.just_completed = false; + self.last_seek_time = Some(self.seek_time); + if self.is_finished() { return; } @@ -536,6 +611,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() { @@ -553,8 +629,10 @@ impl ActiveAnimation { /// Reset back to the initial state as if no time has elapsed. pub fn replay(&mut self) { + self.just_completed = false; self.completions = 0; self.elapsed = 0.0; + self.last_seek_time = None; self.seek_time = 0.0; } @@ -639,13 +717,33 @@ impl ActiveAnimation { } /// Seeks to a specific time in the animation. + /// + /// This will not trigger events between the current time and `seek_time`. + /// Use [`seek_to`](Self::seek_to) if this is desired. + pub fn set_seek_time(&mut self, seek_time: f32) -> &mut Self { + self.last_seek_time = Some(seek_time); + self.seek_time = seek_time; + self + } + + /// Seeks to a specific time in the animation. + /// + /// Note that any events between the current time and `seek_time` + /// will be triggered on the next update. + /// Use [`set_seek_time`](Self::set_seek_time) if this is undisered. pub fn seek_to(&mut self, seek_time: f32) -> &mut Self { + self.last_seek_time = Some(self.seek_time); self.seek_time = seek_time; self } /// Seeks to the beginning of the animation. + /// + /// Note that any events between the current time and `0.0` + /// will be triggered on the next update. + /// Use [`set_seek_time`](Self::set_seek_time) if this is undisered. pub fn rewind(&mut self) -> &mut Self { + self.last_seek_time = Some(self.seek_time); self.seek_time = 0.0; self } @@ -839,6 +937,49 @@ impl AnimationPlayer { } } +/// A system that triggers untargeted animation events for the currently-playing animations. +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, active_animation) in player.active_animations.iter() { + let Some(clip) = graph + .get(*index) + .and_then(|node| match &node.node_type { + AnimationNodeType::Clip(handle) => Some(handle), + AnimationNodeType::Blend | AnimationNodeType::Add => None, + }) + .and_then(|id| clips.get(id)) + else { + continue; + }; + + let Some(triggered_events) = + TriggeredEvents::from_animation(AnimationEventTarget::Root, clip, active_animation) + else { + continue; + }; + + for TimedAnimationEvent { time, event } in triggered_events.iter() { + commands.queue(trigger_animation_event( + entity, + *time, + active_animation.weight, + event.clone().0, + )); + } + } + } +} + /// A system that advances the time for all playing animations. pub fn advance_animations( time: Res