Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Mutate Observer #16183

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
1 change: 1 addition & 0 deletions crates/bevy_ecs/macros/src/world_query.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
7 changes: 7 additions & 0 deletions crates/bevy_ecs/src/archetype.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}

Expand Down Expand Up @@ -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.
Expand Down
169 changes: 167 additions & 2 deletions crates/bevy_ecs/src/change_detection.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
//! Types that detect when their internal data mutate.

use crate::world::entity_change::{EntityChange, EntityChanges};
use crate::{
component::{Tick, TickCells},
ptr::PtrMut,
Expand All @@ -10,6 +11,7 @@ use core::{
mem,
ops::{Deref, DerefMut},
};
use std::cell::RefCell;
#[cfg(feature = "track_change_detection")]
use {
bevy_ptr::ThinSlicePtr,
Expand Down Expand Up @@ -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),*> {
Expand All @@ -387,6 +445,7 @@ macro_rules! impl_methods {
/// <T>`, but you need a `Mut<T>`.
pub fn reborrow(&mut self) -> Mut<'_, $target> {
Mut {
on_change: None,
value: self.value,
ticks: TicksMut {
added: self.ticks.added,
Expand Down Expand Up @@ -423,6 +482,7 @@ macro_rules! impl_methods {
/// ```
pub fn map_unchanged<U: ?Sized>(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")]
Expand All @@ -437,6 +497,96 @@ macro_rules! impl_methods {
pub fn filter_map_unchanged<U: ?Sized>(self, f: impl FnOnce(&mut $target) -> Option<&mut U>) -> Option<Mut<'w, U>> {
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)]
/// <T>`, but you need a `Mut<T>`.
pub fn reborrow(&mut self) -> Mut<'_, $target> {
Mut {
on_change: self.on_change,
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<U: ?Sized>(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")]
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<T>` to a `Mut<U>`, 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<U: ?Sized>(self, f: impl FnOnce(&mut $target) -> Option<&mut U>) -> Option<Mut<'w, U>> {
let value = f(self.value);
value.map(|value| Mut {
on_change: self.on_change,
value,
ticks: self.ticks,
#[cfg(feature = "track_change_detection")]
Expand Down Expand Up @@ -667,6 +817,7 @@ impl<'w, T: Resource> From<ResMut<'w, T>> 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")]
Expand Down Expand Up @@ -703,6 +854,7 @@ impl<'w, T: 'static> From<NonSendMut<'w, T>> 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")]
Expand Down Expand Up @@ -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<EntityChanges>)>,
pub(crate) value: &'w mut T,
pub(crate) ticks: TicksMut<'w>,
#[cfg(feature = "track_change_detection")]
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -950,8 +1104,8 @@ 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.
Expand All @@ -963,6 +1117,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<EntityChanges>)>,
pub(crate) value: PtrMut<'w>,
pub(crate) ticks: TicksMut<'w>,
#[cfg(feature = "track_change_detection")]
Expand All @@ -984,6 +1139,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,
Expand Down Expand Up @@ -1041,6 +1197,7 @@ impl<'w> MutUntyped<'w> {
/// ```
pub fn map_unchanged<T: ?Sized>(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")]
Expand All @@ -1054,6 +1211,7 @@ impl<'w> MutUntyped<'w> {
/// - `T` must be the erased pointee type for this [`MutUntyped`].
pub unsafe fn with_type<T>(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,
Expand Down Expand Up @@ -1098,6 +1256,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();
Expand Down Expand Up @@ -1132,6 +1293,7 @@ impl core::fmt::Debug for MutUntyped<'_> {
impl<'w, T> From<Mut<'w, T>> 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")]
Expand Down Expand Up @@ -1423,6 +1585,7 @@ mod tests {
let mut caller = Location::caller();

let ptr = Mut {
on_change: None,
value: &mut outer,
ticks,
#[cfg(feature = "track_change_detection")]
Expand Down Expand Up @@ -1513,6 +1676,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")]
Expand Down Expand Up @@ -1551,6 +1715,7 @@ mod tests {
let mut caller = Location::caller();

let mut_typed = Mut {
on_change: None,
value: &mut c,
ticks,
#[cfg(feature = "track_change_detection")]
Expand Down
Loading
Loading