AnimationEvent -> Event and other improvements (#16440)

# Objective

Needing to derive `AnimationEvent` for `Event` is unnecessary, and the
trigger logic coupled to it feels like we're coupling "event producer"
logic with the event itself, which feels wrong. It also comes with a
bunch of complexity, which is again unnecessary. We can have the
flexibility of "custom animation event trigger logic" without this
coupling and complexity.

The current `animation_events` example is also needlessly complicated,
due to it needing to work around system ordering issues. The docs
describing it are also slightly wrong. We can make this all a non-issue
by solving the underlying ordering problem.

Related to this, we use the `bevy_animation::Animation` system set to
solve PostUpdate animation order-of-operations issues. If we move this
to bevy_app as part of our "core schedule", we can cut out needless
`bevy_animation` crate dependencies in these instances.

## Solution

- Remove `AnimationEvent`, the derive, and all other infrastructure
associated with it (such as the `bevy_animation/derive` crate)
- Replace all instances of `AnimationEvent` traits with `Event + Clone`
- Store and use functions for custom animation trigger logic (ex:
`clip.add_event_fn()`). For "normal" cases users dont need to think
about this and should use the simpler `clip.add_event()`
- Run the `Animation` system set _before_ updating text
- Move `bevy_animation::Animation` to `bevy_app::Animation`. Remove
unnecessary `bevy_animation` dependency from `bevy_ui`
- Adjust `animation_events` example to use the simpler `clip.add_event`
API, as the workarounds are no longer necessary

This is polishing work that will land in 0.15, and I think it is simple
enough and valuable enough to land in 0.15 with it, in the interest of
making the feature as compelling as possible.
This commit is contained in:
Carter Anderson 2024-11-21 16:16:04 -08:00 committed by GitHub
parent a54d85bb2d
commit 513be52505
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 123 additions and 404 deletions

View file

@ -10,7 +10,6 @@ keywords = ["bevy"]
[dependencies]
# bevy
bevy_animation_derive = { path = "derive", version = "0.15.0-dev" }
bevy_app = { path = "../bevy_app", version = "0.15.0-dev" }
bevy_asset = { path = "../bevy_asset", version = "0.15.0-dev" }
bevy_color = { path = "../bevy_color", version = "0.15.0-dev" }

View file

@ -1,25 +0,0 @@
[package]
name = "bevy_animation_derive"
version = "0.15.0-dev"
edition = "2021"
description = "Derive implementations for bevy_animation"
homepage = "https://bevyengine.org"
repository = "https://github.com/bevyengine/bevy"
license = "MIT OR Apache-2.0"
keywords = ["bevy"]
[lib]
proc-macro = true
[dependencies]
bevy_macro_utils = { path = "../../bevy_macro_utils", version = "0.15.0-dev" }
proc-macro2 = "1.0"
quote = "1.0"
syn = { version = "2.0", features = ["full"] }
[lints]
workspace = true
[package.metadata.docs.rs]
rustdoc-args = ["-Zunstable-options", "--generate-link-to-definition"]
all-features = true

View file

@ -1,29 +0,0 @@
//! Derive macros for `bevy_animation`.
extern crate proc_macro;
use bevy_macro_utils::BevyManifest;
use proc_macro::TokenStream;
use quote::quote;
use syn::{parse_macro_input, DeriveInput};
/// Used to derive `AnimationEvent` for a type.
#[proc_macro_derive(AnimationEvent)]
pub fn derive_animation_event(input: TokenStream) -> TokenStream {
let ast = parse_macro_input!(input as DeriveInput);
let name = ast.ident;
let manifest = BevyManifest::default();
let bevy_animation_path = manifest.get_path("bevy_animation");
let bevy_ecs_path = manifest.get_path("bevy_ecs");
let animation_event_path = quote! { #bevy_animation_path::animation_event };
let (impl_generics, ty_generics, where_clause) = ast.generics.split_for_impl();
// TODO: This could derive Event as well.
quote! {
impl #impl_generics #animation_event_path::AnimationEvent for #name #ty_generics #where_clause {
fn trigger(&self, _time: f32, _weight: f32, entity: #bevy_ecs_path::entity::Entity, world: &mut #bevy_ecs_path::world::World) {
world.entity_mut(entity).trigger(Clone::clone(self));
}
}
}
.into()
}

View file

@ -1,281 +0,0 @@
//! 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 behavior, 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((AnimationGraphHandle(graphs.add(graph)), player));
/// }
/// #
/// # bevy_ecs::system::assert_is_system(setup_animation);
/// ```
#[reflect_trait]
pub trait AnimationEvent: CloneableAnimationEvent + Reflect + Send + Sync {
/// Trigger the event, targeting `entity`.
fn trigger(&self, time: f32, weight: f32, entity: Entity, world: &mut World);
}
/// This trait exist so that manual implementors of [`AnimationEvent`]
/// do not have to implement `clone_value`.
#[diagnostic::on_unimplemented(
message = "`{Self}` does not implement `Clone`",
note = "consider annotating `{Self}` with `#[derive(Clone)]`"
)]
pub trait CloneableAnimationEvent {
/// Clone this value into a new `Box<dyn AnimationEvent>`
fn clone_value(&self) -> Box<dyn AnimationEvent>;
}
impl<T: AnimationEvent + Clone> CloneableAnimationEvent for T {
fn clone_value(&self) -> Box<dyn AnimationEvent> {
Box::new(self.clone())
}
}
/// The data that will be used to trigger an animation event.
#[derive(TypePath)]
pub(crate) struct AnimationEventData(pub(crate) Box<dyn AnimationEvent>);
impl AnimationEventData {
pub(crate) fn new(event: impl AnimationEvent) -> Self {
Self(Box::new(event))
}
}
impl Debug for AnimationEventData {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.write_str("AnimationEventData(")?;
PartialReflect::debug(self.0.as_ref(), f)?;
f.write_str(")")?;
Ok(())
}
}
impl Clone for AnimationEventData {
fn clone(&self) -> Self {
Self(CloneableAnimationEvent::clone_value(self.0.as_ref()))
}
}
// We have to implement `GetTypeRegistration` manually because of the embedded
// `Box<dyn AnimationEvent>`, which can't be automatically derived yet.
impl GetTypeRegistration for AnimationEventData {
fn get_type_registration() -> TypeRegistration {
let mut registration = TypeRegistration::of::<Self>();
registration.insert::<ReflectFromPtr>(FromType::<Self>::from_type());
registration
}
}
// We have to implement `Typed` manually because of the embedded
// `Box<dyn AnimationEvent>`, which can't be automatically derived yet.
impl Typed for AnimationEventData {
fn type_info() -> &'static TypeInfo {
static CELL: NonGenericTypeInfoCell = NonGenericTypeInfoCell::new();
CELL.get_or_set(|| {
TypeInfo::TupleStruct(TupleStructInfo::new::<Self>(&[UnnamedField::new::<()>(0)]))
})
}
}
// We have to implement `TupleStruct` manually because of the embedded
// `Box<dyn AnimationEvent>`, which can't be automatically derived yet.
impl TupleStruct for AnimationEventData {
fn field(&self, index: usize) -> Option<&dyn PartialReflect> {
match index {
0 => Some(self.0.as_partial_reflect()),
_ => None,
}
}
fn field_mut(&mut self, index: usize) -> Option<&mut dyn PartialReflect> {
match index {
0 => Some(self.0.as_partial_reflect_mut()),
_ => None,
}
}
fn field_len(&self) -> usize {
1
}
fn iter_fields(&self) -> TupleStructFieldIter {
TupleStructFieldIter::new(self)
}
fn clone_dynamic(&self) -> DynamicTupleStruct {
DynamicTupleStruct::from_iter([PartialReflect::clone_value(&*self.0)])
}
}
// We have to implement `PartialReflect` manually because of the embedded
// `Box<dyn AnimationEvent>`, which can't be automatically derived yet.
impl PartialReflect for AnimationEventData {
#[inline]
fn get_represented_type_info(&self) -> Option<&'static TypeInfo> {
Some(<Self as Typed>::type_info())
}
#[inline]
fn into_partial_reflect(self: Box<Self>) -> Box<dyn PartialReflect> {
self
}
#[inline]
fn as_partial_reflect(&self) -> &dyn PartialReflect {
self
}
#[inline]
fn as_partial_reflect_mut(&mut self) -> &mut dyn PartialReflect {
self
}
fn try_into_reflect(self: Box<Self>) -> Result<Box<dyn Reflect>, Box<dyn PartialReflect>> {
Ok(self)
}
#[inline]
fn try_as_reflect(&self) -> Option<&dyn Reflect> {
Some(self)
}
#[inline]
fn try_as_reflect_mut(&mut self) -> Option<&mut dyn Reflect> {
Some(self)
}
fn try_apply(&mut self, value: &dyn PartialReflect) -> Result<(), ApplyError> {
if let ReflectRef::TupleStruct(struct_value) = value.reflect_ref() {
for (i, value) in struct_value.iter_fields().enumerate() {
if let Some(v) = self.field_mut(i) {
v.try_apply(value)?;
}
}
} else {
return Err(ApplyError::MismatchedKinds {
from_kind: value.reflect_kind(),
to_kind: ReflectKind::TupleStruct,
});
}
Ok(())
}
fn reflect_ref(&self) -> ReflectRef {
ReflectRef::TupleStruct(self)
}
fn reflect_mut(&mut self) -> ReflectMut {
ReflectMut::TupleStruct(self)
}
fn reflect_owned(self: Box<Self>) -> ReflectOwned {
ReflectOwned::TupleStruct(self)
}
fn clone_value(&self) -> Box<dyn PartialReflect> {
Box::new(Clone::clone(self))
}
}
// We have to implement `Reflect` manually because of the embedded
// `Box<dyn AnimationEvent>`, which can't be automatically derived yet.
impl Reflect for AnimationEventData {
#[inline]
fn into_any(self: Box<Self>) -> Box<dyn Any> {
self
}
#[inline]
fn as_any(&self) -> &dyn Any {
self
}
#[inline]
fn as_any_mut(&mut self) -> &mut dyn Any {
self
}
#[inline]
fn into_reflect(self: Box<Self>) -> Box<dyn Reflect> {
self
}
#[inline]
fn as_reflect(&self) -> &dyn Reflect {
self
}
#[inline]
fn as_reflect_mut(&mut self) -> &mut dyn Reflect {
self
}
#[inline]
fn set(&mut self, value: Box<dyn Reflect>) -> Result<(), Box<dyn Reflect>> {
*self = value.take()?;
Ok(())
}
}
// We have to implement `FromReflect` manually because of the embedded
// `Box<dyn AnimationEvent>`, which can't be automatically derived yet.
impl FromReflect for AnimationEventData {
fn from_reflect(reflect: &dyn PartialReflect) -> Option<Self> {
Some(reflect.try_downcast_ref::<AnimationEventData>()?.clone())
}
}

