diff --git a/Cargo.toml b/Cargo.toml index 4bb5ace1b9729b..c244e5381b70a8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2914,6 +2914,17 @@ description = "Demonstrates how to create a node with a border" category = "UI (User Interface)" wasm = true +[[example]] +name = "responding_to_changes" +path = "examples/ecs/responding_to_changes.rs" +doc-scrape-examples = true + +[package.metadata.example.responding_to_changes] +name = "Responding to Changes" +description = "Demonstrates how and when to use change detection and `OnMutate` hooks and observers" +category = "ECS (Entity Component System)" +wasm = true + [[example]] name = "box_shadow" path = "examples/ui/box_shadow.rs" diff --git a/crates/bevy_ecs/macros/src/world_query.rs b/crates/bevy_ecs/macros/src/world_query.rs index 9d97f185dede96..3150fdccc036b8 100644 --- a/crates/bevy_ecs/macros/src/world_query.rs +++ b/crates/bevy_ecs/macros/src/world_query.rs @@ -151,6 +151,7 @@ pub(crate) fn world_query_impl( } const IS_DENSE: bool = true #(&& <#field_types>::IS_DENSE)*; + const IS_MUTATE: bool = false #(|| <#field_types>::IS_MUTATE)*; /// SAFETY: we call `set_archetype` for each member that implements `Fetch` #[inline] diff --git a/crates/bevy_ecs/src/archetype.rs b/crates/bevy_ecs/src/archetype.rs index 2c2241e29ec6d5..0cfd0ddb13fa77 100644 --- a/crates/bevy_ecs/src/archetype.rs +++ b/crates/bevy_ecs/src/archetype.rs @@ -352,6 +352,7 @@ bitflags::bitflags! { const ON_INSERT_OBSERVER = (1 << 5); const ON_REPLACE_OBSERVER = (1 << 6); const ON_REMOVE_OBSERVER = (1 << 7); + const ON_MUTATE_OBSERVER = (1 << 8); } } @@ -697,6 +698,12 @@ impl Archetype { pub fn has_remove_observer(&self) -> bool { self.flags().contains(ArchetypeFlags::ON_REMOVE_OBSERVER) } + + /// Returns true if any of the components in this archetype have at least one [`OnMutate`] observer + #[inline] + pub fn has_mutate_observer(&self) -> bool { + self.flags().contains(ArchetypeFlags::ON_MUTATE_OBSERVER) + } } /// The next [`ArchetypeId`] in an [`Archetypes`] collection. diff --git a/crates/bevy_ecs/src/change_detection.rs b/crates/bevy_ecs/src/change_detection.rs index 025c3804e73e25..e196411cd3331a 100644 --- a/crates/bevy_ecs/src/change_detection.rs +++ b/crates/bevy_ecs/src/change_detection.rs @@ -10,11 +10,13 @@ use core::{ mem, ops::{Deref, DerefMut}, }; +use std::cell::RefCell; #[cfg(feature = "track_change_detection")] use { bevy_ptr::ThinSlicePtr, core::{cell::UnsafeCell, panic::Location}, }; +use crate::world::entity_change::{EntityChange, EntityChanges}; /// The (arbitrarily chosen) minimum number of world tick increments between `check_tick` scans. /// @@ -370,6 +372,62 @@ macro_rules! change_detection_mut_impl { }; } +macro_rules! change_detection_mut_with_onchange_impl { + ($name:ident < $( $generics:tt ),+ >, $target:ty, $($traits:ident)?) => { + impl<$($generics),* : ?Sized $(+ $traits)?> DetectChangesMut for $name<$($generics),*> { + type Inner = $target; + + #[inline] + #[track_caller] + fn set_changed(&mut self) { + *self.ticks.changed = self.ticks.this_run; + if let Some((change, changes)) = self.on_change { + changes.borrow_mut().push(change); + } + #[cfg(feature = "track_change_detection")] + { + *self.changed_by = Location::caller(); + } + } + + #[inline] + #[track_caller] + fn set_last_changed(&mut self, last_changed: Tick) { + *self.ticks.changed = last_changed; + #[cfg(feature = "track_change_detection")] + { + *self.changed_by = Location::caller(); + } + } + + #[inline] + fn bypass_change_detection(&mut self) -> &mut Self::Inner { + self.value + } + } + + impl<$($generics),* : ?Sized $(+ $traits)?> DerefMut for $name<$($generics),*> { + #[inline] + #[track_caller] + fn deref_mut(&mut self) -> &mut Self::Target { + self.set_changed(); + #[cfg(feature = "track_change_detection")] + { + *self.changed_by = Location::caller(); + } + self.value + } + } + + impl<$($generics),* $(: $traits)?> AsMut<$target> for $name<$($generics),*> { + #[inline] + fn as_mut(&mut self) -> &mut $target { + self.deref_mut() + } + } + }; +} + macro_rules! impl_methods { ($name:ident < $( $generics:tt ),+ >, $target:ty, $($traits:ident)?) => { impl<$($generics),* : ?Sized $(+ $traits)?> $name<$($generics),*> { @@ -387,6 +445,96 @@ macro_rules! impl_methods { /// `, but you need a `Mut`. pub fn reborrow(&mut self) -> Mut<'_, $target> { Mut { + on_change: None, + value: self.value, + ticks: TicksMut { + added: self.ticks.added, + changed: self.ticks.changed, + last_run: self.ticks.last_run, + this_run: self.ticks.this_run, + }, + #[cfg(feature = "track_change_detection")] + changed_by: self.changed_by, + } + } + + /// Maps to an inner value by applying a function to the contained reference, without flagging a change. + /// + /// You should never modify the argument passed to the closure -- if you want to modify the data + /// without flagging a change, consider using [`DetectChangesMut::bypass_change_detection`] to make your intent explicit. + /// + /// ``` + /// # use bevy_ecs::prelude::*; + /// # #[derive(PartialEq)] pub struct Vec2; + /// # impl Vec2 { pub const ZERO: Self = Self; } + /// # #[derive(Component)] pub struct Transform { translation: Vec2 } + /// // When run, zeroes the translation of every entity. + /// fn reset_positions(mut transforms: Query<&mut Transform>) { + /// for transform in &mut transforms { + /// // We pinky promise not to modify `t` within the closure. + /// // Breaking this promise will result in logic errors, but will never cause undefined behavior. + /// let mut translation = transform.map_unchanged(|t| &mut t.translation); + /// // Only reset the translation if it isn't already zero; + /// translation.set_if_neq(Vec2::ZERO); + /// } + /// } + /// # bevy_ecs::system::assert_is_system(reset_positions); + /// ``` + pub fn map_unchanged(self, f: impl FnOnce(&mut $target) -> &mut U) -> Mut<'w, U> { + Mut { + on_change: None, + value: f(self.value), + ticks: self.ticks, + #[cfg(feature = "track_change_detection")] + changed_by: self.changed_by, + } + } + + /// Optionally maps to an inner value by applying a function to the contained reference. + /// This is useful in a situation where you need to convert a `Mut` to a `Mut`, but only if `T` contains `U`. + /// + /// As with `map_unchanged`, you should never modify the argument passed to the closure. + pub fn filter_map_unchanged(self, f: impl FnOnce(&mut $target) -> Option<&mut U>) -> Option> { + let value = f(self.value); + value.map(|value| Mut { + on_change: None, + value, + ticks: self.ticks, + #[cfg(feature = "track_change_detection")] + changed_by: self.changed_by, + }) + } + + /// Allows you access to the dereferenced value of this pointer without immediately + /// triggering change detection. + pub fn as_deref_mut(&mut self) -> Mut<'_, <$target as Deref>::Target> + where $target: DerefMut + { + self.reborrow().map_unchanged(|v| v.deref_mut()) + } + + } + }; +} + +macro_rules! impl_methods_with_onchange { + ($name:ident < $( $generics:tt ),+ >, $target:ty, $($traits:ident)?) => { + impl<$($generics),* : ?Sized $(+ $traits)?> $name<$($generics),*> { + /// Consume `self` and return a mutable reference to the + /// contained value while marking `self` as "changed". + #[inline] + pub fn into_inner(mut self) -> &'w mut $target { + self.set_changed(); + self.value + } + + /// Returns a `Mut<>` with a smaller lifetime. + /// This is useful if you have `&mut + #[doc = stringify!($name)] + /// `, but you need a `Mut`. + pub fn reborrow(&mut self) -> Mut<'_, $target> { + Mut { + on_change: self.on_change, value: self.value, ticks: TicksMut { added: self.ticks.added, @@ -423,6 +571,7 @@ macro_rules! impl_methods { /// ``` pub fn map_unchanged(self, f: impl FnOnce(&mut $target) -> &mut U) -> Mut<'w, U> { Mut { + on_change: self.on_change, value: f(self.value), ticks: self.ticks, #[cfg(feature = "track_change_detection")] @@ -437,6 +586,7 @@ macro_rules! impl_methods { pub fn filter_map_unchanged(self, f: impl FnOnce(&mut $target) -> Option<&mut U>) -> Option> { let value = f(self.value); value.map(|value| Mut { + on_change: self.on_change, value, ticks: self.ticks, #[cfg(feature = "track_change_detection")] @@ -667,6 +817,7 @@ impl<'w, T: Resource> From> for Mut<'w, T> { /// while losing the specificity of `ResMut` for resources. fn from(other: ResMut<'w, T>) -> Mut<'w, T> { Mut { + on_change: None, value: other.value, ticks: other.ticks, #[cfg(feature = "track_change_detection")] @@ -703,6 +854,7 @@ impl<'w, T: 'static> From> for Mut<'w, T> { /// while losing the specificity of `NonSendMut`. fn from(other: NonSendMut<'w, T>) -> Mut<'w, T> { Mut { + on_change: None, value: other.value, ticks: other.ticks, #[cfg(feature = "track_change_detection")] @@ -869,6 +1021,7 @@ impl_debug!(Ref<'w, T>,); /// # fn update_player_position(player: &Player, new_position: Position) {} /// ``` pub struct Mut<'w, T: ?Sized> { + pub(crate) on_change: Option<(EntityChange, &'w RefCell)>, pub(crate) value: &'w mut T, pub(crate) ticks: TicksMut<'w>, #[cfg(feature = "track_change_detection")] @@ -900,6 +1053,7 @@ impl<'w, T: ?Sized> Mut<'w, T> { #[cfg(feature = "track_change_detection")] caller: &'w mut &'static Location<'static>, ) -> Self { Self { + on_change: None, value, ticks: TicksMut { added, @@ -949,9 +1103,10 @@ where } } + change_detection_impl!(Mut<'w, T>, T,); -change_detection_mut_impl!(Mut<'w, T>, T,); -impl_methods!(Mut<'w, T>, T,); +change_detection_mut_with_onchange_impl!(Mut<'w, T>, T,); +impl_methods_with_onchange!(Mut<'w, T>, T,); impl_debug!(Mut<'w, T>,); /// Unique mutable borrow of resources or an entity's component. @@ -963,6 +1118,7 @@ impl_debug!(Mut<'w, T>,); /// [`Mut`], but in situations where the types are not known at compile time /// or are defined outside of rust this can be used. pub struct MutUntyped<'w> { + pub(crate) on_change: Option<(EntityChange, &'w RefCell)>, pub(crate) value: PtrMut<'w>, pub(crate) ticks: TicksMut<'w>, #[cfg(feature = "track_change_detection")] @@ -984,6 +1140,7 @@ impl<'w> MutUntyped<'w> { #[inline] pub fn reborrow(&mut self) -> MutUntyped { MutUntyped { + on_change: self.on_change, value: self.value.reborrow(), ticks: TicksMut { added: self.ticks.added, @@ -1041,6 +1198,7 @@ impl<'w> MutUntyped<'w> { /// ``` pub fn map_unchanged(self, f: impl FnOnce(PtrMut<'w>) -> &'w mut T) -> Mut<'w, T> { Mut { + on_change: None, // TODO value: f(self.value), ticks: self.ticks, #[cfg(feature = "track_change_detection")] @@ -1054,6 +1212,7 @@ impl<'w> MutUntyped<'w> { /// - `T` must be the erased pointee type for this [`MutUntyped`]. pub unsafe fn with_type(self) -> Mut<'w, T> { Mut { + on_change: None, // SAFETY: `value` is `Aligned` and caller ensures the pointee type is `T`. value: unsafe { self.value.deref_mut() }, ticks: self.ticks, @@ -1098,6 +1257,9 @@ impl<'w> DetectChangesMut for MutUntyped<'w> { #[track_caller] fn set_changed(&mut self) { *self.ticks.changed = self.ticks.this_run; + if let Some((change, changes)) = self.on_change { + changes.borrow_mut().push(change); + } #[cfg(feature = "track_change_detection")] { *self.changed_by = Location::caller(); @@ -1132,6 +1294,7 @@ impl core::fmt::Debug for MutUntyped<'_> { impl<'w, T> From> for MutUntyped<'w> { fn from(value: Mut<'w, T>) -> Self { MutUntyped { + on_change: value.on_change, value: value.value.into(), ticks: value.ticks, #[cfg(feature = "track_change_detection")] @@ -1423,6 +1586,7 @@ mod tests { let mut caller = Location::caller(); let ptr = Mut { + on_change: None, value: &mut outer, ticks, #[cfg(feature = "track_change_detection")] @@ -1513,6 +1677,7 @@ mod tests { let mut caller = Location::caller(); let value = MutUntyped { + on_change: None, value: PtrMut::from(&mut value), ticks, #[cfg(feature = "track_change_detection")] @@ -1551,6 +1716,7 @@ mod tests { let mut caller = Location::caller(); let mut_typed = Mut { + on_change: None, value: &mut c, ticks, #[cfg(feature = "track_change_detection")] diff --git a/crates/bevy_ecs/src/observer/mod.rs b/crates/bevy_ecs/src/observer/mod.rs index 674f98f6507253..7407448aa76e41 100644 --- a/crates/bevy_ecs/src/observer/mod.rs +++ b/crates/bevy_ecs/src/observer/mod.rs @@ -247,6 +247,7 @@ pub struct Observers { on_insert: CachedObservers, on_replace: CachedObservers, on_remove: CachedObservers, + on_mutate: CachedObservers, // Map from trigger type to set of observers cache: HashMap, } @@ -258,6 +259,7 @@ impl Observers { ON_INSERT => &mut self.on_insert, ON_REPLACE => &mut self.on_replace, ON_REMOVE => &mut self.on_remove, + ON_MUTATE => &mut self.on_mutate, _ => self.cache.entry(event_type).or_default(), } } @@ -268,6 +270,7 @@ impl Observers { ON_INSERT => Some(&self.on_insert), ON_REPLACE => Some(&self.on_replace), ON_REMOVE => Some(&self.on_remove), + ON_MUTATE => Some(&self.on_mutate), _ => self.cache.get(&event_type), } } @@ -342,6 +345,7 @@ impl Observers { ON_INSERT => Some(ArchetypeFlags::ON_INSERT_OBSERVER), ON_REPLACE => Some(ArchetypeFlags::ON_REPLACE_OBSERVER), ON_REMOVE => Some(ArchetypeFlags::ON_REMOVE_OBSERVER), + ON_MUTATE => Some(ArchetypeFlags::ON_MUTATE_OBSERVER), _ => None, } } @@ -378,6 +382,14 @@ impl Observers { { flags.insert(ArchetypeFlags::ON_REMOVE_OBSERVER); } + + if self + .on_mutate + .component_observers + .contains_key(&component_id) + { + flags.insert(ArchetypeFlags::ON_MUTATE_OBSERVER); + } } } diff --git a/crates/bevy_ecs/src/query/fetch.rs b/crates/bevy_ecs/src/query/fetch.rs index de3796190f172d..00559f51e89a7d 100644 --- a/crates/bevy_ecs/src/query/fetch.rs +++ b/crates/bevy_ecs/src/query/fetch.rs @@ -14,7 +14,9 @@ use crate::{ use bevy_ptr::{ThinSlicePtr, UnsafeCellDeref}; use bevy_utils::all_tuples; use core::{cell::UnsafeCell, marker::PhantomData}; +use std::cell::RefCell; use smallvec::SmallVec; +use crate::world::entity_change::{EntityChange, EntityChanges}; /// Types that can be fetched from a [`World`] using a [`Query`]. /// @@ -314,6 +316,7 @@ unsafe impl WorldQuery for Entity { } const IS_DENSE: bool = true; + const IS_MUTATE: bool = false; #[inline] unsafe fn set_archetype<'w>( @@ -389,6 +392,7 @@ unsafe impl WorldQuery for EntityLocation { // This is set to true to avoid forcing archetypal iteration in compound queries, is likely to be slower // in most practical use case. const IS_DENSE: bool = true; + const IS_MUTATE: bool = false; #[inline] unsafe fn set_archetype<'w>( @@ -464,6 +468,7 @@ unsafe impl<'a> WorldQuery for EntityRef<'a> { } const IS_DENSE: bool = true; + const IS_MUTATE: bool = false; #[inline] unsafe fn set_archetype<'w>( @@ -544,6 +549,7 @@ unsafe impl<'a> WorldQuery for EntityMut<'a> { } const IS_DENSE: bool = true; + const IS_MUTATE: bool = true; #[inline] unsafe fn set_archetype<'w>( @@ -612,6 +618,7 @@ unsafe impl<'a> WorldQuery for FilteredEntityRef<'a> { } const IS_DENSE: bool = false; + const IS_MUTATE: bool = false; unsafe fn init_fetch<'w>( world: UnsafeWorldCell<'w>, @@ -707,6 +714,7 @@ unsafe impl<'a> WorldQuery for FilteredEntityMut<'a> { } const IS_DENSE: bool = false; + const IS_MUTATE: bool = true; unsafe fn init_fetch<'w>( world: UnsafeWorldCell<'w>, @@ -812,6 +820,7 @@ where } const IS_DENSE: bool = true; + const IS_MUTATE: bool = false; unsafe fn set_archetype<'w>( _: &mut Self::Fetch<'w>, @@ -911,6 +920,7 @@ where } const IS_DENSE: bool = true; + const IS_MUTATE: bool = true; unsafe fn set_archetype<'w>( _: &mut Self::Fetch<'w>, @@ -1006,6 +1016,7 @@ unsafe impl WorldQuery for &Archetype { // This could probably be a non-dense query and just set a Option<&Archetype> fetch value in // set_archetypes, but forcing archetypal iteration is likely to be slower in any compound query. const IS_DENSE: bool = true; + const IS_MUTATE: bool = false; #[inline] unsafe fn set_archetype<'w>( @@ -1126,6 +1137,7 @@ unsafe impl WorldQuery for &T { StorageType::SparseSet => false, } }; + const IS_MUTATE: bool = false; #[inline] unsafe fn set_archetype<'w>( @@ -1294,6 +1306,7 @@ unsafe impl<'__w, T: Component> WorldQuery for Ref<'__w, T> { StorageType::SparseSet => false, } }; + const IS_MUTATE: bool = false; #[inline] unsafe fn set_archetype<'w>( @@ -1417,6 +1430,8 @@ unsafe impl<'__w, T: Component> ReadOnlyQueryData for Ref<'__w, T> {} /// The [`WorldQuery::Fetch`] type for `&mut T`. pub struct WriteFetch<'w, T: Component> { + component_id: ComponentId, + changes: &'w RefCell, components: StorageSwitch< T, // T::STORAGE_TYPE = StorageType::Table @@ -1466,6 +1481,8 @@ unsafe impl<'__w, T: Component> WorldQuery for &'__w mut T { this_run: Tick, ) -> WriteFetch<'w, T> { WriteFetch { + component_id, + changes: &world.entity_changes(), components: StorageSwitch::new( || None, || { @@ -1494,6 +1511,8 @@ unsafe impl<'__w, T: Component> WorldQuery for &'__w mut T { } }; + const IS_MUTATE: bool = true; + #[inline] unsafe fn set_archetype<'w>( fetch: &mut WriteFetch<'w, T>, @@ -1552,6 +1571,10 @@ unsafe impl<'__w, T: Component> WorldQuery for &'__w mut T { let caller = unsafe { _callers.get(table_row.as_usize()) }; Mut { + on_change: Some(( + EntityChange::new(entity, fetch.component_id), + fetch.changes, + )), value: component.deref_mut(), ticks: TicksMut { added: added.deref_mut(), @@ -1569,6 +1592,10 @@ unsafe impl<'__w, T: Component> WorldQuery for &'__w mut T { unsafe { sparse_set.get_with_ticks(entity).debug_checked_unwrap() }; Mut { + on_change: Some(( + EntityChange::new(entity, fetch.component_id), + fetch.changes, + )), value: component.assert_unique().deref_mut(), ticks: TicksMut::from_tick_cells(ticks, fetch.last_run, fetch.this_run), #[cfg(feature = "track_change_detection")] @@ -1647,6 +1674,7 @@ unsafe impl<'__w, T: Component> WorldQuery for Mut<'__w, T> { // Forwarded to `&mut T` const IS_DENSE: bool = <&mut T as WorldQuery>::IS_DENSE; + const IS_MUTATE: bool = true; #[inline] // Forwarded to `&mut T` @@ -1766,6 +1794,7 @@ unsafe impl WorldQuery for Option { } const IS_DENSE: bool = T::IS_DENSE; + const IS_MUTATE: bool = T::IS_MUTATE; #[inline] unsafe fn set_archetype<'w>( @@ -1948,6 +1977,7 @@ unsafe impl WorldQuery for Has { StorageType::SparseSet => false, } }; + const IS_MUTATE: bool = false; #[inline] unsafe fn set_archetype<'w>( @@ -2066,6 +2096,7 @@ macro_rules! impl_anytuple_fetch { } const IS_DENSE: bool = true $(&& $name::IS_DENSE)*; + const IS_MUTATE: bool = false $(|| $name::IS_MUTATE)*; #[inline] unsafe fn set_archetype<'w>( @@ -2211,6 +2242,7 @@ unsafe impl WorldQuery for NopWorldQuery { } const IS_DENSE: bool = D::IS_DENSE; + const IS_MUTATE: bool = false; #[inline(always)] unsafe fn set_archetype( @@ -2283,6 +2315,7 @@ unsafe impl WorldQuery for PhantomData { // `PhantomData` does not match any components, so all components it matches // are stored in a Table (vacuous truth). const IS_DENSE: bool = true; + const IS_MUTATE: bool = false; unsafe fn set_archetype<'w>( _fetch: &mut Self::Fetch<'w>, diff --git a/crates/bevy_ecs/src/query/filter.rs b/crates/bevy_ecs/src/query/filter.rs index ff73d6ca500164..58814a6abb689c 100644 --- a/crates/bevy_ecs/src/query/filter.rs +++ b/crates/bevy_ecs/src/query/filter.rs @@ -164,6 +164,7 @@ unsafe impl WorldQuery for With { StorageType::SparseSet => false, } }; + const IS_MUTATE: bool = false; #[inline] unsafe fn set_archetype( @@ -275,7 +276,8 @@ unsafe impl WorldQuery for Without { StorageType::SparseSet => false, } }; - + const IS_MUTATE: bool = false; + #[inline] unsafe fn set_archetype( _fetch: &mut (), @@ -409,6 +411,7 @@ macro_rules! impl_or_query_filter { } const IS_DENSE: bool = true $(&& $filter::IS_DENSE)*; + const IS_MUTATE: bool = false; #[inline] unsafe fn init_fetch<'w>(world: UnsafeWorldCell<'w>, state: &Self::State, last_run: Tick, this_run: Tick) -> Self::Fetch<'w> { @@ -693,6 +696,7 @@ unsafe impl WorldQuery for Added { StorageType::SparseSet => false, } }; + const IS_MUTATE: bool = false; #[inline] unsafe fn set_archetype<'w>( @@ -926,6 +930,7 @@ unsafe impl WorldQuery for Changed { StorageType::SparseSet => false, } }; + const IS_MUTATE: bool = false; #[inline] unsafe fn set_archetype<'w>( diff --git a/crates/bevy_ecs/src/query/world_query.rs b/crates/bevy_ecs/src/query/world_query.rs index f2da8b3558f26e..85e94b720cb02f 100644 --- a/crates/bevy_ecs/src/query/world_query.rs +++ b/crates/bevy_ecs/src/query/world_query.rs @@ -77,6 +77,9 @@ pub unsafe trait WorldQuery { /// iterators. const IS_DENSE: bool; + /// Return true if (and only if) TODO + const IS_MUTATE: bool; + /// Adjusts internal state to account for the next [`Archetype`]. This will always be called on /// archetypes that match this [`WorldQuery`]. /// @@ -185,6 +188,7 @@ macro_rules! impl_tuple_world_query { } const IS_DENSE: bool = true $(&& $name::IS_DENSE)*; + const IS_MUTATE: bool = false $(|| $name::IS_MUTATE)*; #[inline] unsafe fn set_archetype<'w>( diff --git a/crates/bevy_ecs/src/storage/resource.rs b/crates/bevy_ecs/src/storage/resource.rs index ed8700e1c74e2b..6038d0407b4445 100644 --- a/crates/bevy_ecs/src/storage/resource.rs +++ b/crates/bevy_ecs/src/storage/resource.rs @@ -147,6 +147,7 @@ impl ResourceData { pub(crate) fn get_mut(&mut self, last_run: Tick, this_run: Tick) -> Option> { let (ptr, ticks, _caller) = self.get_with_ticks()?; Some(MutUntyped { + on_change: None, // SAFETY: We have exclusive access to the underlying storage. value: unsafe { ptr.assert_unique() }, // SAFETY: We have exclusive access to the underlying storage. diff --git a/crates/bevy_ecs/src/system/system_param.rs b/crates/bevy_ecs/src/system/system_param.rs index c3e26458640949..d59d043cee657b 100644 --- a/crates/bevy_ecs/src/system/system_param.rs +++ b/crates/bevy_ecs/src/system/system_param.rs @@ -28,7 +28,6 @@ use core::{ marker::PhantomData, ops::{Deref, DerefMut}, }; - use super::Populated; /// A parameter that can be used in a [`System`](super::System). @@ -295,6 +294,9 @@ unsafe impl SystemParam for Qu type Item<'w, 's> = Query<'w, 's, D, F>; fn init_state(world: &mut World, system_meta: &mut SystemMeta) -> Self::State { + if D::IS_MUTATE { + system_meta.set_has_deferred() + } let state = QueryState::new_with_access(world, &mut system_meta.archetype_component_access); init_query_param(world, system_meta, &state); state diff --git a/crates/bevy_ecs/src/world/component_constants.rs b/crates/bevy_ecs/src/world/component_constants.rs index 5eea8dc6229ef1..70efcf0a24cd50 100644 --- a/crates/bevy_ecs/src/world/component_constants.rs +++ b/crates/bevy_ecs/src/world/component_constants.rs @@ -14,6 +14,9 @@ pub const ON_REPLACE: ComponentId = ComponentId::new(2); /// [`ComponentId`] for [`OnRemove`] pub const ON_REMOVE: ComponentId = ComponentId::new(3); +/// [`ComponentId`] for [`OnMutate`] +pub const ON_MUTATE: ComponentId = ComponentId::new(4); + /// Trigger emitted when a component is added to an entity. See [`crate::component::ComponentHooks::on_add`] /// for more information. #[derive(Event, Debug)] @@ -41,3 +44,9 @@ pub struct OnReplace; #[cfg_attr(feature = "bevy_reflect", derive(Reflect))] #[cfg_attr(feature = "bevy_reflect", reflect(Debug))] pub struct OnRemove; + +/// Trigger emitted when a component is changed from an entity +#[derive(Event, Debug)] +#[cfg_attr(feature = "bevy_reflect", derive(Reflect))] +#[cfg_attr(feature = "bevy_reflect", reflect(Debug))] +pub struct OnMutate; \ No newline at end of file diff --git a/crates/bevy_ecs/src/world/entity_change.rs b/crates/bevy_ecs/src/world/entity_change.rs new file mode 100644 index 00000000000000..2427cde1f9bf4a --- /dev/null +++ b/crates/bevy_ecs/src/world/entity_change.rs @@ -0,0 +1,56 @@ +use std::cell::RefCell; +use bevy_utils::Parallel; +use crate::component::ComponentId; +use crate::entity::Entity; + +/// A shorthand for [`Vec`]. +pub type EntityChanges = Vec; + +/// A Collection of EntityChange storages +/// Can be accessed via [`World`] +#[derive(Default)] +pub struct ParallelEntityChanges { + list: Parallel, +} + +impl ParallelEntityChanges { + + /// Returns a default `Changes` + + pub fn new() -> Self { + Self::default() + } + + /// Gets a mutable iterator over all of the per-thread [`EntityChanges`]. + pub fn iter_mut(&mut self) -> impl Iterator { + self.list.iter_mut() + } + + /// Get a thread local [`EntityChanges`] instance + pub fn get_local_ref_cell(&self) -> &RefCell { + self.list.borrow_local_cell() + } +} + +/// A Record hint which entity's component has changed +#[derive(Copy, Clone, Debug)] +pub struct EntityChange { + entity: Entity, + component: ComponentId, +} + +impl EntityChange { + /// Return a new EntityChange + pub fn new(entity: Entity, component: ComponentId) -> Self { + EntityChange { entity, component } + } + /// Access change's entity + pub fn entity(&self) -> Entity { + self.entity + } + + /// Access change's component id + pub fn component(&self) -> ComponentId { + self.component + } +} \ No newline at end of file diff --git a/crates/bevy_ecs/src/world/filtered_resource.rs b/crates/bevy_ecs/src/world/filtered_resource.rs index 527957d87c89db..0d178a037330f9 100644 --- a/crates/bevy_ecs/src/world/filtered_resource.rs +++ b/crates/bevy_ecs/src/world/filtered_resource.rs @@ -479,6 +479,7 @@ impl<'w, 's> FilteredResourcesMut<'w, 's> { // SAFETY: We have access to this resource in `access`, and the caller ensures that there are no conflicting borrows for the duration of the returned value. unsafe { self.world.get_resource_with_ticks(component_id) }.map( |(value, ticks, _caller)| MutUntyped { + on_change: None, // SAFETY: We have exclusive access to the underlying storage. value: unsafe { value.assert_unique() }, // SAFETY: We have exclusive access to the underlying storage. diff --git a/crates/bevy_ecs/src/world/mod.rs b/crates/bevy_ecs/src/world/mod.rs index d50f8f421cfc27..b7642d50be0d5d 100644 --- a/crates/bevy_ecs/src/world/mod.rs +++ b/crates/bevy_ecs/src/world/mod.rs @@ -1,6 +1,8 @@ //! Defines the [`World`] and APIs for accessing it directly. pub(crate) mod command_queue; + +pub(crate) mod entity_change; mod component_constants; mod deferred_world; mod entity_fetch; @@ -62,8 +64,8 @@ use core::{ use bevy_ptr::UnsafeCellDeref; use core::panic::Location; - use unsafe_world_cell::{UnsafeEntityCell, UnsafeWorldCell}; +use crate::world::entity_change::ParallelEntityChanges; /// A [`World`] mutation. /// @@ -134,6 +136,7 @@ pub struct World { pub(crate) last_check_tick: Tick, pub(crate) last_trigger_id: u32, pub(crate) command_queue: RawCommandQueue, + pub(crate) entity_changes: ParallelEntityChanges, } impl Default for World { @@ -154,6 +157,7 @@ impl Default for World { last_check_tick: Tick::new(0), last_trigger_id: 0, command_queue: RawCommandQueue::new(), + entity_changes: ParallelEntityChanges::new(), }; world.bootstrap(); world @@ -182,6 +186,7 @@ impl World { assert_eq!(ON_INSERT, self.register_component::()); assert_eq!(ON_REPLACE, self.register_component::()); assert_eq!(ON_REMOVE, self.register_component::()); + assert_eq!(ON_MUTATE, self.register_component::()); } /// Creates a new empty [`World`]. /// @@ -2811,6 +2816,7 @@ impl World { // SAFETY: `ptr` was obtained from the TypeId of `R`. let mut value = unsafe { ptr.read::() }; let value_mut = Mut { + on_change: None, value: &mut value, ticks: TicksMut { added: &mut ticks.added, @@ -3015,6 +3021,18 @@ impl World { } } + pub(crate) fn flush_entity_changes(&mut self) { + let mut parallel_entity_changes = std::mem::take(&mut self.entity_changes); + let mut deferred_world = DeferredWorld::from(&mut *self); + parallel_entity_changes.iter_mut().for_each(|entity_changes| { + entity_changes.drain(..).for_each(|entity_change| unsafe { + // SAFETY: [`OnMutate`] Event is ZST + deferred_world.trigger_observers(ON_MUTATE, entity_change.entity(), std::iter::once(entity_change.component())) + }) + }); + _ = std::mem::replace(&mut self.entity_changes, parallel_entity_changes); + } + /// Flushes queued entities and commands. /// /// Queued entities will be spawned, and then commands will be applied. @@ -3022,6 +3040,7 @@ impl World { pub fn flush(&mut self) { self.flush_entities(); self.flush_commands(); + self.flush_entity_changes(); } /// Increments the world's current change tick and returns the old value. @@ -3204,6 +3223,7 @@ impl World { ref mut sparse_sets, ref mut resources, ref mut non_send_resources, + .. } = self.storages; #[cfg(feature = "trace")] @@ -3489,6 +3509,7 @@ impl World { }; let mut_untyped = MutUntyped { + on_change: None, // SAFETY: // - We have exclusive access to the world, so no other code can be aliasing the `Ptr` // - We iterate one resource at a time, and we let go of each `PtrMut` before getting the next one diff --git a/crates/bevy_ecs/src/world/unsafe_world_cell.rs b/crates/bevy_ecs/src/world/unsafe_world_cell.rs index 4c2813702a40ca..db7b157c276e80 100644 --- a/crates/bevy_ecs/src/world/unsafe_world_cell.rs +++ b/crates/bevy_ecs/src/world/unsafe_world_cell.rs @@ -21,6 +21,8 @@ use bevy_ptr::Ptr; #[cfg(feature = "track_change_detection")] use bevy_ptr::UnsafeCellDeref; use core::{any::TypeId, cell::UnsafeCell, fmt::Debug, marker::PhantomData, ptr}; +use std::cell::RefCell; +use crate::world::entity_change::{EntityChange, EntityChanges}; /// Variant of the [`World`] where resource and component accesses take `&self`, and the responsibility to avoid /// aliasing violations are given to the caller instead of being checked at compile-time by rust's unique XOR shared rule. @@ -320,6 +322,19 @@ impl<'w> UnsafeWorldCell<'w> { &unsafe { self.unsafe_world() }.storages } + /// Provides unchecked access to the internal data stores of the [`World`]. + /// + /// # Safety + /// + /// The caller must ensure that this is only used to access world data + /// that this [`UnsafeWorldCell`] is allowed to. + /// As always, any mutable access to a component must not exist at the same + /// time as any other accesses to that same component. + pub unsafe fn entity_changes(self) -> &'w RefCell { + // SAFETY: The caller promises to only access world data allowed by this instance. + &unsafe { self.unsafe_world() }.entity_changes.get_local_ref_cell() + } + /// Retrieves an [`UnsafeEntityCell`] that exposes read and write operations for the given `entity`. /// Similar to the [`UnsafeWorldCell`], you are in charge of making sure that no aliasing rules are violated. #[inline] @@ -492,6 +507,7 @@ impl<'w> UnsafeWorldCell<'w> { }; Some(MutUntyped { + on_change: None, // SAFETY: // - caller ensures that `self` has permission to access the resource // - caller ensures that the resource is unaliased @@ -558,6 +574,7 @@ impl<'w> UnsafeWorldCell<'w> { unsafe { TicksMut::from_tick_cells(ticks, self.last_change_tick(), change_tick) }; Some(MutUntyped { + on_change: None, // SAFETY: This function has exclusive access to the world so nothing aliases `ptr`. value: unsafe { ptr.assert_unique() }, ticks, @@ -873,6 +890,10 @@ impl<'w> UnsafeEntityCell<'w> { self.location, ) .map(|(value, cells, _caller)| Mut { + on_change: Some(( + EntityChange::new(self.entity, component_id), + self.world.entity_changes(), + )), // SAFETY: returned component is of type T value: value.assert_unique().deref_mut::(), ticks: TicksMut::from_tick_cells(cells, last_change_tick, change_tick), @@ -984,6 +1005,10 @@ impl<'w> UnsafeEntityCell<'w> { self.location, ) .map(|(value, cells, _caller)| MutUntyped { + on_change: Some(( + EntityChange::new(self.entity, component_id), + self.world.entity_changes(), + )), // SAFETY: world access validated by caller and ties world lifetime to `MutUntyped` lifetime value: value.assert_unique(), ticks: TicksMut::from_tick_cells( diff --git a/crates/bevy_render/src/pipelined_rendering.rs b/crates/bevy_render/src/pipelined_rendering.rs index 41279e7d25db13..824fbc405c56f8 100644 --- a/crates/bevy_render/src/pipelined_rendering.rs +++ b/crates/bevy_render/src/pipelined_rendering.rs @@ -44,13 +44,6 @@ impl RenderAppChannels { self.render_app_in_render_thread = true; } - /// Receive the `render_app` from the rendering thread. - /// Return `None` if the render thread has panicked. - pub async fn recv(&mut self) -> Option { - let render_app = self.render_to_app_receiver.recv().await.ok()?; - self.render_app_in_render_thread = false; - Some(render_app) - } } impl Drop for RenderAppChannels { @@ -186,15 +179,16 @@ fn renderer_extract(app_world: &mut World, _world: &mut World) { world.resource_scope(|world, mut render_channels: Mut| { // we use a scope here to run any main thread tasks that the render world still needs to run // while we wait for the render world to be received. + let render_to_app_receiver = render_channels.render_to_app_receiver.clone(); if let Some(mut render_app) = ComputeTaskPool::get() .scope_with_executor(true, Some(&*main_thread_executor.0), |s| { - s.spawn(async { render_channels.recv().await }); + s.spawn(async { render_to_app_receiver.recv().await.ok() }); }) .pop() .unwrap() { + render_channels.render_app_in_render_thread = false; render_app.extract(world); - render_channels.send_blocking(render_app); } else { // Renderer thread panicked diff --git a/crates/bevy_render/src/sync_world.rs b/crates/bevy_render/src/sync_world.rs index 6cee2885d06691..ae60eab851825b 100644 --- a/crates/bevy_render/src/sync_world.rs +++ b/crates/bevy_render/src/sync_world.rs @@ -294,6 +294,7 @@ mod render_entities_world_query_impls { } const IS_DENSE: bool = <&'static RenderEntity as WorldQuery>::IS_DENSE; + const IS_MUTATE: bool = <&'static RenderEntity as WorldQuery>::IS_MUTATE; #[inline] unsafe fn set_archetype<'w>( @@ -393,6 +394,7 @@ mod render_entities_world_query_impls { } const IS_DENSE: bool = <&'static MainEntity as WorldQuery>::IS_DENSE; + const IS_MUTATE: bool = <&'static MainEntity as WorldQuery>::IS_MUTATE; #[inline] unsafe fn set_archetype<'w>( diff --git a/crates/bevy_utils/src/parallel_queue.rs b/crates/bevy_utils/src/parallel_queue.rs index 01377c7573beb8..a328e7ecbd7bb2 100644 --- a/crates/bevy_utils/src/parallel_queue.rs +++ b/crates/bevy_utils/src/parallel_queue.rs @@ -41,6 +41,12 @@ impl Parallel { pub fn borrow_local_mut(&self) -> impl DerefMut + '_ { self.locals.get_or_default().borrow_mut() } + + /// Borrows the thread-local RefCell ref which wrap the value. + /// If there is no thread-local value, it will be initialized to it's default. + pub fn borrow_local_cell(&self) -> &RefCell { + self.locals.get_or_default() + } } impl Parallel diff --git a/examples/README.md b/examples/README.md index 3b6359ad710320..ceef647017d0f0 100644 --- a/examples/README.md +++ b/examples/README.md @@ -309,6 +309,7 @@ Example | Description [One Shot Systems](../examples/ecs/one_shot_systems.rs) | Shows how to flexibly run systems without scheduling them [Parallel Query](../examples/ecs/parallel_query.rs) | Illustrates parallel queries with `ParallelIterator` [Removal Detection](../examples/ecs/removal_detection.rs) | Query for entities that had a specific component removed earlier in the current frame +[Responding to Changes](../examples/ecs/responding_to_changes.rs) | Demonstrates how and when to use change detection and `OnMutate` hooks and observers [Run Conditions](../examples/ecs/run_conditions.rs) | Run systems only when one or multiple conditions are met [Send and receive events](../examples/ecs/send_and_receive_events.rs) | Demonstrates how to send and receive events of the same type in a single system [Startup System](../examples/ecs/startup_system.rs) | Demonstrates a startup system (one that runs once when the app starts up) diff --git a/examples/ecs/responding_to_changes.rs b/examples/ecs/responding_to_changes.rs new file mode 100644 index 00000000000000..eff20be0150e21 --- /dev/null +++ b/examples/ecs/responding_to_changes.rs @@ -0,0 +1,284 @@ + +//! Bevy has two primary ways to respond to changes in your ECS data: +//! +//! 1. **Change detection:** whenever a component or resource is mutated, it will be flagged as changed. +//! 2. **Hooks and observers:** whenever changes or lifecycle events occur, functions will be called to respond to them. +//! +//! While similar, these two methods have different use cases and performance characteristics. +//! Change detection is fundamentally a polling-based mechanism: changes need to be looked for proactively, +//! and so the cost of change detection is paid every time the system runs (generally every frame), +//! regardless of whether or not any changes have occurred. +//! +//! By contrast, hooks and observers are event-driven: they only run when the event they're watching for occurs. +//! However, each event is processed individually, increasing the overhead when many changes occur. +//! +//! As a result, change detection is better suited to use cases where large volumes of data are being processed, +//! while hooks and observers are better suited to use cases where the data is relatively stable and changes are infrequent. +//! +//! There are two more important differences. Firstly, change detection is triggered immediately when the change occurs, +//! while hooks and observers are deferred until the next synchronization point where exclusive world access is available. +//! In Bevy, systems are run in parallel by default, so synchronizing forces the scheduler to wait until +//! all systems have finished running before proceeding and prevent systems before and after the sync point from running concurrently. +//! +//! Second, while change detection systems only run periodically, +//! hooks and observers are checked after every mutation to the world during sync points. +//! +//! Taken together, this means that change detection is good for periodic updates (but it's harder to avoid invalid state), +//! while hooks and observers are good for immediate updates and can chain into other hooks/observers indefinitely, +//! creating a cascade of reactions (but they need to wait until a sync point). +//! +//! You might use change detection for: +//! +//! - physics simulation +//! - AI action planning +//! - performance optimization in existing systems +//! +//! You might use hooks and observers for: +//! +//! - complex logic in turn-based games +//! - responding to user inputs +//! - adding behavior to UI elements +//! - upholding critical invariants, like hierarchical relationships (hooks are better suited than observers for this) +//! +//! # This example +//! +//! In this example, we're demonstrating the APIs available by creating a simple counter +//! in four different ways: +//! +//! 1. Using a system with a `Changed` filter. +//! 2. Use the `Ref` query type and the `is_changed` method. +//! 3. Using a hook. +//! 4. Using an observer. +//! +//! The counter is incremented by pressing the corresponding button. +//! At this scale, we have neither performance nor complexity concerns: +//! see the discussion above for guidance on when to use each method. +//! +//! Hooks are not suitable for this application (as they represent intrinsic functionality for the type), +//! and cannot sensibly be added to the general-purpose [`Interaction`] component just to make these buttons work. +//! Instead, we demonstrate how to use them by adding a on-mutate hook to the [`CounterValue`] component which will +//! update the text of each button whenever the counter is incremented. + +use bevy::ecs::world::OnMutate; +use bevy::prelude::*; + +fn main() { + App::new() + .add_plugins(DefaultPlugins) + // Example setup + .add_systems(Startup, setup_ui) + .add_systems( + Update, + (change_button_color_based_on_interaction, update_button_text) + .in_set(ChangeDetectionSet), + ) + // Change detection based methods + .add_systems( + Update, + ( + update_counter_changed_filter.after(ChangeDetectionSet), + update_counter_ref_query, + ), + ) + .add_observer(update_counter_observer) + .run(); +} + +#[derive(SystemSet, Debug, PartialEq, Eq, Hash, Clone)] +struct ChangeDetectionSet; + +/// Tracks the value of the counter for each button. +#[derive(Component)] +struct CounterValue(u32); + +/// A component that differentiates our buttons by their change-response strategy. +#[derive(Component, PartialEq)] +enum ChangeStrategy { + ChangedFilter, + RefQuery, + Observer, +} + +impl ChangeStrategy { + fn color(&self) -> Srgba { + use bevy::color::palettes::tailwind::*; + + match self { + ChangeStrategy::ChangedFilter => RED_500, + ChangeStrategy::RefQuery => ORANGE_500, + ChangeStrategy::Observer => BLUE_500, + } + } + + fn button_string(&self) -> &'static str { + match self { + ChangeStrategy::ChangedFilter => "Changed Filter", + ChangeStrategy::RefQuery => "Ref Query", + ChangeStrategy::Observer => "Observer", + } + } +} + +/// Generates an interactive button with a counter, +/// returning the entity ID of the button spawned. +fn spawn_button_with_counter(commands: &mut Commands, change_strategy: ChangeStrategy) -> Entity { + commands + .spawn(( + Node { + width: Val::Px(250.), + height: Val::Px(120.), + margin: UiRect::all(Val::Px(20.)), + ..default() + }, + Button::default(), + BorderRadius::all(Val::Px(20.)), + BackgroundColor(change_strategy.color().into()), + change_strategy, + CounterValue(0), + )) + .with_children(|parent| { + // We don't need to set the initial value of the Text component here, + // as Changed filters are triggered whenever the value is mutated OR the component is added. + parent.spawn(( + Node { + align_self: AlignSelf::Center, + width: Val::Percent(100.), + ..default() + }, + Text::default(), + )); + }) + .id() +} + +// This system implicitly filters out any entities whose `Interaction` component hasn't changed. +fn update_counter_changed_filter( + mut query: Query<(&Interaction, &ChangeStrategy, &mut CounterValue), Changed>, +) { + for (interaction, change_strategy, mut counter) in query.iter_mut() { + if change_strategy != &ChangeStrategy::ChangedFilter { + continue; + } + + if *interaction == Interaction::Pressed { + counter.0 += 1; + } + } +} + +// This system works just like the one above, except entries that are not changed will be included. +// We can check if the entity has changed by calling the `is_changed` method on the `Ref` type. +// The [`Mut`] and [`ChangeTrackers`] types also have these methods. +fn update_counter_ref_query( + mut query: Query<(Ref, &ChangeStrategy, &mut CounterValue)>, +) { + for (interaction, change_strategy, mut counter) in query.iter_mut() { + if change_strategy != &ChangeStrategy::RefQuery { + continue; + } + + // Being able to check if the entity has changed inside of the system is + // sometimes useful for more complex logic, but Changed filters are generally clearer. + if interaction.is_changed() && *interaction == Interaction::Pressed { + counter.0 += 1; + } + } +} + +// This observer is added to the app using the `observe` method, +// and will run whenever the `Interaction` component is mutated. +// Like above, we're returning early if the button isn't the one we're interested in. +fn update_counter_observer( + trigger: Trigger, + mut button_query: Query<(&mut CounterValue, &Interaction, &ChangeStrategy)>, +) { + let Ok(( + mut counter, + interaction, + change_strategy + )) = button_query.get_mut(trigger.entity()) else { + // Other entities may have the Interaction component, but we're only interested in these particular buttons. + return; + }; + + if *change_strategy != ChangeStrategy::Observer { + return; + } + + // OnMutate events will be generated whenever *any* change occurs, + // even if it's not to the value we're interested in. + if *interaction == Interaction::Pressed { + counter.0 += 1; + } +} + +fn setup_ui(mut commands: Commands) { + commands.spawn(Camera2d::default()); + + let root_node = commands + .spawn(( + Node { + width: Val::Percent(100.), + height: Val::Percent(100.), + flex_direction: FlexDirection::Column, + justify_content: JustifyContent::Center, + align_items: AlignItems::Center, + ..default() + }, + )) + .id(); + + let changed_filter_button = + spawn_button_with_counter(&mut commands, ChangeStrategy::ChangedFilter); + let ref_query_button = spawn_button_with_counter(&mut commands, ChangeStrategy::RefQuery); + let observer_button = spawn_button_with_counter(&mut commands, ChangeStrategy::Observer); + + commands.entity(root_node).add_children(&[ + changed_filter_button, + ref_query_button, + observer_button, + ]); +} + +// This is another example of a change-detection based system, +// which only acts on buttons whose `Interaction` component has changed to save on work. +// +// Because the operation is idempotent (calling it multiple times has the same effect as calling it once), +// this is purely a performance optimization. +fn change_button_color_based_on_interaction( + mut query: Query<(&mut BackgroundColor, &ChangeStrategy, &Interaction), Changed>, +) { + for (mut background_color, change_strategy, interaction) in query.iter_mut() { + let standard_color = change_strategy.color(); + + *background_color = match interaction { + Interaction::None => standard_color.into(), + Interaction::Hovered => standard_color.darker(0.15).into(), + Interaction::Pressed => standard_color.darker(0.3).into(), + }; + } +} + +// TODO: implement this using hooks +// Like other filters, `Changed` (and `Added`) filters can be composed via `Or` filters. +// The default behavior for both query data and filters is to use AND logic. +// In this case, a truly robust solution should update whenever the counter value +// or the children that point to the text entity change. +fn update_button_text( + counter_query: Query< + (&CounterValue, &ChangeStrategy, &Children), + Or<(Changed, Changed)>, + >, + mut text_query: Query<&mut Text>, +) { + for (counter, change_strategy, children) in counter_query.iter() { + for child in children.iter() { + // By attempting to fetch the Text component on each child and continuing if it fails, + // we can avoid panicking if non-text children are present. + if let Ok(mut text) = text_query.get_mut(*child) { + let string = format!("{}: {}", change_strategy.button_string(), counter.0); + *text = Text::new(string); + } + } + } +}