Added modify_component to EntityWorldMut, DeferredWorld, and World (#16668)
# Objective - Make working with immutable components more ergonomic - Assist #16662 ## Solution Added `modify_component` to `World` and `EntityWorldMut`. This method "removes" a component from an entity, gives a mutable reference to it to a provided closure, and then "re-inserts" the component back onto the entity. This replacement triggers the `OnReplace` and `OnInsert` hooks, but does _not_ cause an archetype move, as the removal is purely simulated. ## Testing - Added doc-tests and a unit test. --- ## Showcase ```rust use bevy_ecs::prelude::*; /// An immutable component. #[derive(Component, PartialEq, Eq, Debug)] #[component(immutable)] struct Foo(bool); let mut world = World::default(); let mut entity = world.spawn(Foo(false)); assert_eq!(entity.get::<Foo>(), Some(&Foo(false))); // Before the closure is executed, the `OnReplace` hooks/observers are triggered entity.modify_component(|foo: &mut Foo| { foo.0 = true; }); // After the closure is executed, `OnInsert` hooks/observers are triggered assert_eq!(entity.get::<Foo>(), Some(&Foo(true))); ``` ## Notes - If the component is not available on the entity, the closure and hooks aren't executed, and `None` is returned. I chose this as an alternative to returning an error or panicking, but I'm open to changing that based on feedback. - This relies on `unsafe`, in particular for accessing the `Archetype` to trigger hooks. All the unsafe operations are contained within `DeferredWorld::modify_component`, and I would appreciate that this function is given special attention to ensure soundness. - The `OnAdd` hook can never be triggered by this method, since the component must already be inserted. I have chosen to not trigger `OnRemove`, as I believe it makes sense that this method is purely a replacement operation, not an actual removal/insertion. --------- Co-authored-by: Alice Cecile <alice.i.cecile@gmail.com> Co-authored-by: Malek <50841145+MalekiRe@users.noreply.github.com>
This commit is contained in:
parent
4460a4d9ed
commit
3f19997096
@ -14,7 +14,7 @@ use crate::{
|
|||||||
world::{error::EntityFetchError, WorldEntityFetch},
|
world::{error::EntityFetchError, WorldEntityFetch},
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::{unsafe_world_cell::UnsafeWorldCell, Mut, World};
|
use super::{unsafe_world_cell::UnsafeWorldCell, Mut, World, ON_INSERT, ON_REPLACE};
|
||||||
|
|
||||||
/// A [`World`] reference that disallows structural ECS changes.
|
/// A [`World`] reference that disallows structural ECS changes.
|
||||||
/// This includes initializing resources, registering components or spawning entities.
|
/// This includes initializing resources, registering components or spawning entities.
|
||||||
@ -82,6 +82,88 @@ impl<'w> DeferredWorld<'w> {
|
|||||||
unsafe { self.world.get_entity(entity)?.get_mut() }
|
unsafe { self.world.get_entity(entity)?.get_mut() }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Temporarily removes a [`Component`] `T` from the provided [`Entity`] and
|
||||||
|
/// runs the provided closure on it, returning the result if `T` was available.
|
||||||
|
/// This will trigger the `OnRemove` and `OnReplace` component hooks without
|
||||||
|
/// causing an archetype move.
|
||||||
|
///
|
||||||
|
/// This is most useful with immutable components, where removal and reinsertion
|
||||||
|
/// is the only way to modify a value.
|
||||||
|
///
|
||||||
|
/// If you do not need to ensure the above hooks are triggered, and your component
|
||||||
|
/// is mutable, prefer using [`get_mut`](DeferredWorld::get_mut).
|
||||||
|
#[inline]
|
||||||
|
pub(crate) fn modify_component<T: Component, R>(
|
||||||
|
&mut self,
|
||||||
|
entity: Entity,
|
||||||
|
f: impl FnOnce(&mut T) -> R,
|
||||||
|
) -> Result<Option<R>, EntityFetchError> {
|
||||||
|
// If the component is not registered, then it doesn't exist on this entity, so no action required.
|
||||||
|
let Some(component_id) = self.component_id::<T>() else {
|
||||||
|
return Ok(None);
|
||||||
|
};
|
||||||
|
|
||||||
|
let entity_cell = match self.get_entity_mut(entity) {
|
||||||
|
Ok(cell) => cell,
|
||||||
|
Err(EntityFetchError::AliasedMutability(..)) => {
|
||||||
|
return Err(EntityFetchError::AliasedMutability(entity))
|
||||||
|
}
|
||||||
|
Err(EntityFetchError::NoSuchEntity(..)) => {
|
||||||
|
return Err(EntityFetchError::NoSuchEntity(entity, self.world))
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if !entity_cell.contains::<T>() {
|
||||||
|
return Ok(None);
|
||||||
|
}
|
||||||
|
|
||||||
|
let archetype = &raw const *entity_cell.archetype();
|
||||||
|
|
||||||
|
// SAFETY:
|
||||||
|
// - DeferredWorld ensures archetype pointer will remain valid as no
|
||||||
|
// relocations will occur.
|
||||||
|
// - component_id exists on this world and this entity
|
||||||
|
// - ON_REPLACE is able to accept ZST events
|
||||||
|
unsafe {
|
||||||
|
let archetype = &*archetype;
|
||||||
|
self.trigger_on_replace(archetype, entity, [component_id].into_iter());
|
||||||
|
if archetype.has_replace_observer() {
|
||||||
|
self.trigger_observers(ON_REPLACE, entity, [component_id].into_iter());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut entity_cell = self
|
||||||
|
.get_entity_mut(entity)
|
||||||
|
.expect("entity access confirmed above");
|
||||||
|
|
||||||
|
// SAFETY: we will run the required hooks to simulate removal/replacement.
|
||||||
|
let mut component = unsafe {
|
||||||
|
entity_cell
|
||||||
|
.get_mut_assume_mutable::<T>()
|
||||||
|
.expect("component access confirmed above")
|
||||||
|
};
|
||||||
|
|
||||||
|
let result = f(&mut component);
|
||||||
|
|
||||||
|
// Simulate adding this component by updating the relevant ticks
|
||||||
|
*component.ticks.added = *component.ticks.changed;
|
||||||
|
|
||||||
|
// SAFETY:
|
||||||
|
// - DeferredWorld ensures archetype pointer will remain valid as no
|
||||||
|
// relocations will occur.
|
||||||
|
// - component_id exists on this world and this entity
|
||||||
|
// - ON_REPLACE is able to accept ZST events
|
||||||
|
unsafe {
|
||||||
|
let archetype = &*archetype;
|
||||||
|
self.trigger_on_insert(archetype, entity, [component_id].into_iter());
|
||||||
|
if archetype.has_insert_observer() {
|
||||||
|
self.trigger_observers(ON_INSERT, entity, [component_id].into_iter());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(Some(result))
|
||||||
|
}
|
||||||
|
|
||||||
/// Returns [`EntityMut`]s that expose read and write operations for the
|
/// Returns [`EntityMut`]s that expose read and write operations for the
|
||||||
/// given `entities`, returning [`Err`] if any of the given entities do not
|
/// given `entities`, returning [`Err`] if any of the given entities do not
|
||||||
/// exist. Instead of immediately unwrapping the value returned from this
|
/// exist. Instead of immediately unwrapping the value returned from this
|
||||||
|
|||||||
@ -1204,6 +1204,59 @@ impl<'w> EntityWorldMut<'w> {
|
|||||||
unsafe { self.get_mut_assume_mutable() }
|
unsafe { self.get_mut_assume_mutable() }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Temporarily removes a [`Component`] `T` from this [`Entity`] and runs the
|
||||||
|
/// provided closure on it, returning the result if `T` was available.
|
||||||
|
/// This will trigger the `OnRemove` and `OnReplace` component hooks without
|
||||||
|
/// causing an archetype move.
|
||||||
|
///
|
||||||
|
/// This is most useful with immutable components, where removal and reinsertion
|
||||||
|
/// is the only way to modify a value.
|
||||||
|
///
|
||||||
|
/// If you do not need to ensure the above hooks are triggered, and your component
|
||||||
|
/// is mutable, prefer using [`get_mut`](EntityWorldMut::get_mut).
|
||||||
|
///
|
||||||
|
/// # Examples
|
||||||
|
///
|
||||||
|
/// ```rust
|
||||||
|
/// # use bevy_ecs::prelude::*;
|
||||||
|
/// #
|
||||||
|
/// #[derive(Component, PartialEq, Eq, Debug)]
|
||||||
|
/// #[component(immutable)]
|
||||||
|
/// struct Foo(bool);
|
||||||
|
///
|
||||||
|
/// # let mut world = World::default();
|
||||||
|
/// # world.register_component::<Foo>();
|
||||||
|
/// #
|
||||||
|
/// # let entity = world.spawn(Foo(false)).id();
|
||||||
|
/// #
|
||||||
|
/// # let mut entity = world.entity_mut(entity);
|
||||||
|
/// #
|
||||||
|
/// # assert_eq!(entity.get::<Foo>(), Some(&Foo(false)));
|
||||||
|
/// #
|
||||||
|
/// entity.modify_component(|foo: &mut Foo| {
|
||||||
|
/// foo.0 = true;
|
||||||
|
/// });
|
||||||
|
/// #
|
||||||
|
/// # assert_eq!(entity.get::<Foo>(), Some(&Foo(true)));
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// # Panics
|
||||||
|
///
|
||||||
|
/// If the entity has been despawned while this `EntityWorldMut` is still alive.
|
||||||
|
#[inline]
|
||||||
|
pub fn modify_component<T: Component, R>(&mut self, f: impl FnOnce(&mut T) -> R) -> Option<R> {
|
||||||
|
self.assert_not_despawned();
|
||||||
|
|
||||||
|
let result = self
|
||||||
|
.world
|
||||||
|
.modify_component(self.entity, f)
|
||||||
|
.expect("entity access must be valid")?;
|
||||||
|
|
||||||
|
self.update_location();
|
||||||
|
|
||||||
|
Some(result)
|
||||||
|
}
|
||||||
|
|
||||||
/// Gets mutable access to the component of type `T` for the current entity.
|
/// Gets mutable access to the component of type `T` for the current entity.
|
||||||
/// Returns `None` if the entity does not have a component of type `T`.
|
/// Returns `None` if the entity does not have a component of type `T`.
|
||||||
///
|
///
|
||||||
@ -5397,4 +5450,87 @@ mod tests {
|
|||||||
.unwrap()
|
.unwrap()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn with_component_activates_hooks() {
|
||||||
|
use core::sync::atomic::{AtomicBool, AtomicU8, Ordering};
|
||||||
|
|
||||||
|
#[derive(Component, PartialEq, Eq, Debug)]
|
||||||
|
#[component(immutable)]
|
||||||
|
struct Foo(bool);
|
||||||
|
|
||||||
|
static EXPECTED_VALUE: AtomicBool = AtomicBool::new(false);
|
||||||
|
|
||||||
|
static ADD_COUNT: AtomicU8 = AtomicU8::new(0);
|
||||||
|
static REMOVE_COUNT: AtomicU8 = AtomicU8::new(0);
|
||||||
|
static REPLACE_COUNT: AtomicU8 = AtomicU8::new(0);
|
||||||
|
static INSERT_COUNT: AtomicU8 = AtomicU8::new(0);
|
||||||
|
|
||||||
|
let mut world = World::default();
|
||||||
|
|
||||||
|
world.register_component::<Foo>();
|
||||||
|
world
|
||||||
|
.register_component_hooks::<Foo>()
|
||||||
|
.on_add(|world, entity, _| {
|
||||||
|
ADD_COUNT.fetch_add(1, Ordering::Relaxed);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
world.get(entity),
|
||||||
|
Some(&Foo(EXPECTED_VALUE.load(Ordering::Relaxed)))
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.on_remove(|world, entity, _| {
|
||||||
|
REMOVE_COUNT.fetch_add(1, Ordering::Relaxed);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
world.get(entity),
|
||||||
|
Some(&Foo(EXPECTED_VALUE.load(Ordering::Relaxed)))
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.on_replace(|world, entity, _| {
|
||||||
|
REPLACE_COUNT.fetch_add(1, Ordering::Relaxed);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
world.get(entity),
|
||||||
|
Some(&Foo(EXPECTED_VALUE.load(Ordering::Relaxed)))
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.on_insert(|world, entity, _| {
|
||||||
|
INSERT_COUNT.fetch_add(1, Ordering::Relaxed);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
world.get(entity),
|
||||||
|
Some(&Foo(EXPECTED_VALUE.load(Ordering::Relaxed)))
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
let entity = world.spawn(Foo(false)).id();
|
||||||
|
|
||||||
|
assert_eq!(ADD_COUNT.load(Ordering::Relaxed), 1);
|
||||||
|
assert_eq!(REMOVE_COUNT.load(Ordering::Relaxed), 0);
|
||||||
|
assert_eq!(REPLACE_COUNT.load(Ordering::Relaxed), 0);
|
||||||
|
assert_eq!(INSERT_COUNT.load(Ordering::Relaxed), 1);
|
||||||
|
|
||||||
|
let mut entity = world.entity_mut(entity);
|
||||||
|
|
||||||
|
let archetype_pointer_before = &raw const *entity.archetype();
|
||||||
|
|
||||||
|
assert_eq!(entity.get::<Foo>(), Some(&Foo(false)));
|
||||||
|
|
||||||
|
entity.modify_component(|foo: &mut Foo| {
|
||||||
|
foo.0 = true;
|
||||||
|
EXPECTED_VALUE.store(foo.0, Ordering::Relaxed);
|
||||||
|
});
|
||||||
|
|
||||||
|
let archetype_pointer_after = &raw const *entity.archetype();
|
||||||
|
|
||||||
|
assert_eq!(entity.get::<Foo>(), Some(&Foo(true)));
|
||||||
|
|
||||||
|
assert_eq!(ADD_COUNT.load(Ordering::Relaxed), 1);
|
||||||
|
assert_eq!(REMOVE_COUNT.load(Ordering::Relaxed), 0);
|
||||||
|
assert_eq!(REPLACE_COUNT.load(Ordering::Relaxed), 1);
|
||||||
|
assert_eq!(INSERT_COUNT.load(Ordering::Relaxed), 2);
|
||||||
|
|
||||||
|
assert_eq!(archetype_pointer_before, archetype_pointer_after);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1282,6 +1282,59 @@ impl World {
|
|||||||
unsafe { self.as_unsafe_world_cell().get_entity(entity)?.get_mut() }
|
unsafe { self.as_unsafe_world_cell().get_entity(entity)?.get_mut() }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Temporarily removes a [`Component`] `T` from the provided [`Entity`] and
|
||||||
|
/// runs the provided closure on it, returning the result if `T` was available.
|
||||||
|
/// This will trigger the `OnRemove` and `OnReplace` component hooks without
|
||||||
|
/// causing an archetype move.
|
||||||
|
///
|
||||||
|
/// This is most useful with immutable components, where removal and reinsertion
|
||||||
|
/// is the only way to modify a value.
|
||||||
|
///
|
||||||
|
/// If you do not need to ensure the above hooks are triggered, and your component
|
||||||
|
/// is mutable, prefer using [`get_mut`](World::get_mut).
|
||||||
|
///
|
||||||
|
/// # Examples
|
||||||
|
///
|
||||||
|
/// ```rust
|
||||||
|
/// # use bevy_ecs::prelude::*;
|
||||||
|
/// #
|
||||||
|
/// #[derive(Component, PartialEq, Eq, Debug)]
|
||||||
|
/// #[component(immutable)]
|
||||||
|
/// struct Foo(bool);
|
||||||
|
///
|
||||||
|
/// # let mut world = World::default();
|
||||||
|
/// # world.register_component::<Foo>();
|
||||||
|
/// #
|
||||||
|
/// # let entity = world.spawn(Foo(false)).id();
|
||||||
|
/// #
|
||||||
|
/// world.modify_component(entity, |foo: &mut Foo| {
|
||||||
|
/// foo.0 = true;
|
||||||
|
/// });
|
||||||
|
/// #
|
||||||
|
/// # assert_eq!(world.get::<Foo>(entity), Some(&Foo(true)));
|
||||||
|
/// ```
|
||||||
|
#[inline]
|
||||||
|
pub fn modify_component<T: Component, R>(
|
||||||
|
&mut self,
|
||||||
|
entity: Entity,
|
||||||
|
f: impl FnOnce(&mut T) -> R,
|
||||||
|
) -> Result<Option<R>, EntityFetchError> {
|
||||||
|
let mut world = DeferredWorld::from(&mut *self);
|
||||||
|
|
||||||
|
let result = match world.modify_component(entity, f) {
|
||||||
|
Ok(result) => result,
|
||||||
|
Err(EntityFetchError::AliasedMutability(..)) => {
|
||||||
|
return Err(EntityFetchError::AliasedMutability(entity))
|
||||||
|
}
|
||||||
|
Err(EntityFetchError::NoSuchEntity(..)) => {
|
||||||
|
return Err(EntityFetchError::NoSuchEntity(entity, self.into()))
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
self.flush();
|
||||||
|
Ok(result)
|
||||||
|
}
|
||||||
|
|
||||||
/// Despawns the given `entity`, if it exists. This will also remove all of the entity's
|
/// Despawns the given `entity`, if it exists. This will also remove all of the entity's
|
||||||
/// [`Component`]s. Returns `true` if the `entity` is successfully despawned and `false` if
|
/// [`Component`]s. Returns `true` if the `entity` is successfully despawned and `false` if
|
||||||
/// the `entity` does not exist.
|
/// the `entity` does not exist.
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user