Skip to content

Commit 3126f8b

Browse files
urben1680ElliottjPiercealice-i-cecile
authored andcommitted
Track spawn Tick of entities, offer methods, query data SpawnDetails and query filter Spawned (bevyengine#19047)
# Objective In my own project I was encountering the issue to find out which entities were spawned after applying commands. I began maintaining a vector of all entities with generational information before and after applying the command and diffing it. This was awfully complicated though and has no constant complexity but grows with the number of entities. ## Solution Looking at `EntyMeta` it seemed obvious to me that struct can track the tick just as it does with `MaybeLocation`, updated from the same call. After that it became almost a given to also introduce query data `SpawnDetails` which offers methods to get the spawn tick and location, and query filter `Spawned` that filters entities out that were not spawned since the last run. ## Testing I expanded a few tests and added new ones, though maybe I forgot a group of tests that should be extended too. I basically searched `bevy_ecs` for mentions of `Changed` and `Added` to see where the tests and docs are. Benchmarks of spawn/despawn can be found [here](bevyengine#19047 (comment)). --- ## Showcase From the added docs, systems with equal complexity since the filter is not archetypal: ```rs fn system1(q: Query<Entity, Spawned>) { for entity in &q { /* entity spawned */ } } fn system2(query: Query<(Entity, SpawnDetails)>) { for (entity, spawned) in &query { if spawned.is_spawned() { /* entity spawned */ } } } ``` `SpawnedDetails` has a few more methods: ```rs fn print_spawn_details(query: Query<(Entity, SpawnDetails)>) { for (entity, spawn_details) in &query { if spawn_details.is_spawned() { print!("new "); } println!( "entity {:?} spawned at {:?} by {:?}", entity, spawn_details.spawned_at(), spawn_details.spawned_by() ); } } ``` ## Changes No public api was changed, I only added to it. That is why I added no migration guide. - query data `SpawnDetails` - query filter `Spawned` - method `Entities::entity_get_spawned_or_despawned_at` - method `EntityRef::spawned_at` - method `EntityMut::spawned_at` - method `EntityWorldMut::spawned_at` - method `UnsafeEntityCell::spawned_at` - method `FilteredEntityRef::spawned_at` - method `FilteredEntityMut::spawned_at` - method `EntityRefExcept::spawned_at` - method `EntityMutExcept::spawned_at` --------- Co-authored-by: Eagster <[email protected]> Co-authored-by: Alice Cecile <[email protected]>
1 parent 99b9ced commit 3126f8b

File tree

12 files changed

+665
-87
lines changed

12 files changed

+665
-87
lines changed

crates/bevy_ecs/src/bundle.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1736,7 +1736,7 @@ impl<'w> BundleSpawner<'w> {
17361736
InsertMode::Replace,
17371737
caller,
17381738
);
1739-
entities.set(entity.index(), location);
1739+
entities.set_spawn_despawn(entity.index(), location, caller, self.change_tick);
17401740
(location, after_effect)
17411741
};
17421742

crates/bevy_ecs/src/entity/mod.rs

Lines changed: 101 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -76,11 +76,18 @@ pub use unique_vec::{UniqueEntityEquivalentVec, UniqueEntityVec};
7676
use crate::{
7777
archetype::{ArchetypeId, ArchetypeRow},
7878
change_detection::MaybeLocation,
79+
component::Tick,
7980
storage::{SparseSetIndex, TableId, TableRow},
8081
};
8182
use alloc::vec::Vec;
8283
use bevy_platform::sync::atomic::Ordering;
83-
use core::{fmt, hash::Hash, mem, num::NonZero, panic::Location};
84+
use core::{
85+
fmt,
86+
hash::Hash,
87+
mem::{self, MaybeUninit},
88+
num::NonZero,
89+
panic::Location,
90+
};
8491
use log::warn;
8592

8693
#[cfg(feature = "serialize")]
@@ -945,7 +952,10 @@ impl Entities {
945952
}
946953

947954
/// Updates the location of an [`Entity`]. This must be called when moving the components of
948-
/// the entity around in storage.
955+
/// the existing entity around in storage.
956+
///
957+
/// For spawning and despawning entities, [`set_spawn_despawn`](Self::set_spawn_despawn) must
958+
/// be used instead.
949959
///
950960
/// # Safety
951961
/// - `index` must be a valid entity index.
@@ -958,6 +968,27 @@ impl Entities {
958968
meta.location = location;
959969
}
960970

