Add insert_batch and variations (#15702)

# Objective

`insert_or_spawn_batch` exists, but a version for just inserting doesn't
- Closes #2693 
- Closes #8384 
- Adopts/supersedes #8600 

## Solution

Add `insert_batch`, along with the most common `insert` variations:
- `World::insert_batch`
- `World::insert_batch_if_new`
- `World::try_insert_batch`
- `World::try_insert_batch_if_new`
- `Commands::insert_batch`
- `Commands::insert_batch_if_new`
- `Commands::try_insert_batch`
- `Commands::try_insert_batch_if_new`

## Testing

Added tests, and added a benchmark for `insert_batch`.
Performance is slightly better than `insert_or_spawn_batch` when only
inserting:


![Code_HPnUN0QeWe](https://github.com/user-attachments/assets/53091e4f-6518-43f4-a63f-ae57d5470c66)

<details>
<summary>old benchmark</summary>

This was before reworking it to remove the `UnsafeWorldCell`:


![Code_QhXJb8sjlJ](https://github.com/user-attachments/assets/1061e2a7-a521-48e1-a799-1b6b8d1c0b93)
</details>

---

## Showcase

Usage is the same as `insert_or_spawn_batch`:
```
use bevy_ecs::{entity::Entity, world::World, component::Component};
#[derive(Component)]
struct A(&'static str);
#[derive(Component, PartialEq, Debug)]
struct B(f32);

let mut world = World::new();
let entity_a = world.spawn_empty().id();
let entity_b = world.spawn_empty().id();
world.insert_batch([
    (entity_a, (A("a"), B(0.0))),
    (entity_b, (A("b"), B(1.0))),
]);

assert_eq!(world.get::<B>(entity_a), Some(&B(0.0)));

```
This commit is contained in:
JaySpruce 2024-10-13 13:14:16 -05:00 committed by GitHub
parent bdd0af6bfb
commit 3d6b24880e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 642 additions and 1 deletions

View File

@ -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();
}

View File

@ -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::<A>(e0),
Some(&A(1)),
"first entity's A component should have been replaced"
);
assert_eq!(
world.get::<B>(e0),
Some(&B(0)),
"first entity should have received B component"
);
assert_eq!(
world.get::<A>(e1),
Some(&A(0)),
"second entity should have received A component"
);
assert_eq!(
world.get::<B>(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::<A>(e0),
Some(&A(0)),
"first entity's A component should not have been replaced"
);
assert_eq!(
world.get::<B>(e0),
Some(&B(0)),
"first entity should have received B component"
);
assert_eq!(
world.get::<A>(e1),
Some(&A(0)),
"second entity should have received A component"
);
assert_eq!(
world.get::<B>(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::<A>(e0),
Some(&A(1)),
"first entity's A component should have been replaced"
);
assert_eq!(
world.get::<B>(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::<A>(e0),
Some(&A(0)),
"first entity's A component should not have been replaced"
);
assert_eq!(
world.get::<B>(e0),
Some(&B(0)),
"first entity should have received B component"
);
}
#[test]
fn required_components() {
#[derive(Component)]

View File

@ -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<I, B>(&mut self, batch: I)
where
I: IntoIterator<Item = (Entity, B)> + 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<I, B>(&mut self, batch: I)
where
I: IntoIterator<Item = (Entity, B)> + 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<I, B>(&mut self, batch: I)
where
I: IntoIterator<Item = (Entity, B)> + 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<I, B>(&mut self, batch: I)
where
I: IntoIterator<Item = (Entity, B)> + 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<I, B>(batch: I) -> impl Command
where
I: IntoIterator<Item = (Entity, B)> + 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<I, B>(batch: I) -> impl Command
where
I: IntoIterator<Item = (Entity, B)> + 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<I, B>(batch: I) -> impl Command
where
I: IntoIterator<Item = (Entity, B)> + 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<I, B>(batch: I) -> impl Command
where
I: IntoIterator<Item = (Entity, B)> + 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.
///

View File

@ -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<I, B>(&mut self, batch: I)
where
I: IntoIterator,
I::IntoIter: Iterator<Item = (Entity, B)>,
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<I, B>(&mut self, batch: I)
where
I: IntoIterator,
I::IntoIter: Iterator<Item = (Entity, B)>,
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<I, B>(
&mut self,
iter: I,
insert_mode: InsertMode,
#[cfg(feature = "track_change_detection")] caller: &'static Location,
) where
I: IntoIterator,
I::IntoIter: Iterator<Item = (Entity, B)>,
B: Bundle,
{
self.flush();
let change_tick = self.change_tick();
let bundle_id = self
.bundles
.register_info::<B>(&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::<B>(), 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::<B>(), 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<I, B>(&mut self, batch: I)
where
I: IntoIterator,
I::IntoIter: Iterator<Item = (Entity, B)>,
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<I, B>(&mut self, batch: I)
where
I: IntoIterator,
I::IntoIter: Iterator<Item = (Entity, B)>,
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<I, B>(
&mut self,
iter: I,
insert_mode: InsertMode,
#[cfg(feature = "track_change_detection")] caller: &'static Location,
) where
I: IntoIterator,
I::IntoIter: Iterator<Item = (Entity, B)>,
B: Bundle,
{
self.flush();
let change_tick = self.change_tick();
let bundle_id = self
.bundles
.register_info::<B>(&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.
///