Skip to content

Commit

Permalink
Add insert_batch and variations (#15702)
Browse files Browse the repository at this point in the history
# 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)));

```
  • Loading branch information
JaySpruce authored Oct 13, 2024
1 parent bdd0af6 commit 3d6b248
Show file tree
Hide file tree
Showing 4 changed files with 642 additions and 1 deletion.
20 changes: 19 additions & 1 deletion benches/benches/bevy_ecs/world/commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -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();
}
Expand Down
128 changes: 128 additions & 0 deletions crates/bevy_ecs/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)]
Expand Down
192 changes: 192 additions & 0 deletions crates/bevy_ecs/src/system/commands/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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.
///
Expand Down
Loading

0 comments on commit 3d6b248

Please sign in to comment.