diff --git a/benches/benches/bevy_ecs/world/commands.rs b/benches/benches/bevy_ecs/world/commands.rs index e7e0483bc8a8f..ea220cf1ce7b1 100644 --- a/benches/benches/bevy_ecs/world/commands.rs +++ b/benches/benches/bevy_ecs/world/commands.rs @@ -91,7 +91,7 @@ pub fn insert_commands(criterion: &mut Criterion) { command_queue.apply(&mut world); }); }); - group.bench_function("insert_batch", |bencher| { + group.bench_function("insert_or_spawn_batch", |bencher| { let mut world = World::default(); let mut command_queue = CommandQueue::default(); let mut entities = Vec::new(); @@ -109,6 +109,24 @@ pub fn insert_commands(criterion: &mut Criterion) { command_queue.apply(&mut world); }); }); + group.bench_function("insert_batch", |bencher| { + let mut world = World::default(); + let mut command_queue = CommandQueue::default(); + let mut entities = Vec::new(); + for _ in 0..entity_count { + entities.push(world.spawn_empty().id()); + } + + bencher.iter(|| { + let mut commands = Commands::new(&mut command_queue, &world); + let mut values = Vec::with_capacity(entity_count); + for entity in &entities { + values.push((*entity, (Matrix::default(), Vec3::default()))); + } + commands.insert_batch(values); + command_queue.apply(&mut world); + }); + }); group.finish(); } diff --git a/crates/bevy_ecs/src/lib.rs b/crates/bevy_ecs/src/lib.rs index 3a15cabe3f529..67254d6298f11 100644 --- a/crates/bevy_ecs/src/lib.rs +++ b/crates/bevy_ecs/src/lib.rs @@ -1699,6 +1699,134 @@ mod tests { ); } + #[test] + fn insert_batch() { + let mut world = World::default(); + let e0 = world.spawn(A(0)).id(); + let e1 = world.spawn(B(0)).id(); + + let values = vec![(e0, (A(1), B(0))), (e1, (A(0), B(1)))]; + + world.insert_batch(values); + + assert_eq!( + world.get::(e0), + Some(&A(1)), + "first entity's A component should have been replaced" + ); + assert_eq!( + world.get::(e0), + Some(&B(0)), + "first entity should have received B component" + ); + assert_eq!( + world.get::(e1), + Some(&A(0)), + "second entity should have received A component" + ); + assert_eq!( + world.get::(e1), + Some(&B(1)), + "second entity's B component should have been replaced" + ); + } + + #[test] + fn insert_batch_same_archetype() { + let mut world = World::default(); + let e0 = world.spawn((A(0), B(0))).id(); + let e1 = world.spawn((A(0), B(0))).id(); + let e2 = world.spawn(B(0)).id(); + + let values = vec![(e0, (B(1), C)), (e1, (B(2), C)), (e2, (B(3), C))]; + + world.insert_batch(values); + let mut query = world.query::<(Option<&A>, &B, &C)>(); + let component_values = query.get_many(&world, [e0, e1, e2]).unwrap(); + + assert_eq!( + component_values, + [(Some(&A(0)), &B(1), &C), (Some(&A(0)), &B(2), &C), (None, &B(3), &C)], + "all entities should have had their B component replaced, received C component, and had their A component (or lack thereof) unchanged" + ); + } + + #[test] + fn insert_batch_if_new() { + let mut world = World::default(); + let e0 = world.spawn(A(0)).id(); + let e1 = world.spawn(B(0)).id(); + + let values = vec![(e0, (A(1), B(0))), (e1, (A(0), B(1)))]; + + world.insert_batch_if_new(values); + + assert_eq!( + world.get::(e0), + Some(&A(0)), + "first entity's A component should not have been replaced" + ); + assert_eq!( + world.get::(e0), + Some(&B(0)), + "first entity should have received B component" + ); + assert_eq!( + world.get::(e1), + Some(&A(0)), + "second entity should have received A component" + ); + assert_eq!( + world.get::(e1), + Some(&B(0)), + "second entity's B component should not have been replaced" + ); + } + + #[test] + fn try_insert_batch() { + let mut world = World::default(); + let e0 = world.spawn(A(0)).id(); + let e1 = Entity::from_raw(1); + + let values = vec![(e0, (A(1), B(0))), (e1, (A(0), B(1)))]; + + world.try_insert_batch(values); + + assert_eq!( + world.get::(e0), + Some(&A(1)), + "first entity's A component should have been replaced" + ); + assert_eq!( + world.get::(e0), + Some(&B(0)), + "first entity should have received B component" + ); + } + + #[test] + fn try_insert_batch_if_new() { + let mut world = World::default(); + let e0 = world.spawn(A(0)).id(); + let e1 = Entity::from_raw(1); + + let values = vec![(e0, (A(1), B(0))), (e1, (A(0), B(1)))]; + + world.try_insert_batch_if_new(values); + + assert_eq!( + world.get::(e0), + Some(&A(0)), + "first entity's A component should not have been replaced" + ); + assert_eq!( + world.get::(e0), + Some(&B(0)), + "first entity should have received B component" + ); + } + #[test] fn required_components() { #[derive(Component)] diff --git a/crates/bevy_ecs/src/system/commands/mod.rs b/crates/bevy_ecs/src/system/commands/mod.rs index 4e10f88bb36b6..5e68742537e8d 100644 --- a/crates/bevy_ecs/src/system/commands/mod.rs +++ b/crates/bevy_ecs/src/system/commands/mod.rs @@ -614,6 +614,110 @@ impl<'w, 's> Commands<'w, 's> { self.queue(insert_or_spawn_batch(bundles_iter)); } + /// Pushes a [`Command`] to the queue for adding a [`Bundle`] type to a batch of [`Entities`](Entity). + /// + /// A batch can be any type that implements [`IntoIterator`] containing `(Entity, Bundle)` tuples, + /// such as a [`Vec<(Entity, Bundle)>`] or an array `[(Entity, Bundle); N]`. + /// + /// When the command is applied, for each `(Entity, Bundle)` pair in the given batch, + /// the `Bundle` is added to the `Entity`, overwriting any existing components shared by the `Bundle`. + /// + /// This method is equivalent to iterating the batch, + /// calling [`entity`](Self::entity) for each pair, + /// and passing the bundle to [`insert`](EntityCommands::insert), + /// but it is faster due to memory pre-allocation. + /// + /// # Panics + /// + /// This command panics if any of the given entities do not exist. + /// + /// For the non-panicking version, see [`try_insert_batch`](Self::try_insert_batch). + #[track_caller] + pub fn insert_batch(&mut self, batch: I) + where + I: IntoIterator + Send + Sync + 'static, + B: Bundle, + { + self.queue(insert_batch(batch)); + } + + /// Pushes a [`Command`] to the queue for adding a [`Bundle`] type to a batch of [`Entities`](Entity). + /// + /// A batch can be any type that implements [`IntoIterator`] containing `(Entity, Bundle)` tuples, + /// such as a [`Vec<(Entity, Bundle)>`] or an array `[(Entity, Bundle); N]`. + /// + /// When the command is applied, for each `(Entity, Bundle)` pair in the given batch, + /// the `Bundle` is added to the `Entity`, except for any components already present on the `Entity`. + /// + /// This method is equivalent to iterating the batch, + /// calling [`entity`](Self::entity) for each pair, + /// and passing the bundle to [`insert_if_new`](EntityCommands::insert_if_new), + /// but it is faster due to memory pre-allocation. + /// + /// # Panics + /// + /// This command panics if any of the given entities do not exist. + /// + /// For the non-panicking version, see [`try_insert_batch_if_new`](Self::try_insert_batch_if_new). + #[track_caller] + pub fn insert_batch_if_new(&mut self, batch: I) + where + I: IntoIterator + Send + Sync + 'static, + B: Bundle, + { + self.queue(insert_batch_if_new(batch)); + } + + /// Pushes a [`Command`] to the queue for adding a [`Bundle`] type to a batch of [`Entities`](Entity). + /// + /// A batch can be any type that implements [`IntoIterator`] containing `(Entity, Bundle)` tuples, + /// such as a [`Vec<(Entity, Bundle)>`] or an array `[(Entity, Bundle); N]`. + /// + /// When the command is applied, for each `(Entity, Bundle)` pair in the given batch, + /// the `Bundle` is added to the `Entity`, overwriting any existing components shared by the `Bundle`. + /// + /// This method is equivalent to iterating the batch, + /// calling [`get_entity`](Self::get_entity) for each pair, + /// and passing the bundle to [`insert`](EntityCommands::insert), + /// but it is faster due to memory pre-allocation. + /// + /// This command silently fails by ignoring any entities that do not exist. + /// + /// For the panicking version, see [`insert_batch`](Self::insert_batch). + #[track_caller] + pub fn try_insert_batch(&mut self, batch: I) + where + I: IntoIterator + Send + Sync + 'static, + B: Bundle, + { + self.queue(try_insert_batch(batch)); + } + + /// Pushes a [`Command`] to the queue for adding a [`Bundle`] type to a batch of [`Entities`](Entity). + /// + /// A batch can be any type that implements [`IntoIterator`] containing `(Entity, Bundle)` tuples, + /// such as a [`Vec<(Entity, Bundle)>`] or an array `[(Entity, Bundle); N]`. + /// + /// When the command is applied, for each `(Entity, Bundle)` pair in the given batch, + /// the `Bundle` is added to the `Entity`, except for any components already present on the `Entity`. + /// + /// This method is equivalent to iterating the batch, + /// calling [`get_entity`](Self::get_entity) for each pair, + /// and passing the bundle to [`insert_if_new`](EntityCommands::insert_if_new), + /// but it is faster due to memory pre-allocation. + /// + /// This command silently fails by ignoring any entities that do not exist. + /// + /// For the panicking version, see [`insert_batch_if_new`](Self::insert_batch_if_new). + #[track_caller] + pub fn try_insert_batch_if_new(&mut self, batch: I) + where + I: IntoIterator + Send + Sync + 'static, + B: Bundle, + { + self.queue(try_insert_batch_if_new(batch)); + } + /// Pushes a [`Command`] to the queue for inserting a [`Resource`] in the [`World`] with an inferred value. /// /// The inferred value is determined by the [`FromWorld`] trait of the resource. @@ -1734,6 +1838,94 @@ where } } +/// A [`Command`] that consumes an iterator to add a series of [`Bundles`](Bundle) to a set of entities. +/// If any entities do not exist in the world, this command will panic. +/// +/// This is more efficient than inserting the bundles individually. +#[track_caller] +fn insert_batch(batch: I) -> impl Command +where + I: IntoIterator + Send + Sync + 'static, + B: Bundle, +{ + #[cfg(feature = "track_change_detection")] + let caller = Location::caller(); + move |world: &mut World| { + world.insert_batch_with_caller( + batch, + InsertMode::Replace, + #[cfg(feature = "track_change_detection")] + caller, + ); + } +} + +/// A [`Command`] that consumes an iterator to add a series of [`Bundles`](Bundle) to a set of entities. +/// If any entities do not exist in the world, this command will panic. +/// +/// This is more efficient than inserting the bundles individually. +#[track_caller] +fn insert_batch_if_new(batch: I) -> impl Command +where + I: IntoIterator + Send + Sync + 'static, + B: Bundle, +{ + #[cfg(feature = "track_change_detection")] + let caller = Location::caller(); + move |world: &mut World| { + world.insert_batch_with_caller( + batch, + InsertMode::Keep, + #[cfg(feature = "track_change_detection")] + caller, + ); + } +} + +/// A [`Command`] that consumes an iterator to add a series of [`Bundles`](Bundle) to a set of entities. +/// If any entities do not exist in the world, this command will ignore them. +/// +/// This is more efficient than inserting the bundles individually. +#[track_caller] +fn try_insert_batch(batch: I) -> impl Command +where + I: IntoIterator + Send + Sync + 'static, + B: Bundle, +{ + #[cfg(feature = "track_change_detection")] + let caller = Location::caller(); + move |world: &mut World| { + world.try_insert_batch_with_caller( + batch, + InsertMode::Replace, + #[cfg(feature = "track_change_detection")] + caller, + ); + } +} + +/// A [`Command`] that consumes an iterator to add a series of [`Bundles`](Bundle) to a set of entities. +/// If any entities do not exist in the world, this command will ignore them. +/// +/// This is more efficient than inserting the bundles individually. +#[track_caller] +fn try_insert_batch_if_new(batch: I) -> impl Command +where + I: IntoIterator + Send + Sync + 'static, + B: Bundle, +{ + #[cfg(feature = "track_change_detection")] + let caller = Location::caller(); + move |world: &mut World| { + world.try_insert_batch_with_caller( + batch, + InsertMode::Keep, + #[cfg(feature = "track_change_detection")] + caller, + ); + } +} + /// A [`Command`] that despawns a specific entity. /// This will emit a warning if the entity does not exist. /// diff --git a/crates/bevy_ecs/src/world/mod.rs b/crates/bevy_ecs/src/world/mod.rs index 00e175cd77e17..fe9752a33400e 100644 --- a/crates/bevy_ecs/src/world/mod.rs +++ b/crates/bevy_ecs/src/world/mod.rs @@ -2466,6 +2466,309 @@ impl World { } } + /// For a given batch of ([`Entity`], [`Bundle`]) pairs, + /// adds the `Bundle` of components to each `Entity`. + /// This is faster than doing equivalent operations one-by-one. + /// + /// A batch can be any type that implements [`IntoIterator`] containing `(Entity, Bundle)` tuples, + /// such as a [`Vec<(Entity, Bundle)>`] or an array `[(Entity, Bundle); N]`. + /// + /// This will overwrite any previous values of components shared by the `Bundle`. + /// See [`World::insert_batch_if_new`] to keep the old values instead. + /// + /// # Panics + /// + /// This function will panic if any of the associated entities do not exist. + /// + /// For the non-panicking version, see [`World::try_insert_batch`]. + #[track_caller] + pub fn insert_batch(&mut self, batch: I) + where + I: IntoIterator, + I::IntoIter: Iterator, + B: Bundle, + { + self.insert_batch_with_caller( + batch, + InsertMode::Replace, + #[cfg(feature = "track_change_detection")] + Location::caller(), + ); + } + + /// For a given batch of ([`Entity`], [`Bundle`]) pairs, + /// adds the `Bundle` of components to each `Entity` without overwriting. + /// This is faster than doing equivalent operations one-by-one. + /// + /// A batch can be any type that implements [`IntoIterator`] containing `(Entity, Bundle)` tuples, + /// such as a [`Vec<(Entity, Bundle)>`] or an array `[(Entity, Bundle); N]`. + /// + /// This is the same as [`World::insert_batch`], but in case of duplicate + /// components it will leave the old values instead of replacing them with new ones. + /// + /// # Panics + /// + /// This function will panic if any of the associated entities do not exist. + /// + /// For the non-panicking version, see [`World::try_insert_batch_if_new`]. + #[track_caller] + pub fn insert_batch_if_new(&mut self, batch: I) + where + I: IntoIterator, + I::IntoIter: Iterator, + B: Bundle, + { + self.insert_batch_with_caller( + batch, + InsertMode::Keep, + #[cfg(feature = "track_change_detection")] + Location::caller(), + ); + } + + /// Split into a new function so we can differentiate the calling location. + /// + /// This can be called by: + /// - [`World::insert_batch`] + /// - [`World::insert_batch_if_new`] + /// - [`Commands::insert_batch`] + /// - [`Commands::insert_batch_if_new`] + #[inline] + pub(crate) fn insert_batch_with_caller( + &mut self, + iter: I, + insert_mode: InsertMode, + #[cfg(feature = "track_change_detection")] caller: &'static Location, + ) where + I: IntoIterator, + I::IntoIter: Iterator, + B: Bundle, + { + self.flush(); + + let change_tick = self.change_tick(); + + let bundle_id = self + .bundles + .register_info::(&mut self.components, &mut self.storages); + + struct InserterArchetypeCache<'w> { + inserter: BundleInserter<'w>, + archetype_id: ArchetypeId, + } + + let mut batch = iter.into_iter(); + + if let Some((first_entity, first_bundle)) = batch.next() { + if let Some(first_location) = self.entities().get(first_entity) { + let mut cache = InserterArchetypeCache { + // SAFETY: we initialized this bundle_id in `register_info` + inserter: unsafe { + BundleInserter::new_with_id( + self, + first_location.archetype_id, + bundle_id, + change_tick, + ) + }, + archetype_id: first_location.archetype_id, + }; + // SAFETY: `entity` is valid, `location` matches entity, bundle matches inserter + unsafe { + cache.inserter.insert( + first_entity, + first_location, + first_bundle, + insert_mode, + #[cfg(feature = "track_change_detection")] + caller, + ) + }; + + for (entity, bundle) in batch { + if let Some(location) = cache.inserter.entities().get(entity) { + if location.archetype_id != cache.archetype_id { + cache = InserterArchetypeCache { + // SAFETY: we initialized this bundle_id in `register_info` + inserter: unsafe { + BundleInserter::new_with_id( + self, + location.archetype_id, + bundle_id, + change_tick, + ) + }, + archetype_id: location.archetype_id, + } + } + // SAFETY: `entity` is valid, `location` matches entity, bundle matches inserter + unsafe { + cache.inserter.insert( + entity, + location, + bundle, + insert_mode, + #[cfg(feature = "track_change_detection")] + caller, + ) + }; + } else { + panic!("error[B0003]: Could not insert a bundle (of type `{}`) for entity {:?} because it doesn't exist in this World. See: https://bevyengine.org/learn/errors/b0003", core::any::type_name::(), entity); + } + } + } else { + panic!("error[B0003]: Could not insert a bundle (of type `{}`) for entity {:?} because it doesn't exist in this World. See: https://bevyengine.org/learn/errors/b0003", core::any::type_name::(), first_entity); + } + } + } + + /// For a given batch of ([`Entity`], [`Bundle`]) pairs, + /// adds the `Bundle` of components to each `Entity`. + /// This is faster than doing equivalent operations one-by-one. + /// + /// A batch can be any type that implements [`IntoIterator`] containing `(Entity, Bundle)` tuples, + /// such as a [`Vec<(Entity, Bundle)>`] or an array `[(Entity, Bundle); N]`. + /// + /// This will overwrite any previous values of components shared by the `Bundle`. + /// See [`World::try_insert_batch_if_new`] to keep the old values instead. + /// + /// This function silently fails by ignoring any entities that do not exist. + /// + /// For the panicking version, see [`World::insert_batch`]. + #[track_caller] + pub fn try_insert_batch(&mut self, batch: I) + where + I: IntoIterator, + I::IntoIter: Iterator, + B: Bundle, + { + self.try_insert_batch_with_caller( + batch, + InsertMode::Replace, + #[cfg(feature = "track_change_detection")] + Location::caller(), + ); + } + /// For a given batch of ([`Entity`], [`Bundle`]) pairs, + /// adds the `Bundle` of components to each `Entity` without overwriting. + /// This is faster than doing equivalent operations one-by-one. + /// + /// A batch can be any type that implements [`IntoIterator`] containing `(Entity, Bundle)` tuples, + /// such as a [`Vec<(Entity, Bundle)>`] or an array `[(Entity, Bundle); N]`. + /// + /// This is the same as [`World::try_insert_batch`], but in case of duplicate + /// components it will leave the old values instead of replacing them with new ones. + /// + /// This function silently fails by ignoring any entities that do not exist. + /// + /// For the panicking version, see [`World::insert_batch_if_new`]. + #[track_caller] + pub fn try_insert_batch_if_new(&mut self, batch: I) + where + I: IntoIterator, + I::IntoIter: Iterator, + B: Bundle, + { + self.try_insert_batch_with_caller( + batch, + InsertMode::Keep, + #[cfg(feature = "track_change_detection")] + Location::caller(), + ); + } + + /// Split into a new function so we can differentiate the calling location. + /// + /// This can be called by: + /// - [`World::try_insert_batch`] + /// - [`World::try_insert_batch_if_new`] + /// - [`Commands::try_insert_batch`] + /// - [`Commands::try_insert_batch_if_new`] + #[inline] + pub(crate) fn try_insert_batch_with_caller( + &mut self, + iter: I, + insert_mode: InsertMode, + #[cfg(feature = "track_change_detection")] caller: &'static Location, + ) where + I: IntoIterator, + I::IntoIter: Iterator, + B: Bundle, + { + self.flush(); + + let change_tick = self.change_tick(); + + let bundle_id = self + .bundles + .register_info::(&mut self.components, &mut self.storages); + + struct InserterArchetypeCache<'w> { + inserter: BundleInserter<'w>, + archetype_id: ArchetypeId, + } + + let mut batch = iter.into_iter(); + + if let Some((first_entity, first_bundle)) = batch.next() { + if let Some(first_location) = self.entities().get(first_entity) { + let mut cache = InserterArchetypeCache { + // SAFETY: we initialized this bundle_id in `register_info` + inserter: unsafe { + BundleInserter::new_with_id( + self, + first_location.archetype_id, + bundle_id, + change_tick, + ) + }, + archetype_id: first_location.archetype_id, + }; + // SAFETY: `entity` is valid, `location` matches entity, bundle matches inserter + unsafe { + cache.inserter.insert( + first_entity, + first_location, + first_bundle, + insert_mode, + #[cfg(feature = "track_change_detection")] + caller, + ) + }; + + for (entity, bundle) in batch { + if let Some(location) = cache.inserter.entities().get(entity) { + if location.archetype_id != cache.archetype_id { + cache = InserterArchetypeCache { + // SAFETY: we initialized this bundle_id in `register_info` + inserter: unsafe { + BundleInserter::new_with_id( + self, + location.archetype_id, + bundle_id, + change_tick, + ) + }, + archetype_id: location.archetype_id, + } + } + // SAFETY: `entity` is valid, `location` matches entity, bundle matches inserter + unsafe { + cache.inserter.insert( + entity, + location, + bundle, + insert_mode, + #[cfg(feature = "track_change_detection")] + caller, + ) + }; + } + } + } + } + } + /// Temporarily removes the requested resource from this [`World`], runs custom user code, /// then re-adds the resource before returning. ///