Skip to content

Commit 732b2e0

Browse files
urben1680ElliottjPiercealice-i-cecile
authored
Track spawn Tick of entities, offer methods, query data SpawnDetails and query filter Spawned (#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](#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 <79881080+ElliottjPierce@users.noreply.github.com> Co-authored-by: Alice Cecile <alice.i.cecile@gmail.com>
1 parent 12aba64 commit 732b2e0

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")]
@@ -899,7 +906,10 @@ impl Entities {
899906
}
900907

901908
/// Updates the location of an [`Entity`]. This must be called when moving the components of
902-
/// the entity around in storage.
909+
/// the existing entity around in storage.
910+
///
911+
/// For spawning and despawning entities, [`set_spawn_despawn`](Self::set_spawn_despawn) must
912+
/// be used instead.
903913
///
904914
/// # Safety
905915
/// - `index` must be a valid entity index.
@@ -912,6 +922,27 @@ impl Entities {
912922
meta.location = location;
913923
}
914924

925+
/// Updates the location of an [`Entity`]. This must be called when moving the components of
926+
/// the spawned or despawned entity around in storage.
927+
///
928+
/// # Safety
929+
/// - `index` must be a valid entity index.
930+
/// - `location` must be valid for the entity at `index` or immediately made valid afterwards
931+
/// before handing control to unknown code.
932+
#[inline]
933+
pub(crate) unsafe fn set_spawn_despawn(
934+
&mut self,
935+
index: u32,
936+
location: EntityLocation,
937+
by: MaybeLocation,
938+
at: Tick,
939+
) {
940+
// SAFETY: Caller guarantees that `index` a valid entity index
941+
let meta = unsafe { self.meta.get_unchecked_mut(index as usize) };
942+
meta.location = location;
943+
meta.spawned_or_despawned = MaybeUninit::new(SpawnedOrDespawned { by, at });
944+
}
945+
915946
/// Increments the `generation` of a freed [`Entity`]. The next entity ID allocated with this
916947
/// `index` will count `generation` starting from the prior `generation` + the specified
917948
/// value + 1.
@@ -1052,19 +1083,6 @@ impl Entities {
10521083
self.len() == 0
10531084
}
10541085

1055-
/// Sets the source code location from which this entity has last been spawned
1056-
/// or despawned.
1057-
#[inline]
1058-
pub(crate) fn set_spawned_or_despawned_by(&mut self, index: u32, caller: MaybeLocation) {
1059-
caller.map(|caller| {
1060-
let meta = self
1061-
.meta
1062-
.get_mut(index as usize)
1063-
.expect("Entity index invalid");
1064-
meta.spawned_or_despawned_by = MaybeLocation::new(Some(caller));
1065-
});
1066-
}
1067-
10681086
/// Returns the source code location from which this entity has last been spawned
10691087
/// or despawned. Returns `None` if its index has been reused by another entity
10701088
/// or if this entity has never existed.
@@ -1073,16 +1091,67 @@ impl Entities {
10731091
entity: Entity,
10741092
) -> MaybeLocation<Option<&'static Location<'static>>> {
10751093
MaybeLocation::new_with_flattened(|| {
1076-
self.meta
1077-
.get(entity.index() as usize)
1078-
.filter(|meta|
1079-
// Generation is incremented immediately upon despawn
1080-
(meta.generation == entity.generation)
1081-
|| (meta.location.archetype_id == ArchetypeId::INVALID)
1082-
&& (meta.generation == entity.generation.after_versions(1)))
1083-
.map(|meta| meta.spawned_or_despawned_by)
1094+
self.entity_get_spawned_or_despawned(entity)
1095+
.map(|spawned_or_despawned| spawned_or_despawned.by)
10841096
})
1085-
.map(Option::flatten)
1097+
}
1098+
1099+
/// Returns the [`Tick`] at which this entity has last been spawned or despawned.
1100+
/// Returns `None` if its index has been reused by another entity or if this entity
1101+
/// has never existed.
1102+
pub fn entity_get_spawned_or_despawned_at(&self, entity: Entity) -> Option<Tick> {
1103+
self.entity_get_spawned_or_despawned(entity)
1104+
.map(|spawned_or_despawned| spawned_or_despawned.at)
1105+
}
1106+
1107+
/// Returns the [`SpawnedOrDespawned`] related to the entity's last spawn or
1108+
/// respawn. Returns `None` if its index has been reused by another entity or if
1109+
/// this entity has never existed.
1110+
#[inline]
1111+
fn entity_get_spawned_or_despawned(&self, entity: Entity) -> Option<SpawnedOrDespawned> {
1112+
self.meta
1113+
.get(entity.index() as usize)
1114+
.filter(|meta|
1115+
// Generation is incremented immediately upon despawn
1116+
(meta.generation == entity.generation)
1117+
|| (meta.location.archetype_id == ArchetypeId::INVALID)
1118+
&& (meta.generation == entity.generation.after_versions(1)))
1119+
.map(|meta| {
1120+
// SAFETY: valid archetype or non-min generation is proof this is init
1121+
unsafe { meta.spawned_or_despawned.assume_init() }
1122+
})
1123+
}
1124+
1125+
/// Returns the source code location from which this entity has last been spawned
1126+
/// or despawned and the Tick of when that happened.
1127+
///
1128+
/// # Safety
1129+
///
1130+
/// The entity index must belong to an entity that is currently alive or, if it
1131+
/// despawned, was not overwritten by a new entity of the same index.
1132+
#[inline]
1133+
pub(crate) unsafe fn entity_get_spawned_or_despawned_unchecked(
1134+
&self,
1135+
entity: Entity,
1136+
) -> (MaybeLocation, Tick) {
1137+
// SAFETY: caller ensures entity is allocated
1138+
let meta = unsafe { self.meta.get_unchecked(entity.index() as usize) };
1139+
// SAFETY: caller ensures entities of this index were at least spawned
1140+
let spawned_or_despawned = unsafe { meta.spawned_or_despawned.assume_init() };
1141+
(spawned_or_despawned.by, spawned_or_despawned.at)
1142+
}
1143+
1144+
#[inline]
1145+
pub(crate) fn check_change_ticks(&mut self, change_tick: Tick) {
1146+
for meta in &mut self.meta {
1147+
if meta.generation != EntityGeneration::FIRST
1148+
|| meta.location.archetype_id != ArchetypeId::INVALID
1149+
{
1150+
// SAFETY: non-min generation or valid archetype is proof this is init
1151+
let spawned_or_despawned = unsafe { meta.spawned_or_despawned.assume_init_mut() };
1152+
spawned_or_despawned.at.check_tick(change_tick);
1153+
}
1154+
}
10861155
}
10871156

10881157
/// Constructs a message explaining why an entity does not exist, if known.
@@ -1145,15 +1214,21 @@ struct EntityMeta {
11451214
/// The current location of the [`EntityRow`]
11461215
pub location: EntityLocation,
11471216
/// Location of the last spawn or despawn of this entity
1148-
spawned_or_despawned_by: MaybeLocation<Option<&'static Location<'static>>>,
1217+
spawned_or_despawned: MaybeUninit<SpawnedOrDespawned>,
1218+
}
1219+
1220+
#[derive(Copy, Clone, Debug)]
1221+
struct SpawnedOrDespawned {
1222+
by: MaybeLocation,
1223+
at: Tick,
11491224
}
11501225

11511226
impl EntityMeta {
11521227
/// meta for **pending entity**
11531228
const EMPTY: EntityMeta = EntityMeta {
11541229
generation: EntityGeneration::FIRST,
11551230
location: EntityLocation::INVALID,
1156-
spawned_or_despawned_by: MaybeLocation::new(None),
1231+
spawned_or_despawned: MaybeUninit::uninit(),
11571232
};
11581233
}
11591234

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)