971+
/// Updates the location of an [`Entity`]. This must be called when moving the components of
972+
/// the spawned or despawned entity around in storage.
973+
///
974+
/// # Safety
975+
/// - `index` must be a valid entity index.
976+
/// - `location` must be valid for the entity at `index` or immediately made valid afterwards
977+
/// before handing control to unknown code.
978+
#[inline]
979+
pub(crate) unsafe fn set_spawn_despawn(
980+
&mut self,
981+
index: u32,
982+
location: EntityLocation,
983+
by: MaybeLocation,
984+
at: Tick,
985+
) {
986+
// SAFETY: Caller guarantees that `index` a valid entity index
987+
let meta = unsafe { self.meta.get_unchecked_mut(index as usize) };
988+
meta.location = location;
989+
meta.spawned_or_despawned = MaybeUninit::new(SpawnedOrDespawned { by, at });
990+
}
991+
961992
/// Increments the `generation` of a freed [`Entity`]. The next entity ID allocated with this
962993
/// `index` will count `generation` starting from the prior `generation` + the specified
963994
/// value + 1.
@@ -1098,19 +1129,6 @@ impl Entities {
10981129
self.len() == 0
10991130
}
11001131

1101-
/// Sets the source code location from which this entity has last been spawned
1102-
/// or despawned.
1103-
#[inline]
1104-
pub(crate) fn set_spawned_or_despawned_by(&mut self, index: u32, caller: MaybeLocation) {
1105-
caller.map(|caller| {
1106-
let meta = self
1107-
.meta
1108-
.get_mut(index as usize)
1109-
.expect("Entity index invalid");
1110-
meta.spawned_or_despawned_by = MaybeLocation::new(Some(caller));
1111-
});
1112-
}
1113-
11141132
/// Returns the source code location from which this entity has last been spawned
11151133
/// or despawned. Returns `None` if its index has been reused by another entity
11161134
/// or if this entity has never existed.
@@ -1119,16 +1137,67 @@ impl Entities {
11191137
entity: Entity,
11201138
) -> MaybeLocation<Option<&'static Location<'static>>> {
11211139
MaybeLocation::new_with_flattened(|| {
1122-
self.meta
1123-
.get(entity.index() as usize)
1124-
.filter(|meta|
1125-
// Generation is incremented immediately upon despawn
1126-
(meta.generation == entity.generation)
1127-
|| (meta.location.archetype_id == ArchetypeId::INVALID)
1128-
&& (meta.generation == entity.generation.after_versions(1)))
1129-
.map(|meta| meta.spawned_or_despawned_by)
1140+
self.entity_get_spawned_or_despawned(entity)
1141+
.map(|spawned_or_despawned| spawned_or_despawned.by)
11301142
})
1131-
.map(Option::flatten)
1143+
}
1144+
1145+
/// Returns the [`Tick`] at which this entity has last been spawned or despawned.
1146+
/// Returns `None` if its index has been reused by another entity or if this entity
1147+
/// has never existed.
1148+
pub fn entity_get_spawned_or_despawned_at(&self, entity: Entity) -> Option<Tick> {
1149+
self.entity_get_spawned_or_despawned(entity)
1150+
.map(|spawned_or_despawned| spawned_or_despawned.at)
1151+
}
1152+
1153+
/// Returns the [`SpawnedOrDespawned`] related to the entity's last spawn or
1154+
/// respawn. Returns `None` if its index has been reused by another entity or if
1155+
/// this entity has never existed.
1156+
#[inline]
1157+
fn entity_get_spawned_or_despawned(&self, entity: Entity) -> Option<SpawnedOrDespawned> {
1158+
self.meta
1159+
.get(entity.index() as usize)
1160+
.filter(|meta|
1161+
// Generation is incremented immediately upon despawn
1162+
(meta.generation == entity.generation)
1163+
|| (meta.location.archetype_id == ArchetypeId::INVALID)
1164+
&& (meta.generation == entity.generation.after_versions(1)))
1165+
.map(|meta| {
1166+
// SAFETY: valid archetype or non-min generation is proof this is init
1167+
unsafe { meta.spawned_or_despawned.assume_init() }
1168+
})
1169+
}
1170+
1171+
/// Returns the source code location from which this entity has last been spawned
1172+
/// or despawned and the Tick of when that happened.
1173+
///
1174+
/// # Safety
1175+
///
1176+
/// The entity index must belong to an entity that is currently alive or, if it
1177+
/// despawned, was not overwritten by a new entity of the same index.
1178+
#[inline]
1179+
pub(crate) unsafe fn entity_get_spawned_or_despawned_unchecked(
1180+
&self,
1181+
entity: Entity,
1182+
) -> (MaybeLocation, Tick) {
1183+
// SAFETY: caller ensures entity is allocated
1184+
let meta = unsafe { self.meta.get_unchecked(entity.index() as usize) };
1185+
// SAFETY: caller ensures entities of this index were at least spawned
1186+
let spawned_or_despawned = unsafe { meta.spawned_or_despawned.assume_init() };
1187+
(spawned_or_despawned.by, spawned_or_despawned.at)
1188+
}
1189+
1190+
#[inline]
1191+
pub(crate) fn check_change_ticks(&mut self, change_tick: Tick) {
1192+
for meta in &mut self.meta {
1193+
if meta.generation != EntityGeneration::FIRST
1194+
|| meta.location.archetype_id != ArchetypeId::INVALID
1195+
{
1196+
// SAFETY: non-min generation or valid archetype is proof this is init
1197+
let spawned_or_despawned = unsafe { meta.spawned_or_despawned.assume_init_mut() };
1198+
spawned_or_despawned.at.check_tick(change_tick);
1199+
}
1200+
}
11321201
}
11331202

