-
-
Notifications
You must be signed in to change notification settings - Fork 3.5k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add Support for Triggering Events via
AnimationEvent
s (#15538)
# 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 <[email protected]> Co-authored-by: Chris Biscardi <[email protected]> Co-authored-by: François Mockers <[email protected]>
- Loading branch information
1 parent
856cab5
commit d9190e4
Showing
9 changed files
with
1,144 additions
and
8 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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() | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<dyn AnimationEvent>, | ||
) -> 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<Say>) { | ||
/// println!("{}", trigger.event().0); | ||
/// } | ||
/// | ||
/// fn setup_animation( | ||
/// mut commands: Commands, | ||
/// mut animations: ResMut<Assets<AnimationClip>>, | ||
/// mut graphs: ResMut<Assets<AnimationGraph>>, | ||
/// ) { | ||
/// // 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<dyn AnimationEvent>` | ||
fn clone_value(&self) -> Box<dyn AnimationEvent>; | ||
} | ||
|
||
impl<T: AnimationEvent + Clone> CloneableAnimationEvent for T { | ||
fn clone_value(&self) -> Box<dyn AnimationEvent> { | ||
Box::new(self.clone()) | ||
} | ||
} | ||
|
||
/// The data that will be used to trigger an animation event. | ||
#[derive(TypePath)] | ||
pub(crate) struct AnimationEventData(pub(crate) Box<dyn AnimationEvent>); | ||
|
||
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<dyn AnimationEvent>`, which can't be automatically derived yet. | ||
impl GetTypeRegistration for AnimationEventData { | ||
fn get_type_registration() -> TypeRegistration { | ||
let mut registration = TypeRegistration::of::<Self>(); | ||
registration.insert::<ReflectFromPtr>(FromType::<Self>::from_type()); | ||
registration | ||
} | ||
} | ||
|
||
// We have to implement `Typed` manually because of the embedded | ||
// `Box<dyn AnimationEvent>`, 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::<Self>(&[UnnamedField::new::<()>(0)])) | ||
}) | ||
} | ||
} | ||
|
||
// We have to implement `TupleStruct` manually because of the embedded | ||
// `Box<dyn AnimationEvent>`, 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<dyn AnimationEvent>`, which can't be automatically derived yet. | ||
impl PartialReflect for AnimationEventData { | ||
#[inline] | ||
fn get_represented_type_info(&self) -> Option<&'static TypeInfo> { | ||
Some(<Self as Typed>::type_info()) | ||
} | ||
|
||
#[inline] | ||
fn into_partial_reflect(self: Box<Self>) -> Box<dyn PartialReflect> { | ||
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<Self>) -> Result<Box<dyn Reflect>, Box<dyn PartialReflect>> { | ||
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<Self>) -> ReflectOwned { | ||
ReflectOwned::TupleStruct(self) | ||
} | ||
|
||
fn clone_value(&self) -> Box<dyn PartialReflect> { | ||
Box::new(Clone::clone(self)) | ||
} | ||
} | ||
|
||
// We have to implement `Reflect` manually because of the embedded | ||
// `Box<dyn AnimationEvent>`, which can't be automatically derived yet. | ||
impl Reflect for AnimationEventData { | ||
#[inline] | ||
fn into_any(self: Box<Self>) -> Box<dyn Any> { | ||
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<Self>) -> Box<dyn Reflect> { | ||
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<dyn Reflect>) -> Result<(), Box<dyn Reflect>> { | ||
*self = value.take()?; | ||
Ok(()) | ||
} | ||
} | ||
|
||
// We have to implement `FromReflect` manually because of the embedded | ||
// `Box<dyn AnimationEvent>`, which can't be automatically derived yet. | ||
impl FromReflect for AnimationEventData { | ||
fn from_reflect(reflect: &dyn PartialReflect) -> Option<Self> { | ||
Some(reflect.try_downcast_ref::<AnimationEventData>()?.clone()) | ||
} | ||
} |
Oops, something went wrong.