Skip to content

Commit

Permalink
Add Support for Triggering Events via AnimationEvents (#15538)
Browse files Browse the repository at this point in the history
# 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 <weatherleymatthew@gmail.com>
Co-authored-by: Chris Biscardi <chris@christopherbiscardi.com>
Co-authored-by: François Mockers <francois.mockers@vleue.com>
  • Loading branch information
4 people authored Oct 6, 2024
1 parent 856cab5 commit d9190e4
Show file tree
Hide file tree
Showing 9 changed files with 1,144 additions and 8 deletions.
11 changes: 11 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
1 change: 1 addition & 0 deletions crates/bevy_animation/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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" }
Expand Down
25 changes: 25 additions & 0 deletions crates/bevy_animation/derive/Cargo.toml
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
29 changes: 29 additions & 0 deletions crates/bevy_animation/derive/src/lib.rs
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()
}
281 changes: 281 additions & 0 deletions crates/bevy_animation/src/animation_event.rs
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())
}
}
Loading

0 comments on commit d9190e4

Please sign in to comment.