From 50aa40e98068a6c5a6643ef04154093b96f47765 Mon Sep 17 00:00:00 2001 From: re0312 <45868716+re0312@users.noreply.github.com> Date: Tue, 3 Jun 2025 06:27:45 +0800 Subject: [PATCH] Trigger ArchetypeCreated event when new archetype is created (#19455) # Objective - Part 1 of #19454 . - Split from PR #18860(authored by @notmd) for better review and limit implementation impact. so all credit for this work belongs to @notmd . ## Solution - Trigger `ArchetypeCreated ` when new archetype is createed --------- Co-authored-by: mgi388 <135186256+mgi388@users.noreply.github.com> --- crates/bevy_ecs/src/archetype.rs | 28 ++++++--- crates/bevy_ecs/src/bundle.rs | 99 ++++++++++++++++++++++++-------- 2 files changed, 93 insertions(+), 34 deletions(-) diff --git a/crates/bevy_ecs/src/archetype.rs b/crates/bevy_ecs/src/archetype.rs index f12cd03a69..4e5b28dde8 100644 --- a/crates/bevy_ecs/src/archetype.rs +++ b/crates/bevy_ecs/src/archetype.rs @@ -23,17 +23,22 @@ use crate::{ bundle::BundleId, component::{ComponentId, Components, RequiredComponentConstructor, StorageType}, entity::{Entity, EntityLocation}, + event::Event, observer::Observers, storage::{ImmutableSparseSet, SparseArray, SparseSet, TableId, TableRow}, }; use alloc::{boxed::Box, vec::Vec}; -use bevy_platform::collections::HashMap; +use bevy_platform::collections::{hash_map::Entry, HashMap}; use core::{ hash::Hash, ops::{Index, IndexMut, RangeFrom}, }; use nonmax::NonMaxU32; +#[derive(Event)] +#[expect(dead_code, reason = "Prepare for the upcoming Query as Entities")] +pub(crate) struct ArchetypeCreated(pub ArchetypeId); + /// An opaque location within a [`Archetype`]. /// /// This can be used in conjunction with [`ArchetypeId`] to find the exact location @@ -869,6 +874,10 @@ impl Archetypes { } /// Gets the archetype id matching the given inputs or inserts a new one if it doesn't exist. + /// + /// Specifically, it returns a tuple where the first element + /// is the [`ArchetypeId`] that the given inputs belong to, and the second element is a boolean indicating whether a new archetype was created. + /// /// `table_components` and `sparse_set_components` must be sorted /// /// # Safety @@ -881,7 +890,7 @@ impl Archetypes { table_id: TableId, table_components: Vec, sparse_set_components: Vec, - ) -> ArchetypeId { + ) -> (ArchetypeId, bool) { let archetype_identity = ArchetypeComponents { sparse_set_components: sparse_set_components.into_boxed_slice(), table_components: table_components.into_boxed_slice(), @@ -889,14 +898,13 @@ impl Archetypes { let archetypes = &mut self.archetypes; let component_index = &mut self.by_component; - *self - .by_components - .entry(archetype_identity) - .or_insert_with_key(move |identity| { + match self.by_components.entry(archetype_identity) { + Entry::Occupied(occupied) => (*occupied.get(), false), + Entry::Vacant(vacant) => { let ArchetypeComponents { table_components, sparse_set_components, - } = identity; + } = vacant.key(); let id = ArchetypeId::new(archetypes.len()); archetypes.push(Archetype::new( components, @@ -907,8 +915,10 @@ impl Archetypes { table_components.iter().copied(), sparse_set_components.iter().copied(), )); - id - }) + vacant.insert(id); + (id, true) + } + } } /// Clears all entities from all archetypes. diff --git a/crates/bevy_ecs/src/bundle.rs b/crates/bevy_ecs/src/bundle.rs index df5c51eef1..f0641ff80c 100644 --- a/crates/bevy_ecs/src/bundle.rs +++ b/crates/bevy_ecs/src/bundle.rs @@ -6,8 +6,8 @@ pub use bevy_ecs_macros::Bundle; use crate::{ archetype::{ - Archetype, ArchetypeAfterBundleInsert, ArchetypeId, Archetypes, BundleComponentStatus, - ComponentStatus, SpawnBundleStatus, + Archetype, ArchetypeAfterBundleInsert, ArchetypeCreated, ArchetypeId, Archetypes, + BundleComponentStatus, ComponentStatus, SpawnBundleStatus, }, change_detection::MaybeLocation, component::{ @@ -732,7 +732,7 @@ impl BundleInfo { } } - /// Inserts a bundle into the given archetype and returns the resulting archetype. + /// Inserts a bundle into the given archetype and returns the resulting archetype and whether a new archetype was created. /// This could be the same [`ArchetypeId`], in the event that inserting the given bundle /// does not result in an [`Archetype`] change. /// @@ -747,12 +747,12 @@ impl BundleInfo { components: &Components, observers: &Observers, archetype_id: ArchetypeId, - ) -> ArchetypeId { + ) -> (ArchetypeId, bool) { if let Some(archetype_after_insert_id) = archetypes[archetype_id] .edges() .get_archetype_after_bundle_insert(self.id) { - return archetype_after_insert_id; + return (archetype_after_insert_id, false); } let mut new_table_components = Vec::new(); let mut new_sparse_set_components = Vec::new(); @@ -806,7 +806,7 @@ impl BundleInfo { added, existing, ); - archetype_id + (archetype_id, false) } else { let table_id; let table_components; @@ -842,13 +842,14 @@ impl BundleInfo { }; }; // SAFETY: ids in self must be valid - let new_archetype_id = archetypes.get_id_or_insert( + let (new_archetype_id, is_new_created) = archetypes.get_id_or_insert( components, observers, table_id, table_components, sparse_set_components, ); + // Add an edge from the old archetype to the new archetype. archetypes[archetype_id] .edges_mut() @@ -860,11 +861,11 @@ impl BundleInfo { added, existing, ); - new_archetype_id + (new_archetype_id, is_new_created) } } - /// Removes a bundle from the given archetype and returns the resulting archetype + /// Removes a bundle from the given archetype and returns the resulting archetype and whether a new archetype was created. /// (or `None` if the removal was invalid). /// This could be the same [`ArchetypeId`], in the event that removing the given bundle /// does not result in an [`Archetype`] change. @@ -887,7 +888,7 @@ impl BundleInfo { observers: &Observers, archetype_id: ArchetypeId, intersection: bool, - ) -> Option { + ) -> (Option, bool) { // Check the archetype graph to see if the bundle has been // removed from this archetype in the past. let archetype_after_remove_result = { @@ -898,9 +899,9 @@ impl BundleInfo { edges.get_archetype_after_bundle_take(self.id()) } }; - let result = if let Some(result) = archetype_after_remove_result { + let (result, is_new_created) = if let Some(result) = archetype_after_remove_result { // This bundle removal result is cached. Just return that! - result + (result, false) } else { let mut next_table_components; let mut next_sparse_set_components; @@ -925,7 +926,7 @@ impl BundleInfo { current_archetype .edges_mut() .cache_archetype_after_bundle_take(self.id(), None); - return None; + return (None, false); } } @@ -953,14 +954,14 @@ impl BundleInfo { }; } - let new_archetype_id = archetypes.get_id_or_insert( + let (new_archetype_id, is_new_created) = archetypes.get_id_or_insert( components, observers, next_table_id, next_table_components, next_sparse_set_components, ); - Some(new_archetype_id) + (Some(new_archetype_id), is_new_created) }; let current_archetype = &mut archetypes[archetype_id]; // Cache the result in an edge. @@ -973,7 +974,7 @@ impl BundleInfo { .edges_mut() .cache_archetype_after_bundle_take(self.id(), result); } - result + (result, is_new_created) } } @@ -1036,14 +1037,15 @@ impl<'w> BundleInserter<'w> { // SAFETY: We will not make any accesses to the command queue, component or resource data of this world let bundle_info = world.bundles.get_unchecked(bundle_id); let bundle_id = bundle_info.id(); - let new_archetype_id = bundle_info.insert_bundle_into_archetype( + let (new_archetype_id, is_new_created) = bundle_info.insert_bundle_into_archetype( &mut world.archetypes, &mut world.storages, &world.components, &world.observers, archetype_id, ); - if new_archetype_id == archetype_id { + + let inserter = if new_archetype_id == archetype_id { let archetype = &mut world.archetypes[archetype_id]; // SAFETY: The edge is assured to be initialized when we called insert_bundle_into_archetype let archetype_after_insert = unsafe { @@ -1103,7 +1105,15 @@ impl<'w> BundleInserter<'w> { world: world.as_unsafe_world_cell(), } } + }; + + if is_new_created { + inserter + .world + .into_deferred() + .trigger(ArchetypeCreated(new_archetype_id)); } + inserter } /// # Safety @@ -1421,7 +1431,7 @@ impl<'w> BundleRemover<'w> { ) -> Option { let bundle_info = world.bundles.get_unchecked(bundle_id); // SAFETY: Caller ensures archetype and bundle ids are correct. - let new_archetype_id = unsafe { + let (new_archetype_id, is_new_created) = unsafe { bundle_info.remove_bundle_from_archetype( &mut world.archetypes, &mut world.storages, @@ -1429,11 +1439,14 @@ impl<'w> BundleRemover<'w> { &world.observers, archetype_id, !require_all, - )? + ) }; + let new_archetype_id = new_archetype_id?; + if new_archetype_id == archetype_id { return None; } + let (old_archetype, new_archetype) = world.archetypes.get_2_mut(archetype_id, new_archetype_id); @@ -1447,13 +1460,20 @@ impl<'w> BundleRemover<'w> { Some((old.into(), new.into())) }; - Some(Self { + let remover = Self { bundle_info: bundle_info.into(), new_archetype: new_archetype.into(), old_archetype: old_archetype.into(), old_and_new_table: tables, world: world.as_unsafe_world_cell(), - }) + }; + if is_new_created { + remover + .world + .into_deferred() + .trigger(ArchetypeCreated(new_archetype_id)); + } + Some(remover) } /// This can be passed to [`remove`](Self::remove) as the `pre_remove` function if you don't want to do anything before removing. @@ -1675,22 +1695,30 @@ impl<'w> BundleSpawner<'w> { change_tick: Tick, ) -> Self { let bundle_info = world.bundles.get_unchecked(bundle_id); - let new_archetype_id = bundle_info.insert_bundle_into_archetype( + let (new_archetype_id, is_new_created) = bundle_info.insert_bundle_into_archetype( &mut world.archetypes, &mut world.storages, &world.components, &world.observers, ArchetypeId::EMPTY, ); + let archetype = &mut world.archetypes[new_archetype_id]; let table = &mut world.storages.tables[archetype.table_id()]; - Self { + let spawner = Self { bundle_info: bundle_info.into(), table: table.into(), archetype: archetype.into(), change_tick, world: world.as_unsafe_world_cell(), + }; + if is_new_created { + spawner + .world + .into_deferred() + .trigger(ArchetypeCreated(new_archetype_id)); } + spawner } #[inline] @@ -2043,7 +2071,9 @@ fn sorted_remove(source: &mut Vec, remove: &[T]) { #[cfg(test)] mod tests { - use crate::{component::HookContext, prelude::*, world::DeferredWorld}; + use crate::{ + archetype::ArchetypeCreated, component::HookContext, prelude::*, world::DeferredWorld, + }; use alloc::vec; #[derive(Component)] @@ -2280,4 +2310,23 @@ mod tests { assert_eq!(a, vec![1]); } + + #[test] + fn new_archetype_created() { + let mut world = World::new(); + #[derive(Resource, Default)] + struct Count(u32); + world.init_resource::(); + world.add_observer(|_t: Trigger, mut count: ResMut| { + count.0 += 1; + }); + + let mut e = world.spawn((A, B)); + e.insert(C); + e.remove::(); + e.insert(A); + e.insert(A); + + assert_eq!(world.resource::().0, 3); + } }