Skip to content

Commit 3d6b248

Browse files
authored
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))); ```
1 parent bdd0af6 commit 3d6b248

File tree

4 files changed

+642
-1
lines changed

4 files changed

+642
-1
lines changed

benches/benches/bevy_ecs/world/commands.rs

+19-1
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@ pub fn insert_commands(criterion: &mut Criterion) {
9191
command_queue.apply(&mut world);
9292
});
9393
});
94-
group.bench_function("insert_batch", |bencher| {
94+
group.bench_function("insert_or_spawn_batch", |bencher| {
9595
let mut world = World::default();
9696
let mut command_queue = CommandQueue::default();
9797
let mut entities = Vec::new();
@@ -109,6 +109,24 @@ pub fn insert_commands(criterion: &mut Criterion) {
109109
command_queue.apply(&mut world);
110110
});
111111
});
112+
group.bench_function("insert_batch", |bencher| {
113+
let mut world = World::default();
114+
let mut command_queue = CommandQueue::default();
115+
let mut entities = Vec::new();
116+
for _ in 0..entity_count {
117+
entities.push(world.spawn_empty().id());
118+
}
119+
120+
bencher.iter(|| {
121+
let mut commands = Commands::new(&mut command_queue, &world);
122+
let mut values = Vec::with_capacity(entity_count);
123+
for entity in &entities {
124+
values.push((*entity, (Matrix::default(), Vec3::default())));
125+
}
126+
commands.insert_batch(values);
127+
command_queue.apply(&mut world);
128+
});
129+
});
112130

113131
group.finish();
114132
}

crates/bevy_ecs/src/lib.rs

+128
Original file line numberDiff line numberDiff line change
@@ -1699,6 +1699,134 @@ mod tests {
16991699
);
17001700
}
17011701

1702+
#[test]
1703+
fn insert_batch() {
1704+
let mut world = World::default();
1705+
let e0 = world.spawn(A(0)).id();
1706+
let e1 = world.spawn(B(0)).id();
1707+
1708+
let values = vec![(e0, (A(1), B(0))), (e1, (A(0), B(1)))];
1709+
1710+
world.insert_batch(values);
1711+
1712+
assert_eq!(
1713+
world.get::<A>(e0),
1714+
Some(&A(1)),
1715+
"first entity's A component should have been replaced"
1716+
);
1717+
assert_eq!(
1718+
world.get::<B>(e0),
1719+
Some(&B(0)),
1720+
"first entity should have received B component"
1721+
);
1722+
assert_eq!(
1723+
world.get::<A>(e1),
1724+
Some(&A(0)),
1725+
"second entity should have received A component"
1726+
);
1727+
assert_eq!(
1728+
world.get::<B>(e1),
1729+
Some(&B(1)),
1730+
"second entity's B component should have been replaced"
1731+
);
1732+
}
1733+
1734+
#[test]
1735+
fn insert_batch_same_archetype() {
1736+
let mut world = World::default();
1737+
let e0 = world.spawn((A(0), B(0))).id();
1738+
let e1 = world.spawn((A(0), B(0))).id();
1739+
let e2 = world.spawn(B(0)).id();
1740+
1741+
let values = vec![(e0, (B(1), C)), (e1, (B(2), C)), (e2, (B(3), C))];
1742+
1743+
world.insert_batch(values);
1744+
let mut query = world.query::<(Option<&A>, &B, &C)>();
1745+
let component_values = query.get_many(&world, [e0, e1, e2]).unwrap();
1746+
1747+
assert_eq!(
1748+
component_values,
1749+
[(Some(&A(0)), &B(1), &C), (Some(&A(0)), &B(2), &C), (None, &B(3), &C)],
1750+
"all entities should have had their B component replaced, received C component, and had their A component (or lack thereof) unchanged"
1751+
);
1752+
}
1753+
1754+
#[test]
1755+
fn insert_batch_if_new() {
1756+
let mut world = World::default();
1757+
let e0 = world.spawn(A(0)).id();
1758+
let e1 = world.spawn(B(0)).id();
1759+
1760+
let values = vec![(e0, (A(1), B(0))), (e1, (A(0), B(1)))];
1761+
1762+
world.insert_batch_if_new(values);
1763+
1764+
assert_eq!(
1765+
world.get::<A>(e0),
1766+
Some(&A(0)),
1767+
"first entity's A component should not have been replaced"
1768+
);
1769+
assert_eq!(
1770+
world.get::<B>(e0),
1771+
Some(&B(0)),
1772+
"first entity should have received B component"
1773+
);
1774+
assert_eq!(
1775+
world.get::<A>(e1),
1776+
Some(&A(0)),
1777+
"second entity should have received A component"
1778+
);
1779+
assert_eq!(
1780+
world.get::<B>(e1),
1781+
Some(&B(0)),
1782+
"second entity's B component should not have been replaced"
1783+
);
1784+
}
1785+
1786+
#[test]
1787+
fn try_insert_batch() {
1788+
let mut world = World::default();
1789+
let e0 = world.spawn(A(0)).id();
1790+
let e1 = Entity::from_raw(1);
1791+
1792+
let values = vec![(e0, (A(1), B(0))), (e1, (A(0), B(1)))];
1793+
1794+
world.try_insert_batch(values);
1795+
1796+
assert_eq!(
1797+
world.get::<A>(e0),
1798+
Some(&A(1)),
1799+
"first entity's A component should have been replaced"
1800+
);
1801+
assert_eq!(
1802+
world.get::<B>(e0),
1803+
Some(&B(0)),
1804+
"first entity should have received B component"
1805+
);
1806+
}
1807+
1808+
#[test]
1809+
fn try_insert_batch_if_new() {
1810+
let mut world = World::default();
1811+
let e0 = world.spawn(A(0)).id();
1812+
let e1 = Entity::from_raw(1);
1813+
1814+
let values = vec![(e0, (A(1), B(0))), (e1, (A(0), B(1)))];
1815+
1816+
world.try_insert_batch_if_new(values);
1817+
1818+
assert_eq!(
1819+
world.get::<A>(e0),
1820+
Some(&A(0)),
1821+
"first entity's A component should not have been replaced"
1822+
);
1823+
assert_eq!(
1824+
world.get::<B>(e0),
1825+
Some(&B(0)),
1826+
"first entity should have received B component"
1827+
);
1828+
}
1829+
17021830
#[test]
17031831
fn required_components() {
17041832
#[derive(Component)]

crates/bevy_ecs/src/system/commands/mod.rs

+192
Original file line numberDiff line numberDiff line change
@@ -614,6 +614,110 @@ impl<'w, 's> Commands<'w, 's> {
614614
self.queue(insert_or_spawn_batch(bundles_iter));
615615
}
616616

617+
/// Pushes a [`Command`] to the queue for adding a [`Bundle`] type to a batch of [`Entities`](Entity).
618+
///
619+
/// A batch can be any type that implements [`IntoIterator`] containing `(Entity, Bundle)` tuples,
620+
/// such as a [`Vec<(Entity, Bundle)>`] or an array `[(Entity, Bundle); N]`.
621+
///
622+
/// When the command is applied, for each `(Entity, Bundle)` pair in the given batch,
623+
/// the `Bundle` is added to the `Entity`, overwriting any existing components shared by the `Bundle`.
624+
///
625+
/// This method is equivalent to iterating the batch,
626+
/// calling [`entity`](Self::entity) for each pair,
627+
/// and passing the bundle to [`insert`](EntityCommands::insert),
628+
/// but it is faster due to memory pre-allocation.
629+
///
630+
/// # Panics
631+
///
632+
/// This command panics if any of the given entities do not exist.
633+
///
634+
/// For the non-panicking version, see [`try_insert_batch`](Self::try_insert_batch).
635+
#[track_caller]
636+
pub fn insert_batch<I, B>(&mut self, batch: I)
637+
where
638+
I: IntoIterator<Item = (Entity, B)> + Send + Sync + 'static,
639+
B: Bundle,
640+
{
641+
self.queue(insert_batch(batch));
642+
}
643+
644+
/// Pushes a [`Command`] to the queue for adding a [`Bundle`] type to a batch of [`Entities`](Entity).
645+
///
646+
/// A batch can be any type that implements [`IntoIterator`] containing `(Entity, Bundle)` tuples,
647+
/// such as a [`Vec<(Entity, Bundle)>`] or an array `[(Entity, Bundle); N]`.
648+
///
649+
/// When the command is applied, for each `(Entity, Bundle)` pair in the given batch,
650+
/// the `Bundle` is added to the `Entity`, except for any components already present on the `Entity`.
651+
///
652+
/// This method is equivalent to iterating the batch,
653+
/// calling [`entity`](Self::entity) for each pair,
654+
/// and passing the bundle to [`insert_if_new`](EntityCommands::insert_if_new),
655+
/// but it is faster due to memory pre-allocation.
656+
///
657+
/// # Panics
658+
///
659+
/// This command panics if any of the given entities do not exist.
660+
///
661+
/// For the non-panicking version, see [`try_insert_batch_if_new`](Self::try_insert_batch_if_new).
662+
#[track_caller]
663+
pub fn insert_batch_if_new<I, B>(&mut self, batch: I)
664+
where
665+
I: IntoIterator<Item = (Entity, B)> + Send + Sync + 'static,
666+
B: Bundle,
667+
{
668+
self.queue(insert_batch_if_new(batch));
669+
}
670+
671+
/// Pushes a [`Command`] to the queue for adding a [`Bundle`] type to a batch of [`Entities`](Entity).
672+
///
673+
/// A batch can be any type that implements [`IntoIterator`] containing `(Entity, Bundle)` tuples,
674+
/// such as a [`Vec<(Entity, Bundle)>`] or an array `[(Entity, Bundle); N]`.
675+
///
676+
/// When the command is applied, for each `(Entity, Bundle)` pair in the given batch,
677+
/// the `Bundle` is added to the `Entity`, overwriting any existing components shared by the `Bundle`.
678+
///
679+
/// This method is equivalent to iterating the batch,
680+
/// calling [`get_entity`](Self::get_entity) for each pair,
681+
/// and passing the bundle to [`insert`](EntityCommands::insert),
682+
/// but it is faster due to memory pre-allocation.
683+
///
684+
/// This command silently fails by ignoring any entities that do not exist.
685+
///
686+
/// For the panicking version, see [`insert_batch`](Self::insert_batch).
687+
#[track_caller]
688+
pub fn try_insert_batch<I, B>(&mut self, batch: I)
689+
where
690+
I: IntoIterator<Item = (Entity, B)> + Send + Sync + 'static,
691+
B: Bundle,
692+
{
693+
self.queue(try_insert_batch(batch));
694+
}
695+
696+
/// Pushes a [`Command`] to the queue for adding a [`Bundle`] type to a batch of [`Entities`](Entity).
697+
///
698+
/// A batch can be any type that implements [`IntoIterator`] containing `(Entity, Bundle)` tuples,
699+
/// such as a [`Vec<(Entity, Bundle)>`] or an array `[(Entity, Bundle); N]`.
700+
///
701+
/// When the command is applied, for each `(Entity, Bundle)` pair in the given batch,
702+
/// the `Bundle` is added to the `Entity`, except for any components already present on the `Entity`.
703+
///
704+
/// This method is equivalent to iterating the batch,
705+
/// calling [`get_entity`](Self::get_entity) for each pair,
706+
/// and passing the bundle to [`insert_if_new`](EntityCommands::insert_if_new),
707+
/// but it is faster due to memory pre-allocation.
708+
///
709+
/// This command silently fails by ignoring any entities that do not exist.
710+
///
711+
/// For the panicking version, see [`insert_batch_if_new`](Self::insert_batch_if_new).
712+
#[track_caller]
713+
pub fn try_insert_batch_if_new<I, B>(&mut self, batch: I)
714+
where
715+
I: IntoIterator<Item = (Entity, B)> + Send + Sync + 'static,
716+
B: Bundle,
717+
{
718+
self.queue(try_insert_batch_if_new(batch));
719+
}
720+
617721
/// Pushes a [`Command`] to the queue for inserting a [`Resource`] in the [`World`] with an inferred value.
618722
///
619723
/// The inferred value is determined by the [`FromWorld`] trait of the resource.
@@ -1734,6 +1838,94 @@ where
17341838
}
17351839
}
17361840

1841+
/// A [`Command`] that consumes an iterator to add a series of [`Bundles`](Bundle) to a set of entities.
1842+
/// If any entities do not exist in the world, this command will panic.
1843+
///
1844+
/// This is more efficient than inserting the bundles individually.
1845+
#[track_caller]
1846+
fn insert_batch<I, B>(batch: I) -> impl Command
1847+
where
1848+
I: IntoIterator<Item = (Entity, B)> + Send + Sync + 'static,
1849+
B: Bundle,
1850+
{
1851+
#[cfg(feature = "track_change_detection")]
1852+
let caller = Location::caller();
1853+
move |world: &mut World| {
1854+
world.insert_batch_with_caller(
1855+
batch,
1856+
InsertMode::Replace,
1857+
#[cfg(feature = "track_change_detection")]
1858+
caller,
1859+
);
1860+
}
1861+
}
1862+
1863+
/// A [`Command`] that consumes an iterator to add a series of [`Bundles`](Bundle) to a set of entities.
1864+
/// If any entities do not exist in the world, this command will panic.
1865+
///
1866+
/// This is more efficient than inserting the bundles individually.
1867+
#[track_caller]
1868+
fn insert_batch_if_new<I, B>(batch: I) -> impl Command
1869+
where
1870+
I: IntoIterator<Item = (Entity, B)> + Send + Sync + 'static,
1871+
B: Bundle,
1872+
{
1873+
#[cfg(feature = "track_change_detection")]
1874+
let caller = Location::caller();
1875+
move |world: &mut World| {
1876+
world.insert_batch_with_caller(
1877+
batch,
1878+
InsertMode::Keep,
1879+
#[cfg(feature = "track_change_detection")]
1880+
caller,
1881+
);
1882+
}
1883+
}
1884+
1885+
/// A [`Command`] that consumes an iterator to add a series of [`Bundles`](Bundle) to a set of entities.
1886+
/// If any entities do not exist in the world, this command will ignore them.
1887+
///
1888+
/// This is more efficient than inserting the bundles individually.
1889+
#[track_caller]
1890+
fn try_insert_batch<I, B>(batch: I) -> impl Command
1891+
where
1892+
I: IntoIterator<Item = (Entity, B)> + Send + Sync + 'static,
1893+
B: Bundle,
1894+
{
1895+
#[cfg(feature = "track_change_detection")]
1896+
let caller = Location::caller();
1897+
move |world: &mut World| {
1898+
world.try_insert_batch_with_caller(
1899+
batch,
1900+
InsertMode::Replace,
1901+
#[cfg(feature = "track_change_detection")]
1902+
caller,
1903+
);
1904+
}
1905+
}
1906+
1907+
/// A [`Command`] that consumes an iterator to add a series of [`Bundles`](Bundle) to a set of entities.
1908+
/// If any entities do not exist in the world, this command will ignore them.
1909+
///
1910+
/// This is more efficient than inserting the bundles individually.
1911+
#[track_caller]
1912+
fn try_insert_batch_if_new<I, B>(batch: I) -> impl Command
1913+
where
1914+
I: IntoIterator<Item = (Entity, B)> + Send + Sync + 'static,
1915+
B: Bundle,
1916+
{
1917+
#[cfg(feature = "track_change_detection")]
1918+
let caller = Location::caller();
1919+
move |world: &mut World| {
1920+
world.try_insert_batch_with_caller(
1921+
batch,
1922+
InsertMode::Keep,
1923+
#[cfg(feature = "track_change_detection")]
1924+
caller,
1925+
);
1926+
}
1927+
}
1928+
17371929
/// A [`Command`] that despawns a specific entity.
17381930
/// This will emit a warning if the entity does not exist.
17391931
///

0 commit comments

Comments
 (0)