From 41a22f347ee2793bade584b4d51cf98e4d8deb9e Mon Sep 17 00:00:00 2001 From: Urben1680 Date: Sat, 3 May 2025 17:50:49 +0200 Subject: [PATCH 01/32] track spawn tick --- crates/bevy_ecs/src/entity/mod.rs | 68 +++++++++++++------ crates/bevy_ecs/src/world/entity_ref.rs | 52 ++++++++++---- crates/bevy_ecs/src/world/mod.rs | 22 +++++- .../bevy_ecs/src/world/unsafe_world_cell.rs | 8 +++ 4 files changed, 116 insertions(+), 34 deletions(-) diff --git a/crates/bevy_ecs/src/entity/mod.rs b/crates/bevy_ecs/src/entity/mod.rs index 3d5aa6d566753..dbfcb6bc3c273 100644 --- a/crates/bevy_ecs/src/entity/mod.rs +++ b/crates/bevy_ecs/src/entity/mod.rs @@ -77,6 +77,7 @@ pub use unique_array::UniqueEntityArray; use crate::{ archetype::{ArchetypeId, ArchetypeRow}, change_detection::MaybeLocation, + component::Tick, identifier::{ error::IdentifierError, kinds::IdKind, @@ -1000,14 +1001,12 @@ impl Entities { /// Sets the source code location from which this entity has last been spawned /// or despawned. #[inline] - pub(crate) fn set_spawned_or_despawned_by(&mut self, index: u32, caller: MaybeLocation) { - caller.map(|caller| { - let meta = self - .meta - .get_mut(index as usize) - .expect("Entity index invalid"); - meta.spawned_or_despawned_by = MaybeLocation::new(Some(caller)); - }); + pub(crate) fn set_spawned_or_despawned(&mut self, index: u32, by: MaybeLocation, at: Tick) { + let meta = self + .meta + .get_mut(index as usize) + .expect("Entity index invalid"); + meta.spawned_or_despawned = Some(SpawnedOrdDespawnedMeta { by, at }); } /// Returns the source code location from which this entity has last been spawned @@ -1018,16 +1017,41 @@ impl Entities { entity: Entity, ) -> MaybeLocation>> { MaybeLocation::new_with_flattened(|| { - self.meta - .get(entity.index() as usize) - .filter(|meta| - // Generation is incremented immediately upon despawn - (meta.generation == entity.generation) - || (meta.location.archetype_id == ArchetypeId::INVALID) - && (meta.generation == IdentifierMask::inc_masked_high_by(entity.generation, 1))) - .map(|meta| meta.spawned_or_despawned_by) + self.entity_get_spawned_or_despawned(entity) + .map(|spawned_or_despawned| spawned_or_despawned.by) }) - .map(Option::flatten) + } + + /// Returns the [`Tick`] at which this entity has last been spawned or despawned. + /// Returns `None` if its index has been reused by another entity or if this entity + /// has never existed. + pub fn entity_get_spawned_or_despawned_at(&self, entity: Entity) -> Option { + self.entity_get_spawned_or_despawned(entity) + .map(|spawned_or_despawned| spawned_or_despawned.at) + } + + /// Returns the [`SpawnedOrdDespawnedMeta`] related to the entity's las spawn or + /// respawn. Returns `None` if its index has been reused by another entity or if + /// this entity has never existed. + #[inline] + fn entity_get_spawned_or_despawned(&self, entity: Entity) -> Option { + self.meta + .get(entity.index() as usize) + .filter(|meta| + // Generation is incremented immediately upon despawn + (meta.generation == entity.generation) + || (meta.location.archetype_id == ArchetypeId::INVALID) + && (meta.generation == IdentifierMask::inc_masked_high_by(entity.generation, 1))) + .and_then(|meta| meta.spawned_or_despawned) + } + + #[inline] + pub(crate) fn check_change_ticks(&mut self, change_tick: Tick) { + for meta in &mut self.meta { + if let Some(spawned_or_despawned) = &mut meta.spawned_or_despawned { + spawned_or_despawned.at.check_tick(change_tick); + } + } } /// Constructs a message explaining why an entity does not exist, if known. @@ -1090,7 +1114,13 @@ struct EntityMeta { /// The current location of the [`Entity`] pub location: EntityLocation, /// Location of the last spawn or despawn of this entity - spawned_or_despawned_by: MaybeLocation>>, + spawned_or_despawned: Option, +} + +#[derive(Copy, Clone, Debug)] +struct SpawnedOrdDespawnedMeta { + by: MaybeLocation, + at: Tick, } impl EntityMeta { @@ -1098,7 +1128,7 @@ impl EntityMeta { const EMPTY: EntityMeta = EntityMeta { generation: NonZero::::MIN, location: EntityLocation::INVALID, - spawned_or_despawned_by: MaybeLocation::new(None), + spawned_or_despawned: None, }; } diff --git a/crates/bevy_ecs/src/world/entity_ref.rs b/crates/bevy_ecs/src/world/entity_ref.rs index 7c6d8ac3603b5..409a88449293a 100644 --- a/crates/bevy_ecs/src/world/entity_ref.rs +++ b/crates/bevy_ecs/src/world/entity_ref.rs @@ -5,7 +5,7 @@ use crate::{ DynamicBundle, InsertMode, }, change_detection::{MaybeLocation, MutUntyped}, - component::{Component, ComponentId, ComponentTicks, Components, Mutable, StorageType}, + component::{Component, ComponentId, ComponentTicks, Components, Mutable, StorageType, Tick}, entity::{ Entities, Entity, EntityBorrow, EntityCloner, EntityClonerBuilder, EntityLocation, TrustedEntityBorrow, @@ -886,6 +886,11 @@ impl<'w> EntityMut<'w> { pub fn spawned_by(&self) -> MaybeLocation { self.cell.spawned_by() } + + /// Returns the [`Tick`] at which this entity has been spawned. + pub fn spawned_at(&self) -> Tick { + self.cell.spawned_at() + } } impl<'w> From<&'w mut EntityMut<'_>> for EntityMut<'w> { @@ -2373,12 +2378,13 @@ impl<'w> EntityWorldMut<'w> { .set_entity_table_row(moved_location.archetype_row, table_row); } world.flush(); + let change_tick = world.change_tick(); // SAFETY: No structural changes unsafe { world .entities_mut() - .set_spawned_or_despawned_by(self.entity.index(), caller); + .set_spawned_or_despawned(self.entity.index(), caller, change_tick); } } @@ -2718,6 +2724,14 @@ impl<'w> EntityWorldMut<'w> { .entity_get_spawned_or_despawned_by(self.entity) .map(|location| location.unwrap()) } + + /// Returns the [`Tick`] at which this entity has last been spawned. + pub fn spawned_at(&self) -> Tick { + self.world() + .entities() + .entity_get_spawned_or_despawned_at(self.entity) + .unwrap() + } } /// # Safety @@ -4568,7 +4582,7 @@ mod tests { use core::panic::AssertUnwindSafe; use std::sync::OnceLock; - use crate::component::HookContext; + use crate::component::{HookContext, Tick}; use crate::{ change_detection::{MaybeLocation, MutUntyped}, component::ComponentId, @@ -5856,22 +5870,27 @@ mod tests { #[component(on_remove = get_tracked)] struct C; - static TRACKED: OnceLock = OnceLock::new(); + static TRACKED: OnceLock<(MaybeLocation, Tick)> = OnceLock::new(); fn get_tracked(world: DeferredWorld, HookContext { entity, .. }: HookContext) { TRACKED.get_or_init(|| { - world + let by = world .entities .entity_get_spawned_or_despawned_by(entity) - .map(|l| l.unwrap()) + .map(|l| l.unwrap()); + let at = world + .entities + .entity_get_spawned_or_despawned_at(entity) + .unwrap(); + (by, at) }); } #[track_caller] - fn caller_spawn(world: &mut World) -> (Entity, MaybeLocation) { + fn caller_spawn(world: &mut World) -> (Entity, MaybeLocation, Tick) { let caller = MaybeLocation::caller(); - (world.spawn(C).id(), caller) + (world.spawn(C).id(), caller, world.change_tick()) } - let (entity, spawner) = caller_spawn(&mut world); + let (entity, spawner, spawn_tick) = caller_spawn(&mut world); assert_eq!( spawner, @@ -5882,13 +5901,13 @@ mod tests { ); #[track_caller] - fn caller_despawn(world: &mut World, entity: Entity) -> MaybeLocation { + fn caller_despawn(world: &mut World, entity: Entity) -> (MaybeLocation, Tick) { world.despawn(entity); - MaybeLocation::caller() + (MaybeLocation::caller(), world.change_tick()) } - let despawner = caller_despawn(&mut world, entity); + let (despawner, despawn_tick) = caller_despawn(&mut world, entity); - assert_eq!(spawner, *TRACKED.get().unwrap()); + assert_eq!((spawner, spawn_tick), *TRACKED.get().unwrap()); assert_eq!( despawner, world @@ -5896,6 +5915,13 @@ mod tests { .entity_get_spawned_or_despawned_by(entity) .map(|l| l.unwrap()) ); + assert_eq!( + despawn_tick, + world + .entities() + .entity_get_spawned_or_despawned_at(entity) + .unwrap() + ); } #[test] diff --git a/crates/bevy_ecs/src/world/mod.rs b/crates/bevy_ecs/src/world/mod.rs index e61abd792024a..840344801cdc4 100644 --- a/crates/bevy_ecs/src/world/mod.rs +++ b/crates/bevy_ecs/src/world/mod.rs @@ -1117,7 +1117,7 @@ impl World { } self.entities - .set_spawned_or_despawned_by(entity.index(), caller); + .set_spawned_or_despawned(entity.index(), caller, change_tick); // SAFETY: entity and location are valid, as they were just created above let mut entity = unsafe { EntityWorldMut::new(self, entity, entity_location) }; @@ -1140,8 +1140,9 @@ impl World { let location = unsafe { archetype.allocate(entity, table_row) }; self.entities.set(entity.index(), location); + let change_tick = self.change_tick(); self.entities - .set_spawned_or_despawned_by(entity.index(), caller); + .set_spawned_or_despawned(entity.index(), caller, change_tick); EntityWorldMut::new(self, entity, location) } @@ -3029,6 +3030,7 @@ impl World { sparse_sets.check_change_ticks(change_tick); resources.check_change_ticks(change_tick); non_send_resources.check_change_ticks(change_tick); + self.entities.check_change_ticks(change_tick); if let Some(mut schedules) = self.get_resource_mut::() { schedules.check_change_ticks(change_tick); @@ -4299,22 +4301,38 @@ mod tests { world.entities.entity_get_spawned_or_despawned_by(entity), MaybeLocation::new(Some(Location::caller())) ); + assert_eq!( + world.entities.entity_get_spawned_or_despawned_at(entity), + Some(world.change_tick()) + ); world.despawn(entity); assert_eq!( world.entities.entity_get_spawned_or_despawned_by(entity), MaybeLocation::new(Some(Location::caller())) ); + assert_eq!( + world.entities.entity_get_spawned_or_despawned_at(entity), + Some(world.change_tick()) + ); let new = world.spawn_empty().id(); assert_eq!(entity.index(), new.index()); assert_eq!( world.entities.entity_get_spawned_or_despawned_by(entity), MaybeLocation::new(None) ); + assert_eq!( + world.entities.entity_get_spawned_or_despawned_at(entity), + None + ); world.despawn(new); assert_eq!( world.entities.entity_get_spawned_or_despawned_by(entity), MaybeLocation::new(None) ); + assert_eq!( + world.entities.entity_get_spawned_or_despawned_at(entity), + None + ); } #[test] diff --git a/crates/bevy_ecs/src/world/unsafe_world_cell.rs b/crates/bevy_ecs/src/world/unsafe_world_cell.rs index f094906b1267b..1c582722525d8 100644 --- a/crates/bevy_ecs/src/world/unsafe_world_cell.rs +++ b/crates/bevy_ecs/src/world/unsafe_world_cell.rs @@ -1088,6 +1088,14 @@ impl<'w> UnsafeEntityCell<'w> { .entity_get_spawned_or_despawned_by(self.entity) .map(|o| o.unwrap()) } + + /// Returns the [`Tick`] at which this entity has been spawned. + pub fn spawned_at(self) -> Tick { + self.world() + .entities() + .entity_get_spawned_or_despawned_at(self.entity) + .unwrap() + } } /// Error that may be returned when calling [`UnsafeEntityCell::get_mut_by_id`]. From 5bd6c816e431a932714c5b65910fa24dd765e9e0 Mon Sep 17 00:00:00 2001 From: Urben1680 Date: Sat, 3 May 2025 17:51:34 +0200 Subject: [PATCH 02/32] SpawnedTick/Spawned query data/filter --- crates/bevy_ecs/src/query/fetch.rs | 83 ++++++++++ crates/bevy_ecs/src/query/filter.rs | 144 +++++++++++++++++- crates/bevy_ecs/src/query/state.rs | 5 +- crates/bevy_ecs/src/system/function_system.rs | 2 +- crates/bevy_ecs/src/system/mod.rs | 36 ++++- crates/bevy_ecs/src/system/query.rs | 46 +++--- 6 files changed, 289 insertions(+), 27 deletions(-) diff --git a/crates/bevy_ecs/src/query/fetch.rs b/crates/bevy_ecs/src/query/fetch.rs index cd632f7b14f22..49e994c1ab3e7 100644 --- a/crates/bevy_ecs/src/query/fetch.rs +++ b/crates/bevy_ecs/src/query/fetch.rs @@ -31,6 +31,8 @@ use variadics_please::all_tuples; /// Gets the identifier of the queried entity. /// - **[`EntityLocation`].** /// Gets the location metadata of the queried entity. +/// - **[`SpawnedTick`].** +/// Gets the tick the entity was spawned at. /// - **[`EntityRef`].** /// Read-only access to arbitrary components on the queried entity. /// - **[`EntityMut`].** @@ -470,6 +472,87 @@ unsafe impl QueryData for EntityLocation { /// SAFETY: access is read only unsafe impl ReadOnlyQueryData for EntityLocation {} +/// The `SpawnedTick` query parameter fetches the [`Tick`] the entity was spawned at. +#[derive(Clone, Copy)] +pub struct SpawnedTick(()); + +// SAFETY: +// No components are accessed. +unsafe impl WorldQuery for SpawnedTick { + type Fetch<'w> = &'w Entities; + type State = (); + + fn shrink_fetch<'wlong: 'wshort, 'wshort>(fetch: Self::Fetch<'wlong>) -> Self::Fetch<'wshort> { + fetch + } + + unsafe fn init_fetch<'w>( + world: UnsafeWorldCell<'w>, + _state: &Self::State, + _last_run: Tick, + _this_run: Tick, + ) -> Self::Fetch<'w> { + world.entities() + } + + const IS_DENSE: bool = true; + + #[inline] + unsafe fn set_archetype<'w>( + _fetch: &mut Self::Fetch<'w>, + _state: &Self::State, + _archetype: &'w Archetype, + _table: &'w Table, + ) { + } + + #[inline] + unsafe fn set_table<'w>(_fetch: &mut Self::Fetch<'w>, _state: &Self::State, _table: &'w Table) { + } + + fn update_component_access(_state: &Self::State, _access: &mut FilteredAccess) {} + + fn init_state(_world: &mut World) {} + + fn get_state(_components: &Components) -> Option<()> { + Some(()) + } + + fn matches_component_set( + _state: &Self::State, + _set_contains_id: &impl Fn(ComponentId) -> bool, + ) -> bool { + true + } +} + +// SAFETY: +// No components are accessed. +// Is its own ReadOnlyQueryData. +unsafe impl QueryData for SpawnedTick { + const IS_READ_ONLY: bool = true; + type ReadOnly = Self; + type Item<'w> = Tick; + + fn shrink<'wlong: 'wshort, 'wshort>(item: Self::Item<'wlong>) -> Self::Item<'wshort> { + item + } + + #[inline(always)] + unsafe fn fetch<'w>( + fetch: &mut Self::Fetch<'w>, + entity: Entity, + _table_row: TableRow, + ) -> Self::Item<'w> { + let spawned = fetch.entity_get_spawned_or_despawned_at(entity); + // SAFETY: queried entity must have a spawned tick + unsafe { spawned.debug_checked_unwrap() } + } +} + +/// SAFETY: access is read only +unsafe impl ReadOnlyQueryData for SpawnedTick {} + /// SAFETY: /// `fetch` accesses all components in a readonly way. /// This is sound because `update_component_access` and `update_archetype_component_access` set read access for all components and panic when appropriate. diff --git a/crates/bevy_ecs/src/query/filter.rs b/crates/bevy_ecs/src/query/filter.rs index e4e1f0fd668d3..aee3225824bd1 100644 --- a/crates/bevy_ecs/src/query/filter.rs +++ b/crates/bevy_ecs/src/query/filter.rs @@ -1,7 +1,7 @@ use crate::{ archetype::Archetype, component::{Component, ComponentId, Components, StorageType, Tick}, - entity::Entity, + entity::{Entities, Entity}, query::{DebugCheckedUnwrap, FilteredAccess, StorageSwitch, WorldQuery}, storage::{ComponentSparseSet, Table, TableRow}, world::{unsafe_world_cell::UnsafeWorldCell, World}, @@ -17,6 +17,8 @@ use variadics_please::all_tuples; /// [`With`] and [`Without`] filters can be applied to check if the queried entity does or does not contain a particular component. /// - **Change detection filters.** /// [`Added`] and [`Changed`] filters can be applied to detect component changes to an entity. +/// - **Spawned filter.** +/// [`Spawned`] filter can be applied to check if the queried entity was spawned recently. /// - **`QueryFilter` tuples.** /// If every element of a tuple implements `QueryFilter`, then the tuple itself also implements the same trait. /// This enables a single `Query` to filter over multiple conditions. @@ -1005,6 +1007,144 @@ unsafe impl QueryFilter for Changed { } } +/// A filter that only retains results the first time after the entity has been spawned. +/// +/// A common use for this filter is one-time initialization. +/// +/// To retain all results without filtering but still check whether they were spawned after the +/// system last ran, use [`todo`]. +/// +/// **Note** that this includes entities that spawned before the first time this Query was run. +/// +/// # Deferred +/// +/// Note, that entity spawns issued with [`Commands`](crate::system::Commands) +/// are visible only after deferred operations are applied, +/// typically at the end of the schedule iteration. +/// +/// # Time complexity +/// +/// `Spawned` is not [`ArchetypeFilter`], which practically means that if query matches million +/// entities, `Spawned` filter will iterate over all of them even if none of them were spawned. +/// +/// For example, these two systems are roughly equivalent in terms of performance: +/// +/// ``` +/// # use bevy_ecs::entity::Entity; +/// # use bevy_ecs::system::Query; +/// # use bevy_ecs::system::SystemChangeTick; +/// # use bevy_ecs::query::Spawned; +/// # use bevy_ecs::query::SpawnedTick; +/// +/// fn system1(q: Query) { +/// for entity in &q { /* entity spawned */ } +/// } +/// +/// fn system2(q: Query<(Entity, SpawnedTick)>, system_ticks: SystemChangeTick) { +/// for (entity, spawned) in &q { +/// if spawned.is_newer_than(system_ticks.last_run(), system_ticks.this_run()) { +/// /* entity spawned */ +/// } +/// } +/// } +/// ``` +/// +/// # Examples +/// +/// ``` +/// # use bevy_ecs::component::Component; +/// # use bevy_ecs::query::Spawned; +/// # use bevy_ecs::system::IntoSystem; +/// # use bevy_ecs::system::Query; +/// # +/// # #[derive(Component, Debug)] +/// # struct Name {}; +/// +/// fn print_spawning_entities(query: Query<&Name, Spawned>) { +/// for name in &query { +/// println!("Entity spawned: {:?}", name); +/// } +/// } +/// +/// # bevy_ecs::system::assert_is_system(print_spawning_entities); +/// ``` +pub struct Spawned(()); + +#[doc(hidden)] +#[derive(Clone)] +pub struct SpawnedFetch<'w> { + entities: &'w Entities, + last_run: Tick, + this_run: Tick, +} + +unsafe impl WorldQuery for Spawned { + type Fetch<'w> = SpawnedFetch<'w>; + type State = (); + + fn shrink_fetch<'wlong: 'wshort, 'wshort>(fetch: Self::Fetch<'wlong>) -> Self::Fetch<'wshort> { + fetch + } + + #[inline] + unsafe fn init_fetch<'w>( + world: UnsafeWorldCell<'w>, + _state: &(), + last_run: Tick, + this_run: Tick, + ) -> Self::Fetch<'w> { + SpawnedFetch { + entities: world.entities(), + last_run, + this_run, + } + } + + const IS_DENSE: bool = true; + + #[inline] + unsafe fn set_archetype<'w>( + _fetch: &mut Self::Fetch<'w>, + _state: &(), + _archetype: &'w Archetype, + _table: &'w Table, + ) { + } + + #[inline] + unsafe fn set_table<'w>(_fetch: &mut Self::Fetch<'w>, _state: &(), _table: &'w Table) {} + + #[inline] + fn update_component_access(_state: &(), _access: &mut FilteredAccess) {} + + fn init_state(_world: &mut World) -> () {} + + fn get_state(_components: &Components) -> Option<()> { + Some(()) + } + + fn matches_component_set(_state: &(), _set_contains_id: &impl Fn(ComponentId) -> bool) -> bool { + true + } +} + +// SAFETY: WorldQuery impl performs only read access on ticks +unsafe impl QueryFilter for Spawned { + const IS_ARCHETYPAL: bool = false; + + #[inline(always)] + unsafe fn filter_fetch( + fetch: &mut Self::Fetch<'_>, + entity: Entity, + _table_row: TableRow, + ) -> bool { + let spawned = fetch.entities.entity_get_spawned_or_despawned_at(entity); + // SAFETY: queried entity must have a spawned tick + let spawned = unsafe { spawned.debug_checked_unwrap() }; + spawned.is_newer_than(fetch.last_run, fetch.this_run) + } +} + /// A marker trait to indicate that the filter works at an archetype level. /// /// This is needed to implement [`ExactSizeIterator`] for @@ -1015,7 +1155,7 @@ unsafe impl QueryFilter for Changed { /// [Tuples](prim@tuple) and [`Or`] filters are automatically implemented with the trait only if its containing types /// also implement the same trait. /// -/// [`Added`] and [`Changed`] works with entities, and therefore are not archetypal. As such +/// [`Added`], [`Changed`] and [`Spawned`] work with entities, and therefore are not archetypal. As such /// they do not implement [`ArchetypeFilter`]. #[diagnostic::on_unimplemented( message = "`{Self}` is not a valid `Query` filter based on archetype information", diff --git a/crates/bevy_ecs/src/query/state.rs b/crates/bevy_ecs/src/query/state.rs index 9f6b55fa891b2..79d5f30b8154b 100644 --- a/crates/bevy_ecs/src/query/state.rs +++ b/crates/bevy_ecs/src/query/state.rs @@ -452,8 +452,8 @@ impl QueryState { /// /// This is equivalent to `self.iter().next().is_none()`, and thus the worst case runtime will be `O(n)` /// where `n` is the number of *potential* matches. This can be notably expensive for queries that rely - /// on non-archetypal filters such as [`Added`] or [`Changed`] which must individually check each query - /// result for a match. + /// on non-archetypal filters such as [`Added`], [`Changed`] or [`Spawned`] which must individually check + /// each query result for a match. /// /// # Panics /// @@ -461,6 +461,7 @@ impl QueryState { /// /// [`Added`]: crate::query::Added /// [`Changed`]: crate::query::Changed + /// [`Spawned`]: crate::query::Spawned #[inline] pub fn is_empty(&self, world: &World, last_run: Tick, this_run: Tick) -> bool { self.validate_world(world.id()); diff --git a/crates/bevy_ecs/src/system/function_system.rs b/crates/bevy_ecs/src/system/function_system.rs index 0f3950d1d4ba6..ca95a69d7e40b 100644 --- a/crates/bevy_ecs/src/system/function_system.rs +++ b/crates/bevy_ecs/src/system/function_system.rs @@ -283,7 +283,7 @@ where /// [`SystemState`] values created can be cached to improve performance, /// and *must* be cached and reused in order for system parameters that rely on local state to work correctly. /// These include: -/// - [`Added`](crate::query::Added) and [`Changed`](crate::query::Changed) query filters +/// - [`Added`](crate::query::Added), [`Changed`](crate::query::Changed) and [`Spawned`](crate::query::Spawned) query filters /// - [`Local`](crate::system::Local) variables that hold state /// - [`EventReader`](crate::event::EventReader) system parameters, which rely on a [`Local`](crate::system::Local) to track which events have been seen /// diff --git a/crates/bevy_ecs/src/system/mod.rs b/crates/bevy_ecs/src/system/mod.rs index 9be853bf12bd1..63ce4a6361b87 100644 --- a/crates/bevy_ecs/src/system/mod.rs +++ b/crates/bevy_ecs/src/system/mod.rs @@ -337,7 +337,7 @@ mod tests { entity::{Entities, Entity}, error::Result, prelude::{AnyOf, EntityRef}, - query::{Added, Changed, Or, With, Without}, + query::{Added, Changed, Or, Spawned, SpawnedTick, With, Without}, removal_detection::RemovedComponents, resource::Resource, schedule::{ @@ -1248,6 +1248,25 @@ mod tests { } } + #[test] + fn system_state_spawned() { + let mut world = World::default(); + world.spawn_empty(); + let spawn_tick = world.change_tick(); + + let mut system_state: SystemState>> = + SystemState::new(&mut world); + { + let query = system_state.get(&world); + assert_eq!(*query.unwrap(), spawn_tick); + } + + { + let query = system_state.get(&world); + assert!(query.is_none()); + } + } + #[test] #[should_panic] fn system_state_invalid_world() { @@ -1473,6 +1492,21 @@ mod tests { let mut sys = IntoSystem::into_system(mutable_query); sys.initialize(&mut world); } + + { + let mut world = World::new(); + + fn mutable_query(mut query: Query<(&mut A, &mut B, SpawnedTick), Spawned>) { + for _ in &mut query {} + + immutable_query(query.as_readonly()); + } + + fn immutable_query(_: Query<(&A, &B, SpawnedTick), Spawned>) {} + + let mut sys = IntoSystem::into_system(mutable_query); + sys.initialize(&mut world); + } } #[test] diff --git a/crates/bevy_ecs/src/system/query.rs b/crates/bevy_ecs/src/system/query.rs index 22f0d1df20f45..8b2972fcaf67f 100644 --- a/crates/bevy_ecs/src/system/query.rs +++ b/crates/bevy_ecs/src/system/query.rs @@ -309,7 +309,7 @@ use core::{ /// |([`get_`][`get_many_mut`])[`many_mut`]|O(k2)| /// |[`single`]\[[`_mut`][`single_mut`]],
[`single`]\[[`_mut`][`single_mut`]]|O(a)| /// |Archetype based filtering ([`With`], [`Without`], [`Or`])|O(a)| -/// |Change detection filtering ([`Added`], [`Changed`])|O(a + n)| +/// |Change detection filtering ([`Added`], [`Changed`], [`Spawned`])|O(a + n)| /// /// # `Iterator::for_each` /// @@ -343,6 +343,7 @@ use core::{ /// [`AnyOf`]: crate::query::AnyOf /// [binomial coefficient]: https://en.wikipedia.org/wiki/Binomial_coefficient /// [`Changed`]: crate::query::Changed +/// [`Spawned`]: crate::query::Spawned /// [components]: crate::component::Component /// [entity identifiers]: Entity /// [`EntityRef`]: crate::world::EntityRef @@ -1833,8 +1834,8 @@ impl<'w, 's, D: QueryData, F: QueryFilter> Query<'w, 's, D, F> { /// /// This is equivalent to `self.iter().next().is_none()`, and thus the worst case runtime will be `O(n)` /// where `n` is the number of *potential* matches. This can be notably expensive for queries that rely - /// on non-archetypal filters such as [`Added`] or [`Changed`] which must individually check each query - /// result for a match. + /// on non-archetypal filters such as [`Added`], [`Changed`] or [`Spawned`] which must individually check + /// each query result for a match. /// /// # Example /// @@ -1857,6 +1858,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter> Query<'w, 's, D, F> { /// /// [`Added`]: crate::query::Added /// [`Changed`]: crate::query::Changed + /// [`Spawned`]: crate::query::Spawned #[inline] pub fn is_empty(&self) -> bool { self.as_nop().iter().next().is_none() @@ -1895,9 +1897,9 @@ impl<'w, 's, D: QueryData, F: QueryFilter> Query<'w, 's, D, F> { /// /// For example, this can transform a `Query<(&A, &mut B)>` to a `Query<&B>`. /// This can be useful for passing the query to another function. Note that since - /// filter terms are dropped, non-archetypal filters like [`Added`](crate::query::Added) and - /// [`Changed`](crate::query::Changed) will not be respected. To maintain or change filter - /// terms see [`Self::transmute_lens_filtered`] + /// filter terms are dropped, non-archetypal filters like [`Added`](crate::query::Added), + /// [`Changed`](crate::query::Changed) and [`Spawned`](crate::query::Spawned) will not be + /// respected. To maintain or change filter terms see [`Self::transmute_lens_filtered`] /// /// ## Panics /// @@ -2054,9 +2056,9 @@ impl<'w, 's, D: QueryData, F: QueryFilter> Query<'w, 's, D, F> { /// /// For example, this can transform a `Query<(&A, &mut B)>` to a `Query<&B>`. /// This can be useful for passing the query to another function. Note that since - /// filter terms are dropped, non-archetypal filters like [`Added`](crate::query::Added) and - /// [`Changed`](crate::query::Changed) will not be respected. To maintain or change filter - /// terms see [`Self::transmute_lens_filtered`] + /// filter terms are dropped, non-archetypal filters like [`Added`](crate::query::Added), + /// [`Changed`](crate::query::Changed) and [`Spawned`](crate::query::Spawned) will not be + /// respected. To maintain or change filter terms see [`Self::transmute_lens_filtered`] /// /// ## Panics /// @@ -2127,8 +2129,9 @@ impl<'w, 's, D: QueryData, F: QueryFilter> Query<'w, 's, D, F> { /// /// Note that the lens will iterate the same tables and archetypes as the original query. This means that /// additional archetypal query terms like [`With`](crate::query::With) and [`Without`](crate::query::Without) - /// will not necessarily be respected and non-archetypal terms like [`Added`](crate::query::Added) and - /// [`Changed`](crate::query::Changed) will only be respected if they are in the type signature. + /// will not necessarily be respected and non-archetypal terms like [`Added`](crate::query::Added), + /// [`Changed`](crate::query::Changed) and [`Spawned`](crate::query::Spawned) will only be respected if they + /// are in the type signature. #[track_caller] pub fn transmute_lens_filtered( &mut self, @@ -2141,8 +2144,8 @@ impl<'w, 's, D: QueryData, F: QueryFilter> Query<'w, 's, D, F> { /// /// Note that the lens will iterate the same tables and archetypes as the original query. This means that /// additional archetypal query terms like [`With`](crate::query::With) and [`Without`](crate::query::Without) - /// will not necessarily be respected and non-archetypal terms like [`Added`](crate::query::Added) and - /// [`Changed`](crate::query::Changed) will only be respected if they are in the type signature. + /// will not necessarily be respected and non-archetypal terms like [`Added`](crate::query::Added), + /// [`Changed`](crate::query::Changed) and [`Spawned`](crate::query::Spawned) will only be respected if they /// /// # See also /// @@ -2178,7 +2181,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter> Query<'w, 's, D, F> { /// /// For example, this can take a `Query<&A>` and a `Query<&B>` and return a `Query<(&A, &B)>`. /// The returned query will only return items with both `A` and `B`. Note that since filters - /// are dropped, non-archetypal filters like `Added` and `Changed` will not be respected. + /// are dropped, non-archetypal filters like `Added`, `Changed` and `Spawned` will not be respected. /// To maintain or change filter terms see `Self::join_filtered`. /// /// ## Example @@ -2240,7 +2243,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter> Query<'w, 's, D, F> { /// /// For example, this can take a `Query<&A>` and a `Query<&B>` and return a `Query<(&A, &B)>`. /// The returned query will only return items with both `A` and `B`. Note that since filters - /// are dropped, non-archetypal filters like `Added` and `Changed` will not be respected. + /// are dropped, non-archetypal filters like `Added`, `Changed` and `Spawned` will not be respected. /// To maintain or change filter terms see `Self::join_filtered`. /// /// ## Panics @@ -2267,8 +2270,8 @@ impl<'w, 's, D: QueryData, F: QueryFilter> Query<'w, 's, D, F> { /// Note that the lens with iterate a subset of the original queries' tables /// and archetypes. This means that additional archetypal query terms like /// `With` and `Without` will not necessarily be respected and non-archetypal - /// terms like `Added` and `Changed` will only be respected if they are in - /// the type signature. + /// terms like `Added`, `Changed` and `Spawned` will only be respected if they + /// are in the type signature. pub fn join_filtered< OtherD: QueryData, OtherF: QueryFilter, @@ -2287,8 +2290,8 @@ impl<'w, 's, D: QueryData, F: QueryFilter> Query<'w, 's, D, F> { /// Note that the lens with iterate a subset of the original queries' tables /// and archetypes. This means that additional archetypal query terms like /// `With` and `Without` will not necessarily be respected and non-archetypal - /// terms like `Added` and `Changed` will only be respected if they are in - /// the type signature. + /// terms like `Added`, `Changed` and `Spawned` will only be respected if they + /// are in the type signature. /// /// # See also /// @@ -2469,8 +2472,9 @@ impl<'w, D: QueryData, F: QueryFilter> Single<'w, D, F> { /// This will cause a panic, but can be configured to do nothing or warn once. /// /// Much like [`Query::is_empty`] the worst case runtime will be `O(n)` where `n` is the number of *potential* matches. -/// This can be notably expensive for queries that rely on non-archetypal filters such as [`Added`](crate::query::Added) or [`Changed`](crate::query::Changed) -/// which must individually check each query result for a match. +/// This can be notably expensive for queries that rely on non-archetypal filters such as [`Added`](crate::query::Added), +/// [`Changed`](crate::query::Changed) of [`Spawned`](crate::query::Spawned) which must individually check each query +/// result for a match. /// /// See [`Query`] for more details. /// From 2c92f8df93f04198184e8cd570df12a7dca47d26 Mon Sep 17 00:00:00 2001 From: Urben1680 Date: Sat, 3 May 2025 18:54:12 +0200 Subject: [PATCH 03/32] EntityRef::spawned_at --- crates/bevy_ecs/src/world/entity_ref.rs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/crates/bevy_ecs/src/world/entity_ref.rs b/crates/bevy_ecs/src/world/entity_ref.rs index 0d593a033384f..313c833afd48d 100644 --- a/crates/bevy_ecs/src/world/entity_ref.rs +++ b/crates/bevy_ecs/src/world/entity_ref.rs @@ -299,6 +299,11 @@ impl<'w> EntityRef<'w> { pub fn spawned_by(&self) -> MaybeLocation { self.cell.spawned_by() } + + /// Returns the [`Tick`] at which this entity has been spawned. + pub fn spawned_at(&self) -> Tick { + self.cell.spawned_at() + } } impl<'w> From> for EntityRef<'w> { From a7288e561f7dc88a709877044fcbbf355ce924e6 Mon Sep 17 00:00:00 2001 From: Urben1680 Date: Sat, 3 May 2025 19:04:26 +0200 Subject: [PATCH 04/32] ci fixes --- crates/bevy_ecs/src/query/filter.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/crates/bevy_ecs/src/query/filter.rs b/crates/bevy_ecs/src/query/filter.rs index aee3225824bd1..f0feef70d6ebd 100644 --- a/crates/bevy_ecs/src/query/filter.rs +++ b/crates/bevy_ecs/src/query/filter.rs @@ -1078,6 +1078,7 @@ pub struct SpawnedFetch<'w> { this_run: Tick, } +// SAFETY: WorldQuery impl accesses no components unsafe impl WorldQuery for Spawned { type Fetch<'w> = SpawnedFetch<'w>; type State = (); @@ -1117,7 +1118,7 @@ unsafe impl WorldQuery for Spawned { #[inline] fn update_component_access(_state: &(), _access: &mut FilteredAccess) {} - fn init_state(_world: &mut World) -> () {} + fn init_state(_world: &mut World) {} fn get_state(_components: &Components) -> Option<()> { Some(()) From 2bfd9e3abf4c48cb6bd70b7a1dc0dd1a90466c35 Mon Sep 17 00:00:00 2001 From: Urben1680 Date: Sat, 3 May 2025 19:38:12 +0200 Subject: [PATCH 05/32] doc fixes --- crates/bevy_ecs/src/query/fetch.rs | 30 +++++++++++++++++++++++++++++ crates/bevy_ecs/src/query/filter.rs | 6 +++--- crates/bevy_ecs/src/system/query.rs | 2 ++ 3 files changed, 35 insertions(+), 3 deletions(-) diff --git a/crates/bevy_ecs/src/query/fetch.rs b/crates/bevy_ecs/src/query/fetch.rs index 49e994c1ab3e7..5cd0d6da735ff 100644 --- a/crates/bevy_ecs/src/query/fetch.rs +++ b/crates/bevy_ecs/src/query/fetch.rs @@ -473,6 +473,36 @@ unsafe impl QueryData for EntityLocation { unsafe impl ReadOnlyQueryData for EntityLocation {} /// The `SpawnedTick` query parameter fetches the [`Tick`] the entity was spawned at. +/// +/// To evaluate whether the spawn happened since the last time the system ran, the system +/// param [`SystemChangeTick`](bevy_ecs::system::SystemChangeTick) needs to be used. +/// +/// If the query should filter for spawned entities instead, use the +/// [`Spawned`](bevy_ecs::query::Spawned) query filter instead. +/// +/// # Examples +/// +/// ``` +/// # use bevy_ecs::entity::Entity; +/// # use bevy_ecs::system::Query; +/// # use bevy_ecs::system::SystemChangeTick; +/// # use bevy_ecs::query::Spawned; +/// # use bevy_ecs::query::SpawnedTick; +/// # +/// # #[derive(Component, Debug)] +/// # struct Name {}; +/// +/// fn print_spawn_ticks(query: Query<(Entity, SpawnedTick)>, system_ticks: SystemChangeTick) { +/// for (entity, spawned) in &q { +/// if spawned.is_newer_than(system_ticks.last_run(), system_ticks.this_run()) { +/// print!("new "); +/// } +/// println!("entity {entity:?} spawned at {spawned}"); +/// } +/// } +/// +/// # bevy_ecs::system::assert_is_system(print_spawning_entities); +/// ``` #[derive(Clone, Copy)] pub struct SpawnedTick(()); diff --git a/crates/bevy_ecs/src/query/filter.rs b/crates/bevy_ecs/src/query/filter.rs index f0feef70d6ebd..acfca9a984d50 100644 --- a/crates/bevy_ecs/src/query/filter.rs +++ b/crates/bevy_ecs/src/query/filter.rs @@ -1012,7 +1012,7 @@ unsafe impl QueryFilter for Changed { /// A common use for this filter is one-time initialization. /// /// To retain all results without filtering but still check whether they were spawned after the -/// system last ran, use [`todo`]. +/// system last ran, use [`SpawnedTick`](crate::query::SpawnedTick) instead. /// /// **Note** that this includes entities that spawned before the first time this Query was run. /// @@ -1078,7 +1078,7 @@ pub struct SpawnedFetch<'w> { this_run: Tick, } -// SAFETY: WorldQuery impl accesses no components +// SAFETY: WorldQuery impl accesses no components or component ticks unsafe impl WorldQuery for Spawned { type Fetch<'w> = SpawnedFetch<'w>; type State = (); @@ -1129,7 +1129,7 @@ unsafe impl WorldQuery for Spawned { } } -// SAFETY: WorldQuery impl performs only read access on ticks +// SAFETY: WorldQuery impl accesses no components or component ticks unsafe impl QueryFilter for Spawned { const IS_ARCHETYPAL: bool = false; diff --git a/crates/bevy_ecs/src/system/query.rs b/crates/bevy_ecs/src/system/query.rs index 4e5a7921e998b..bd8a44d8ca6a1 100644 --- a/crates/bevy_ecs/src/system/query.rs +++ b/crates/bevy_ecs/src/system/query.rs @@ -478,6 +478,8 @@ use core::{ /// # /// # bevy_ecs::system::assert_is_system(system); /// ``` +/// +/// [autovectorization]: https://en.wikipedia.org/wiki/Automatic_vectorization pub struct Query<'world, 'state, D: QueryData, F: QueryFilter = ()> { // SAFETY: Must have access to the components registered in `state`. world: UnsafeWorldCell<'world>, From 4883f192f7e4d2b0e17af20d696543f99bdd793e Mon Sep 17 00:00:00 2001 From: Urben1680 Date: Sat, 3 May 2025 19:40:15 +0200 Subject: [PATCH 06/32] typo fix --- crates/bevy_ecs/src/entity/mod.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/crates/bevy_ecs/src/entity/mod.rs b/crates/bevy_ecs/src/entity/mod.rs index bb03df31e1dfa..578da56dac3b1 100644 --- a/crates/bevy_ecs/src/entity/mod.rs +++ b/crates/bevy_ecs/src/entity/mod.rs @@ -1013,7 +1013,7 @@ impl Entities { .meta .get_mut(index as usize) .expect("Entity index invalid"); - meta.spawned_or_despawned = Some(SpawnedOrdDespawnedMeta { by, at }); + meta.spawned_or_despawned = Some(SpawnedOrDespawned { by, at }); } /// Returns the source code location from which this entity has last been spawned @@ -1041,7 +1041,7 @@ impl Entities { /// respawn. Returns `None` if its index has been reused by another entity or if /// this entity has never existed. #[inline] - fn entity_get_spawned_or_despawned(&self, entity: Entity) -> Option { + fn entity_get_spawned_or_despawned(&self, entity: Entity) -> Option { self.meta .get(entity.index() as usize) .filter(|meta| @@ -1121,11 +1121,11 @@ struct EntityMeta { /// The current location of the [`Entity`] pub location: EntityLocation, /// Location of the last spawn or despawn of this entity - spawned_or_despawned: Option, + spawned_or_despawned: Option, } #[derive(Copy, Clone, Debug)] -struct SpawnedOrdDespawnedMeta { +struct SpawnedOrDespawned { by: MaybeLocation, at: Tick, } From 5ff548c24385ed1c2250bd473a4a65cc911fabab Mon Sep 17 00:00:00 2001 From: Urben1680 Date: Sat, 3 May 2025 19:49:22 +0200 Subject: [PATCH 07/32] doc fixes --- crates/bevy_ecs/src/query/fetch.rs | 2 +- crates/bevy_ecs/src/query/filter.rs | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/crates/bevy_ecs/src/query/fetch.rs b/crates/bevy_ecs/src/query/fetch.rs index 5cd0d6da735ff..3f4de27a7df0f 100644 --- a/crates/bevy_ecs/src/query/fetch.rs +++ b/crates/bevy_ecs/src/query/fetch.rs @@ -501,7 +501,7 @@ unsafe impl ReadOnlyQueryData for EntityLocation {} /// } /// } /// -/// # bevy_ecs::system::assert_is_system(print_spawning_entities); +/// # bevy_ecs::system::assert_is_system(print_spawn_ticks); /// ``` #[derive(Clone, Copy)] pub struct SpawnedTick(()); diff --git a/crates/bevy_ecs/src/query/filter.rs b/crates/bevy_ecs/src/query/filter.rs index acfca9a984d50..7cc7e438afaa1 100644 --- a/crates/bevy_ecs/src/query/filter.rs +++ b/crates/bevy_ecs/src/query/filter.rs @@ -1036,12 +1036,12 @@ unsafe impl QueryFilter for Changed { /// # use bevy_ecs::query::Spawned; /// # use bevy_ecs::query::SpawnedTick; /// -/// fn system1(q: Query) { -/// for entity in &q { /* entity spawned */ } +/// fn system1(query: Query) { +/// for entity in &query { /* entity spawned */ } /// } /// -/// fn system2(q: Query<(Entity, SpawnedTick)>, system_ticks: SystemChangeTick) { -/// for (entity, spawned) in &q { +/// fn system2(query: Query<(Entity, SpawnedTick)>, system_ticks: SystemChangeTick) { +/// for (entity, spawned) in &query { /// if spawned.is_newer_than(system_ticks.last_run(), system_ticks.this_run()) { /// /* entity spawned */ /// } From 7f115f146e6420933bed41a548aba251be2bbcea Mon Sep 17 00:00:00 2001 From: Urben1680 Date: Sat, 3 May 2025 19:57:25 +0200 Subject: [PATCH 08/32] doc fixes --- crates/bevy_ecs/src/query/fetch.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/bevy_ecs/src/query/fetch.rs b/crates/bevy_ecs/src/query/fetch.rs index 3f4de27a7df0f..51fd914c7cd93 100644 --- a/crates/bevy_ecs/src/query/fetch.rs +++ b/crates/bevy_ecs/src/query/fetch.rs @@ -493,7 +493,7 @@ unsafe impl ReadOnlyQueryData for EntityLocation {} /// # struct Name {}; /// /// fn print_spawn_ticks(query: Query<(Entity, SpawnedTick)>, system_ticks: SystemChangeTick) { -/// for (entity, spawned) in &q { +/// for (entity, spawned) in &query { /// if spawned.is_newer_than(system_ticks.last_run(), system_ticks.this_run()) { /// print!("new "); /// } From 911ad78f7e647461ef762e61944cd649568e789c Mon Sep 17 00:00:00 2001 From: Urben1680 Date: Sat, 3 May 2025 20:03:47 +0200 Subject: [PATCH 09/32] doc fixes --- crates/bevy_ecs/src/query/fetch.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/bevy_ecs/src/query/fetch.rs b/crates/bevy_ecs/src/query/fetch.rs index 51fd914c7cd93..bd85003e69dab 100644 --- a/crates/bevy_ecs/src/query/fetch.rs +++ b/crates/bevy_ecs/src/query/fetch.rs @@ -497,7 +497,7 @@ unsafe impl ReadOnlyQueryData for EntityLocation {} /// if spawned.is_newer_than(system_ticks.last_run(), system_ticks.this_run()) { /// print!("new "); /// } -/// println!("entity {entity:?} spawned at {spawned}"); +/// println!("entity {entity:?} spawned at {spawned:?}"); /// } /// } /// From 19c23aa294d663af9933302a2529f921000c50cb Mon Sep 17 00:00:00 2001 From: Urben1680 Date: Sat, 3 May 2025 20:13:40 +0200 Subject: [PATCH 10/32] doc fixes --- crates/bevy_ecs/src/query/fetch.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/bevy_ecs/src/query/fetch.rs b/crates/bevy_ecs/src/query/fetch.rs index bd85003e69dab..9a06974ae4962 100644 --- a/crates/bevy_ecs/src/query/fetch.rs +++ b/crates/bevy_ecs/src/query/fetch.rs @@ -483,6 +483,7 @@ unsafe impl ReadOnlyQueryData for EntityLocation {} /// # Examples /// /// ``` +/// # use bevy_ecs::component::Component; /// # use bevy_ecs::entity::Entity; /// # use bevy_ecs::system::Query; /// # use bevy_ecs::system::SystemChangeTick; From b479adf37db8f2ce2b0df221cb3c5ebffbf83386 Mon Sep 17 00:00:00 2001 From: Urben1680 Date: Sat, 3 May 2025 20:26:16 +0200 Subject: [PATCH 11/32] doc fixes --- crates/bevy_ecs/src/entity/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/bevy_ecs/src/entity/mod.rs b/crates/bevy_ecs/src/entity/mod.rs index 578da56dac3b1..5c304b5f0a1f5 100644 --- a/crates/bevy_ecs/src/entity/mod.rs +++ b/crates/bevy_ecs/src/entity/mod.rs @@ -1037,7 +1037,7 @@ impl Entities { .map(|spawned_or_despawned| spawned_or_despawned.at) } - /// Returns the [`SpawnedOrdDespawnedMeta`] related to the entity's las spawn or + /// Returns the [`SpawnedOrDespawned`] related to the entity's las spawn or /// respawn. Returns `None` if its index has been reused by another entity or if /// this entity has never existed. #[inline] From 088a2a4c42f0b832b9214658105d8b5468319096 Mon Sep 17 00:00:00 2001 From: urben1680 <55257931+urben1680@users.noreply.github.com> Date: Sat, 3 May 2025 23:17:49 +0200 Subject: [PATCH 12/32] Update crates/bevy_ecs/src/query/filter.rs Co-authored-by: Eagster <79881080+ElliottjPierce@users.noreply.github.com> --- crates/bevy_ecs/src/query/filter.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/bevy_ecs/src/query/filter.rs b/crates/bevy_ecs/src/query/filter.rs index 7cc7e438afaa1..842aa5c289ec3 100644 --- a/crates/bevy_ecs/src/query/filter.rs +++ b/crates/bevy_ecs/src/query/filter.rs @@ -1068,7 +1068,7 @@ unsafe impl QueryFilter for Changed { /// /// # bevy_ecs::system::assert_is_system(print_spawning_entities); /// ``` -pub struct Spawned(()); +pub struct Spawned; #[doc(hidden)] #[derive(Clone)] From 96e4a4ae9b08f9c0988885d0d86249c2fffa72f5 Mon Sep 17 00:00:00 2001 From: urben1680 <55257931+urben1680@users.noreply.github.com> Date: Sat, 3 May 2025 23:18:00 +0200 Subject: [PATCH 13/32] Update crates/bevy_ecs/src/query/fetch.rs Co-authored-by: Eagster <79881080+ElliottjPierce@users.noreply.github.com> --- crates/bevy_ecs/src/query/fetch.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/bevy_ecs/src/query/fetch.rs b/crates/bevy_ecs/src/query/fetch.rs index 9a06974ae4962..aea5e15b75709 100644 --- a/crates/bevy_ecs/src/query/fetch.rs +++ b/crates/bevy_ecs/src/query/fetch.rs @@ -505,7 +505,7 @@ unsafe impl ReadOnlyQueryData for EntityLocation {} /// # bevy_ecs::system::assert_is_system(print_spawn_ticks); /// ``` #[derive(Clone, Copy)] -pub struct SpawnedTick(()); +pub struct SpawnedTick; // SAFETY: // No components are accessed. From 44cbd933b76117797a5c23f05c57a6c612a3b4a7 Mon Sep 17 00:00:00 2001 From: Urben1680 Date: Sun, 4 May 2025 01:18:02 +0200 Subject: [PATCH 14/32] SpawnedTick/Spawned using unchecked tick read --- crates/bevy_ecs/src/entity/mod.rs | 14 ++++++++++++++ crates/bevy_ecs/src/query/fetch.rs | 5 ++--- crates/bevy_ecs/src/query/filter.rs | 9 ++++++--- 3 files changed, 22 insertions(+), 6 deletions(-) diff --git a/crates/bevy_ecs/src/entity/mod.rs b/crates/bevy_ecs/src/entity/mod.rs index 5c304b5f0a1f5..7cc3fd6f795f8 100644 --- a/crates/bevy_ecs/src/entity/mod.rs +++ b/crates/bevy_ecs/src/entity/mod.rs @@ -1037,6 +1037,20 @@ impl Entities { .map(|spawned_or_despawned| spawned_or_despawned.at) } + /// Returns the [`Tick`] at which this entity has last been spawned or despawned. + /// + /// # Safety + /// + /// The given entity must be alive or must have been alive without being reused. + pub unsafe fn entity_get_spawned_or_despawned_at_unchecked(&self, entity: Entity) -> Tick { + let meta = self.meta.get(entity.index() as usize); + // SAFETY: user ensured entity is allocated and generation is valid + let meta = unsafe { meta.unwrap_unchecked() }; + // SAFETY: user ensured entity is either spawned or despawned + let spawned_or_despawned = unsafe { meta.spawned_or_despawned.unwrap_unchecked() }; + spawned_or_despawned.at + } + /// Returns the [`SpawnedOrDespawned`] related to the entity's las spawn or /// respawn. Returns `None` if its index has been reused by another entity or if /// this entity has never existed. diff --git a/crates/bevy_ecs/src/query/fetch.rs b/crates/bevy_ecs/src/query/fetch.rs index aea5e15b75709..88ed04ac785da 100644 --- a/crates/bevy_ecs/src/query/fetch.rs +++ b/crates/bevy_ecs/src/query/fetch.rs @@ -575,9 +575,8 @@ unsafe impl QueryData for SpawnedTick { entity: Entity, _table_row: TableRow, ) -> Self::Item<'w> { - let spawned = fetch.entity_get_spawned_or_despawned_at(entity); - // SAFETY: queried entity must have a spawned tick - unsafe { spawned.debug_checked_unwrap() } + // SAFETY: only living entities are queried + unsafe { fetch.entity_get_spawned_or_despawned_at_unchecked(entity) } } } diff --git a/crates/bevy_ecs/src/query/filter.rs b/crates/bevy_ecs/src/query/filter.rs index 842aa5c289ec3..dd8e7dac89aba 100644 --- a/crates/bevy_ecs/src/query/filter.rs +++ b/crates/bevy_ecs/src/query/filter.rs @@ -1139,9 +1139,12 @@ unsafe impl QueryFilter for Spawned { entity: Entity, _table_row: TableRow, ) -> bool { - let spawned = fetch.entities.entity_get_spawned_or_despawned_at(entity); - // SAFETY: queried entity must have a spawned tick - let spawned = unsafe { spawned.debug_checked_unwrap() }; + let spawned = unsafe { + // SAFETY: only living entities are queried + fetch + .entities + .entity_get_spawned_or_despawned_at_unchecked(entity) + }; spawned.is_newer_than(fetch.last_run, fetch.this_run) } } From 8c55d76941a12de38b12885977e7a64231d99741 Mon Sep 17 00:00:00 2001 From: Urben1680 Date: Sun, 4 May 2025 01:32:03 +0200 Subject: [PATCH 15/32] using unsafe variant for entity pointer types --- crates/bevy_ecs/src/world/entity_ref.rs | 32 ++++++++++++++++--- .../bevy_ecs/src/world/unsafe_world_cell.rs | 10 +++--- 2 files changed, 34 insertions(+), 8 deletions(-) diff --git a/crates/bevy_ecs/src/world/entity_ref.rs b/crates/bevy_ecs/src/world/entity_ref.rs index 313c833afd48d..7b6cee806141a 100644 --- a/crates/bevy_ecs/src/world/entity_ref.rs +++ b/crates/bevy_ecs/src/world/entity_ref.rs @@ -2966,10 +2966,14 @@ impl<'w> EntityWorldMut<'w> { /// Returns the [`Tick`] at which this entity has last been spawned. pub fn spawned_at(&self) -> Tick { - self.world() - .entities() - .entity_get_spawned_or_despawned_at(self.entity) - .unwrap() + self.assert_not_despawned(); + + unsafe { + // SAFETY: entity being alive was asserted + self.world() + .entities() + .entity_get_spawned_or_despawned_at_unchecked(self.entity) + } } } @@ -3525,6 +3529,11 @@ impl<'w> FilteredEntityRef<'w> { pub fn spawned_by(&self) -> MaybeLocation { self.entity.spawned_by() } + + /// Returns the [`Tick`] at which this entity has been spawned. + pub fn spawned_at(&self) -> Tick { + self.entity.spawned_at() + } } impl<'w> From> for FilteredEntityRef<'w> { @@ -3906,6 +3915,11 @@ impl<'w> FilteredEntityMut<'w> { pub fn spawned_by(&self) -> MaybeLocation { self.entity.spawned_by() } + + /// Returns the [`Tick`] at which this entity has been spawned. + pub fn spawned_at(&self) -> Tick { + self.entity.spawned_at() + } } impl<'a> From> for FilteredEntityMut<'a> { @@ -4104,6 +4118,11 @@ where self.entity.spawned_by() } + /// Returns the [`Tick`] at which this entity has been spawned. + pub fn spawned_at(&self) -> Tick { + self.entity.spawned_at() + } + /// Gets the component of the given [`ComponentId`] from the entity. /// /// **You should prefer to use the typed API [`Self::get`] where possible and only @@ -4348,6 +4367,11 @@ where self.entity.spawned_by() } + /// Returns the [`Tick`] at which this entity has been spawned. + pub fn spawned_at(&self) -> Tick { + self.entity.spawned_at() + } + /// Returns `true` if the current entity has a component of type `T`. /// Otherwise, this returns `false`. /// diff --git a/crates/bevy_ecs/src/world/unsafe_world_cell.rs b/crates/bevy_ecs/src/world/unsafe_world_cell.rs index 8dd35ca375734..34079d36a31ba 100644 --- a/crates/bevy_ecs/src/world/unsafe_world_cell.rs +++ b/crates/bevy_ecs/src/world/unsafe_world_cell.rs @@ -1139,10 +1139,12 @@ impl<'w> UnsafeEntityCell<'w> { /// Returns the [`Tick`] at which this entity has been spawned. pub fn spawned_at(self) -> Tick { - self.world() - .entities() - .entity_get_spawned_or_despawned_at(self.entity) - .unwrap() + unsafe { + // SAFETY: UnsafeEntityCell is only constructed for living entities and offers no despawn method + self.world() + .entities() + .entity_get_spawned_or_despawned_at_unchecked(self.entity) + } } } From 632976d65a15c053d82042cec4d86bfafcfab41f Mon Sep 17 00:00:00 2001 From: Urben1680 Date: Sun, 4 May 2025 01:38:21 +0200 Subject: [PATCH 16/32] CI fix --- crates/bevy_ecs/src/query/filter.rs | 2 +- crates/bevy_ecs/src/world/entity_ref.rs | 2 +- crates/bevy_ecs/src/world/unsafe_world_cell.rs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/bevy_ecs/src/query/filter.rs b/crates/bevy_ecs/src/query/filter.rs index dd8e7dac89aba..638a6eff0c993 100644 --- a/crates/bevy_ecs/src/query/filter.rs +++ b/crates/bevy_ecs/src/query/filter.rs @@ -1139,8 +1139,8 @@ unsafe impl QueryFilter for Spawned { entity: Entity, _table_row: TableRow, ) -> bool { + // SAFETY: only living entities are queried let spawned = unsafe { - // SAFETY: only living entities are queried fetch .entities .entity_get_spawned_or_despawned_at_unchecked(entity) diff --git a/crates/bevy_ecs/src/world/entity_ref.rs b/crates/bevy_ecs/src/world/entity_ref.rs index 7b6cee806141a..3af5d6753e75a 100644 --- a/crates/bevy_ecs/src/world/entity_ref.rs +++ b/crates/bevy_ecs/src/world/entity_ref.rs @@ -2968,8 +2968,8 @@ impl<'w> EntityWorldMut<'w> { pub fn spawned_at(&self) -> Tick { self.assert_not_despawned(); + // SAFETY: entity being alive was asserted unsafe { - // SAFETY: entity being alive was asserted self.world() .entities() .entity_get_spawned_or_despawned_at_unchecked(self.entity) diff --git a/crates/bevy_ecs/src/world/unsafe_world_cell.rs b/crates/bevy_ecs/src/world/unsafe_world_cell.rs index 34079d36a31ba..02fbde92b1704 100644 --- a/crates/bevy_ecs/src/world/unsafe_world_cell.rs +++ b/crates/bevy_ecs/src/world/unsafe_world_cell.rs @@ -1139,8 +1139,8 @@ impl<'w> UnsafeEntityCell<'w> { /// Returns the [`Tick`] at which this entity has been spawned. pub fn spawned_at(self) -> Tick { + // SAFETY: UnsafeEntityCell is only constructed for living entities and offers no despawn method unsafe { - // SAFETY: UnsafeEntityCell is only constructed for living entities and offers no despawn method self.world() .entities() .entity_get_spawned_or_despawned_at_unchecked(self.entity) From 77af9f480e7c0d1a11e8659db7521929efc93bac Mon Sep 17 00:00:00 2001 From: Urben1680 Date: Sun, 4 May 2025 13:37:25 +0200 Subject: [PATCH 17/32] remove Entities::set_spawned_or_despawned_by, introduce Entities::set_spawn_despawn as a variant of Entities::set --- crates/bevy_ecs/src/bundle.rs | 2 +- crates/bevy_ecs/src/entity/mod.rs | 31 +++++++++++++++---------- crates/bevy_ecs/src/world/entity_ref.rs | 17 ++++++-------- crates/bevy_ecs/src/world/mod.rs | 8 +------ 4 files changed, 28 insertions(+), 30 deletions(-) diff --git a/crates/bevy_ecs/src/bundle.rs b/crates/bevy_ecs/src/bundle.rs index 5666d90c53a42..d4635ab0ad210 100644 --- a/crates/bevy_ecs/src/bundle.rs +++ b/crates/bevy_ecs/src/bundle.rs @@ -1455,7 +1455,7 @@ impl<'w> BundleSpawner<'w> { InsertMode::Replace, caller, ); - entities.set(entity.index(), location); + entities.set_spawn_despawn(entity.index(), location, caller, self.change_tick); (location, after_effect) }; diff --git a/crates/bevy_ecs/src/entity/mod.rs b/crates/bevy_ecs/src/entity/mod.rs index 7cc3fd6f795f8..24ccf3ca27d52 100644 --- a/crates/bevy_ecs/src/entity/mod.rs +++ b/crates/bevy_ecs/src/entity/mod.rs @@ -854,7 +854,10 @@ impl Entities { } /// Updates the location of an [`Entity`]. This must be called when moving the components of - /// the entity around in storage. + /// the existing entity around in storage. + /// + /// For spawning and despawning entities, [`set_spawn_despawn`](Self::set_spawn_despawn) must + /// be used instead. /// /// # Safety /// - `index` must be a valid entity index. @@ -867,6 +870,21 @@ impl Entities { meta.location = location; } + /// Updates the location of an [`Entity`]. This must be called when moving the components of + /// the spawned or despawned entity around in storage. + /// + /// # Safety + /// - `index` must be a valid entity index. + /// - `location` must be valid for the entity at `index` or immediately made valid afterwards + /// before handing control to unknown code. + #[inline] + pub(crate) unsafe fn set_spawn_despawn(&mut self, index: u32, location: EntityLocation, by: MaybeLocation, at: Tick) { + // SAFETY: Caller guarantees that `index` a valid entity index + let meta = unsafe { self.meta.get_unchecked_mut(index as usize) }; + meta.location = location; + meta.spawned_or_despawned = Some(SpawnedOrDespawned { by, at }); + } + /// Increments the `generation` of a freed [`Entity`]. The next entity ID allocated with this /// `index` will count `generation` starting from the prior `generation` + the specified /// value + 1. @@ -1005,17 +1023,6 @@ impl Entities { self.len() == 0 } - /// Sets the source code location from which this entity has last been spawned - /// or despawned. - #[inline] - pub(crate) fn set_spawned_or_despawned(&mut self, index: u32, by: MaybeLocation, at: Tick) { - let meta = self - .meta - .get_mut(index as usize) - .expect("Entity index invalid"); - meta.spawned_or_despawned = Some(SpawnedOrDespawned { by, at }); - } - /// Returns the source code location from which this entity has last been spawned /// or despawned. Returns `None` if its index has been reused by another entity /// or if this entity has never existed. diff --git a/crates/bevy_ecs/src/world/entity_ref.rs b/crates/bevy_ecs/src/world/entity_ref.rs index 3af5d6753e75a..753d1120bda48 100644 --- a/crates/bevy_ecs/src/world/entity_ref.rs +++ b/crates/bevy_ecs/src/world/entity_ref.rs @@ -2565,6 +2565,7 @@ impl<'w> EntityWorldMut<'w> { .expect("entity should exist at this point."); let table_row; let moved_entity; + let change_tick = world.change_tick(); { let archetype = &mut world.archetypes[self.location.archetype_id]; @@ -2574,7 +2575,7 @@ impl<'w> EntityWorldMut<'w> { // SAFETY: swapped_entity is valid and the swapped entity's components are // moved to the new location immediately after. unsafe { - world.entities.set( + world.entities.set_spawn_despawn( swapped_entity.index(), EntityLocation { archetype_id: swapped_location.archetype_id, @@ -2582,6 +2583,8 @@ impl<'w> EntityWorldMut<'w> { table_id: swapped_location.table_id, table_row: swapped_location.table_row, }, + caller, + change_tick ); } } @@ -2603,7 +2606,7 @@ impl<'w> EntityWorldMut<'w> { // SAFETY: `moved_entity` is valid and the provided `EntityLocation` accurately reflects // the current location of the entity and its component data. unsafe { - world.entities.set( + world.entities.set_spawn_despawn( moved_entity.index(), EntityLocation { archetype_id: moved_location.archetype_id, @@ -2611,20 +2614,14 @@ impl<'w> EntityWorldMut<'w> { table_id: moved_location.table_id, table_row, }, + caller, + change_tick ); } world.archetypes[moved_location.archetype_id] .set_entity_table_row(moved_location.archetype_row, table_row); } world.flush(); - let change_tick = world.change_tick(); - - // SAFETY: No structural changes - unsafe { - world - .entities_mut() - .set_spawned_or_despawned(self.entity.index(), caller, change_tick); - } } /// Ensures any commands triggered by the actions of Self are applied, equivalent to [`World::flush`] diff --git a/crates/bevy_ecs/src/world/mod.rs b/crates/bevy_ecs/src/world/mod.rs index 5d09302c646fb..733c092e59474 100644 --- a/crates/bevy_ecs/src/world/mod.rs +++ b/crates/bevy_ecs/src/world/mod.rs @@ -1181,9 +1181,6 @@ impl World { .unwrap_or(EntityLocation::INVALID); } - self.entities - .set_spawned_or_despawned(entity.index(), caller, change_tick); - // SAFETY: entity and location are valid, as they were just created above let mut entity = unsafe { EntityWorldMut::new(self, entity, entity_location) }; after_effect.apply(&mut entity); @@ -1203,11 +1200,8 @@ impl World { // SAFETY: no components are allocated by archetype.allocate() because the archetype is // empty let location = unsafe { archetype.allocate(entity, table_row) }; - self.entities.set(entity.index(), location); - let change_tick = self.change_tick(); - self.entities - .set_spawned_or_despawned(entity.index(), caller, change_tick); + self.entities.set_spawn_despawn(entity.index(), location, caller, change_tick); EntityWorldMut::new(self, entity, location) } From c0fec57c0878feb0a9862699d2df5ca356f3471b Mon Sep 17 00:00:00 2001 From: Urben1680 Date: Sun, 4 May 2025 13:39:52 +0200 Subject: [PATCH 18/32] fmt --- crates/bevy_ecs/src/entity/mod.rs | 10 ++++++++-- crates/bevy_ecs/src/world/entity_ref.rs | 4 ++-- crates/bevy_ecs/src/world/mod.rs | 3 ++- 3 files changed, 12 insertions(+), 5 deletions(-) diff --git a/crates/bevy_ecs/src/entity/mod.rs b/crates/bevy_ecs/src/entity/mod.rs index 24ccf3ca27d52..24e8ab72ae3d3 100644 --- a/crates/bevy_ecs/src/entity/mod.rs +++ b/crates/bevy_ecs/src/entity/mod.rs @@ -855,7 +855,7 @@ impl Entities { /// Updates the location of an [`Entity`]. This must be called when moving the components of /// the existing entity around in storage. - /// + /// /// For spawning and despawning entities, [`set_spawn_despawn`](Self::set_spawn_despawn) must /// be used instead. /// @@ -878,7 +878,13 @@ impl Entities { /// - `location` must be valid for the entity at `index` or immediately made valid afterwards /// before handing control to unknown code. #[inline] - pub(crate) unsafe fn set_spawn_despawn(&mut self, index: u32, location: EntityLocation, by: MaybeLocation, at: Tick) { + pub(crate) unsafe fn set_spawn_despawn( + &mut self, + index: u32, + location: EntityLocation, + by: MaybeLocation, + at: Tick, + ) { // SAFETY: Caller guarantees that `index` a valid entity index let meta = unsafe { self.meta.get_unchecked_mut(index as usize) }; meta.location = location; diff --git a/crates/bevy_ecs/src/world/entity_ref.rs b/crates/bevy_ecs/src/world/entity_ref.rs index 753d1120bda48..e27177c4fe1e5 100644 --- a/crates/bevy_ecs/src/world/entity_ref.rs +++ b/crates/bevy_ecs/src/world/entity_ref.rs @@ -2584,7 +2584,7 @@ impl<'w> EntityWorldMut<'w> { table_row: swapped_location.table_row, }, caller, - change_tick + change_tick, ); } } @@ -2615,7 +2615,7 @@ impl<'w> EntityWorldMut<'w> { table_row, }, caller, - change_tick + change_tick, ); } world.archetypes[moved_location.archetype_id] diff --git a/crates/bevy_ecs/src/world/mod.rs b/crates/bevy_ecs/src/world/mod.rs index 733c092e59474..25392139c43c1 100644 --- a/crates/bevy_ecs/src/world/mod.rs +++ b/crates/bevy_ecs/src/world/mod.rs @@ -1201,7 +1201,8 @@ impl World { // empty let location = unsafe { archetype.allocate(entity, table_row) }; let change_tick = self.change_tick(); - self.entities.set_spawn_despawn(entity.index(), location, caller, change_tick); + self.entities + .set_spawn_despawn(entity.index(), location, caller, change_tick); EntityWorldMut::new(self, entity, location) } From abe17b702274878f24fb84d90870e5aa35f86702 Mon Sep 17 00:00:00 2001 From: Urben1680 Date: Sun, 4 May 2025 22:45:12 +0200 Subject: [PATCH 19/32] variant with separate Tick vector in Entities --- crates/bevy_ecs/src/entity/mod.rs | 103 +++++++++++++++++++++--------- 1 file changed, 74 insertions(+), 29 deletions(-) diff --git a/crates/bevy_ecs/src/entity/mod.rs b/crates/bevy_ecs/src/entity/mod.rs index 24e8ab72ae3d3..cca531199a3c9 100644 --- a/crates/bevy_ecs/src/entity/mod.rs +++ b/crates/bevy_ecs/src/entity/mod.rs @@ -85,7 +85,13 @@ use crate::{ }; use alloc::vec::Vec; use bevy_platform::sync::atomic::Ordering; -use core::{fmt, hash::Hash, mem, num::NonZero, panic::Location}; +use core::{ + fmt, + hash::Hash, + mem::{self, MaybeUninit}, + num::NonZero, + panic::Location, +}; use log::warn; #[cfg(feature = "serialize")] @@ -539,6 +545,15 @@ unsafe impl EntitySetIterator for ReserveEntitiesIterator<'_> {} pub struct Entities { meta: Vec, + /// The `Tick` the entity of the same index did spawn or despawn at. + /// + /// Has the same length as [`Self::meta`]. + /// + /// Value is init if the entity of this index had a valid archetype at some point. + /// + /// Value is uninit if entity was merely allocated yet. + spawned_or_despawned_at: Vec>, + /// The `pending` and `free_cursor` fields describe three sets of Entity IDs /// that have been freed or are in the process of being allocated: /// @@ -587,6 +602,7 @@ impl Entities { pub(crate) const fn new() -> Self { Entities { meta: Vec::new(), + spawned_or_despawned_at: Vec::new(), pending: Vec::new(), free_cursor: AtomicIdCursor::new(0), } @@ -687,6 +703,7 @@ impl Entities { } else { let index = u32::try_from(self.meta.len()).expect("too many entities"); self.meta.push(EntityMeta::EMPTY); + self.spawned_or_despawned_at.push(MaybeUninit::uninit()); Entity::from_raw(index) } } @@ -818,7 +835,9 @@ impl Entities { .expect("64-bit atomic operations are not supported on this platform.") - freelist_size; if shortfall > 0 { - self.meta.reserve(shortfall as usize); + let additional = shortfall as usize; + self.meta.reserve(additional); + self.spawned_or_despawned_at.reserve(additional); } } @@ -833,6 +852,7 @@ impl Entities { /// Clears all [`Entity`] from the World. pub fn clear(&mut self) { self.meta.clear(); + self.spawned_or_despawned_at.clear(); self.pending.clear(); *self.free_cursor.get_mut() = 0; } @@ -888,7 +908,14 @@ impl Entities { // SAFETY: Caller guarantees that `index` a valid entity index let meta = unsafe { self.meta.get_unchecked_mut(index as usize) }; meta.location = location; - meta.spawned_or_despawned = Some(SpawnedOrDespawned { by, at }); + by.map(|by| meta.spawned_or_despawned_by = MaybeLocation::new(Some(by))); + + // SAFETY: Caller guarantees that `index` a valid entity index + let spawned_or_despawned_at = unsafe { + self.spawned_or_despawned_at + .get_unchecked_mut(index as usize) + }; + *spawned_or_despawned_at = MaybeUninit::new(at); } /// Increments the `generation` of a freed [`Entity`]. The next entity ID allocated with this @@ -954,6 +981,8 @@ impl Entities { let old_meta_len = self.meta.len(); let new_meta_len = old_meta_len + -current_free_cursor as usize; self.meta.resize(new_meta_len, EntityMeta::EMPTY); + self.spawned_or_despawned_at + .resize(new_meta_len, MaybeUninit::uninit()); for (index, meta) in self.meta.iter_mut().enumerate().skip(old_meta_len) { init( Entity::from_raw_and_generation(index as u32, meta.generation), @@ -1037,8 +1066,8 @@ impl Entities { entity: Entity, ) -> MaybeLocation>> { MaybeLocation::new_with_flattened(|| { - self.entity_get_spawned_or_despawned(entity) - .map(|spawned_or_despawned| spawned_or_despawned.by) + self.get_entity_meta(entity) + .map(|meta| meta.spawned_or_despawned_by.map(|l| l.unwrap())) }) } @@ -1046,8 +1075,14 @@ impl Entities { /// Returns `None` if its index has been reused by another entity or if this entity /// has never existed. pub fn entity_get_spawned_or_despawned_at(&self, entity: Entity) -> Option { - self.entity_get_spawned_or_despawned(entity) - .map(|spawned_or_despawned| spawned_or_despawned.at) + self.get_entity_meta(entity).is_some().then(|| { + unsafe { + // SAFETY: + // self.meta and self.spawned_or_despawned_at have the same length + // get_entity_meta returning Some: index is in range, spawn/despawn tick is init + self.entity_get_spawned_or_despawned_at_unchecked(entity) + } + }) } /// Returns the [`Tick`] at which this entity has last been spawned or despawned. @@ -1056,34 +1091,50 @@ impl Entities { /// /// The given entity must be alive or must have been alive without being reused. pub unsafe fn entity_get_spawned_or_despawned_at_unchecked(&self, entity: Entity) -> Tick { - let meta = self.meta.get(entity.index() as usize); - // SAFETY: user ensured entity is allocated and generation is valid - let meta = unsafe { meta.unwrap_unchecked() }; - // SAFETY: user ensured entity is either spawned or despawned - let spawned_or_despawned = unsafe { meta.spawned_or_despawned.unwrap_unchecked() }; - spawned_or_despawned.at + let spawned_or_despawned_at = unsafe { + // SAFETY: + // user ensured the entity is alive or despawned while not being overridden + self.spawned_or_despawned_at + .get_unchecked(entity.index() as usize) + }; + unsafe { + // SAFETY: + // user ensured the entity is alive or despawned while not being overridden + MaybeUninit::assume_init(*spawned_or_despawned_at) + } } - /// Returns the [`SpawnedOrDespawned`] related to the entity's las spawn or - /// respawn. Returns `None` if its index has been reused by another entity or if - /// this entity has never existed. + /// todo #[inline] - fn entity_get_spawned_or_despawned(&self, entity: Entity) -> Option { + fn get_entity_meta(&self, entity: Entity) -> Option { self.meta .get(entity.index() as usize) + .copied() .filter(|meta| // Generation is incremented immediately upon despawn (meta.generation == entity.generation) || (meta.location.archetype_id == ArchetypeId::INVALID) && (meta.generation == IdentifierMask::inc_masked_high_by(entity.generation, 1))) - .and_then(|meta| meta.spawned_or_despawned) } #[inline] pub(crate) fn check_change_ticks(&mut self, change_tick: Tick) { - for meta in &mut self.meta { - if let Some(spawned_or_despawned) = &mut meta.spawned_or_despawned { - spawned_or_despawned.at.check_tick(change_tick); + for (index, meta) in self.meta.iter_mut().enumerate() { + if meta.generation > NonZero::::MIN + || meta.location.archetype_id != ArchetypeId::INVALID + { + let spawned_or_despawned_at = unsafe { + // SAFETY: + // self.spawned_or_despawned_at has the same length as self.meta + self.spawned_or_despawned_at.get_unchecked_mut(index) + }; + let spawned_or_despawned_at = unsafe { + // SAFETY: + // increased generation indicates the entity was once spawned OR + // valid archetype means the entity was spawned + MaybeUninit::assume_init_mut(spawned_or_despawned_at) + }; + spawned_or_despawned_at.check_tick(change_tick); } } } @@ -1148,13 +1199,7 @@ struct EntityMeta { /// The current location of the [`Entity`] pub location: EntityLocation, /// Location of the last spawn or despawn of this entity - spawned_or_despawned: Option, -} - -#[derive(Copy, Clone, Debug)] -struct SpawnedOrDespawned { - by: MaybeLocation, - at: Tick, + spawned_or_despawned_by: MaybeLocation>>, } impl EntityMeta { @@ -1162,7 +1207,7 @@ impl EntityMeta { const EMPTY: EntityMeta = EntityMeta { generation: NonZero::::MIN, location: EntityLocation::INVALID, - spawned_or_despawned: None, + spawned_or_despawned_by: MaybeLocation::new(None), }; } From 8c55e74aaacf1b2fe9c7f0d6542766ba71f9536b Mon Sep 17 00:00:00 2001 From: Urben1680 Date: Sun, 4 May 2025 22:52:27 +0200 Subject: [PATCH 20/32] formatting --- crates/bevy_ecs/src/entity/mod.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/crates/bevy_ecs/src/entity/mod.rs b/crates/bevy_ecs/src/entity/mod.rs index cca531199a3c9..77bf1edb217ff 100644 --- a/crates/bevy_ecs/src/entity/mod.rs +++ b/crates/bevy_ecs/src/entity/mod.rs @@ -550,7 +550,7 @@ pub struct Entities { /// Has the same length as [`Self::meta`]. /// /// Value is init if the entity of this index had a valid archetype at some point. - /// + /// /// Value is uninit if entity was merely allocated yet. spawned_or_despawned_at: Vec>, @@ -1104,7 +1104,8 @@ impl Entities { } } - /// todo + /// Returns the [`EntityMeta`] for the given entity if the entity is alive or if + /// it was despawned without being overwritten. #[inline] fn get_entity_meta(&self, entity: Entity) -> Option { self.meta From be76c89a0d5350b7e6e74f72f22c4fbd2208e083 Mon Sep 17 00:00:00 2001 From: Urben1680 Date: Sun, 4 May 2025 22:55:30 +0200 Subject: [PATCH 21/32] formatting --- crates/bevy_ecs/src/entity/mod.rs | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/crates/bevy_ecs/src/entity/mod.rs b/crates/bevy_ecs/src/entity/mod.rs index 77bf1edb217ff..be9d9e196092f 100644 --- a/crates/bevy_ecs/src/entity/mod.rs +++ b/crates/bevy_ecs/src/entity/mod.rs @@ -1076,10 +1076,10 @@ impl Entities { /// has never existed. pub fn entity_get_spawned_or_despawned_at(&self, entity: Entity) -> Option { self.get_entity_meta(entity).is_some().then(|| { + // SAFETY: + // self.meta and self.spawned_or_despawned_at have the same length + // get_entity_meta returning Some: index is in range, spawn/despawn tick is init unsafe { - // SAFETY: - // self.meta and self.spawned_or_despawned_at have the same length - // get_entity_meta returning Some: index is in range, spawn/despawn tick is init self.entity_get_spawned_or_despawned_at_unchecked(entity) } }) @@ -1091,15 +1091,15 @@ impl Entities { /// /// The given entity must be alive or must have been alive without being reused. pub unsafe fn entity_get_spawned_or_despawned_at_unchecked(&self, entity: Entity) -> Tick { + // SAFETY: + // user ensured the entity is alive or despawned while not being overridden let spawned_or_despawned_at = unsafe { - // SAFETY: - // user ensured the entity is alive or despawned while not being overridden self.spawned_or_despawned_at .get_unchecked(entity.index() as usize) }; + // SAFETY: + // user ensured the entity is alive or despawned while not being overridden unsafe { - // SAFETY: - // user ensured the entity is alive or despawned while not being overridden MaybeUninit::assume_init(*spawned_or_despawned_at) } } @@ -1124,15 +1124,15 @@ impl Entities { if meta.generation > NonZero::::MIN || meta.location.archetype_id != ArchetypeId::INVALID { + // SAFETY: + // self.spawned_or_despawned_at has the same length as self.meta let spawned_or_despawned_at = unsafe { - // SAFETY: - // self.spawned_or_despawned_at has the same length as self.meta self.spawned_or_despawned_at.get_unchecked_mut(index) }; + // SAFETY: + // increased generation indicates the entity was once spawned OR + // valid archetype means the entity was spawned let spawned_or_despawned_at = unsafe { - // SAFETY: - // increased generation indicates the entity was once spawned OR - // valid archetype means the entity was spawned MaybeUninit::assume_init_mut(spawned_or_despawned_at) }; spawned_or_despawned_at.check_tick(change_tick); From 7f10ae0b38e68b427d0f6e3c41158900eaa7b6df Mon Sep 17 00:00:00 2001 From: Urben1680 Date: Sun, 4 May 2025 22:56:24 +0200 Subject: [PATCH 22/32] formatting --- crates/bevy_ecs/src/entity/mod.rs | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/crates/bevy_ecs/src/entity/mod.rs b/crates/bevy_ecs/src/entity/mod.rs index be9d9e196092f..21e406b7e37e2 100644 --- a/crates/bevy_ecs/src/entity/mod.rs +++ b/crates/bevy_ecs/src/entity/mod.rs @@ -1079,9 +1079,7 @@ impl Entities { // SAFETY: // self.meta and self.spawned_or_despawned_at have the same length // get_entity_meta returning Some: index is in range, spawn/despawn tick is init - unsafe { - self.entity_get_spawned_or_despawned_at_unchecked(entity) - } + unsafe { self.entity_get_spawned_or_despawned_at_unchecked(entity) } }) } @@ -1099,9 +1097,7 @@ impl Entities { }; // SAFETY: // user ensured the entity is alive or despawned while not being overridden - unsafe { - MaybeUninit::assume_init(*spawned_or_despawned_at) - } + unsafe { MaybeUninit::assume_init(*spawned_or_despawned_at) } } /// Returns the [`EntityMeta`] for the given entity if the entity is alive or if @@ -1126,15 +1122,13 @@ impl Entities { { // SAFETY: // self.spawned_or_despawned_at has the same length as self.meta - let spawned_or_despawned_at = unsafe { - self.spawned_or_despawned_at.get_unchecked_mut(index) - }; + let spawned_or_despawned_at = + unsafe { self.spawned_or_despawned_at.get_unchecked_mut(index) }; // SAFETY: // increased generation indicates the entity was once spawned OR // valid archetype means the entity was spawned - let spawned_or_despawned_at = unsafe { - MaybeUninit::assume_init_mut(spawned_or_despawned_at) - }; + let spawned_or_despawned_at = + unsafe { MaybeUninit::assume_init_mut(spawned_or_despawned_at) }; spawned_or_despawned_at.check_tick(change_tick); } } From d094ae73ca3212308852ca181e0c362a3992de65 Mon Sep 17 00:00:00 2001 From: Urben1680 Date: Sun, 4 May 2025 23:11:02 +0200 Subject: [PATCH 23/32] test fixes --- crates/bevy_ecs/src/entity/mod.rs | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/crates/bevy_ecs/src/entity/mod.rs b/crates/bevy_ecs/src/entity/mod.rs index 21e406b7e37e2..270dae8ad810a 100644 --- a/crates/bevy_ecs/src/entity/mod.rs +++ b/crates/bevy_ecs/src/entity/mod.rs @@ -724,8 +724,10 @@ impl Entities { .extend((self.meta.len() as u32)..entity.index()); let new_free_cursor = self.pending.len() as IdCursor; *self.free_cursor.get_mut() = new_free_cursor; - self.meta - .resize(entity.index() as usize + 1, EntityMeta::EMPTY); + let new_len = entity.index() as usize + 1; + self.meta.resize(new_len, EntityMeta::EMPTY); + self.spawned_or_despawned_at + .resize(new_len, MaybeUninit::uninit()); None } else if let Some(index) = self.pending.iter().position(|item| *item == entity.index()) { self.pending.swap_remove(index); @@ -766,8 +768,10 @@ impl Entities { .extend((self.meta.len() as u32)..entity.index()); let new_free_cursor = self.pending.len() as IdCursor; *self.free_cursor.get_mut() = new_free_cursor; - self.meta - .resize(entity.index() as usize + 1, EntityMeta::EMPTY); + let new_len = entity.index() as usize + 1; + self.meta.resize(new_len, EntityMeta::EMPTY); + self.spawned_or_despawned_at + .resize(new_len, MaybeUninit::uninit()); AllocAtWithoutReplacement::DidNotExist } else if let Some(index) = self.pending.iter().position(|item| *item == entity.index()) { self.pending.swap_remove(index); @@ -1116,7 +1120,7 @@ impl Entities { #[inline] pub(crate) fn check_change_ticks(&mut self, change_tick: Tick) { - for (index, meta) in self.meta.iter_mut().enumerate() { + for (index, meta) in self.meta.iter().enumerate() { if meta.generation > NonZero::::MIN || meta.location.archetype_id != ArchetypeId::INVALID { From 38c0d78aa68bbf2069598f2b60f7e34abd333c7b Mon Sep 17 00:00:00 2001 From: Urben1680 Date: Mon, 5 May 2025 22:51:03 +0200 Subject: [PATCH 24/32] SpawnedFrame -> SpawnDetails, tick stored in EntityMeta --- crates/bevy_ecs/src/entity/mod.rs | 124 +++++++----------- crates/bevy_ecs/src/query/fetch.rs | 73 +++++++++-- crates/bevy_ecs/src/query/filter.rs | 14 +- crates/bevy_ecs/src/system/mod.rs | 10 +- crates/bevy_ecs/src/system/query.rs | 4 +- crates/bevy_ecs/src/world/entity_ref.rs | 3 +- .../bevy_ecs/src/world/unsafe_world_cell.rs | 3 +- 7 files changed, 125 insertions(+), 106 deletions(-) diff --git a/crates/bevy_ecs/src/entity/mod.rs b/crates/bevy_ecs/src/entity/mod.rs index 270dae8ad810a..fadee2f691b96 100644 --- a/crates/bevy_ecs/src/entity/mod.rs +++ b/crates/bevy_ecs/src/entity/mod.rs @@ -545,15 +545,6 @@ unsafe impl EntitySetIterator for ReserveEntitiesIterator<'_> {} pub struct Entities { meta: Vec, - /// The `Tick` the entity of the same index did spawn or despawn at. - /// - /// Has the same length as [`Self::meta`]. - /// - /// Value is init if the entity of this index had a valid archetype at some point. - /// - /// Value is uninit if entity was merely allocated yet. - spawned_or_despawned_at: Vec>, - /// The `pending` and `free_cursor` fields describe three sets of Entity IDs /// that have been freed or are in the process of being allocated: /// @@ -602,7 +593,6 @@ impl Entities { pub(crate) const fn new() -> Self { Entities { meta: Vec::new(), - spawned_or_despawned_at: Vec::new(), pending: Vec::new(), free_cursor: AtomicIdCursor::new(0), } @@ -703,7 +693,6 @@ impl Entities { } else { let index = u32::try_from(self.meta.len()).expect("too many entities"); self.meta.push(EntityMeta::EMPTY); - self.spawned_or_despawned_at.push(MaybeUninit::uninit()); Entity::from_raw(index) } } @@ -724,10 +713,8 @@ impl Entities { .extend((self.meta.len() as u32)..entity.index()); let new_free_cursor = self.pending.len() as IdCursor; *self.free_cursor.get_mut() = new_free_cursor; - let new_len = entity.index() as usize + 1; - self.meta.resize(new_len, EntityMeta::EMPTY); - self.spawned_or_despawned_at - .resize(new_len, MaybeUninit::uninit()); + self.meta + .resize(entity.index() as usize + 1, EntityMeta::EMPTY); None } else if let Some(index) = self.pending.iter().position(|item| *item == entity.index()) { self.pending.swap_remove(index); @@ -768,10 +755,8 @@ impl Entities { .extend((self.meta.len() as u32)..entity.index()); let new_free_cursor = self.pending.len() as IdCursor; *self.free_cursor.get_mut() = new_free_cursor; - let new_len = entity.index() as usize + 1; - self.meta.resize(new_len, EntityMeta::EMPTY); - self.spawned_or_despawned_at - .resize(new_len, MaybeUninit::uninit()); + self.meta + .resize(entity.index() as usize + 1, EntityMeta::EMPTY); AllocAtWithoutReplacement::DidNotExist } else if let Some(index) = self.pending.iter().position(|item| *item == entity.index()) { self.pending.swap_remove(index); @@ -839,9 +824,7 @@ impl Entities { .expect("64-bit atomic operations are not supported on this platform.") - freelist_size; if shortfall > 0 { - let additional = shortfall as usize; - self.meta.reserve(additional); - self.spawned_or_despawned_at.reserve(additional); + self.meta.reserve(shortfall as usize); } } @@ -856,7 +839,6 @@ impl Entities { /// Clears all [`Entity`] from the World. pub fn clear(&mut self) { self.meta.clear(); - self.spawned_or_despawned_at.clear(); self.pending.clear(); *self.free_cursor.get_mut() = 0; } @@ -912,14 +894,7 @@ impl Entities { // SAFETY: Caller guarantees that `index` a valid entity index let meta = unsafe { self.meta.get_unchecked_mut(index as usize) }; meta.location = location; - by.map(|by| meta.spawned_or_despawned_by = MaybeLocation::new(Some(by))); - - // SAFETY: Caller guarantees that `index` a valid entity index - let spawned_or_despawned_at = unsafe { - self.spawned_or_despawned_at - .get_unchecked_mut(index as usize) - }; - *spawned_or_despawned_at = MaybeUninit::new(at); + meta.spawned_or_despawned = MaybeUninit::new(SpawnedOrDespawned { by, at }); } /// Increments the `generation` of a freed [`Entity`]. The next entity ID allocated with this @@ -985,8 +960,6 @@ impl Entities { let old_meta_len = self.meta.len(); let new_meta_len = old_meta_len + -current_free_cursor as usize; self.meta.resize(new_meta_len, EntityMeta::EMPTY); - self.spawned_or_despawned_at - .resize(new_meta_len, MaybeUninit::uninit()); for (index, meta) in self.meta.iter_mut().enumerate().skip(old_meta_len) { init( Entity::from_raw_and_generation(index as u32, meta.generation), @@ -1070,8 +1043,8 @@ impl Entities { entity: Entity, ) -> MaybeLocation>> { MaybeLocation::new_with_flattened(|| { - self.get_entity_meta(entity) - .map(|meta| meta.spawned_or_despawned_by.map(|l| l.unwrap())) + self.entity_get_spawned_or_despawned(entity) + .map(|spawned_or_despawned| spawned_or_despawned.by) }) } @@ -1079,61 +1052,56 @@ impl Entities { /// Returns `None` if its index has been reused by another entity or if this entity /// has never existed. pub fn entity_get_spawned_or_despawned_at(&self, entity: Entity) -> Option { - self.get_entity_meta(entity).is_some().then(|| { - // SAFETY: - // self.meta and self.spawned_or_despawned_at have the same length - // get_entity_meta returning Some: index is in range, spawn/despawn tick is init - unsafe { self.entity_get_spawned_or_despawned_at_unchecked(entity) } - }) - } - - /// Returns the [`Tick`] at which this entity has last been spawned or despawned. - /// - /// # Safety - /// - /// The given entity must be alive or must have been alive without being reused. - pub unsafe fn entity_get_spawned_or_despawned_at_unchecked(&self, entity: Entity) -> Tick { - // SAFETY: - // user ensured the entity is alive or despawned while not being overridden - let spawned_or_despawned_at = unsafe { - self.spawned_or_despawned_at - .get_unchecked(entity.index() as usize) - }; - // SAFETY: - // user ensured the entity is alive or despawned while not being overridden - unsafe { MaybeUninit::assume_init(*spawned_or_despawned_at) } + self.entity_get_spawned_or_despawned(entity) + .map(|spawned_or_despawned| spawned_or_despawned.at) } - /// Returns the [`EntityMeta`] for the given entity if the entity is alive or if - /// it was despawned without being overwritten. + /// Returns the [`SpawnedOrDespawned`] related to the entity's last spawn or + /// respawn. Returns `None` if its index has been reused by another entity or if + /// this entity has never existed. #[inline] - fn get_entity_meta(&self, entity: Entity) -> Option { + fn entity_get_spawned_or_despawned(&self, entity: Entity) -> Option { self.meta .get(entity.index() as usize) - .copied() .filter(|meta| // Generation is incremented immediately upon despawn (meta.generation == entity.generation) || (meta.location.archetype_id == ArchetypeId::INVALID) && (meta.generation == IdentifierMask::inc_masked_high_by(entity.generation, 1))) + .map(|meta| { + // SAFETY: valid archetype or non-min generation is proof this is init + unsafe { meta.spawned_or_despawned.assume_init() } + }) + } + + /// Returns the source code location from which this entity has last been spawned + /// or despawned and the Tick of when that happened. + /// + /// # Safety + /// + /// The entity index must belong to an entity that is currently alive or, if it + /// despawned, was not overwritten by a new entity of the same index. + #[inline] + pub(crate) unsafe fn entity_get_spawned_or_despawned_unchecked( + &self, + entity: Entity, + ) -> (MaybeLocation, Tick) { + // SAFETY: caller ensures entity is allocated + let meta = unsafe { self.meta.get_unchecked(entity.index() as usize) }; + // SAFETY: caller ensures entities of this index were at least spawned + let spawned_or_despawned = unsafe { meta.spawned_or_despawned.assume_init() }; + (spawned_or_despawned.by, spawned_or_despawned.at) } #[inline] pub(crate) fn check_change_ticks(&mut self, change_tick: Tick) { - for (index, meta) in self.meta.iter().enumerate() { + for meta in &mut self.meta { if meta.generation > NonZero::::MIN || meta.location.archetype_id != ArchetypeId::INVALID { - // SAFETY: - // self.spawned_or_despawned_at has the same length as self.meta - let spawned_or_despawned_at = - unsafe { self.spawned_or_despawned_at.get_unchecked_mut(index) }; - // SAFETY: - // increased generation indicates the entity was once spawned OR - // valid archetype means the entity was spawned - let spawned_or_despawned_at = - unsafe { MaybeUninit::assume_init_mut(spawned_or_despawned_at) }; - spawned_or_despawned_at.check_tick(change_tick); + // SAFETY: non-min generation or valid archetype is proof this is init + let spawned_or_despawned = unsafe { meta.spawned_or_despawned.assume_init_mut() }; + spawned_or_despawned.at.check_tick(change_tick); } } } @@ -1198,7 +1166,13 @@ struct EntityMeta { /// The current location of the [`Entity`] pub location: EntityLocation, /// Location of the last spawn or despawn of this entity - spawned_or_despawned_by: MaybeLocation>>, + spawned_or_despawned: MaybeUninit, +} + +#[derive(Copy, Clone, Debug)] +struct SpawnedOrDespawned { + by: MaybeLocation, + at: Tick, } impl EntityMeta { @@ -1206,7 +1180,7 @@ impl EntityMeta { const EMPTY: EntityMeta = EntityMeta { generation: NonZero::::MIN, location: EntityLocation::INVALID, - spawned_or_despawned_by: MaybeLocation::new(None), + spawned_or_despawned: MaybeUninit::uninit(), }; } diff --git a/crates/bevy_ecs/src/query/fetch.rs b/crates/bevy_ecs/src/query/fetch.rs index 88ed04ac785da..4c4af8f8016c5 100644 --- a/crates/bevy_ecs/src/query/fetch.rs +++ b/crates/bevy_ecs/src/query/fetch.rs @@ -31,7 +31,7 @@ use variadics_please::all_tuples; /// Gets the identifier of the queried entity. /// - **[`EntityLocation`].** /// Gets the location metadata of the queried entity. -/// - **[`SpawnedTick`].** +/// - **[`SpawnDetails`].** /// Gets the tick the entity was spawned at. /// - **[`EntityRef`].** /// Read-only access to arbitrary components on the queried entity. @@ -472,7 +472,7 @@ unsafe impl QueryData for EntityLocation { /// SAFETY: access is read only unsafe impl ReadOnlyQueryData for EntityLocation {} -/// The `SpawnedTick` query parameter fetches the [`Tick`] the entity was spawned at. +/// The `SpawnDetails` query parameter fetches the [`Tick`] the entity was spawned at. /// /// To evaluate whether the spawn happened since the last time the system ran, the system /// param [`SystemChangeTick`](bevy_ecs::system::SystemChangeTick) needs to be used. @@ -488,12 +488,12 @@ unsafe impl ReadOnlyQueryData for EntityLocation {} /// # use bevy_ecs::system::Query; /// # use bevy_ecs::system::SystemChangeTick; /// # use bevy_ecs::query::Spawned; -/// # use bevy_ecs::query::SpawnedTick; +/// # use bevy_ecs::query::SpawnDetails; /// # /// # #[derive(Component, Debug)] /// # struct Name {}; /// -/// fn print_spawn_ticks(query: Query<(Entity, SpawnedTick)>, system_ticks: SystemChangeTick) { +/// fn print_spawn_ticks(query: Query<(Entity, SpawnDetails)>, system_ticks: SystemChangeTick) { /// for (entity, spawned) in &query { /// if spawned.is_newer_than(system_ticks.last_run(), system_ticks.this_run()) { /// print!("new "); @@ -505,12 +505,43 @@ unsafe impl ReadOnlyQueryData for EntityLocation {} /// # bevy_ecs::system::assert_is_system(print_spawn_ticks); /// ``` #[derive(Clone, Copy)] -pub struct SpawnedTick; +pub struct SpawnDetails { + spawned_by: MaybeLocation, + spawned_at: Tick, + last_run: Tick, + this_run: Tick, +} + +impl SpawnDetails { + /// Returns `true` if the entity spawned since the last time this system ran. + /// Otherwise, returns `false`. + pub fn is_spawned(self) -> bool { + self.spawned_at.is_newer_than(self.last_run, self.this_run) + } + + /// Returns the `Tick` this entity spawned at. + pub fn spawned_at(self) -> Tick { + self.spawned_at + } + + /// Returns the source code location from which this entity has been spawned. + pub fn spawned_by(self) -> MaybeLocation { + self.spawned_by + } +} + +#[doc(hidden)] +#[derive(Clone)] +pub struct SpawnDetailsFetch<'w> { + entities: &'w Entities, + last_run: Tick, + this_run: Tick, +} // SAFETY: // No components are accessed. -unsafe impl WorldQuery for SpawnedTick { - type Fetch<'w> = &'w Entities; +unsafe impl WorldQuery for SpawnDetails { + type Fetch<'w> = SpawnDetailsFetch<'w>; type State = (); fn shrink_fetch<'wlong: 'wshort, 'wshort>(fetch: Self::Fetch<'wlong>) -> Self::Fetch<'wshort> { @@ -520,10 +551,14 @@ unsafe impl WorldQuery for SpawnedTick { unsafe fn init_fetch<'w>( world: UnsafeWorldCell<'w>, _state: &Self::State, - _last_run: Tick, - _this_run: Tick, + last_run: Tick, + this_run: Tick, ) -> Self::Fetch<'w> { - world.entities() + SpawnDetailsFetch { + entities: world.entities(), + last_run, + this_run, + } } const IS_DENSE: bool = true; @@ -560,10 +595,10 @@ unsafe impl WorldQuery for SpawnedTick { // SAFETY: // No components are accessed. // Is its own ReadOnlyQueryData. -unsafe impl QueryData for SpawnedTick { +unsafe impl QueryData for SpawnDetails { const IS_READ_ONLY: bool = true; type ReadOnly = Self; - type Item<'w> = Tick; + type Item<'w> = Self; fn shrink<'wlong: 'wshort, 'wshort>(item: Self::Item<'wlong>) -> Self::Item<'wshort> { item @@ -576,12 +611,22 @@ unsafe impl QueryData for SpawnedTick { _table_row: TableRow, ) -> Self::Item<'w> { // SAFETY: only living entities are queried - unsafe { fetch.entity_get_spawned_or_despawned_at_unchecked(entity) } + let (spawned_by, spawned_at) = unsafe { + fetch + .entities + .entity_get_spawned_or_despawned_unchecked(entity) + }; + Self { + spawned_by, + spawned_at, + last_run: fetch.last_run, + this_run: fetch.this_run, + } } } /// SAFETY: access is read only -unsafe impl ReadOnlyQueryData for SpawnedTick {} +unsafe impl ReadOnlyQueryData for SpawnDetails {} /// SAFETY: /// `fetch` accesses all components in a readonly way. diff --git a/crates/bevy_ecs/src/query/filter.rs b/crates/bevy_ecs/src/query/filter.rs index 638a6eff0c993..198e40e007d22 100644 --- a/crates/bevy_ecs/src/query/filter.rs +++ b/crates/bevy_ecs/src/query/filter.rs @@ -1012,7 +1012,7 @@ unsafe impl QueryFilter for Changed { /// A common use for this filter is one-time initialization. /// /// To retain all results without filtering but still check whether they were spawned after the -/// system last ran, use [`SpawnedTick`](crate::query::SpawnedTick) instead. +/// system last ran, use [`SpawnDetails`](crate::query::SpawnDetails) instead. /// /// **Note** that this includes entities that spawned before the first time this Query was run. /// @@ -1032,19 +1032,16 @@ unsafe impl QueryFilter for Changed { /// ``` /// # use bevy_ecs::entity::Entity; /// # use bevy_ecs::system::Query; -/// # use bevy_ecs::system::SystemChangeTick; /// # use bevy_ecs::query::Spawned; -/// # use bevy_ecs::query::SpawnedTick; +/// # use bevy_ecs::query::SpawnDetails; /// /// fn system1(query: Query) { /// for entity in &query { /* entity spawned */ } /// } /// -/// fn system2(query: Query<(Entity, SpawnedTick)>, system_ticks: SystemChangeTick) { +/// fn system2(query: Query<(Entity, SpawnDetails)>) { /// for (entity, spawned) in &query { -/// if spawned.is_newer_than(system_ticks.last_run(), system_ticks.this_run()) { -/// /* entity spawned */ -/// } +/// if spawned.is_spawned() { /* entity spawned */ } /// } /// } /// ``` @@ -1143,7 +1140,8 @@ unsafe impl QueryFilter for Spawned { let spawned = unsafe { fetch .entities - .entity_get_spawned_or_despawned_at_unchecked(entity) + .entity_get_spawned_or_despawned_unchecked(entity) + .1 }; spawned.is_newer_than(fetch.last_run, fetch.this_run) } diff --git a/crates/bevy_ecs/src/system/mod.rs b/crates/bevy_ecs/src/system/mod.rs index 5e3d3f0a480b3..a014143d1d54c 100644 --- a/crates/bevy_ecs/src/system/mod.rs +++ b/crates/bevy_ecs/src/system/mod.rs @@ -339,7 +339,7 @@ mod tests { error::Result, name::Name, prelude::{AnyOf, EntityRef, Trigger}, - query::{Added, Changed, Or, Spawned, SpawnedTick, With, Without}, + query::{Added, Changed, Or, SpawnDetails, Spawned, With, Without}, removal_detection::RemovedComponents, resource::Resource, schedule::{ @@ -1261,11 +1261,11 @@ mod tests { world.spawn_empty(); let spawn_tick = world.change_tick(); - let mut system_state: SystemState>> = + let mut system_state: SystemState>> = SystemState::new(&mut world); { let query = system_state.get(&world); - assert_eq!(*query.unwrap(), spawn_tick); + assert_eq!(query.unwrap().spawned_at(), spawn_tick); } { @@ -1503,13 +1503,13 @@ mod tests { { let mut world = World::new(); - fn mutable_query(mut query: Query<(&mut A, &mut B, SpawnedTick), Spawned>) { + fn mutable_query(mut query: Query<(&mut A, &mut B, SpawnDetails), Spawned>) { for _ in &mut query {} immutable_query(query.as_readonly()); } - fn immutable_query(_: Query<(&A, &B, SpawnedTick), Spawned>) {} + fn immutable_query(_: Query<(&A, &B, SpawnDetails), Spawned>) {} let mut sys = IntoSystem::into_system(mutable_query); sys.initialize(&mut world); diff --git a/crates/bevy_ecs/src/system/query.rs b/crates/bevy_ecs/src/system/query.rs index bd8a44d8ca6a1..ef5c0f99c68fd 100644 --- a/crates/bevy_ecs/src/system/query.rs +++ b/crates/bevy_ecs/src/system/query.rs @@ -2206,7 +2206,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter> Query<'w, 's, D, F> { /// * Tuples of query data and `#[derive(QueryData)]` structs have the union of the access of their subqueries /// * [`EntityMut`](crate::world::EntityMut) has read and write access to all components, but no required access /// * [`EntityRef`](crate::world::EntityRef) has read access to all components, but no required access - /// * [`Entity`], [`EntityLocation`], [`SpawnedTick`], [`&Archetype`], [`Has`], and [`PhantomData`] have no access at all, + /// * [`Entity`], [`EntityLocation`], [`SpawnDetails`], [`&Archetype`], [`Has`], and [`PhantomData`] have no access at all, /// so can be added to any query /// * [`FilteredEntityRef`](crate::world::FilteredEntityRef) and [`FilteredEntityMut`](crate::world::FilteredEntityMut) /// have access determined by the [`QueryBuilder`](crate::query::QueryBuilder) used to construct them. @@ -2296,7 +2296,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter> Query<'w, 's, D, F> { /// ``` /// /// [`EntityLocation`]: crate::entity::EntityLocation - /// [`SpawnedTick`]: crate::query::SpawnedTick + /// [`SpawnDetails`]: crate::query::SpawnDetails /// [`&Archetype`]: crate::archetype::Archetype /// [`Has`]: crate::query::Has #[track_caller] diff --git a/crates/bevy_ecs/src/world/entity_ref.rs b/crates/bevy_ecs/src/world/entity_ref.rs index e27177c4fe1e5..bca1c1d825993 100644 --- a/crates/bevy_ecs/src/world/entity_ref.rs +++ b/crates/bevy_ecs/src/world/entity_ref.rs @@ -2969,7 +2969,8 @@ impl<'w> EntityWorldMut<'w> { unsafe { self.world() .entities() - .entity_get_spawned_or_despawned_at_unchecked(self.entity) + .entity_get_spawned_or_despawned_unchecked(self.entity) + .1 } } } diff --git a/crates/bevy_ecs/src/world/unsafe_world_cell.rs b/crates/bevy_ecs/src/world/unsafe_world_cell.rs index 02fbde92b1704..8e9319df5f890 100644 --- a/crates/bevy_ecs/src/world/unsafe_world_cell.rs +++ b/crates/bevy_ecs/src/world/unsafe_world_cell.rs @@ -1143,7 +1143,8 @@ impl<'w> UnsafeEntityCell<'w> { unsafe { self.world() .entities() - .entity_get_spawned_or_despawned_at_unchecked(self.entity) + .entity_get_spawned_or_despawned_unchecked(self.entity) + .1 } } } From ce168a467d1c29cd364e70afe125029371f1aa37 Mon Sep 17 00:00:00 2001 From: Urben1680 Date: Mon, 5 May 2025 22:59:24 +0200 Subject: [PATCH 25/32] doc fix --- crates/bevy_ecs/src/query/fetch.rs | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/crates/bevy_ecs/src/query/fetch.rs b/crates/bevy_ecs/src/query/fetch.rs index 4c4af8f8016c5..40519868a09ee 100644 --- a/crates/bevy_ecs/src/query/fetch.rs +++ b/crates/bevy_ecs/src/query/fetch.rs @@ -486,19 +486,18 @@ unsafe impl ReadOnlyQueryData for EntityLocation {} /// # use bevy_ecs::component::Component; /// # use bevy_ecs::entity::Entity; /// # use bevy_ecs::system::Query; -/// # use bevy_ecs::system::SystemChangeTick; /// # use bevy_ecs::query::Spawned; /// # use bevy_ecs::query::SpawnDetails; /// # /// # #[derive(Component, Debug)] /// # struct Name {}; /// -/// fn print_spawn_ticks(query: Query<(Entity, SpawnDetails)>, system_ticks: SystemChangeTick) { -/// for (entity, spawned) in &query { -/// if spawned.is_newer_than(system_ticks.last_run(), system_ticks.this_run()) { +/// fn print_spawn_ticks(query: Query<(Entity, SpawnDetails)>) { +/// for (entity, spawn_details) in &query { +/// if spawn_details.is_spawned() { /// print!("new "); /// } -/// println!("entity {entity:?} spawned at {spawned:?}"); +/// println!("entity {entity:?} spawned at {:?}", spawn_details.spawned_at()); /// } /// } /// From aa3c31d2d1109b5eb905aff4b01020c474cfc84a Mon Sep 17 00:00:00 2001 From: Urben1680 Date: Mon, 5 May 2025 23:05:39 +0200 Subject: [PATCH 26/32] doc fix --- crates/bevy_ecs/src/query/fetch.rs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/crates/bevy_ecs/src/query/fetch.rs b/crates/bevy_ecs/src/query/fetch.rs index 40519868a09ee..097490b424143 100644 --- a/crates/bevy_ecs/src/query/fetch.rs +++ b/crates/bevy_ecs/src/query/fetch.rs @@ -497,7 +497,12 @@ unsafe impl ReadOnlyQueryData for EntityLocation {} /// if spawn_details.is_spawned() { /// print!("new "); /// } -/// println!("entity {entity:?} spawned at {:?}", spawn_details.spawned_at()); +/// println!( +/// "entity {:?} spawned at {:?} by {:?}", +/// entity, +/// spawn_details.spawned_at(), +/// spawn_details.spawned_by() +/// ); /// } /// } /// From c30e6ecb143d0eb60badaec9caeab66ba5a4247f Mon Sep 17 00:00:00 2001 From: Urben1680 Date: Mon, 5 May 2025 23:34:38 +0200 Subject: [PATCH 27/32] rename system in doc --- crates/bevy_ecs/src/query/fetch.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/bevy_ecs/src/query/fetch.rs b/crates/bevy_ecs/src/query/fetch.rs index 097490b424143..ced1d2f327b57 100644 --- a/crates/bevy_ecs/src/query/fetch.rs +++ b/crates/bevy_ecs/src/query/fetch.rs @@ -492,7 +492,7 @@ unsafe impl ReadOnlyQueryData for EntityLocation {} /// # #[derive(Component, Debug)] /// # struct Name {}; /// -/// fn print_spawn_ticks(query: Query<(Entity, SpawnDetails)>) { +/// fn print_spawn_details(query: Query<(Entity, SpawnDetails)>) { /// for (entity, spawn_details) in &query { /// if spawn_details.is_spawned() { /// print!("new "); @@ -506,7 +506,7 @@ unsafe impl ReadOnlyQueryData for EntityLocation {} /// } /// } /// -/// # bevy_ecs::system::assert_is_system(print_spawn_ticks); +/// # bevy_ecs::system::assert_is_system(print_spawn_details); /// ``` #[derive(Clone, Copy)] pub struct SpawnDetails { From 7510a26f23260f0f01d94cc5c8553362f2c3334e Mon Sep 17 00:00:00 2001 From: Urben1680 Date: Tue, 6 May 2025 18:38:46 +0200 Subject: [PATCH 28/32] doc adjustments, Debug derive on SpawnDetails --- crates/bevy_ecs/src/query/fetch.rs | 15 +++++++++------ crates/bevy_ecs/src/query/filter.rs | 13 ++++++------- 2 files changed, 15 insertions(+), 13 deletions(-) diff --git a/crates/bevy_ecs/src/query/fetch.rs b/crates/bevy_ecs/src/query/fetch.rs index ced1d2f327b57..635015eef8442 100644 --- a/crates/bevy_ecs/src/query/fetch.rs +++ b/crates/bevy_ecs/src/query/fetch.rs @@ -497,18 +497,21 @@ unsafe impl ReadOnlyQueryData for EntityLocation {} /// if spawn_details.is_spawned() { /// print!("new "); /// } -/// println!( -/// "entity {:?} spawned at {:?} by {:?}", +/// print!( +/// "entity {:?} spawned at {:?}", /// entity, -/// spawn_details.spawned_at(), -/// spawn_details.spawned_by() -/// ); +/// spawn_details.spawned_at() +/// ); +/// match spawn_details.spawned_by().into_option() { +/// Some(location) => println!(" by {:?}", location), +/// None => println!() +/// } /// } /// } /// /// # bevy_ecs::system::assert_is_system(print_spawn_details); /// ``` -#[derive(Clone, Copy)] +#[derive(Clone, Copy, Debug)] pub struct SpawnDetails { spawned_by: MaybeLocation, spawned_at: Tick, diff --git a/crates/bevy_ecs/src/query/filter.rs b/crates/bevy_ecs/src/query/filter.rs index 198e40e007d22..1de1586afd57f 100644 --- a/crates/bevy_ecs/src/query/filter.rs +++ b/crates/bevy_ecs/src/query/filter.rs @@ -569,8 +569,8 @@ all_tuples!( /// # Deferred /// /// Note, that entity modifications issued with [`Commands`](crate::system::Commands) -/// are visible only after deferred operations are applied, -/// typically at the end of the schedule iteration. +/// are visible only after deferred operations are applied, typically after the system +/// that queued them. /// /// # Time complexity /// @@ -794,9 +794,8 @@ unsafe impl QueryFilter for Added { /// # Deferred /// /// Note, that entity modifications issued with [`Commands`](crate::system::Commands) -/// (like entity creation or entity component addition or removal) -/// are visible only after deferred operations are applied, -/// typically at the end of the schedule iteration. +/// (like entity creation or entity component addition or removal) are visible only +/// after deferred operations are applied, typically after the system that queued them. /// /// # Time complexity /// @@ -1019,8 +1018,8 @@ unsafe impl QueryFilter for Changed { /// # Deferred /// /// Note, that entity spawns issued with [`Commands`](crate::system::Commands) -/// are visible only after deferred operations are applied, -/// typically at the end of the schedule iteration. +/// are visible only after deferred operations are applied, typically after the +/// system that queued them. /// /// # Time complexity /// From 5dc96955353664d9474d63e6aa6b005f49ad50b2 Mon Sep 17 00:00:00 2001 From: Urben1680 Date: Tue, 6 May 2025 21:02:32 +0200 Subject: [PATCH 29/32] release notes --- crates/bevy_ecs/src/query/fetch.rs | 3 - .../release-notes/entity-spawn-ticks.md | 83 +++++++++++++++++++ 2 files changed, 83 insertions(+), 3 deletions(-) create mode 100644 release-content/release-notes/entity-spawn-ticks.md diff --git a/crates/bevy_ecs/src/query/fetch.rs b/crates/bevy_ecs/src/query/fetch.rs index 01c7d4b1b9f0e..9df60f046d3ba 100644 --- a/crates/bevy_ecs/src/query/fetch.rs +++ b/crates/bevy_ecs/src/query/fetch.rs @@ -504,9 +504,6 @@ unsafe impl ReadOnlyQueryData for EntityLocation {} /// # use bevy_ecs::system::Query; /// # use bevy_ecs::query::Spawned; /// # use bevy_ecs::query::SpawnDetails; -/// # -/// # #[derive(Component, Debug)] -/// # struct Name {}; /// /// fn print_spawn_details(query: Query<(Entity, SpawnDetails)>) { /// for (entity, spawn_details) in &query { diff --git a/release-content/release-notes/entity-spawn-ticks.md b/release-content/release-notes/entity-spawn-ticks.md new file mode 100644 index 0000000000000..88a1396b09798 --- /dev/null +++ b/release-content/release-notes/entity-spawn-ticks.md @@ -0,0 +1,83 @@ +--- +title: Entity Spawn Ticks +authors: ["@urben1680"] +pull_requests: [19047] +--- + +Keeping track which entities have been spawned since the last time a system ran could only be done indirectly by inserting marker components and do your logic on entities that match an `Added` filter or in `MyMarker`'s `on_add` hook. + +This has the issue however that not add reacts on a spawn but also insertions at existing entities. Sometimes you cannot even add your marker because the spawn is hidden in some non-public API. + +The new `SpawnDetails` query data and `Spawned` query filter enable you to find recently spawned entities without any marker components. + +## `SpawnDetails` + +Use this in your query when you want to get information about it's spawn. You might want to do that to log debug information about it, using it's `Debug` implementation. + +You can also get specific information via methods. The following example prints the entity id (prefixed with "new" if it showed up for the first time), the `Tick` it spawned at and, if the `track_location` feature is activated, the source code location where it was spawned. Said feature is not enabled by default because it comes with a runtime cost. + +```rs +fn print_spawn_details(query: Query<(Entity, SpawnDetails)>) { + for (entity, spawn_details) in &query { + if spawn_details.is_spawned() { + print!("new "); + } + print!( + "entity {:?} spawned at {:?}", + entity, + spawn_details.spawned_at() + ); + match spawn_details.spawned_by().into_option() { + Some(location) => println!(" by {:?}", location), + None => println!() + } + } +} +``` + +## `Spawned` + +Use this filter in your query if you are only interested in entities that were spawned after the last time your system ran. + +Note that this, like `Added` and `Changed`, is a non-archetypal filter. This means that your query could still go through millions of entities without yielding any recently spawned ones. Unlike filters like `With` that can easily skip all entities that do not have `T` without checking them one-by-one. + +Because of this, these systems have roughly the same performance: + +```rs +fn system1(q: Query) { + for entity in &q { /* entity spawned */ } +} + +fn system2(query: Query<(Entity, SpawnDetails)>) { + for (entity, spawned) in &query { + if spawned.is_spawned() { /* entity spawned */ } + } +} +``` + +## Getter methods + +Getting around this weakness of non-archetypal filters can be to check only specific entities when they are spawned at: The method `spawned_at` was added to all entity pointer structs, such as `EntityRef`, `EntityMut` and `EntityWorldMut`. + +In this example we want to filter for entities that were spawned after a certain `tick`: + +```rs +fn filter_spawned_after( + entities: impl IntoIterator, + world: &World, + tick: Tick, +) -> impl Iterator { + let now = world.last_change_tick(); + entities.into_iter().filter(move |entity| world + .entity(*entity) + .spawned_at() + .is_newer_than(tick, now) + ) +} +``` + +--- + +The tick is stored in `Entities`. It's method `entity_get_spawned_or_despawned_at` not only returns when a living entity spawned at, it also returns when a despawned entity found it's bitter end. + +Note however that despawned entities can be replaced by bevy at any following spawn. Then this method returns `None` for the despawned entity. The same is true if the entity was not even spawned yet, only allocated. \ No newline at end of file From dbb5b6d3d626903473ef8c17c883b0da68f0486a Mon Sep 17 00:00:00 2001 From: Urben1680 Date: Tue, 6 May 2025 21:06:42 +0200 Subject: [PATCH 30/32] CI fix --- release-content/release-notes/entity-spawn-ticks.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/release-content/release-notes/entity-spawn-ticks.md b/release-content/release-notes/entity-spawn-ticks.md index 88a1396b09798..56458cc412435 100644 --- a/release-content/release-notes/entity-spawn-ticks.md +++ b/release-content/release-notes/entity-spawn-ticks.md @@ -80,4 +80,4 @@ fn filter_spawned_after( The tick is stored in `Entities`. It's method `entity_get_spawned_or_despawned_at` not only returns when a living entity spawned at, it also returns when a despawned entity found it's bitter end. -Note however that despawned entities can be replaced by bevy at any following spawn. Then this method returns `None` for the despawned entity. The same is true if the entity was not even spawned yet, only allocated. \ No newline at end of file +Note however that despawned entities can be replaced by bevy at any following spawn. Then this method returns `None` for the despawned entity. The same is true if the entity was not even spawned yet, only allocated. From 0f84dff105e8d011ae73105e5a239946c5f045ab Mon Sep 17 00:00:00 2001 From: urben1680 <55257931+urben1680@users.noreply.github.com> Date: Tue, 6 May 2025 23:05:21 +0200 Subject: [PATCH 31/32] Update entity-spawn-ticks.md Typos --- release-content/release-notes/entity-spawn-ticks.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/release-content/release-notes/entity-spawn-ticks.md b/release-content/release-notes/entity-spawn-ticks.md index 56458cc412435..57ab8531b6b64 100644 --- a/release-content/release-notes/entity-spawn-ticks.md +++ b/release-content/release-notes/entity-spawn-ticks.md @@ -6,13 +6,13 @@ pull_requests: [19047] Keeping track which entities have been spawned since the last time a system ran could only be done indirectly by inserting marker components and do your logic on entities that match an `Added` filter or in `MyMarker`'s `on_add` hook. -This has the issue however that not add reacts on a spawn but also insertions at existing entities. Sometimes you cannot even add your marker because the spawn is hidden in some non-public API. +This has the issue however that not every add reacts on a spawn but also on insertions at existing entities. Sometimes you cannot even add your marker because the spawn call is hidden in some non-public API. The new `SpawnDetails` query data and `Spawned` query filter enable you to find recently spawned entities without any marker components. ## `SpawnDetails` -Use this in your query when you want to get information about it's spawn. You might want to do that to log debug information about it, using it's `Debug` implementation. +Use this in your query when you want to get information about it's spawn. You might want to do that for debug purposes, using it's `Debug` implementation. You can also get specific information via methods. The following example prints the entity id (prefixed with "new" if it showed up for the first time), the `Tick` it spawned at and, if the `track_location` feature is activated, the source code location where it was spawned. Said feature is not enabled by default because it comes with a runtime cost. @@ -39,7 +39,7 @@ fn print_spawn_details(query: Query<(Entity, SpawnDetails)>) { Use this filter in your query if you are only interested in entities that were spawned after the last time your system ran. -Note that this, like `Added` and `Changed`, is a non-archetypal filter. This means that your query could still go through millions of entities without yielding any recently spawned ones. Unlike filters like `With` that can easily skip all entities that do not have `T` without checking them one-by-one. +Note that this, like `Added` and `Changed`, is a non-archetypal filter. This means that your query could still go through millions of entities without yielding any recently spawned ones. Unlike filters like `With` which can easily skip all entities that do not have `T` without checking them one-by-one. Because of this, these systems have roughly the same performance: @@ -57,7 +57,7 @@ fn system2(query: Query<(Entity, SpawnDetails)>) { ## Getter methods -Getting around this weakness of non-archetypal filters can be to check only specific entities when they are spawned at: The method `spawned_at` was added to all entity pointer structs, such as `EntityRef`, `EntityMut` and `EntityWorldMut`. +Getting around this weakness of non-archetypal filters can be to check only specific entities for their spawn tick: The method `spawned_at` was added to all entity pointer structs, such as `EntityRef`, `EntityMut` and `EntityWorldMut`. In this example we want to filter for entities that were spawned after a certain `tick`: @@ -80,4 +80,4 @@ fn filter_spawned_after( The tick is stored in `Entities`. It's method `entity_get_spawned_or_despawned_at` not only returns when a living entity spawned at, it also returns when a despawned entity found it's bitter end. -Note however that despawned entities can be replaced by bevy at any following spawn. Then this method returns `None` for the despawned entity. The same is true if the entity was not even spawned yet, only allocated. +Note however that despawned entities can be replaced by bevy at any following spawn. Then this method returns `None` for the despawned entity. The same is true if the entity is not even spawned yet, only allocated. From e33db63e6157b74205ed42b436ee88a2487d0406 Mon Sep 17 00:00:00 2001 From: urben1680 <55257931+urben1680@users.noreply.github.com> Date: Wed, 7 May 2025 09:09:47 +0200 Subject: [PATCH 32/32] Update entity-spawn-ticks.md Made a sentence more clear --- release-content/release-notes/entity-spawn-ticks.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/release-content/release-notes/entity-spawn-ticks.md b/release-content/release-notes/entity-spawn-ticks.md index 57ab8531b6b64..54c6d664fe424 100644 --- a/release-content/release-notes/entity-spawn-ticks.md +++ b/release-content/release-notes/entity-spawn-ticks.md @@ -12,7 +12,7 @@ The new `SpawnDetails` query data and `Spawned` query filter enable you to find ## `SpawnDetails` -Use this in your query when you want to get information about it's spawn. You might want to do that for debug purposes, using it's `Debug` implementation. +Use this in your query when you want to get information about the entity's spawn. You might want to do that for debug purposes, using the struct's `Debug` implementation. You can also get specific information via methods. The following example prints the entity id (prefixed with "new" if it showed up for the first time), the `Tick` it spawned at and, if the `track_location` feature is activated, the source code location where it was spawned. Said feature is not enabled by default because it comes with a runtime cost.