Skip to content

Add Command::insert_batch and World::insert_batch #8600

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

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion crates/bevy_ecs/src/entity/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -821,7 +821,7 @@ pub struct EntityLocation {

impl EntityLocation {
/// location for **pending entity** and **invalid entity**
const INVALID: EntityLocation = EntityLocation {
pub const INVALID: EntityLocation = EntityLocation {
archetype_id: ArchetypeId::INVALID,
archetype_row: ArchetypeRow::INVALID,
table_id: TableId::INVALID,
Expand Down
60 changes: 60 additions & 0 deletions crates/bevy_ecs/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1708,4 +1708,64 @@ mod tests {
"new entity was spawned and received C component"
);
}

#[test]
fn insert_batch() {
let mut world = World::default();
let e0 = world.spawn(A(0)).id();
let e1 = world.spawn(B(1)).id();

let values = vec![(e0, (B(0), C)), (e1, (B(1), C))];

world.insert_batch(values);

assert_eq!(
world.get::<A>(e0),
Some(&A(0)),
"existing component was preserved"
);
assert_eq!(
world.get::<B>(e0),
Some(&B(0)),
"pre-existing entity received correct B component"
);
assert_eq!(
world.get::<B>(e1),
Some(&B(1)),
"pre-existing entity received correct B component"
);
assert_eq!(
world.get::<C>(e0),
Some(&C),
"pre-existing entity received C component"
);
assert_eq!(
world.get::<C>(e1),
Some(&C),
"pre-existing entity received C component"
);
}

#[test]
#[should_panic]
fn insert_batch_not_spawned() {
let mut world = World::default();
let e0 = Entity::from_raw(0);

let values = vec![(e0, (B(0), C))];

world.insert_batch(values);
}

#[test]
#[should_panic]
fn insert_batch_invalid() {
let mut world = World::default();
let e0 = world.spawn_empty().id();
let invalid_e0 = Entity::new(e0.index(), 1);

let values = vec![(invalid_e0, (B(2), C))];

world.insert_batch(values);
}
}
50 changes: 50 additions & 0 deletions crates/bevy_ecs/src/system/commands/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -423,6 +423,36 @@ impl<'w, 's> Commands<'w, 's> {
self.queue.push(InsertOrSpawnBatch { bundles_iter });
}

/// Pushes a [`Command`] to the queue for for adding a bundle to already created entities.
///
/// `bundles_iter` is a type that can be converted into an ([`Entity`], [`Bundle`]) iterator
/// (it can also be a collection).
///
/// When the command is applied,
/// for each (`Entity`, `Bundle`) pair in the given `bundles_iter`,
/// The `Bundle` is added to the entity.
///
/// This method is equivalent to iterating `bundles_iter`,
/// calling [`get`](Self::get) for each bundle,
/// and passing it to [`insert`](EntityCommands::insert),
/// but it is faster due to memory pre-allocation.
///
/// # Note
///
/// TODO: Is the below note still something we should attach here since this method doesn't do any spawning?
///
/// Spawning a specific `entity` value is rarely the right choice. Most apps should use [`Commands::spawn_batch`].
/// This method should generally only be used for sharing entities across apps, and only when they have a scheme
/// worked out to share an ID space (which doesn't happen by default).
pub fn insert_batch<I, B>(&mut self, bundles_iter: I)
where
I: IntoIterator + Send + Sync + 'static,
I::IntoIter: Iterator<Item = (Entity, B)>,
B: Bundle,
{
self.queue.push(InsertBatch { bundles_iter });
}

/// 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 @@ -907,6 +937,26 @@ where
}
}

pub struct InsertBatch<I, B>
where
I: IntoIterator + Send + Sync + 'static,
B: Bundle,
I::IntoIter: Iterator<Item = (Entity, B)>,
{
pub bundles_iter: I,
}

impl<I, B> Command for InsertBatch<I, B>
where
I: IntoIterator + Send + Sync + 'static,
B: Bundle,
I::IntoIter: Iterator<Item = (Entity, B)>,
{
fn write(self, world: &mut World) {
world.insert_batch(self.bundles_iter);
}
}

#[derive(Debug)]
pub struct Despawn {
pub entity: Entity,
Expand Down
67 changes: 67 additions & 0 deletions crates/bevy_ecs/src/world/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1266,6 +1266,73 @@ impl World {
}
}

/// For a given batch of ([Entity], [Bundle]) pairs, inserts the [Bundle] (if the entity already exists) else
/// panics
/// This is faster than doing equivalent operations one-by-one and faster than `insert_or_spawn_batch`
/// No return value. Function will panic at the first [Entity] that was not already spawned
///
/// # Note
/// Spawning a specific `entity` value is rarely the right choice. Most apps should use [`World::spawn_batch`].
/// This method should generally only be used for sharing entities across apps, and only when they have a scheme
/// worked out to share an ID space (which doesn't happen by default).
///
/// ```
/// 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 e0 = world.spawn_empty().id();
/// let e1 = world.spawn_empty().id();
/// world.insert_batch(vec![
/// (e0, (A("a"), B(0.0))), // the first entity
/// (e1, (A("b"), B(1.0))), // the second entity
/// ]);
///
/// assert_eq!(world.get::<B>(e0), Some(&B(0.0)));
/// ```
pub fn insert_batch<I, B>(&mut self, iter: I)
where
I: IntoIterator,
I::IntoIter: Iterator<Item = (Entity, B)>,
B: Bundle,
{
self.flush();

let change_tick = self.change_tick();

let bundle_info = self
.bundles
.init_info::<B>(&mut self.components, &mut self.storages);

for (entity, bundle) in iter {
// Directly get the Entity location instead of polling via `alloc_at_without_replacement`
match self.entities.get(entity) {
Some(location) if location != EntityLocation::INVALID => {
let mut inserter = bundle_info.get_bundle_inserter(
&mut self.entities,
&mut self.archetypes,
&mut self.components,
&mut self.storages,
location.archetype_id,
change_tick,
);

// SAFETY: `entity` is valid, `location` matches entity
unsafe { inserter.insert(entity, location, bundle) };
}

Some(location) if location == EntityLocation::INVALID => {
panic!("Entity was either pending or invalid and has an invalid EntityLocation")
}

Some(_) | None => panic!("Entity location could not be determined - Index: {} Generation {}", entity.index(), entity.generation())
}
}
}

/// Temporarily removes the requested resource from this [`World`], runs custom user code,
/// then re-adds the resource before returning.
///
Expand Down