mirror of
https://github.com/bevyengine/bevy
synced 2025-02-16 14:08:32 +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
|
hidden = true
|
||||||
|
|
||||||
# Animation
|
# 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]]
|
[[example]]
|
||||||
name = "animated_fox"
|
name = "animated_fox"
|
||||||
path = "examples/animation/animated_fox.rs"
|
path = "examples/animation/animated_fox.rs"
|
||||||
|
|
|
@ -10,6 +10,7 @@ keywords = ["bevy"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
# bevy
|
# bevy
|
||||||
|
bevy_animation_derive = { path = "derive", version = "0.15.0-dev" }
|
||||||
bevy_app = { path = "../bevy_app", 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_asset = { path = "../bevy_asset", version = "0.15.0-dev" }
|
||||||
bevy_color = { path = "../bevy_color", 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 animatable;
|
||||||
pub mod animation_curves;
|
pub mod animation_curves;
|
||||||
|
pub mod animation_event;
|
||||||
pub mod gltf_curves;
|
pub mod gltf_curves;
|
||||||
pub mod graph;
|
pub mod graph;
|
||||||
pub mod transition;
|
pub mod transition;
|
||||||
mod util;
|
mod util;
|
||||||
|
|
||||||
|
use animation_event::{trigger_animation_event, AnimationEvent, AnimationEventData};
|
||||||
use core::{
|
use core::{
|
||||||
any::{Any, TypeId},
|
any::{Any, TypeId},
|
||||||
cell::RefCell,
|
cell::RefCell,
|
||||||
fmt::Debug,
|
fmt::Debug,
|
||||||
hash::{Hash, Hasher},
|
hash::{Hash, Hasher},
|
||||||
iter,
|
iter, slice,
|
||||||
};
|
};
|
||||||
use graph::AnimationNodeType;
|
use graph::AnimationNodeType;
|
||||||
use prelude::AnimationCurveEvaluator;
|
use prelude::AnimationCurveEvaluator;
|
||||||
|
@ -37,6 +39,7 @@ use bevy_ecs::{
|
||||||
reflect::{ReflectMapEntities, ReflectVisitEntities, ReflectVisitEntitiesMut},
|
reflect::{ReflectMapEntities, ReflectVisitEntities, ReflectVisitEntitiesMut},
|
||||||
world::EntityMutExcept,
|
world::EntityMutExcept,
|
||||||
};
|
};
|
||||||
|
use bevy_math::FloatOrd;
|
||||||
use bevy_reflect::{
|
use bevy_reflect::{
|
||||||
prelude::ReflectDefault, utility::NonGenericTypeInfoCell, ApplyError, DynamicTupleStruct,
|
prelude::ReflectDefault, utility::NonGenericTypeInfoCell, ApplyError, DynamicTupleStruct,
|
||||||
FromReflect, FromType, GetTypeRegistration, PartialReflect, Reflect, ReflectFromPtr,
|
FromReflect, FromType, GetTypeRegistration, PartialReflect, Reflect, ReflectFromPtr,
|
||||||
|
@ -61,8 +64,12 @@ use uuid::Uuid;
|
||||||
pub mod prelude {
|
pub mod prelude {
|
||||||
#[doc(hidden)]
|
#[doc(hidden)]
|
||||||
pub use crate::{
|
pub use crate::{
|
||||||
animatable::*, animation_curves::*, graph::*, transition::*, AnimationClip,
|
animatable::*,
|
||||||
AnimationPlayer, AnimationPlugin, VariableCurve,
|
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)]
|
#[derive(Asset, Reflect, Clone, Debug, Default)]
|
||||||
pub struct AnimationClip {
|
pub struct AnimationClip {
|
||||||
curves: AnimationCurves,
|
curves: AnimationCurves,
|
||||||
|
events: AnimationEvents,
|
||||||
duration: f32,
|
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
|
/// A mapping from [`AnimationTargetId`] (e.g. bone in a skinned mesh) to the
|
||||||
/// animation curves.
|
/// animation curves.
|
||||||
pub type AnimationCurves = HashMap<AnimationTargetId, Vec<VariableCurve>, NoOpHash>;
|
pub type AnimationCurves = HashMap<AnimationTargetId, Vec<VariableCurve>, NoOpHash>;
|
||||||
|
@ -435,6 +457,50 @@ impl AnimationClip {
|
||||||
.or_default()
|
.or_default()
|
||||||
.push(variable_curve);
|
.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.
|
/// 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]
|
/// Note: This will always be in the range [0.0, animation clip duration]
|
||||||
seek_time: f32,
|
seek_time: f32,
|
||||||
|
/// The `seek_time` of the previous tick, if any.
|
||||||
|
last_seek_time: Option<f32>,
|
||||||
/// Number of times the animation has completed.
|
/// Number of times the animation has completed.
|
||||||
/// If the animation is playing in reverse, this increments when the animation passes the start.
|
/// If the animation is playing in reverse, this increments when the animation passes the start.
|
||||||
completions: u32,
|
completions: u32,
|
||||||
|
/// `true` if the animation was completed at least once this tick.
|
||||||
|
just_completed: bool,
|
||||||
paused: bool,
|
paused: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -503,7 +573,9 @@ impl Default for ActiveAnimation {
|
||||||
speed: 1.0,
|
speed: 1.0,
|
||||||
elapsed: 0.0,
|
elapsed: 0.0,
|
||||||
seek_time: 0.0,
|
seek_time: 0.0,
|
||||||
|
last_seek_time: None,
|
||||||
completions: 0,
|
completions: 0,
|
||||||
|
just_completed: false,
|
||||||
paused: false,
|
paused: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -525,6 +597,9 @@ impl ActiveAnimation {
|
||||||
/// Update the animation given the delta time and the duration of the clip being played.
|
/// Update the animation given the delta time and the duration of the clip being played.
|
||||||
#[inline]
|
#[inline]
|
||||||
fn update(&mut self, delta: f32, clip_duration: f32) {
|
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() {
|
if self.is_finished() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -536,6 +611,7 @@ impl ActiveAnimation {
|
||||||
let under_time = self.speed < 0.0 && self.seek_time < 0.0;
|
let under_time = self.speed < 0.0 && self.seek_time < 0.0;
|
||||||
|
|
||||||
if over_time || under_time {
|
if over_time || under_time {
|
||||||
|
self.just_completed = true;
|
||||||
self.completions += 1;
|
self.completions += 1;
|
||||||
|
|
||||||
if self.is_finished() {
|
if self.is_finished() {
|
||||||
|
@ -553,8 +629,10 @@ impl ActiveAnimation {
|
||||||
|
|
||||||
/// Reset back to the initial state as if no time has elapsed.
|
/// Reset back to the initial state as if no time has elapsed.
|
||||||
pub fn replay(&mut self) {
|
pub fn replay(&mut self) {
|
||||||
|
self.just_completed = false;
|
||||||
self.completions = 0;
|
self.completions = 0;
|
||||||
self.elapsed = 0.0;
|
self.elapsed = 0.0;
|
||||||
|
self.last_seek_time = None;
|
||||||
self.seek_time = 0.0;
|
self.seek_time = 0.0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -639,13 +717,33 @@ impl ActiveAnimation {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Seeks to a specific time in the animation.
|
/// 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 {
|
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.seek_time = seek_time;
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Seeks to the beginning of the animation.
|
/// 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 {
|
pub fn rewind(&mut self) -> &mut Self {
|
||||||
|
self.last_seek_time = Some(self.seek_time);
|
||||||
self.seek_time = 0.0;
|
self.seek_time = 0.0;
|
||||||
self
|
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.
|
/// A system that advances the time for all playing animations.
|
||||||
pub fn advance_animations(
|
pub fn advance_animations(
|
||||||
time: Res<Time>,
|
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)
|
/// A system that modifies animation targets (e.g. bones in a skinned mesh)
|
||||||
/// according to the currently-playing animations.
|
/// according to the currently-playing animations.
|
||||||
pub fn animate_targets(
|
pub fn animate_targets(
|
||||||
|
par_commands: ParallelCommands,
|
||||||
clips: Res<Assets<AnimationClip>>,
|
clips: Res<Assets<AnimationClip>>,
|
||||||
graphs: Res<Assets<AnimationGraph>>,
|
graphs: Res<Assets<AnimationGraph>>,
|
||||||
threaded_animation_graphs: Res<ThreadedAnimationGraphs>,
|
threaded_animation_graphs: Res<ThreadedAnimationGraphs>,
|
||||||
players: Query<(&AnimationPlayer, &Handle<AnimationGraph>)>,
|
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>>>,
|
animation_evaluation_state: Local<ThreadLocal<RefCell<AnimationEvaluationState>>>,
|
||||||
) {
|
) {
|
||||||
// Evaluate all animation targets in parallel.
|
// Evaluate all animation targets in parallel.
|
||||||
targets
|
targets
|
||||||
.par_iter_mut()
|
.par_iter_mut()
|
||||||
.for_each(|(target, transform, entity_mut)| {
|
.for_each(|(entity, target, transform, entity_mut)| {
|
||||||
let &AnimationTarget {
|
let &AnimationTarget {
|
||||||
id: target_id,
|
id: target_id,
|
||||||
player: player_id,
|
player: player_id,
|
||||||
|
@ -996,6 +1143,28 @@ pub fn animate_targets(
|
||||||
continue;
|
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 {
|
let Some(curves) = clip.curves_for_target(target_id) else {
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
|
@ -1077,6 +1246,7 @@ impl Plugin for AnimationPlugin {
|
||||||
animate_targets
|
animate_targets
|
||||||
.after(bevy_render::mesh::morph::inherit_weights)
|
.after(bevy_render::mesh::morph::inherit_weights)
|
||||||
.ambiguous_with_all(),
|
.ambiguous_with_all(),
|
||||||
|
trigger_untargeted_animation_events,
|
||||||
expire_completed_transitions,
|
expire_completed_transitions,
|
||||||
)
|
)
|
||||||
.chain()
|
.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 {
|
impl From<&Name> for AnimationTargetId {
|
||||||
fn from(name: &Name) -> Self {
|
fn from(name: &Name) -> Self {
|
||||||
AnimationTargetId::from_name(name)
|
AnimationTargetId::from_name(name)
|
||||||
|
@ -1173,3 +1359,293 @@ impl AnimationEvaluationState {
|
||||||
Ok(())
|
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 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 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
|
[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 Graph](../examples/animation/animation_graph.rs) | Blends multiple animations together with a graph
|
||||||
[Animation Masks](../examples/animation/animation_masks.rs) | Demonstrates animation masks
|
[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
|
[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 std::{f32::consts::PI, time::Duration};
|
||||||
|
|
||||||
use bevy::{
|
use bevy::{
|
||||||
animation::{animate_targets, RepeatAnimation},
|
animation::{animate_targets, AnimationTargetId, RepeatAnimation},
|
||||||
|
color::palettes::css::WHITE,
|
||||||
pbr::CascadeShadowConfigBuilder,
|
pbr::CascadeShadowConfigBuilder,
|
||||||
prelude::*,
|
prelude::*,
|
||||||
};
|
};
|
||||||
|
use rand::{thread_rng, Rng};
|
||||||
|
|
||||||
const FOX_PATH: &str = "models/animated/Fox.glb";
|
const FOX_PATH: &str = "models/animated/Fox.glb";
|
||||||
|
|
||||||
|
@ -17,19 +19,49 @@ fn main() {
|
||||||
brightness: 2000.,
|
brightness: 2000.,
|
||||||
})
|
})
|
||||||
.add_plugins(DefaultPlugins)
|
.add_plugins(DefaultPlugins)
|
||||||
|
.init_resource::<ParticleAssets>()
|
||||||
|
.init_resource::<FoxFeetTargets>()
|
||||||
.add_systems(Startup, setup)
|
.add_systems(Startup, setup)
|
||||||
.add_systems(Update, setup_scene_once_loaded.before(animate_targets))
|
.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();
|
.run();
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Resource)]
|
#[derive(Resource)]
|
||||||
struct Animations {
|
struct Animations {
|
||||||
animations: Vec<AnimationNodeIndex>,
|
animations: Vec<AnimationNodeIndex>,
|
||||||
#[allow(dead_code)]
|
|
||||||
graph: Handle<AnimationGraph>,
|
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(
|
fn setup(
|
||||||
mut commands: Commands,
|
mut commands: Commands,
|
||||||
asset_server: Res<AssetServer>,
|
asset_server: Res<AssetServer>,
|
||||||
|
@ -97,9 +129,37 @@ fn setup(
|
||||||
fn setup_scene_once_loaded(
|
fn setup_scene_once_loaded(
|
||||||
mut commands: Commands,
|
mut commands: Commands,
|
||||||
animations: Res<Animations>,
|
animations: Res<Animations>,
|
||||||
|
feet: Res<FoxFeetTargets>,
|
||||||
|
graphs: Res<Assets<AnimationGraph>>,
|
||||||
|
mut clips: ResMut<Assets<AnimationClip>>,
|
||||||
mut players: Query<(Entity, &mut AnimationPlayer), Added<AnimationPlayer>>,
|
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 {
|
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();
|
let mut transitions = AnimationTransitions::new();
|
||||||
|
|
||||||
// Make sure to start the animation via the `AnimationTransitions`
|
// 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…
Add table
Reference in a new issue