diff --git a/crates/bevy_ecs/src/bundle.rs b/crates/bevy_ecs/src/bundle.rs index d640581c969aa..0a6ae6bb1cbc4 100644 --- a/crates/bevy_ecs/src/bundle.rs +++ b/crates/bevy_ecs/src/bundle.rs @@ -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}, }; @@ -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" @@ -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); + 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; diff --git a/crates/bevy_ecs/src/component/mod.rs b/crates/bevy_ecs/src/component/mod.rs index 3ee166d9c0aee..9913a2a7246c3 100644 --- a/crates/bevy_ecs/src/component/mod.rs +++ b/crates/bevy_ecs/src/component/mod.rs @@ -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, +} diff --git a/crates/bevy_ecs/src/lib.rs b/crates/bevy_ecs/src/lib.rs index f45c6790a5780..e113c654fef0d 100644 --- a/crates/bevy_ecs/src/lib.rs +++ b/crates/bevy_ecs/src/lib.rs @@ -867,20 +867,26 @@ mod tests { .get_archetype_component_id(resource_id) .unwrap(); - assert_eq!(*world.get_resource::().expect("resource exists"), 123); + assert_eq!( + *world.get_resource::().expect("Resource not found"), + 123 + ); assert!(world.contains_resource::()); world.insert_resource(456u64); assert_eq!( - *world.get_resource::().expect("resource exists"), + *world.get_resource::().expect("Resource not found"), 456u64 ); world.insert_resource(789u64); - assert_eq!(*world.get_resource::().expect("resource exists"), 789); + assert_eq!( + *world.get_resource::().expect("Resource not found"), + 789 + ); { - let mut value = world.get_resource_mut::().expect("resource exists"); + let mut value = world.get_resource_mut::().expect("Resource not found"); assert_eq!(*value, 789); *value = 10; } @@ -941,6 +947,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::().expect("Resource not found"), + 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::().expect("Resource not found"), + 32u64 + ); + } + #[test] fn remove_intersection() { let mut world = World::default(); diff --git a/crates/bevy_ecs/src/system/commands.rs b/crates/bevy_ecs/src/system/commands.rs index 627b9e4e73531..d0e4f180ff1da 100644 --- a/crates/bevy_ecs/src/system/commands.rs +++ b/crates/bevy_ecs/src/system/commands.rs @@ -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, @@ -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 /// @@ -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(&mut self) -> &mut Self where @@ -342,6 +380,20 @@ where } } +pub struct TryInsertBundle { + entity: Entity, + bundle: T, +} + +impl Command for TryInsertBundle +where + T: Bundle + 'static, +{ + fn write(self: Box, world: &mut World) { + world.entity_mut(self.entity).try_insert_bundle(self.bundle); + } +} + #[derive(Debug)] pub struct Insert { pub entity: Entity, @@ -357,6 +409,21 @@ where } } +#[derive(Debug)] +pub(crate) struct TryInsert { + entity: Entity, + component: T, +} + +impl Command for TryInsert +where + T: Component, +{ + fn write(self: Box, world: &mut World) { + world.entity_mut(self.entity).try_insert(self.component); + } +} + #[derive(Debug)] pub struct Remove { entity: Entity, @@ -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::>(); + 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::>(); + 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::>(); + 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::>(); + 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::>(); + assert_eq!(results, vec![(1u32, 4u64)]); + } + #[test] fn remove_components() { let mut world = World::default(); diff --git a/crates/bevy_ecs/src/world/entity_ref.rs b/crates/bevy_ecs/src/world/entity_ref.rs index f8cb1dc60a183..d3b00ae73f9ea 100644 --- a/crates/bevy_ecs/src/world/entity_ref.rs +++ b/crates/bevy_ecs/src/world/entity_ref.rs @@ -1,7 +1,9 @@ use crate::{ archetype::{Archetype, ArchetypeId, Archetypes, ComponentStatus}, bundle::{Bundle, BundleInfo}, - component::{Component, ComponentId, ComponentTicks, Components, StorageType}, + component::{ + CollisionBehaviour, Component, ComponentId, ComponentTicks, Components, StorageType, + }, entity::{Entities, Entity, EntityLocation}, storage::{SparseSet, Storages}, world::{Mut, World}, @@ -194,76 +196,50 @@ impl<'w> EntityMut<'w> { let bundle_info = self.world.bundles.init_info::(components); let current_location = self.location; - // Use a non-generic function to cut down on monomorphization - unsafe fn get_insert_bundle_info<'a>( - entities: &mut Entities, - archetypes: &'a mut Archetypes, - components: &mut Components, - storages: &mut Storages, - bundle_info: &BundleInfo, - current_location: EntityLocation, - entity: Entity, - ) -> (&'a Archetype, &'a Vec, EntityLocation) { - // SAFE: component ids in `bundle_info` and self.location are valid - let new_archetype_id = add_bundle_to_archetype( + let (archetype, bundle_status, new_location) = unsafe { + Self::get_insert_bundle_info( + entities, archetypes, - storages, components, - current_location.archetype_id, + storages, bundle_info, - ); - if new_archetype_id == current_location.archetype_id { - let archetype = &archetypes[current_location.archetype_id]; - let edge = archetype.edges().get_add_bundle(bundle_info.id).unwrap(); - (archetype, &edge.bundle_status, current_location) - } else { - let (old_table_row, old_table_id) = { - let old_archetype = &mut archetypes[current_location.archetype_id]; - let result = old_archetype.swap_remove(current_location.index); - if let Some(swapped_entity) = result.swapped_entity { - entities.meta[swapped_entity.id as usize].location = current_location; - } - (result.table_row, old_archetype.table_id()) - }; - - let new_table_id = archetypes[new_archetype_id].table_id(); - - let new_location = if old_table_id == new_table_id { - archetypes[new_archetype_id].allocate(entity, old_table_row) - } else { - let (old_table, new_table) = - storages.tables.get_2_mut(old_table_id, new_table_id); - // PERF: store "non bundle" components in edge, then just move those to avoid - // redundant copies - let move_result = - old_table.move_to_superset_unchecked(old_table_row, new_table); - - let new_location = - archetypes[new_archetype_id].allocate(entity, move_result.new_row); - // if an entity was moved into this entity's table spot, update its table row - if let Some(swapped_entity) = move_result.swapped_entity { - let swapped_location = entities.get(swapped_entity).unwrap(); - archetypes[swapped_location.archetype_id] - .set_entity_table_row(swapped_location.index, old_table_row); - } - new_location - }; - - entities.meta[entity.id as usize].location = new_location; - let (old_archetype, new_archetype) = - archetypes.get_2_mut(current_location.archetype_id, new_archetype_id); - let edge = old_archetype - .edges() - .get_add_bundle(bundle_info.id) - .unwrap(); - (&*new_archetype, &edge.bundle_status, new_location) - - // Sparse set components are intentionally ignored here. They don't need to move - } - } + current_location, + entity, + ) + }; + self.location = new_location; + + let table = &storages.tables[archetype.table_id()]; + let table_row = archetype.entity_table_row(new_location.index); + // SAFE: table row is valid + unsafe { + bundle_info.write_components( + &mut storages.sparse_sets, + entity, + table, + table_row, + bundle_status, + bundle, + change_tick, + CollisionBehaviour::Overwrite, + ) + }; + self + } + + pub fn try_insert_bundle(&mut self, bundle: T) -> &mut Self { + let entity = self.entity; + let change_tick = self.world.change_tick(); + let entities = &mut self.world.entities; + let archetypes = &mut self.world.archetypes; + let components = &mut self.world.components; + let storages = &mut self.world.storages; + + let bundle_info = self.world.bundles.init_info::(components); + let current_location = self.location; let (archetype, bundle_status, new_location) = unsafe { - get_insert_bundle_info( + Self::get_insert_bundle_info( entities, archetypes, components, @@ -287,11 +263,78 @@ impl<'w> EntityMut<'w> { bundle_status, bundle, change_tick, + CollisionBehaviour::Skip, ) }; self } + // Use a non-generic function to cut down on monomorphization + unsafe fn get_insert_bundle_info<'a>( + entities: &mut Entities, + archetypes: &'a mut Archetypes, + components: &mut Components, + storages: &mut Storages, + bundle_info: &BundleInfo, + current_location: EntityLocation, + entity: Entity, + ) -> (&'a Archetype, &'a Vec, EntityLocation) { + // SAFE: component ids in `bundle_info` and self.location are valid + let new_archetype_id = add_bundle_to_archetype( + archetypes, + storages, + components, + current_location.archetype_id, + bundle_info, + ); + if new_archetype_id == current_location.archetype_id { + let archetype = &archetypes[current_location.archetype_id]; + let edge = archetype.edges().get_add_bundle(bundle_info.id).unwrap(); + (archetype, &edge.bundle_status, current_location) + } else { + let (old_table_row, old_table_id) = { + let old_archetype = &mut archetypes[current_location.archetype_id]; + let result = old_archetype.swap_remove(current_location.index); + if let Some(swapped_entity) = result.swapped_entity { + entities.meta[swapped_entity.id as usize].location = current_location; + } + (result.table_row, old_archetype.table_id()) + }; + + let new_table_id = archetypes[new_archetype_id].table_id(); + + let new_location = if old_table_id == new_table_id { + archetypes[new_archetype_id].allocate(entity, old_table_row) + } else { + let (old_table, new_table) = storages.tables.get_2_mut(old_table_id, new_table_id); + // PERF: store "non bundle" components in edge, then just move those to avoid + // redundant copies + let move_result = old_table.move_to_superset_unchecked(old_table_row, new_table); + + let new_location = + archetypes[new_archetype_id].allocate(entity, move_result.new_row); + // if an entity was moved into this entity's table spot, update its table row + if let Some(swapped_entity) = move_result.swapped_entity { + let swapped_location = entities.get(swapped_entity).unwrap(); + archetypes[swapped_location.archetype_id] + .set_entity_table_row(swapped_location.index, old_table_row); + } + new_location + }; + + entities.meta[entity.id as usize].location = new_location; + let (old_archetype, new_archetype) = + archetypes.get_2_mut(current_location.archetype_id, new_archetype_id); + let edge = old_archetype + .edges() + .get_add_bundle(bundle_info.id) + .unwrap(); + (&*new_archetype, &edge.bundle_status, new_location) + + // Sparse set components are intentionally ignored here. They don't need to move + } + } + pub fn remove_bundle(&mut self) -> Option { let archetypes = &mut self.world.archetypes; let storages = &mut self.world.storages; @@ -461,6 +504,10 @@ impl<'w> EntityMut<'w> { self.insert_bundle((value,)) } + pub fn try_insert(&mut self, value: T) -> &mut Self { + self.try_insert_bundle((value,)) + } + pub fn remove(&mut self) -> Option { self.remove_bundle::<(T,)>().map(|v| v.0) } diff --git a/crates/bevy_ecs/src/world/mod.rs b/crates/bevy_ecs/src/world/mod.rs index 2646f58a1dbfa..908dc26415e38 100644 --- a/crates/bevy_ecs/src/world/mod.rs +++ b/crates/bevy_ecs/src/world/mod.rs @@ -12,8 +12,8 @@ use crate::{ archetype::{ArchetypeComponentId, ArchetypeComponentInfo, ArchetypeId, Archetypes}, bundle::{Bundle, Bundles}, component::{ - Component, ComponentDescriptor, ComponentId, ComponentTicks, Components, ComponentsError, - StorageType, + CollisionBehaviour, Component, ComponentDescriptor, ComponentId, ComponentTicks, + Components, ComponentsError, StorageType, }, entity::{Entities, Entity}, query::{FilterFetch, QueryState, WorldQuery}, @@ -517,13 +517,23 @@ impl World { } } - /// Inserts a new resource with the given `value`. + /// Inserts a new resource with the given `value`, + /// overwriting any existing resource of type T. /// Resources are "unique" data of a given type. #[inline] pub fn insert_resource(&mut self, value: T) { let component_id = self.components.get_or_insert_resource_id::(); // SAFE: component_id just initialized and corresponds to resource of type T - unsafe { self.insert_resource_with_id(component_id, value) }; + unsafe { self.insert_resource_with_id(component_id, value, CollisionBehaviour::Overwrite) }; + } + + /// Inserts a new resource with the given `value`. + /// If a resource of type T already exists, the new resource is not inserted. + #[inline] + pub fn try_insert_resource(&mut self, value: T) { + let component_id = self.components.get_or_insert_resource_id::(); + // SAFE: component_id just initialized and corresponds to resource of type T + unsafe { self.insert_resource_with_id(component_id, value, CollisionBehaviour::Skip) }; } /// Inserts a new non-send resource with the given `value`. @@ -533,7 +543,7 @@ impl World { self.validate_non_send_access::(); let component_id = self.components.get_or_insert_non_send_resource_id::(); // SAFE: component_id just initialized and corresponds to resource of type T - unsafe { self.insert_resource_with_id(component_id, value) }; + unsafe { self.insert_resource_with_id(component_id, value, CollisionBehaviour::Overwrite) }; } /// Removes the resource of a given type and returns it, if it exists. Otherwise returns [None]. @@ -781,7 +791,12 @@ impl World { /// # Safety /// `component_id` must be valid and correspond to a resource component of type T #[inline] - unsafe fn insert_resource_with_id(&mut self, component_id: ComponentId, mut value: T) { + unsafe fn insert_resource_with_id( + &mut self, + component_id: ComponentId, + mut value: T, + overwrite_existing: CollisionBehaviour, + ) { let change_tick = self.change_tick(); let column = self.initialize_resource_internal(component_id); if column.is_empty() { @@ -794,7 +809,7 @@ impl World { std::mem::forget(value); // SAFE: index was just allocated above *column.get_ticks_unchecked_mut(row) = ComponentTicks::new(change_tick); - } else { + } else if matches!(overwrite_existing, CollisionBehaviour::Overwrite) { // SAFE: column is of type T and has already been allocated *column.get_unchecked(0).cast::() = value; column.get_ticks_unchecked_mut(0).set_changed(change_tick); diff --git a/crates/bevy_ecs/src/world/spawn_batch.rs b/crates/bevy_ecs/src/world/spawn_batch.rs index 95830dc72b696..a6730b6624c15 100644 --- a/crates/bevy_ecs/src/world/spawn_batch.rs +++ b/crates/bevy_ecs/src/world/spawn_batch.rs @@ -1,6 +1,7 @@ use crate::{ archetype::{Archetype, ArchetypeId, ComponentStatus}, bundle::{Bundle, BundleInfo}, + component::CollisionBehaviour, entity::{Entities, Entity}, storage::{SparseSets, Table}, world::{add_bundle_to_archetype, World}, @@ -104,6 +105,7 @@ where self.bundle_status, bundle, self.change_tick, + CollisionBehaviour::Overwrite, ); self.entities.meta[entity.id as usize].location = location; }