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

Add non-overwriting variants of component/resource insertion functions #2061

Closed
wants to merge 8 commits into from
37 changes: 25 additions & 12 deletions crates/bevy_ecs/src/bundle.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@ pub use bevy_ecs_macros::Bundle;

use crate::{
archetype::ComponentStatus,
component::{Component, ComponentId, ComponentTicks, Components, StorageType, TypeInfo},
component::{
CollisionBehaviour, Component, ComponentId, ComponentTicks, Components, StorageType,
TypeInfo,
},
entity::Entity,
storage::{SparseSetIndex, SparseSets, Table},
};
Expand Down Expand Up @@ -135,6 +138,7 @@ impl BundleInfo {
bundle_status: &[ComponentStatus],
bundle: T,
change_tick: u32,
overwrite_existing: CollisionBehaviour,
) {
// NOTE: get_components calls this closure on each component in "bundle order".
// bundle_info.component_ids are also in "bundle order"
Expand All @@ -145,21 +149,30 @@ impl BundleInfo {
let component_status = bundle_status.get_unchecked(bundle_component);
match self.storage_types[bundle_component] {
StorageType::Table => {
let column = table.get_column(component_id).unwrap();
column.set_unchecked(table_row, component_ptr);
let column_status = column.get_ticks_unchecked_mut(table_row);
match component_status {
ComponentStatus::Added => {
*column_status = ComponentTicks::new(change_tick);
}
ComponentStatus::Mutated => {
column_status.set_changed(change_tick);
if !matches!(
(component_status, overwrite_existing),
(ComponentStatus::Mutated, CollisionBehaviour::Skip),
) {
let column = table.get_column(component_id).unwrap();
column.set_unchecked(table_row, component_ptr);
let column_status = column.get_ticks_unchecked_mut(table_row);
GarettCooper marked this conversation as resolved.
Show resolved Hide resolved
match component_status {
ComponentStatus::Added => {
*column_status = ComponentTicks::new(change_tick);
}
ComponentStatus::Mutated => {
column_status.set_changed(change_tick);
}
}
}
}
StorageType::SparseSet => {
let sparse_set = sparse_sets.get_mut(component_id).unwrap();
sparse_set.insert(entity, component_ptr, change_tick);
if matches!(component_status, ComponentStatus::Added)
|| matches!(overwrite_existing, CollisionBehaviour::Overwrite)
{
let sparse_set = sparse_sets.get_mut(component_id).unwrap();
sparse_set.insert(entity, component_ptr, change_tick);
}
}
}
bundle_component += 1;
Expand Down
11 changes: 11 additions & 0 deletions crates/bevy_ecs/src/component/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -370,3 +370,14 @@ fn check_tick(last_change_tick: &mut u32, change_tick: u32) {
*last_change_tick = change_tick.wrapping_sub(MAX_DELTA);
}
}

/// Used by [BundleInfo::write_components](crate::bundle::BundleInfo::write_components())
/// and [World::write_components](crate::world::World::insert_resource_with_id()) to control
/// how collisions between newly inserted and existing component types should be handled.
#[derive(Debug, Copy, Clone, Eq, PartialEq)]
pub enum CollisionBehaviour {
/// Overwrite existing component/resource of the same type.
Overwrite,
/// Skip and do not write the new component if it already exists.
Skip,
}
21 changes: 21 additions & 0 deletions crates/bevy_ecs/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -941,6 +941,27 @@ mod tests {
);
}

#[test]
fn try_insert_resource_no_collision() {
let mut world = World::default();
world.try_insert_resource(64u64);
assert_eq!(
*world.get_resource::<u64>().expect("Resource exists"),
GarettCooper marked this conversation as resolved.
Show resolved Hide resolved
64u64
);
}

#[test]
fn try_insert_resource_collision() {
let mut world = World::default();
world.insert_resource(32u64);
world.try_insert_resource(64u64);
assert_eq!(
*world.get_resource::<u64>().expect("Resource exists"),
GarettCooper marked this conversation as resolved.
Show resolved Hide resolved
32u64
);
}

#[test]
fn remove_intersection() {
let mut world = World::default();
Expand Down
164 changes: 164 additions & 0 deletions crates/bevy_ecs/src/system/commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,10 @@ impl<'a, 'b> EntityCommands<'a, 'b> {
}

/// Adds a [`Bundle`] of components to the current entity.
///
/// If any of the components in the [`Bundle`] are already present on the entity,
/// those components will be overwritten. To add only the components in the Bundle which
/// are not already present on the entity, see [try_insert_bundle](Self::try_insert_bundle()).
pub fn insert_bundle(&mut self, bundle: impl Bundle) -> &mut Self {
self.commands.add(InsertBundle {
entity: self.entity,
Expand All @@ -206,8 +210,23 @@ impl<'a, 'b> EntityCommands<'a, 'b> {
self
}

/// Adds a [`Bundle`] of components to the current entity.
///
/// If any of the components in the [`Bundle`] are already present on the entity,
/// those components will be silenty skipped. To overwrite components already present
/// on the entity with the value in the Bundle, see [insert_bundle](Self::insert_bundle()).
pub fn try_insert_bundle(&mut self, bundle: impl Bundle) -> &mut Self {
self.commands.add(TryInsertBundle {
entity: self.entity,
bundle,
});
self
}

/// Adds a single [`Component`] to the current entity.
///
/// If an instance of the Component is already present on the entity, that component will be overwritten.
/// For adding a [`Component`] to an entity only when an instance is not already present, see [try_insert](Self::try_insert()).
///
/// # Warning
///
Expand Down Expand Up @@ -246,6 +265,25 @@ impl<'a, 'b> EntityCommands<'a, 'b> {
self
}

/// Adds a single [`Component`] to the current entity.
///
/// If an instance of the Component is already present on the entity, `try_insert` will silently return without changing it.
/// For adding a [`Component`] to an entity and overwriting the existing instance, see [insert](Self::insert()).
///
/// # Warning
///
/// It's possible to call this with a bundle, but this is likely not intended and
/// [`Self::try_insert_bundle`] should be used instead. If `try_insert` is called with a bundle, the
/// bundle itself will be added as a component instead of the bundles' inner components each
/// being added.
pub fn try_insert(&mut self, component: impl Component) -> &mut Self {
self.commands.add(TryInsert {
entity: self.entity,
component,
});
self
}

/// See [`EntityMut::remove_bundle`](crate::world::EntityMut::remove_bundle).
pub fn remove_bundle<T>(&mut self) -> &mut Self
where
Expand Down Expand Up @@ -342,6 +380,20 @@ where
}
}

pub struct TryInsertBundle<T> {
entity: Entity,
bundle: T,
}

impl<T> Command for TryInsertBundle<T>
where
T: Bundle + 'static,
{
fn write(self: Box<Self>, world: &mut World) {
world.entity_mut(self.entity).try_insert_bundle(self.bundle);
}
}

#[derive(Debug)]
pub struct Insert<T> {
pub entity: Entity,
Expand All @@ -357,6 +409,21 @@ where
}
}

#[derive(Debug)]
pub(crate) struct TryInsert<T> {
entity: Entity,
component: T,
}

impl<T> Command for TryInsert<T>
where
T: Component,
{
fn write(self: Box<Self>, world: &mut World) {
world.entity_mut(self.entity).try_insert(self.component);
}
}

#[derive(Debug)]
pub struct Remove<T> {
entity: Entity,
Expand Down Expand Up @@ -451,6 +518,103 @@ mod tests {
assert_eq!(results2, vec![]);
}

#[test]
fn try_insert_component_not_present() {
let mut world = World::default();
let mut command_queue = CommandQueue::default();
let _ = Commands::new(&mut command_queue, &world)
.spawn()
.try_insert(2u32)
.id();

command_queue.apply(&mut world);
assert!(world.entities().len() == 1);
let results = world
.query::<&u32>()
.iter(&world)
.copied()
.collect::<Vec<_>>();
assert_eq!(results, vec![2u32]);
}

#[test]
fn try_insert_component_present() {
let mut world = World::default();
let mut command_queue = CommandQueue::default();
let _ = Commands::new(&mut command_queue, &world)
.spawn()
.insert(1u32)
.try_insert(2u32)
.id();

command_queue.apply(&mut world);
assert!(world.entities().len() == 1);
let results = world
.query::<&u32>()
.iter(&world)
.copied()
.collect::<Vec<_>>();
assert_eq!(results, vec![1u32]);
}

#[test]
fn try_insert_bundle_components_not_present() {
let mut world = World::default();
let mut command_queue = CommandQueue::default();
let _ = Commands::new(&mut command_queue, &world)
.spawn()
.try_insert_bundle((2u32, 4u64))
.id();

command_queue.apply(&mut world);
assert!(world.entities().len() == 1);
let results = world
.query::<(&u32, &u64)>()
.iter(&world)
.map(|(a, b)| (*a, *b))
.collect::<Vec<_>>();
assert_eq!(results, vec![(2u32, 4u64)]);
}

#[test]
fn try_insert_bundle_components_all_present() {
let mut world = World::default();
let mut command_queue = CommandQueue::default();
let _ = Commands::new(&mut command_queue, &world)
.spawn_bundle((1u32, 2u64))
.try_insert_bundle((2u32, 4u64))
.id();

command_queue.apply(&mut world);
assert!(world.entities().len() == 1);
let results = world
.query::<(&u32, &u64)>()
.iter(&world)
.map(|(a, b)| (*a, *b))
.collect::<Vec<_>>();
assert_eq!(results, vec![(1u32, 2u64)]);
}

#[test]
fn try_insert_bundle_components_some_present() {
let mut world = World::default();
let mut command_queue = CommandQueue::default();
let _ = Commands::new(&mut command_queue, &world)
.spawn()
.insert(1u32)
.try_insert_bundle((2u32, 4u64))
.id();

command_queue.apply(&mut world);
assert!(world.entities().len() == 1);
let results = world
.query::<(&u32, &u64)>()
.iter(&world)
.map(|(a, b)| (*a, *b))
.collect::<Vec<_>>();
assert_eq!(results, vec![(1u32, 4u64)]);
}

#[test]
fn remove_components() {
let mut world = World::default();
Expand Down
Loading