mirror of
https://github.com/bevyengine/bevy
synced 2024-11-22 04:33:37 +00:00
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 <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:
parent
856cab56f9
commit
d9190e4ff6
9 changed files with 1144 additions and 8 deletions
11
Cargo.toml
11
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"
|
||||
|
|
|
@ -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" }
|
||||
|
|
25
crates/bevy_animation/derive/Cargo.toml
Normal file
25
crates/bevy_animation/derive/Cargo.toml
Normal 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
|
29
crates/bevy_animation/derive/src/lib.rs
Normal file
29
crates/bevy_animation/derive/src/lib.rs
Normal 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()
|
||||
}
|
281
crates/bevy_animation/src/animation_event.rs
Normal file
281
crates/bevy_animation/src/animation_event.rs
Normal 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())
|
||||
}
|
||||
}
|
|
@ -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]);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
121
examples/animation/animation_events.rs
Normal file
121
examples/animation/animation_events.rs
Normal 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());
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue