diff --git a/crates/bevy_ecs/src/entity/mod.rs b/crates/bevy_ecs/src/entity/mod.rs index 38fecdbd51944..7c569974c1d39 100644 --- a/crates/bevy_ecs/src/entity/mod.rs +++ b/crates/bevy_ecs/src/entity/mod.rs @@ -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, diff --git a/crates/bevy_ecs/src/lib.rs b/crates/bevy_ecs/src/lib.rs index 67a65ee64e7c0..a0208c770e919 100644 --- a/crates/bevy_ecs/src/lib.rs +++ b/crates/bevy_ecs/src/lib.rs @@ -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::(e0), + Some(&A(0)), + "existing component was preserved" + ); + assert_eq!( + world.get::(e0), + Some(&B(0)), + "pre-existing entity received correct B component" + ); + assert_eq!( + world.get::(e1), + Some(&B(1)), + "pre-existing entity received correct B component" + ); + assert_eq!( + world.get::(e0), + Some(&C), + "pre-existing entity received C component" + ); + assert_eq!( + world.get::(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); + } } diff --git a/crates/bevy_ecs/src/system/commands/mod.rs b/crates/bevy_ecs/src/system/commands/mod.rs index e5db31f50b8a0..2ce1bef844d5f 100644 --- a/crates/bevy_ecs/src/system/commands/mod.rs +++ b/crates/bevy_ecs/src/system/commands/mod.rs @@ -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(&mut self, bundles_iter: I) + where + I: IntoIterator + Send + Sync + 'static, + I::IntoIter: Iterator, + 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. @@ -907,6 +937,26 @@ where } } +pub struct InsertBatch +where + I: IntoIterator + Send + Sync + 'static, + B: Bundle, + I::IntoIter: Iterator, +{ + pub bundles_iter: I, +} + +impl Command for InsertBatch +where + I: IntoIterator + Send + Sync + 'static, + B: Bundle, + I::IntoIter: Iterator, +{ + fn write(self, world: &mut World) { + world.insert_batch(self.bundles_iter); + } +} + #[derive(Debug)] pub struct Despawn { pub entity: Entity, diff --git a/crates/bevy_ecs/src/world/mod.rs b/crates/bevy_ecs/src/world/mod.rs index f270e85105915..b8ed0672042ad 100644 --- a/crates/bevy_ecs/src/world/mod.rs +++ b/crates/bevy_ecs/src/world/mod.rs @@ -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::(e0), Some(&B(0.0))); + /// ``` + pub fn insert_batch(&mut self, iter: I) + where + I: IntoIterator, + I::IntoIter: Iterator, + B: Bundle, + { + self.flush(); + + let change_tick = self.change_tick(); + + let bundle_info = self + .bundles + .init_info::(&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. ///