Skip to content

Track spawn Tick of entities, offer methods, query data SpawnDetails and query filter Spawned #19047

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 43 commits into from
May 8, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
41a22f3
track spawn tick
urben1680 May 3, 2025
5bd6c81
SpawnedTick/Spawned query data/filter
urben1680 May 3, 2025
3683a36
merge main
urben1680 May 3, 2025
2c92f8d
EntityRef::spawned_at
urben1680 May 3, 2025
a7288e5
ci fixes
urben1680 May 3, 2025
2bfd9e3
doc fixes
urben1680 May 3, 2025
4883f19
typo fix
urben1680 May 3, 2025
5ff548c
doc fixes
urben1680 May 3, 2025
7f115f1
doc fixes
urben1680 May 3, 2025
911ad78
doc fixes
urben1680 May 3, 2025
19c23aa
doc fixes
urben1680 May 3, 2025
b479adf
doc fixes
urben1680 May 3, 2025
088a2a4
Update crates/bevy_ecs/src/query/filter.rs
urben1680 May 3, 2025
96e4a4a
Update crates/bevy_ecs/src/query/fetch.rs
urben1680 May 3, 2025
44cbd93
SpawnedTick/Spawned using unchecked tick read
urben1680 May 3, 2025
8c55d76
using unsafe variant for entity pointer types
urben1680 May 3, 2025
632976d
CI fix
urben1680 May 3, 2025
96b7a8d
Merge branch 'main' into track-spawn-tick
urben1680 May 4, 2025
ae4dc6b
Merge branch 'main' into track-spawn-tick
urben1680 May 4, 2025
77af9f4
remove Entities::set_spawned_or_despawned_by, introduce Entities::set…
urben1680 May 4, 2025
b7029e2
Merge branch 'track-spawn-tick' of https://github.com/urben1680/bevy …
urben1680 May 4, 2025
c0fec57
fmt
urben1680 May 4, 2025
abe17b7
variant with separate Tick vector in Entities
urben1680 May 4, 2025
8c55e74
formatting
urben1680 May 4, 2025
be76c89
formatting
urben1680 May 4, 2025
7f10ae0
formatting
urben1680 May 4, 2025
d094ae7
test fixes
urben1680 May 4, 2025
38c0d78
SpawnedFrame -> SpawnDetails, tick stored in EntityMeta
urben1680 May 5, 2025
ce168a4
doc fix
urben1680 May 5, 2025
aa3c31d
doc fix
urben1680 May 5, 2025
c30e6ec
rename system in doc
urben1680 May 5, 2025
a2b5f64
Merge branch 'main' into track-spawn-tick
urben1680 May 5, 2025
c84a9ed
Merge branch 'main' into track-spawn-tick
alice-i-cecile May 6, 2025
7510a26
doc adjustments, Debug derive on SpawnDetails
urben1680 May 6, 2025
5394a69
Merge branch 'track-spawn-tick' of https://github.com/urben1680/bevy …
urben1680 May 6, 2025
9c0e564
Merge branch 'main' into track-spawn-tick
urben1680 May 6, 2025
5dc9695
release notes
urben1680 May 6, 2025
dbb5b6d
CI fix
urben1680 May 6, 2025
0f84dff
Update entity-spawn-ticks.md
urben1680 May 6, 2025
e33db63
Update entity-spawn-ticks.md
urben1680 May 7, 2025
aa562e8
Merge branch 'main' into track-spawn-tick
urben1680 May 7, 2025
153949f
Merge branch 'main' into track-spawn-tick
urben1680 May 7, 2025
2ff9417
Merge branch 'main' into track-spawn-tick
urben1680 May 8, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion crates/bevy_ecs/src/bundle.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1736,7 +1736,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)
};