View file

@ -11,13 +11,11 @@ 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,
@ -30,7 +28,7 @@ use prelude::AnimationCurveEvaluator;
use crate::graph::{AnimationGraphHandle, ThreadedAnimationGraphs};
use bevy_app::{App, Plugin, PostUpdate};
use bevy_app::{Animation, App, Plugin, PostUpdate};
use bevy_asset::{Asset, AssetApp, Assets};
use bevy_core::Name;
use bevy_ecs::{
@ -64,12 +62,8 @@ use uuid::Uuid;
pub mod prelude {
#[doc(hidden)]
pub use crate::{
animatable::*,
animation_curves::*,
animation_event::{AnimationEvent, ReflectAnimationEvent},
graph::*,
transition::*,
AnimationClip, AnimationPlayer, AnimationPlugin, VariableCurve,
animatable::*, animation_curves::*, graph::*, transition::*, AnimationClip,
AnimationPlayer, AnimationPlugin, VariableCurve,
};
}
@ -78,6 +72,7 @@ use crate::{
graph::{AnimationGraph, AnimationGraphAssetLoader, AnimationNodeIndex},
transition::{advance_transitions, expire_completed_transitions, AnimationTransitions},
};
use alloc::sync::Arc;
/// The [UUID namespace] of animation targets (e.g. bones).
///
@ -289,7 +284,34 @@ pub struct AnimationClip {
#[derive(Reflect, Debug, Clone)]
struct TimedAnimationEvent {
time: f32,
event: AnimationEventData,
event: AnimationEvent,
}
#[derive(Reflect, Debug, Clone)]
struct AnimationEvent {
#[reflect(ignore)]
trigger: AnimationEventFn,
}
impl AnimationEvent {
fn trigger(&self, commands: &mut Commands, entity: Entity, time: f32, weight: f32) {
(self.trigger.0)(commands, entity, time, weight);
}
}
#[derive(Reflect, Clone)]
struct AnimationEventFn(Arc<dyn Fn(&mut Commands, Entity, f32, f32) + Send + Sync>);
impl Default for AnimationEventFn {
fn default() -> Self {
Self(Arc::new(|_commands, _entity, _time, _weight| {}))
}
}
impl Debug for AnimationEventFn {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.debug_tuple("AnimationEventFn").finish()
}
}
#[derive(Reflect, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone)]
@ -472,9 +494,24 @@ impl AnimationClip {
.push(variable_curve);
}
/// Add an [`AnimationEvent`] to an [`AnimationTarget`] named by an [`AnimationTargetId`].
/// Add a untargeted [`Event`] to this [`AnimationClip`].
///
/// The `event` will trigger on the entity matching the target once the `time` (in seconds)
/// The `event` will be cloned and triggered 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 Event + Clone) {
self.add_event_fn(
time,
move |commands: &mut Commands, entity: Entity, _time: f32, _weight: f32| {
commands.entity(entity).trigger(event.clone());
},
);
}
/// Add an [`Event`] to an [`AnimationTarget`] named by an [`AnimationTargetId`].
///
/// The `event` will be cloned and triggered 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.
@ -482,26 +519,69 @@ impl AnimationClip {
&mut self,
target_id: AnimationTargetId,
time: f32,
event: impl AnimationEvent,
event: impl Event + Clone,
) {
self.add_event_to_target_inner(AnimationEventTarget::Node(target_id), time, event);
self.add_event_fn_to_target(
target_id,
time,
move |commands: &mut Commands, entity: Entity, _time: f32, _weight: f32| {
commands.entity(entity).trigger(event.clone());
},
);
}
/// Add a untargeted [`AnimationEvent`] to this [`AnimationClip`].
/// Add a untargeted event function to this [`AnimationClip`].
///
/// The `event` will trigger on the [`AnimationPlayer`] entity once the `time` (in seconds)
/// The `func` will trigger on the [`AnimationPlayer`] entity once the `time` (in seconds)
/// is reached in the animation.
///
/// For a simpler [`Event`]-based alternative, see [`AnimationClip::add_event`].
/// 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);
///
/// ```
/// # use bevy_animation::AnimationClip;
/// # let mut clip = AnimationClip::default();
/// clip.add_event_fn(1.0, |commands, entity, time, weight| {
/// println!("Animation Event Triggered {entity:#?} at time {time} with weight {weight}");
/// })
/// ```
pub fn add_event_fn(
&mut self,
time: f32,
func: impl Fn(&mut Commands, Entity, f32, f32) + Send + Sync + 'static,
) {
self.add_event_internal(AnimationEventTarget::Root, time, func);
}
fn add_event_to_target_inner(
/// Add an event function to an [`AnimationTarget`] named by an [`AnimationTargetId`].
///
/// The `func` will trigger on the entity matching the target once the `time` (in seconds)
/// is reached in the animation.
///
/// For a simpler [`Event`]-based alternative, see [`AnimationClip::add_event_to_target`].
/// Use [`add_event`](Self::add_event) instead if you don't have a specific target.
///
/// ```
/// # use bevy_animation::{AnimationClip, AnimationTargetId};
/// # let mut clip = AnimationClip::default();
/// clip.add_event_fn_to_target(AnimationTargetId::from_iter(["Arm", "Hand"]), 1.0, |commands, entity, time, weight| {
/// println!("Animation Event Triggered {entity:#?} at time {time} with weight {weight}");
/// })
/// ```
pub fn add_event_fn_to_target(
&mut self,
target_id: AnimationTargetId,
time: f32,
func: impl Fn(&mut Commands, Entity, f32, f32) + Send + Sync + 'static,
) {
self.add_event_internal(AnimationEventTarget::Node(target_id), time, func);
}
fn add_event_internal(
&mut self,
target: AnimationEventTarget,
time: f32,
event: impl AnimationEvent,
trigger_fn: impl Fn(&mut Commands, Entity, f32, f32) + Send + Sync + 'static,
) {
self.duration = self.duration.max(time);
let triggers = self.events.entry(target).or_default();
@ -510,7 +590,9 @@ impl AnimationClip {
index,
TimedAnimationEvent {
time,
event: AnimationEventData::new(event),
event: AnimationEvent {
trigger: AnimationEventFn(Arc::new(trigger_fn)),
},
},
),
}
@ -988,12 +1070,7 @@ fn trigger_untargeted_animation_events(
};
for TimedAnimationEvent { time, event } in triggered_events.iter() {
commands.queue(trigger_animation_event(
entity,
*time,
active_animation.weight,
event.clone().0,
));
event.trigger(&mut commands, entity, *time, active_animation.weight);
}
}
}
@ -1195,12 +1272,12 @@ pub fn animate_targets(
for TimedAnimationEvent { time, event } in
triggered_events.iter()
{
commands.queue(trigger_animation_event(
event.trigger(
&mut commands,
entity,
*time,
active_animation.weight,
event.clone().0,
));
);
}
});
}
@ -1252,10 +1329,6 @@ pub fn animate_targets(
});
}
/// Animation system set
#[derive(SystemSet, Debug, Hash, PartialEq, Eq, Clone)]
pub struct Animation;
/// Adds animation support to an app
#[derive(Default)]
pub struct AnimationPlugin;
@ -1573,12 +1646,6 @@ mod tests {
#[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,

View file

@ -181,6 +181,10 @@ pub struct PostUpdate;
#[derive(ScheduleLabel, Clone, Debug, PartialEq, Eq, Hash)]
pub struct Last;
/// Animation system set. This exists in [`PostUpdate`].
#[derive(SystemSet, Debug, Hash, PartialEq, Eq, Clone)]
pub struct Animation;
/// Defines the schedules to be run for the [`Main`] schedule, including
/// their order.
#[derive(Resource, Debug)]

View file

@ -71,7 +71,7 @@ pub mod prelude {
};
}
use bevy_app::prelude::*;
use bevy_app::{prelude::*, Animation};
use bevy_asset::AssetApp;
#[cfg(feature = "default_font")]
use bevy_asset::{load_internal_binary_asset, Handle};
@ -138,7 +138,8 @@ impl Plugin for TextPlugin {
calculate_bounds_text2d.in_set(VisibilitySystems::CalculateBounds),
)
.chain()
.in_set(Update2dText),
.in_set(Update2dText)
.after(Animation),
)
.add_systems(Last, trim_cosmic_cache);