11341203
/// Constructs a message explaining why an entity does not exist, if known.
@@ -1191,15 +1260,21 @@ struct EntityMeta {
11911260
/// The current location of the [`EntityRow`]
11921261
pub location: EntityLocation,
11931262
/// Location of the last spawn or despawn of this entity
1194-
spawned_or_despawned_by: MaybeLocation<Option<&'static Location<'static>>>,
1263+
spawned_or_despawned: MaybeUninit<SpawnedOrDespawned>,
1264+
}
1265+
1266+
#[derive(Copy, Clone, Debug)]
1267+
struct SpawnedOrDespawned {
1268+
by: MaybeLocation,
1269+
at: Tick,
11951270
}
11961271

11971272
impl EntityMeta {
11981273
/// meta for **pending entity**
11991274
const EMPTY: EntityMeta = EntityMeta {
12001275
generation: EntityGeneration::FIRST,
12011276
location: EntityLocation::INVALID,
1202-
spawned_or_despawned_by: MaybeLocation::new(None),
1277+
spawned_or_despawned: MaybeUninit::uninit(),
12031278
};
12041279
}
12051280

crates/bevy_ecs/src/query/fetch.rs

Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@ use variadics_please::all_tuples;
3131
/// Gets the identifier of the queried entity.
3232
/// - **[`EntityLocation`].**
3333
/// Gets the location metadata of the queried entity.
34+
/// - **[`SpawnDetails`].**
35+
/// Gets the tick the entity was spawned at.
3436
/// - **[`EntityRef`].**
3537
/// Read-only access to arbitrary components on the queried entity.
3638
/// - **[`EntityMut`].**
@@ -486,6 +488,166 @@ unsafe impl QueryData for EntityLocation {
486488
/// SAFETY: access is read only
487489
unsafe impl ReadOnlyQueryData for EntityLocation {}
488490

491+
/// The `SpawnDetails` query parameter fetches the [`Tick`] the entity was spawned at.
492+
///
493+
/// To evaluate whether the spawn happened since the last time the system ran, the system
494+
/// param [`SystemChangeTick`](bevy_ecs::system::SystemChangeTick) needs to be used.
495+
///
496+
/// If the query should filter for spawned entities instead, use the
497+
/// [`Spawned`](bevy_ecs::query::Spawned) query filter instead.
498+
///
499+
/// # Examples
500+
///
501+
/// ```
502+
/// # use bevy_ecs::component::Component;
503+
/// # use bevy_ecs::entity::Entity;
504+
/// # use bevy_ecs::system::Query;
505+
/// # use bevy_ecs::query::Spawned;
506+
/// # use bevy_ecs::query::SpawnDetails;
507+
///
508+
/// fn print_spawn_details(query: Query<(Entity, SpawnDetails)>) {
509+
/// for (entity, spawn_details) in &query {
510+
/// if spawn_details.is_spawned() {
511+
/// print!("new ");
512+
/// }
513+
/// print!(
514+
/// "entity {:?} spawned at {:?}",
515+
/// entity,
516+
/// spawn_details.spawned_at()
517+
/// );
518+
/// match spawn_details.spawned_by().into_option() {
519+
/// Some(location) => println!(" by {:?}", location),
520+
/// None => println!()
521+
/// }
522+
/// }
523+
/// }
524+
///
525+
/// # bevy_ecs::system::assert_is_system(print_spawn_details);
526+
/// ```
527+
#[derive(Clone, Copy, Debug)]
528+
pub struct SpawnDetails {
529+
spawned_by: MaybeLocation,
530+
spawned_at: Tick,
531+
last_run: Tick,
532+
this_run: Tick,
533+
}
534+
535+
impl SpawnDetails {
536+
/// Returns `true` if the entity spawned since the last time this system ran.
537+
/// Otherwise, returns `false`.
538+
pub fn is_spawned(self) -> bool {
539+
self.spawned_at.is_newer_than(self.last_run, self.this_run)
540+
}
541+
542+
/// Returns the `Tick` this entity spawned at.
543+
pub fn spawned_at(self) -> Tick {
544+
self.spawned_at
545+
}
546+
547+
/// Returns the source code location from which this entity has been spawned.
548+
pub fn spawned_by(self) -> MaybeLocation {
549+
self.spawned_by
550+
}
551+
}
552+
553+
#[doc(hidden)]
554+
#[derive(Clone)]
555+
pub struct SpawnDetailsFetch<'w> {
556+
entities: &'w Entities,
557+
last_run: Tick,
558+
this_run: Tick,
559+
}
560+
561+
// SAFETY:
562+
// No components are accessed.
563+
unsafe impl WorldQuery for SpawnDetails {
564+
type Fetch<'w> = SpawnDetailsFetch<'w>;
565+
type State = ();
566+
567+
fn shrink_fetch<'wlong: 'wshort, 'wshort>(fetch: Self::Fetch<'wlong>) -> Self::Fetch<'wshort> {
568+
fetch
569+
}
570+
571+
unsafe fn init_fetch<'w>(
572+
world: UnsafeWorldCell<'w>,
573+
_state: &Self::State,
574+
last_run: Tick,
575+
this_run: Tick,
576+
) -> Self::Fetch<'w> {
577+
SpawnDetailsFetch {
578+
entities: world.entities(),
579+
last_run,
580+
this_run,
581+
}
582+
}
583+
584+
const IS_DENSE: bool = true;
585+
586+
#[inline]
587+
unsafe fn set_archetype<'w>(
588+
_fetch: &mut Self::Fetch<'w>,
589+
_state: &Self::State,
590+
_archetype: &'w Archetype,
591+
_table: &'w Table,
592+
) {
593+
}
594+
595+
#[inline]
596+
unsafe fn set_table<'w>(_fetch: &mut Self::Fetch<'w>, _state: &Self::State, _table: &'w Table) {
597+
}
598+
599+
fn update_component_access(_state: &Self::State, _access: &mut FilteredAccess<ComponentId>) {}
600+
601+
fn init_state(_world: &mut World) {}
602+
603+
fn get_state(_components: &Components) -> Option<()> {
604+
Some(())
605+
}
606+
607+
fn matches_component_set(
608+
_state: &Self::State,
609+
_set_contains_id: &impl Fn(ComponentId) -> bool,
610+
) -> bool {
611+
true
612+
}
613+
}
614+
615+
// SAFETY:
616+
// No components are accessed.
617+
// Is its own ReadOnlyQueryData.
618+
unsafe impl QueryData for SpawnDetails {
619+
const IS_READ_ONLY: bool = true;
620+
type ReadOnly = Self;
621+
type Item<'w> = Self;
622+
623+
fn shrink<'wlong: 'wshort, 'wshort>(item: Self::Item<'wlong>) -> Self::Item<'wshort> {
624+
item
625+
}
626+
627+
#[inline(always)]
628+
unsafe fn fetch<'w>(
629+
fetch: &mut Self::Fetch<'w>,
630+
entity: Entity,
631+
_table_row: TableRow,
632+
) -> Self::Item<'w> {
633+
// SAFETY: only living entities are queried
634+
let (spawned_by, spawned_at) = unsafe {
635+
fetch
636+
.entities
637+
.entity_get_spawned_or_despawned_unchecked(entity)
638+
};
639+
Self {
640+
spawned_by,
641+
spawned_at,
642+
last_run: fetch.last_run,
643+
this_run: fetch.this_run,
644+
}
645+
}
646+
}
647+
648+
/// SAFETY: access is read only
649+
unsafe impl ReadOnlyQueryData for SpawnDetails {}
650+
489651
/// The [`WorldQuery::Fetch`] type for WorldQueries that can fetch multiple components from an entity
490652
/// ([`EntityRef`], [`EntityMut`], etc.)
491653
#[derive(Copy, Clone)]

0 commit comments

Comments
 (0)