bevy/crates/bevy_ecs/src/world/spawn_batch.rs
Chris Russell eee7fd5b3e
Encapsulate cfg(feature = "track_location") in a type. (#17602)
# Objective

Eliminate the need to write `cfg(feature = "track_location")` every time
one uses an API that may use location tracking. It's verbose, and a
little intimidating. And it requires code outside of `bevy_ecs` that
wants to use location tracking needs to either unconditionally enable
the feature, or include conditional compilation of its own. It would be
good for users to be able to log locations when they are available
without needing to add feature flags to their own crates.

Reduce the number of cases where code compiles with the `track_location`
feature enabled, but not with it disabled, or vice versa. It can be hard
to remember to test it both ways!

Remove the need to store a `None` in `HookContext` when the
`track_location` feature is disabled.

## Solution

Create an `MaybeLocation<T>` type that contains a `T` if the
`track_location` feature is enabled, and is a ZST if it is not. The
overall API is similar to `Option`, but whether the value is `Some` or
`None` is set at compile time and is the same for all values.

Default `T` to `&'static Location<'static>`, since that is the most
common case.

Remove all `cfg(feature = "track_location")` blocks outside of the
implementation of that type, and instead call methods on it.

When `track_location` is disabled, `MaybeLocation` is a ZST and all
methods are `#[inline]` and empty, so they should be entirely removed by
the compiler. But the code will still be visible to the compiler and
checked, so if it compiles with the feature disabled then it should also
compile with it enabled, and vice versa.

## Open Questions

Where should these types live? I put them in `change_detection` because
that's where the existing `MaybeLocation` types were, but we now use
these outside of change detection.

While I believe that the compiler should be able to remove all of these
calls, I have not actually tested anything. If we want to take this
approach, what testing is required to ensure it doesn't impact
performance?

## Migration Guide

Methods like `Ref::changed_by()` that return a `&'static
Location<'static>` will now be available even when the `track_location`
feature is disabled, but they will return a new `MaybeLocation` type.
`MaybeLocation` wraps a `&'static Location<'static>` when the feature is
enabled, and is a ZST when the feature is disabled.

Existing code that needs a `&Location` can call `into_option().unwrap()`
to recover it. Many trait impls are forwarded, so if you only need
`Display` then no changes will be necessary.

If that code was conditionally compiled, you may instead want to use the
methods on `MaybeLocation` to remove the need for conditional
compilation.

Code that constructs a `Ref`, `Mut`, `Res`, or `ResMut` will now need to
provide location information unconditionally. If you are creating them
from existing Bevy types, you can obtain a `MaybeLocation` from methods
like `Table::get_changed_by_slice_for()` or
`ComponentSparseSet::get_with_ticks`. Otherwise, you will need to store
a `MaybeLocation` next to your data and use methods like `as_ref()` or
`as_mut()` to obtain wrapped references.
2025-02-10 21:21:20 +00:00

108 lines
2.6 KiB
Rust

use crate::{
bundle::{Bundle, BundleSpawner, NoBundleEffect},
change_detection::MaybeLocation,
entity::{Entity, EntitySetIterator},
world::World,
};
use core::iter::FusedIterator;
/// An iterator that spawns a series of entities and returns the [ID](Entity) of
/// each spawned entity.
///
/// If this iterator is not fully exhausted, any remaining entities will be spawned when this type is dropped.
pub struct SpawnBatchIter<'w, I>
where
I: Iterator,
I::Item: Bundle,
{
inner: I,
spawner: BundleSpawner<'w>,
caller: MaybeLocation,
}
impl<'w, I> SpawnBatchIter<'w, I>
where
I: Iterator,
I::Item: Bundle<Effect: NoBundleEffect>,
{
#[inline]
#[track_caller]
pub(crate) fn new(world: &'w mut World, iter: I, caller: MaybeLocation) -> Self {
// Ensure all entity allocations are accounted for so `self.entities` can realloc if
// necessary
world.flush();
let change_tick = world.change_tick();
let (lower, upper) = iter.size_hint();
let length = upper.unwrap_or(lower);
world.entities.reserve(length as u32);
let mut spawner = BundleSpawner::new::<I::Item>(world, change_tick);
spawner.reserve_storage(length);
Self {
inner: iter,
spawner,
caller,
}
}
}
impl<I> Drop for SpawnBatchIter<'_, I>
where
I: Iterator,
I::Item: Bundle,
{
fn drop(&mut self) {
// Iterate through self in order to spawn remaining bundles.
for _ in &mut *self {}
// Apply any commands from those operations.
// SAFETY: `self.spawner` will be dropped immediately after this call.
unsafe { self.spawner.flush_commands() };
}
}
impl<I> Iterator for SpawnBatchIter<'_, I>
where
I: Iterator,
I::Item: Bundle,
{
type Item = Entity;
fn next(&mut self) -> Option<Entity> {
let bundle = self.inner.next()?;
// SAFETY: bundle matches spawner type
unsafe { Some(self.spawner.spawn(bundle, self.caller).0) }
}
fn size_hint(&self) -> (usize, Option<usize>) {
self.inner.size_hint()
}
}
impl<I, T> ExactSizeIterator for SpawnBatchIter<'_, I>
where
I: ExactSizeIterator<Item = T>,
T: Bundle,
{
fn len(&self) -> usize {
self.inner.len()
}
}
impl<I, T> FusedIterator for SpawnBatchIter<'_, I>
where
I: FusedIterator<Item = T>,
T: Bundle,
{
}
// SAFETY: Newly spawned entities are unique.
unsafe impl<I: Iterator, T> EntitySetIterator for SpawnBatchIter<'_, I>
where
I: FusedIterator<Item = T>,
T: Bundle,
{
}