View file

@ -25,7 +25,6 @@ bevy_reflect = { path = "../bevy_reflect", version = "0.15.0-dev", features = [
"bevy",
] }
bevy_render = { path = "../bevy_render", version = "0.15.0-dev" }
bevy_animation = { path = "../bevy_animation", version = "0.15.0-dev" }
bevy_sprite = { path = "../bevy_sprite", version = "0.15.0-dev" }
bevy_text = { path = "../bevy_text", version = "0.15.0-dev" }
bevy_picking = { path = "../bevy_picking", version = "0.15.0-dev", optional = true }

View file

@ -66,7 +66,7 @@ pub mod prelude {
};
}
use bevy_app::prelude::*;
use bevy_app::{prelude::*, Animation};
use bevy_ecs::prelude::*;
use bevy_input::InputSystem;
use bevy_render::{
@ -171,9 +171,7 @@ impl Plugin for UiPlugin {
PostUpdate,
(
CameraUpdateSystem,
UiSystem::Prepare
.before(UiSystem::Stack)
.after(bevy_animation::Animation),
UiSystem::Prepare.before(UiSystem::Stack).after(Animation),
UiSystem::Layout,
UiSystem::PostLayout,
)

View file

@ -34,8 +34,7 @@ struct Animations {
graph: Handle<AnimationGraph>,
}
#[derive(Event, AnimationEvent, Reflect, Clone)]
#[reflect(AnimationEvent)]
#[derive(Event, Reflect, Clone)]
struct OnStep;
fn observe_on_step(

View file

@ -11,39 +11,27 @@ fn main() {
.add_plugins(DefaultPlugins)
.add_event::<MessageEvent>()
.add_systems(Startup, setup)
.add_systems(PreUpdate, (animate_text_opacity, edit_message))
.add_systems(Update, animate_text_opacity)
.add_observer(edit_message)
.run();
}
#[derive(Component)]
struct MessageText;
#[derive(Event, Reflect, Clone)]
#[reflect(AnimationEvent)]
#[derive(Event, Clone)]
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>,
trigger: Trigger<MessageEvent>,
text: Single<(&mut Text2d, &mut TextColor), With<MessageText>>,
) {
let (mut text, mut color) = text.into_inner();
for event in event_reader.read() {
text.0 = event.value.clone();
color.0 = event.color;
}
text.0 = trigger.event().value.clone();
color.0 = trigger.event().color;
}
fn setup(

View file

@ -34,7 +34,6 @@ crates=(
bevy_core_pipeline
bevy_input
bevy_gilrs
bevy_animation/derive
bevy_animation
bevy_pbr
bevy_gltf