Expand Down
127 changes: 101 additions & 26 deletions crates/bevy_ecs/src/entity/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -76,11 +76,18 @@ pub use unique_vec::{UniqueEntityEquivalentVec, UniqueEntityVec};
use crate::{
archetype::{ArchetypeId, ArchetypeRow},
change_detection::MaybeLocation,
component::Tick,
storage::{SparseSetIndex, TableId, TableRow},
};
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")]
Expand Down Expand Up @@ -899,7 +906,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.
Expand All @@ -912,6 +922,27 @@ 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 = MaybeUninit::new(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.
Expand Down Expand Up @@ -1052,19 +1083,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_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));
});
}

/// 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.
Expand All @@ -1073,16 +1091,67 @@ impl Entities {
entity: Entity,
) -> MaybeLocation<Option<&'static Location<'static>>> {
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 == entity.generation.after_versions(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<Tick> {
self.entity_get_spawned_or_despawned(entity)
.map(|spawned_or_despawned| spawned_or_despawned.at)
}

/// 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 entity_get_spawned_or_despawned(&self, entity: Entity) -> Option<SpawnedOrDespawned> {
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 == entity.generation.after_versions(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 meta in &mut self.meta {
if meta.generation != EntityGeneration::FIRST
|| meta.location.archetype_id != ArchetypeId::INVALID
{
// 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);
}
}
}

/// Constructs a message explaining why an entity does not exist, if known.
Expand Down Expand Up @@ -1145,15 +1214,21 @@ struct EntityMeta {
/// The current location of the [`EntityRow`]
pub location: EntityLocation,
/// Location of the last spawn or despawn of this entity
spawned_or_despawned_by: MaybeLocation<Option<&'static Location<'static>>>,
spawned_or_despawned: MaybeUninit<SpawnedOrDespawned>,
}

#[derive(Copy, Clone, Debug)]
struct SpawnedOrDespawned {
by: MaybeLocation,
at: Tick,
}

impl EntityMeta {
/// meta for **pending entity**
const EMPTY: EntityMeta = EntityMeta {
generation: EntityGeneration::FIRST,
location: EntityLocation::INVALID,
spawned_or_despawned_by: MaybeLocation::new(None),
spawned_or_despawned: MaybeUninit::uninit(),
};
}

Expand Down
162 changes: 162 additions & 0 deletions crates/bevy_ecs/src/query/fetch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
/// - **[`SpawnDetails`].**
/// Gets the tick the entity was spawned at.
/// - **[`EntityRef`].**
/// Read-only access to arbitrary components on the queried entity.
/// - **[`EntityMut`].**
Expand Down Expand Up @@ -486,6 +488,166 @@ unsafe impl QueryData for EntityLocation {
/// SAFETY: access is read only
unsafe impl ReadOnlyQueryData for EntityLocation {}

/// 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.
///
/// If the query should filter for spawned entities instead, use the
/// [`Spawned`](bevy_ecs::query::Spawned) query filter instead.
///
/// # Examples
///
/// ```
/// # use bevy_ecs::component::Component;
/// # use bevy_ecs::entity::Entity;
/// # use bevy_ecs::system::Query;
/// # use bevy_ecs::query::Spawned;
/// # use bevy_ecs::query::SpawnDetails;
///
/// 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!()
/// }
/// }
/// }
///
/// # bevy_ecs::system::assert_is_system(print_spawn_details);
/// ```
#[derive(Clone, Copy, Debug)]
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 SpawnDetails {
type Fetch<'w> = SpawnDetailsFetch<'w>;
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> {
SpawnDetailsFetch {
entities: world.entities(),
last_run,
this_run,
}
}

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<ComponentId>) {}

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 SpawnDetails {
const IS_READ_ONLY: bool = true;
type ReadOnly = Self;
type Item<'w> = Self;

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> {
// SAFETY: only living entities are queried
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 SpawnDetails {}

/// The [`WorldQuery::Fetch`] type for WorldQueries that can fetch multiple components from an entity
/// ([`EntityRef`], [`EntityMut`], etc.)
#[derive(Copy, Clone)]
Expand Down
Loading