Add Support for Triggering Events via AnimationEvents (#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 <weatherleymatthew@gmail.com>
Co-authored-by: Chris Biscardi <chris@christopherbiscardi.com>
Co-authored-by: François Mockers <francois.mockers@vleue.com>
This commit is contained in:
poopy 2024-10-06 12:03:05 +02:00 committed by GitHub
parent 856cab56f9
commit d9190e4ff6
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 1144 additions and 8 deletions

View file

@ -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"

View file

@ -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" }

View file

@ -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

View file

@ -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()
}

View file

@ -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())
}
}

View file

@ -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<AnimationEventTarget, Vec<TimedAnimationEvent>>;
/// A mapping from [`AnimationTargetId`] (e.g. bone in a skinned mesh) to the
/// animation curves.
pub type AnimationCurves = HashMap<AnimationTargetId, Vec<VariableCurve>, 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<f32>,
/// 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<Assets<AnimationClip>>,
graphs: Res<Assets<AnimationGraph>>,
players: Query<(Entity, &AnimationPlayer, &Handle<AnimationGraph>)>,
) {
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<Time>,
@ -892,17 +1033,23 @@ 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(
par_commands: ParallelCommands,
clips: Res<Assets<AnimationClip>>,
graphs: Res<Assets<AnimationGraph>>,
threaded_animation_graphs: Res<ThreadedAnimationGraphs>,
players: Query<(&AnimationPlayer, &Handle<AnimationGraph>)>,
mut targets: Query<(&AnimationTarget, Option<&mut Transform>, AnimationEntityMut)>,
mut targets: Query<(
Entity,
&AnimationTarget,
Option<&mut Transform>,
AnimationEntityMut,
)>,
animation_evaluation_state: Local<ThreadLocal<RefCell<AnimationEvaluationState>>>,
) {
// Evaluate all animation targets in parallel.
targets
.par_iter_mut()
.for_each(|(target, transform, entity_mut)| {
.for_each(|(entity, target, transform, entity_mut)| {
let &AnimationTarget {
id: target_id,
player: player_id,
@ -996,6 +1143,28 @@ pub fn animate_targets(
continue;
};
// Trigger all animation events that occurred this tick, if any.
if let Some(triggered_events) = TriggeredEvents::from_animation(
AnimationEventTarget::Node(target_id),
clip,
active_animation,
) {
if !triggered_events.is_empty() {
par_commands.command_scope(move |mut commands| {
for TimedAnimationEvent { time, event } in
triggered_events.iter()
{
commands.queue(trigger_animation_event(
entity,
*time,
active_animation.weight,
event.clone().0,
));
}
});
}
}
let Some(curves) = clip.curves_for_target(target_id) else {
continue;
};
@ -1077,6 +1246,7 @@ impl Plugin for AnimationPlugin {
animate_targets
.after(bevy_render::mesh::morph::inherit_weights)
.ambiguous_with_all(),
trigger_untargeted_animation_events,
expire_completed_transitions,
)
.chain()
@ -1107,6 +1277,22 @@ impl AnimationTargetId {
}
}
impl<T: AsRef<str>> FromIterator<T> for AnimationTargetId {
/// Creates a new [`AnimationTargetId`] by hashing a list of strings.
///
/// Typically, this will be the path from the animation root to the
/// animation target (e.g. bone) that is to be animated.
fn from_iter<I: IntoIterator<Item = T>>(iter: I) -> Self {
let mut blake3 = blake3::Hasher::new();
blake3.update(ANIMATION_TARGET_NAMESPACE.as_bytes());
for str in iter {
blake3.update(str.as_ref().as_bytes());
}
let hash = blake3.finalize().as_bytes()[0..16].try_into().unwrap();
Self(*uuid::Builder::from_sha1_bytes(hash).as_uuid())
}
}
impl From<&Name> for AnimationTargetId {
fn from(name: &Name) -> Self {
AnimationTargetId::from_name(name)
@ -1173,3 +1359,293 @@ impl AnimationEvaluationState {
Ok(())
}
}
/// All the events from an [`AnimationClip`] that occurred this tick.
#[derive(Debug, Clone)]
struct TriggeredEvents<'a> {
direction: TriggeredEventsDir,
lower: &'a [TimedAnimationEvent],
upper: &'a [TimedAnimationEvent],
}
impl<'a> TriggeredEvents<'a> {
fn from_animation(
target: AnimationEventTarget,
clip: &'a AnimationClip,
active_animation: &ActiveAnimation,
) -> Option<Self> {
let events = clip.events.get(&target)?;
let reverse = active_animation.is_playback_reversed();
let is_finished = active_animation.is_finished();
// Return early if the animation have finished on a previous tick.
if is_finished && !active_animation.just_completed {
return None;
}
// The animation completed this tick, while still playing.
let looping = active_animation.just_completed && !is_finished;
let direction = match (reverse, looping) {
(false, false) => TriggeredEventsDir::Forward,
(false, true) => TriggeredEventsDir::ForwardLooping,
(true, false) => TriggeredEventsDir::Reverse,
(true, true) => TriggeredEventsDir::ReverseLooping,
};
let last_time = active_animation.last_seek_time?;
let this_time = active_animation.seek_time;
let (lower, upper) = match direction {
// Return all events where last_time <= event.time < this_time.
TriggeredEventsDir::Forward => {
let start = events.partition_point(|event| event.time < last_time);
// The animation finished this tick, return any remaining events.
if is_finished {
(&events[start..], &events[0..0])
} else {
let end = events.partition_point(|event| event.time < this_time);
(&events[start..end], &events[0..0])
}
}
// Return all events where this_time < event.time <= last_time.
TriggeredEventsDir::Reverse => {
let end = events.partition_point(|event| event.time <= last_time);
// The animation finished, return any remaining events.
if is_finished {
(&events[..end], &events[0..0])
} else {
let start = events.partition_point(|event| event.time <= this_time);
(&events[start..end], &events[0..0])
}
}
// The animation is looping this tick and we have to return events where
// either last_tick <= event.time or event.time < this_tick.
TriggeredEventsDir::ForwardLooping => {
let upper_start = events.partition_point(|event| event.time < last_time);
let lower_end = events.partition_point(|event| event.time < this_time);
let upper = &events[upper_start..];
let lower = &events[..lower_end];
(lower, upper)
}
// The animation is looping this tick and we have to return events where
// either last_tick >= event.time or event.time > this_tick.
TriggeredEventsDir::ReverseLooping => {
let lower_end = events.partition_point(|event| event.time <= last_time);
let upper_start = events.partition_point(|event| event.time <= this_time);
let upper = &events[upper_start..];
let lower = &events[..lower_end];
(lower, upper)
}
};
Some(Self {
direction,
lower,
upper,
})
}
fn is_empty(&self) -> bool {
self.lower.is_empty() && self.upper.is_empty()
}
fn iter(&self) -> TriggeredEventsIter {
match self.direction {
TriggeredEventsDir::Forward => TriggeredEventsIter::Forward(self.lower.iter()),
TriggeredEventsDir::Reverse => TriggeredEventsIter::Reverse(self.lower.iter().rev()),
TriggeredEventsDir::ForwardLooping => TriggeredEventsIter::ForwardLooping {
upper: self.upper.iter(),
lower: self.lower.iter(),
},
TriggeredEventsDir::ReverseLooping => TriggeredEventsIter::ReverseLooping {
lower: self.lower.iter().rev(),
upper: self.upper.iter().rev(),
},
}
}
}
#[derive(Debug, Clone, Copy)]
enum TriggeredEventsDir {
/// The animation is playing normally
Forward,
/// The animation is playing in reverse
Reverse,
/// The animation is looping this tick
ForwardLooping,
/// The animation playing in reverse and looping this tick
ReverseLooping,
}
#[derive(Debug, Clone)]
enum TriggeredEventsIter<'a> {
Forward(slice::Iter<'a, TimedAnimationEvent>),
Reverse(iter::Rev<slice::Iter<'a, TimedAnimationEvent>>),
ForwardLooping {
upper: slice::Iter<'a, TimedAnimationEvent>,
lower: slice::Iter<'a, TimedAnimationEvent>,
},
ReverseLooping {
lower: iter::Rev<slice::Iter<'a, TimedAnimationEvent>>,
upper: iter::Rev<slice::Iter<'a, TimedAnimationEvent>>,
},
}
impl<'a> Iterator for TriggeredEventsIter<'a> {
type Item = &'a TimedAnimationEvent;
fn next(&mut self) -> Option<Self::Item> {
match self {
TriggeredEventsIter::Forward(iter) => iter.next(),
TriggeredEventsIter::Reverse(rev) => rev.next(),
TriggeredEventsIter::ForwardLooping { upper, lower } => {
upper.next().or_else(|| lower.next())
}
TriggeredEventsIter::ReverseLooping { lower, upper } => {
lower.next().or_else(|| upper.next())
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[derive(Event, Reflect, Clone)]
struct A;
impl AnimationEvent for A {
fn trigger(&self, _time: f32, _weight: f32, target: Entity, world: &mut World) {
world.entity_mut(target).trigger(self.clone());
}
}
#[track_caller]
fn assert_triggered_events_with(
active_animation: &ActiveAnimation,
clip: &AnimationClip,
expected: impl Into<Vec<f32>>,
) {
let Some(events) =
TriggeredEvents::from_animation(AnimationEventTarget::Root, clip, active_animation)
else {
assert_eq!(expected.into(), Vec::<f32>::new());
return;
};
let got: Vec<_> = events.iter().map(|t| t.time).collect();
assert_eq!(
expected.into(),
got,
"\n{events:#?}\nlast_time: {:?}\nthis_time:{}",
active_animation.last_seek_time,
active_animation.seek_time
);
}
#[test]
fn test_multiple_events_triggers() {
let mut active_animation = ActiveAnimation {
repeat: RepeatAnimation::Forever,
..Default::default()
};
let mut clip = AnimationClip {
duration: 1.0,
..Default::default()
};
clip.add_event(0.5, A);
clip.add_event(0.5, A);
clip.add_event(0.5, A);
assert_triggered_events_with(&active_animation, &clip, []);
active_animation.update(0.8, clip.duration); // 0.0 : 0.8
assert_triggered_events_with(&active_animation, &clip, [0.5, 0.5, 0.5]);
clip.add_event(1.0, A);
clip.add_event(0.0, A);
clip.add_event(1.0, A);
clip.add_event(0.0, A);
active_animation.update(0.4, clip.duration); // 0.8 : 0.2
assert_triggered_events_with(&active_animation, &clip, [1.0, 1.0, 0.0, 0.0]);
}
#[test]
fn test_events_triggers() {
let mut active_animation = ActiveAnimation::default();
let mut clip = AnimationClip::default();
clip.add_event(0.2, A);
clip.add_event(0.0, A);
assert_eq!(0.2, clip.duration);
assert_triggered_events_with(&active_animation, &clip, []);
active_animation.update(0.1, clip.duration); // 0.0 : 0.1
assert_triggered_events_with(&active_animation, &clip, [0.0]);
active_animation.update(0.1, clip.duration); // 0.1 : 0.2
assert_triggered_events_with(&active_animation, &clip, [0.2]);
active_animation.update(0.1, clip.duration); // 0.2 : 0.2
assert_triggered_events_with(&active_animation, &clip, []);
active_animation.update(0.1, clip.duration); // 0.2 : 0.2
assert_triggered_events_with(&active_animation, &clip, []);
active_animation.speed = -1.0;
active_animation.completions = 0;
assert_triggered_events_with(&active_animation, &clip, []);
active_animation.update(0.1, clip.duration); // 0.2 : 0.1
assert_triggered_events_with(&active_animation, &clip, [0.2]);
active_animation.update(0.1, clip.duration); // 0.1 : 0.0
assert_triggered_events_with(&active_animation, &clip, []);
active_animation.update(0.1, clip.duration); // 0.0 : 0.0
assert_triggered_events_with(&active_animation, &clip, [0.0]);
active_animation.update(0.1, clip.duration); // 0.0 : 0.0
assert_triggered_events_with(&active_animation, &clip, []);
}
#[test]
fn test_events_triggers_looping() {
let mut active_animation = ActiveAnimation {
repeat: RepeatAnimation::Forever,
..Default::default()
};
let mut clip = AnimationClip::default();
clip.add_event(0.3, A);
clip.add_event(0.0, A);
clip.add_event(0.2, A);
assert_eq!(0.3, clip.duration);
assert_triggered_events_with(&active_animation, &clip, []);
active_animation.update(0.1, clip.duration); // 0.0 : 0.1
assert_triggered_events_with(&active_animation, &clip, [0.0]);
active_animation.update(0.1, clip.duration); // 0.1 : 0.2
assert_triggered_events_with(&active_animation, &clip, []);
active_animation.update(0.1, clip.duration); // 0.2 : 0.3
assert_triggered_events_with(&active_animation, &clip, [0.2, 0.3]);
active_animation.update(0.1, clip.duration); // 0.3 : 0.1
assert_triggered_events_with(&active_animation, &clip, [0.0]);
active_animation.update(0.1, clip.duration); // 0.1 : 0.2
assert_triggered_events_with(&active_animation, &clip, []);
active_animation.speed = -1.0;
active_animation.update(0.1, clip.duration); // 0.2 : 0.1
assert_triggered_events_with(&active_animation, &clip, [0.2]);
active_animation.update(0.1, clip.duration); // 0.1 : 0.0
assert_triggered_events_with(&active_animation, &clip, []);
active_animation.update(0.1, clip.duration); // 0.0 : 0.2
assert_triggered_events_with(&active_animation, &clip, [0.0, 0.3]);
active_animation.update(0.1, clip.duration); // 0.2 : 0.1
assert_triggered_events_with(&active_animation, &clip, [0.2]);
active_animation.update(0.1, clip.duration); // 0.1 : 0.0
assert_triggered_events_with(&active_animation, &clip, []);
active_animation.replay();
active_animation.update(clip.duration, clip.duration); // 0.0 : 0.0
assert_triggered_events_with(&active_animation, &clip, [0.0, 0.3, 0.2]);
active_animation.replay();
active_animation.seek_time = clip.duration;
active_animation.last_seek_time = Some(clip.duration);
active_animation.update(clip.duration, clip.duration); // 0.3 : 0.0
assert_triggered_events_with(&active_animation, &clip, [0.3, 0.2]);
}
}

View file

@ -193,6 +193,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

View file

@ -3,10 +3,12 @@
use std::{f32::consts::PI, time::Duration};
use bevy::{
animation::{animate_targets, RepeatAnimation},
animation::{animate_targets, AnimationTargetId, RepeatAnimation},
color::palettes::css::WHITE,
pbr::CascadeShadowConfigBuilder,
prelude::*,
};
use rand::{thread_rng, Rng};
const FOX_PATH: &str = "models/animated/Fox.glb";
@ -17,19 +19,49 @@ fn main() {
brightness: 2000.,
})
.add_plugins(DefaultPlugins)
.init_resource::<ParticleAssets>()
.init_resource::<FoxFeetTargets>()
.add_systems(Startup, setup)
.add_systems(Update, setup_scene_once_loaded.before(animate_targets))
.add_systems(Update, keyboard_animation_control)
.add_systems(Update, (keyboard_animation_control, simulate_particles))
.observe(observe_on_step)
.run();
}
#[derive(Resource)]
struct Animations {
animations: Vec<AnimationNodeIndex>,
#[allow(dead_code)]
graph: Handle<AnimationGraph>,
}
#[derive(Event, AnimationEvent, Reflect, Clone)]
#[reflect(AnimationEvent)]
struct OnStep;
fn observe_on_step(
trigger: Trigger<OnStep>,
particle: Res<ParticleAssets>,
mut commands: Commands,
transforms: Query<&GlobalTransform>,
) {
let translation = transforms.get(trigger.entity()).unwrap().translation();
let mut rng = thread_rng();
// Spawn a bunch of particles.
for _ in 0..14 {
let horizontal = rng.gen::<Dir2>() * rng.gen_range(8.0..12.0);
let vertical = rng.gen_range(0.0..4.0);
let size = rng.gen_range(0.2..1.0);
commands.queue(spawn_particle(
particle.mesh.clone(),
particle.material.clone(),
translation.reject_from_normalized(Vec3::Y),
rng.gen_range(0.2..0.6),
size,
Vec3::new(horizontal.x, vertical, horizontal.y) * 10.0,
));
}
}
fn setup(
mut commands: Commands,
asset_server: Res<AssetServer>,
@ -97,9 +129,37 @@ fn setup(
fn setup_scene_once_loaded(
mut commands: Commands,
animations: Res<Animations>,
feet: Res<FoxFeetTargets>,
graphs: Res<Assets<AnimationGraph>>,
mut clips: ResMut<Assets<AnimationClip>>,
mut players: Query<(Entity, &mut AnimationPlayer), Added<AnimationPlayer>>,
) {
fn get_clip<'a>(
node: AnimationNodeIndex,
graph: &AnimationGraph,
clips: &'a mut Assets<AnimationClip>,
) -> &'a mut AnimationClip {
let node = graph.get(node).unwrap();
let clip = match &node.node_type {
AnimationNodeType::Clip(handle) => clips.get_mut(handle),
_ => unreachable!(),
};
clip.unwrap()
}
for (entity, mut player) in &mut players {
let graph = graphs.get(&animations.graph).unwrap();
// Send `OnStep` events once the fox feet hits the ground in the running animation.
let running_animation = get_clip(animations.animations[0], graph, &mut clips);
// You can determine the time an event should trigger if you know witch frame it occurs and
// the frame rate of the animation. Let's say we want to trigger an event at frame 15,
// and the animation has a frame rate of 24 fps, then time = 15 / 24 = 0.625.
running_animation.add_event_to_target(feet.front_left, 0.625, OnStep);
running_animation.add_event_to_target(feet.front_right, 0.5, OnStep);
running_animation.add_event_to_target(feet.back_left, 0.0, OnStep);
running_animation.add_event_to_target(feet.back_right, 0.125, OnStep);
let mut transitions = AnimationTransitions::new();
// Make sure to start the animation via the `AnimationTransitions`
@ -200,3 +260,134 @@ fn keyboard_animation_control(
}
}
}
fn simulate_particles(
mut commands: Commands,
mut query: Query<(Entity, &mut Transform, &mut Particle)>,
time: Res<Time>,
) {
for (entity, mut transform, mut particle) in &mut query {
if particle.lifeteime_timer.tick(time.delta()).just_finished() {
commands.entity(entity).despawn();
} else {
transform.translation += particle.velocity * time.delta_seconds();
transform.scale =
Vec3::splat(particle.size.lerp(0.0, particle.lifeteime_timer.fraction()));
particle
.velocity
.smooth_nudge(&Vec3::ZERO, 4.0, time.delta_seconds());
}
}
}
fn spawn_particle<M: Material>(
mesh: Handle<Mesh>,
material: Handle<M>,
translation: Vec3,
lifetime: f32,
size: f32,
velocity: Vec3,
) -> impl Command {
move |world: &mut World| {
world.spawn((
Particle {
lifeteime_timer: Timer::from_seconds(lifetime, TimerMode::Once),
size,
velocity,
},
Mesh3d(mesh),
MeshMaterial3d(material),
Transform {
translation,
scale: Vec3::splat(size),
..Default::default()
},
));
}
}
#[derive(Component)]
struct Particle {
lifeteime_timer: Timer,
size: f32,
velocity: Vec3,
}
#[derive(Resource)]
struct ParticleAssets {
mesh: Handle<Mesh>,
material: Handle<StandardMaterial>,
}
impl FromWorld for ParticleAssets {
fn from_world(world: &mut World) -> Self {
Self {
mesh: world.resource_mut::<Assets<Mesh>>().add(Sphere::new(10.0)),
material: world
.resource_mut::<Assets<StandardMaterial>>()
.add(StandardMaterial {
base_color: WHITE.into(),
..Default::default()
}),
}
}
}
#[derive(Resource)]
struct FoxFeetTargets {
front_right: AnimationTargetId,
front_left: AnimationTargetId,
back_left: AnimationTargetId,
back_right: AnimationTargetId,
}
impl Default for FoxFeetTargets {
fn default() -> Self {
// Get the id's of the feet and store them in a resource.
let hip_node = ["root", "_rootJoint", "b_Root_00", "b_Hip_01"];
let front_left_foot = hip_node.iter().chain(
[
"b_Spine01_02",
"b_Spine02_03",
"b_LeftUpperArm_09",
"b_LeftForeArm_010",
"b_LeftHand_011",
]
.iter(),
);
let front_right_foot = hip_node.iter().chain(
[
"b_Spine01_02",
"b_Spine02_03",
"b_RightUpperArm_06",
"b_RightForeArm_07",
"b_RightHand_08",
]
.iter(),
);
let back_left_foot = hip_node.iter().chain(
[
"b_LeftLeg01_015",
"b_LeftLeg02_016",
"b_LeftFoot01_017",
"b_LeftFoot02_018",
]
.iter(),
);
let back_right_foot = hip_node.iter().chain(
[
"b_RightLeg01_019",
"b_RightLeg02_020",
"b_RightFoot01_021",
"b_RightFoot02_022",
]
.iter(),
);
Self {
front_left: AnimationTargetId::from_iter(front_left_foot),
front_right: AnimationTargetId::from_iter(front_right_foot),
back_left: AnimationTargetId::from_iter(back_left_foot),
back_right: AnimationTargetId::from_iter(back_right_foot),
}
}
}

View file

@ -0,0 +1,121 @@
//! Demonstrate how to use animation events.
use bevy::{
color::palettes::css::{ALICE_BLUE, BLACK, CRIMSON},
core_pipeline::bloom::Bloom,
prelude::*,
};
fn main() {
App::new()
.add_plugins(DefaultPlugins)
.add_event::<MessageEvent>()
.add_systems(Startup, setup)
.add_systems(PreUpdate, (animate_text_opacity, edit_message))
.run();
}
#[derive(Component)]
struct MessageText;
#[derive(Event, Reflect, Clone)]
#[reflect(AnimationEvent)]
struct MessageEvent {
value: String,
color: Color,
}
// AnimationEvent can also be derived, but doing so will
// trigger it as an observer event which is triggered in PostUpdate.
// We need to set the message text before that so it is
// updated before rendering without a one frame delay.
impl AnimationEvent for MessageEvent {
fn trigger(&self, _time: f32, _weight: f32, _entity: Entity, world: &mut World) {
world.send_event(self.clone());
}
}
fn edit_message(
mut event_reader: EventReader<MessageEvent>,
mut text: Single<&mut Text, With<MessageText>>,
) {
for event in event_reader.read() {
text.sections[0].value = event.value.clone();
text.sections[0].style.color = event.color;
}
}
fn setup(
mut commands: Commands,
mut animations: ResMut<Assets<AnimationClip>>,
mut graphs: ResMut<Assets<AnimationGraph>>,
) {
// Camera
commands.spawn((
Camera2d,
Camera {
clear_color: ClearColorConfig::Custom(BLACK.into()),
hdr: true,
..Default::default()
},
Bloom {
intensity: 0.4,
..Bloom::NATURAL
},
));
// The text that will be changed by animation events.
commands.spawn((
MessageText,
Text2dBundle {
text: Text::from_section(
"",
TextStyle {
font_size: 119.0,
color: Color::NONE,
..Default::default()
},
),
..Default::default()
},
));
// Create a new animation clip.
let mut animation = AnimationClip::default();
// This is only necessary if you want the duration of the
// animation to be longer than the last event in the clip.
animation.set_duration(2.0);
// Add events at the specified time.
animation.add_event(
0.0,
MessageEvent {
value: "HELLO".into(),
color: ALICE_BLUE.into(),
},
);
animation.add_event(
1.0,
MessageEvent {
value: "BYE".into(),
color: CRIMSON.into(),
},
);
// Create the animation graph.
let (graph, animation_index) = AnimationGraph::from_clip(animations.add(animation));
let mut player = AnimationPlayer::default();
player.play(animation_index).repeat();
commands.spawn((graphs.add(graph), player));
}
// Slowly fade out the text opacity.
fn animate_text_opacity(mut query: Query<&mut Text>, time: Res<Time>) {
for mut text in &mut query {
let color = &mut text.sections[0].style.color;
let a = color.alpha();
color.set_alpha(a - time.delta_seconds());
}
}