diff --git a/benches/benches/bevy_ecs/world/world_get.rs b/benches/benches/bevy_ecs/world/world_get.rs index 81e0bf2b0f..8efe626a4b 100644 --- a/benches/benches/bevy_ecs/world/world_get.rs +++ b/benches/benches/bevy_ecs/world/world_get.rs @@ -51,9 +51,10 @@ pub fn world_entity(criterion: &mut Criterion) { for entity_count in RANGE.map(|i| i * 10_000) { group.bench_function(format!("{entity_count}_entities"), |bencher| { let world = setup::(entity_count); + let offset = world.resource_count(); bencher.iter(|| { - for i in 0..entity_count { + for i in offset..(entity_count + offset) { let entity = // SAFETY: Range is exclusive. Entity::from_raw(EntityRow::new(unsafe { NonMaxU32::new_unchecked(i) })); @@ -74,9 +75,10 @@ pub fn world_get(criterion: &mut Criterion) { for entity_count in RANGE.map(|i| i * 10_000) { group.bench_function(format!("{entity_count}_entities_table"), |bencher| { let world = setup::
(entity_count); + let offset = world.resource_count(); bencher.iter(|| { - for i in 0..entity_count { + for i in offset..(entity_count + offset) { let entity = // SAFETY: Range is exclusive. Entity::from_raw(EntityRow::new(unsafe { NonMaxU32::new_unchecked(i) })); @@ -86,9 +88,10 @@ pub fn world_get(criterion: &mut Criterion) { }); group.bench_function(format!("{entity_count}_entities_sparse"), |bencher| { let world = setup::(entity_count); + let offset = world.resource_count(); bencher.iter(|| { - for i in 0..entity_count { + for i in offset..(entity_count + offset) { let entity = // SAFETY: Range is exclusive. Entity::from_raw(EntityRow::new(unsafe { NonMaxU32::new_unchecked(i) })); @@ -109,10 +112,11 @@ pub fn world_query_get(criterion: &mut Criterion) { for entity_count in RANGE.map(|i| i * 10_000) { group.bench_function(format!("{entity_count}_entities_table"), |bencher| { let mut world = setup::
(entity_count); + let offset = world.resource_count(); let mut query = world.query::<&Table>(); bencher.iter(|| { - for i in 0..entity_count { + for i in offset..(entity_count + offset) { let entity = // SAFETY: Range is exclusive. Entity::from_raw(EntityRow::new(unsafe { NonMaxU32::new_unchecked(i) })); @@ -137,9 +141,10 @@ pub fn world_query_get(criterion: &mut Criterion) { &WideTable<4>, &WideTable<5>, )>(); + let offset = world.resource_count(); bencher.iter(|| { - for i in 0..entity_count { + for i in offset..(entity_count + offset) { let entity = // SAFETY: Range is exclusive. Entity::from_raw(EntityRow::new(unsafe { NonMaxU32::new_unchecked(i) })); @@ -149,10 +154,11 @@ pub fn world_query_get(criterion: &mut Criterion) { }); group.bench_function(format!("{entity_count}_entities_sparse"), |bencher| { let mut world = setup::(entity_count); + let offset = world.resource_count(); let mut query = world.query::<&Sparse>(); bencher.iter(|| { - for i in 0..entity_count { + for i in offset..(entity_count + offset) { let entity = // SAFETY: Range is exclusive. Entity::from_raw(EntityRow::new(unsafe { NonMaxU32::new_unchecked(i) })); @@ -177,9 +183,10 @@ pub fn world_query_get(criterion: &mut Criterion) { &WideSparse<4>, &WideSparse<5>, )>(); + let offset = world.resource_count(); bencher.iter(|| { - for i in 0..entity_count { + for i in offset..(entity_count + offset) { // SAFETY: Range is exclusive. let entity = Entity::from_raw(EntityRow::new(unsafe { NonMaxU32::new_unchecked(i) })); diff --git a/crates/bevy_ecs/src/component/info.rs b/crates/bevy_ecs/src/component/info.rs index 3c3059f6be..ba1e13277e 100644 --- a/crates/bevy_ecs/src/component/info.rs +++ b/crates/bevy_ecs/src/component/info.rs @@ -1,5 +1,5 @@ use alloc::{borrow::Cow, vec::Vec}; -use bevy_platform::{collections::HashSet, sync::PoisonError}; +use bevy_platform::{collections::HashMap, collections::HashSet, sync::PoisonError}; use bevy_ptr::OwningPtr; #[cfg(feature = "bevy_reflect")] use bevy_reflect::Reflect; @@ -18,6 +18,7 @@ use crate::{ RequiredComponents, StorageType, }, lifecycle::ComponentHooks, + prelude::Entity, query::DebugCheckedUnwrap as _, resource::Resource, storage::SparseSetIndex, @@ -346,6 +347,9 @@ pub struct Components { pub(super) components: Vec>, pub(super) indices: TypeIdMap, pub(super) resource_indices: TypeIdMap, + /// A lookup for the entities on which resources are stored. + /// It uses `ComponentId`s instead of `TypeId`s for untyped APIs + pub(crate) resource_entities: HashMap, // This is kept internal and local to verify that no deadlocks can occor. pub(super) queued: bevy_platform::sync::RwLock, } diff --git a/crates/bevy_ecs/src/entity_disabling.rs b/crates/bevy_ecs/src/entity_disabling.rs index 39d2aa27e8..65f9fe2dd2 100644 --- a/crates/bevy_ecs/src/entity_disabling.rs +++ b/crates/bevy_ecs/src/entity_disabling.rs @@ -70,6 +70,7 @@ use crate::{ component::{ComponentId, Components, StorageType}, query::FilteredAccess, + resource::IsResource, world::{FromWorld, World}, }; use bevy_ecs_macros::{Component, Resource}; @@ -143,6 +144,8 @@ impl FromWorld for DefaultQueryFilters { let mut filters = DefaultQueryFilters::empty(); let disabled_component_id = world.register_component::(); filters.register_disabling_component(disabled_component_id); + let is_resource_component_id = world.register_component::(); + filters.register_disabling_component(is_resource_component_id); filters } } diff --git a/crates/bevy_ecs/src/lib.rs b/crates/bevy_ecs/src/lib.rs index 8580df7a3b..e68031d749 100644 --- a/crates/bevy_ecs/src/lib.rs +++ b/crates/bevy_ecs/src/lib.rs @@ -378,9 +378,9 @@ mod tests { let mut world = World::new(); let e = world.spawn((TableStored("abc"), A(123))).id(); let f = world.spawn((TableStored("def"), A(456))).id(); - assert_eq!(world.entities.len(), 2); + assert_eq!(world.entity_count(), 2); assert!(world.despawn(e)); - assert_eq!(world.entities.len(), 1); + assert_eq!(world.entity_count(), 1); assert!(world.get::(e).is_none()); assert!(world.get::(e).is_none()); assert_eq!(world.get::(f).unwrap().0, "def"); @@ -393,9 +393,9 @@ mod tests { let e = world.spawn((TableStored("abc"), SparseStored(123))).id(); let f = world.spawn((TableStored("def"), SparseStored(456))).id(); - assert_eq!(world.entities.len(), 2); + assert_eq!(world.entity_count(), 2); assert!(world.despawn(e)); - assert_eq!(world.entities.len(), 1); + assert_eq!(world.entity_count(), 1); assert!(world.get::(e).is_none()); assert!(world.get::(e).is_none()); assert_eq!(world.get::(f).unwrap().0, "def"); @@ -1786,7 +1786,7 @@ mod tests { fn try_insert_batch() { let mut world = World::default(); let e0 = world.spawn(A(0)).id(); - let e1 = Entity::from_raw_u32(1).unwrap(); + let e1 = Entity::from_raw_u32(2).unwrap(); let values = vec![(e0, (A(1), B(0))), (e1, (A(0), B(1)))]; @@ -1810,7 +1810,7 @@ mod tests { fn try_insert_batch_if_new() { let mut world = World::default(); let e0 = world.spawn(A(0)).id(); - let e1 = Entity::from_raw_u32(1).unwrap(); + let e1 = Entity::from_raw_u32(2).unwrap(); let values = vec![(e0, (A(1), B(0))), (e1, (A(0), B(1)))]; diff --git a/crates/bevy_ecs/src/name.rs b/crates/bevy_ecs/src/name.rs index 317c8f5017..2887475cc6 100644 --- a/crates/bevy_ecs/src/name.rs +++ b/crates/bevy_ecs/src/name.rs @@ -278,7 +278,7 @@ mod tests { let mut query = world.query::(); let d1 = query.get(&world, e1).unwrap(); // NameOrEntity Display for entities without a Name should be {index}v{generation} - assert_eq!(d1.to_string(), "0v0"); + assert_eq!(d1.to_string(), "1v0"); let d2 = query.get(&world, e2).unwrap(); // NameOrEntity Display for entities with a Name should be the Name assert_eq!(d2.to_string(), "MyName"); diff --git a/crates/bevy_ecs/src/query/builder.rs b/crates/bevy_ecs/src/query/builder.rs index b545caad8f..33266f0c1c 100644 --- a/crates/bevy_ecs/src/query/builder.rs +++ b/crates/bevy_ecs/src/query/builder.rs @@ -275,6 +275,7 @@ impl<'w, D: QueryData, F: QueryFilter> QueryBuilder<'w, D, F> { mod tests { use crate::{ prelude::*, + resource::IsResource, world::{EntityMutExcept, EntityRefExcept, FilteredEntityMut, FilteredEntityRef}, }; use std::dbg; @@ -332,6 +333,7 @@ mod tests { #[test] fn builder_or() { let mut world = World::new(); + world.spawn((A(0), B(0))); world.spawn(B(0)); world.spawn(C(0)); @@ -485,6 +487,7 @@ mod tests { let mut query = QueryBuilder::<(FilteredEntityMut, EntityMutExcept)>::new(&mut world) .data::() + .filter::>() .build(); // Removing `EntityMutExcept` just leaves A @@ -496,6 +499,7 @@ mod tests { let mut query = QueryBuilder::<(FilteredEntityMut, EntityRefExcept)>::new(&mut world) .data::() + .filter::>() .build(); // Removing `EntityRefExcept` just leaves A, plus read access diff --git a/crates/bevy_ecs/src/query/iter.rs b/crates/bevy_ecs/src/query/iter.rs index eb49204434..1771a464f9 100644 --- a/crates/bevy_ecs/src/query/iter.rs +++ b/crates/bevy_ecs/src/query/iter.rs @@ -2669,6 +2669,7 @@ mod tests { #[test] fn query_iter_sorts() { let mut world = World::new(); + for i in 0..100 { world.spawn(A(i as f32)); world.spawn((A(i as f32), Sparse(i))); diff --git a/crates/bevy_ecs/src/query/state.rs b/crates/bevy_ecs/src/query/state.rs index 00d8b6f970..35251fe9d5 100644 --- a/crates/bevy_ecs/src/query/state.rs +++ b/crates/bevy_ecs/src/query/state.rs @@ -1850,6 +1850,7 @@ mod tests { #[test] fn can_transmute_empty_tuple() { let mut world = World::new(); + world.register_component::(); let entity = world.spawn(A(10)).id(); @@ -2172,9 +2173,7 @@ mod tests { world.spawn((B(0), C(0))); world.spawn(C(0)); - let mut df = DefaultQueryFilters::empty(); - df.register_disabling_component(world.register_component::()); - world.insert_resource(df); + world.register_disabling_component::(); // Without only matches the first entity let mut query = QueryState::<()>::new(&mut world); @@ -2216,9 +2215,9 @@ mod tests { assert!(query.is_dense); assert_eq!(3, query.iter(&world).count()); - let mut df = DefaultQueryFilters::empty(); - df.register_disabling_component(world.register_component::()); + let df = DefaultQueryFilters::from_world(&mut world); world.insert_resource(df); + world.register_disabling_component::(); let mut query = QueryState::<()>::new(&mut world); // The query doesn't ask for sparse components, but the default filters adds @@ -2226,9 +2225,9 @@ mod tests { assert!(!query.is_dense); assert_eq!(1, query.iter(&world).count()); - let mut df = DefaultQueryFilters::empty(); - df.register_disabling_component(world.register_component::
()); + let df = DefaultQueryFilters::from_world(&mut world); world.insert_resource(df); + world.register_disabling_component::
(); let mut query = QueryState::<()>::new(&mut world); // If the filter is instead a table components, the query can still be dense diff --git a/crates/bevy_ecs/src/resource.rs b/crates/bevy_ecs/src/resource.rs index 7da4f31113..06c5a05c30 100644 --- a/crates/bevy_ecs/src/resource.rs +++ b/crates/bevy_ecs/src/resource.rs @@ -1,5 +1,10 @@ //! Resources are unique, singleton-like data types that can be accessed from systems and stored in the [`World`](crate::world::World). +use crate::prelude::Component; +use crate::prelude::ReflectComponent; +use bevy_reflect::prelude::ReflectDefault; +use bevy_reflect::Reflect; +use core::marker::PhantomData; // The derive macro for the `Resource` trait pub use bevy_ecs_macros::Resource; @@ -73,3 +78,89 @@ pub use bevy_ecs_macros::Resource; note = "consider annotating `{Self}` with `#[derive(Resource)]`" )] pub trait Resource: Send + Sync + 'static {} + +/// A marker component for the entity that stores the resource of type `T`. +/// +/// This component is automatically inserted when a resource of type `T` is inserted into the world, +/// and can be used to find the entity that stores a particular resource. +/// +/// By contrast, the [`IsResource`] component is used to find all entities that store resources, +/// regardless of the type of resource they store. +/// +/// This component comes with a hook that ensures that at most one entity has this component for any given `R`: +/// adding this component to an entity (or spawning an entity with this component) will despawn any other entity with this component. +#[derive(Component, Debug)] +#[require(IsResource)] +#[cfg_attr(feature = "bevy_reflect", derive(Reflect), reflect(Component, Default))] +pub struct ResourceEntity(#[reflect(ignore)] PhantomData); + +impl Default for ResourceEntity { + fn default() -> Self { + ResourceEntity(PhantomData) + } +} + +/// A marker component for entities which store resources. +/// +/// By contrast, the [`ResourceEntity`] component is used to find the entity that stores a particular resource. +/// This component is required by the [`ResourceEntity`] component, and will automatically be added. +#[cfg_attr( + feature = "bevy_reflect", + derive(Reflect), + reflect(Component, Default, Debug) +)] +#[derive(Component, Default, Debug)] +pub struct IsResource; + +/// Used in conjunction with [`ResourceEntity`], when no type information is available. +/// This is used by [`World::insert_resource_by_id`](crate::world::World). +#[derive(Resource)] +pub(crate) struct TypeErasedResource; + +#[cfg(test)] +mod tests { + use crate::change_detection::MaybeLocation; + use crate::ptr::OwningPtr; + use crate::resource::Resource; + use crate::world::World; + use bevy_platform::prelude::String; + + #[test] + fn unique_resource_entities() { + #[derive(Default, Resource)] + struct TestResource1; + + #[derive(Resource)] + #[expect(dead_code, reason = "field needed for testing")] + struct TestResource2(String); + + #[derive(Resource)] + #[expect(dead_code, reason = "field needed for testing")] + struct TestResource3(u8); + + let mut world = World::new(); + let start = world.entities().len(); + world.init_resource::(); + assert_eq!(world.entities().len(), start + 1); + world.insert_resource(TestResource2(String::from("Foo"))); + assert_eq!(world.entities().len(), start + 2); + // like component registration, which just makes it known to the world that a component exists, + // registering a resource should not spawn an entity. + let id = world.register_resource::(); + assert_eq!(world.entities().len(), start + 2); + OwningPtr::make(20_u8, |ptr| { + // SAFETY: id was just initialized and corresponds to a resource. + unsafe { + world.insert_resource_by_id(id, ptr, MaybeLocation::caller()); + } + }); + assert_eq!(world.entities().len(), start + 3); + assert!(world.remove_resource_by_id(id).is_some()); + assert_eq!(world.entities().len(), start + 2); + world.remove_resource::(); + assert_eq!(world.entities().len(), start + 1); + // make sure that trying to add a resource twice results, doesn't change the entity count + world.insert_resource(TestResource2(String::from("Bar"))); + assert_eq!(world.entities().len(), start + 1); + } +} diff --git a/crates/bevy_ecs/src/system/system.rs b/crates/bevy_ecs/src/system/system.rs index fb28ea081a..cccd6d493b 100644 --- a/crates/bevy_ecs/src/system/system.rs +++ b/crates/bevy_ecs/src/system/system.rs @@ -501,9 +501,9 @@ mod tests { #[test] fn command_processing() { let mut world = World::new(); - assert_eq!(world.entities.len(), 0); + assert_eq!(world.entity_count(), 0); world.run_system_once(spawn_entity).unwrap(); - assert_eq!(world.entities.len(), 1); + assert_eq!(world.entity_count(), 1); } #[test] diff --git a/crates/bevy_ecs/src/world/entity_ref.rs b/crates/bevy_ecs/src/world/entity_ref.rs index 4303c5f55e..709861efc5 100644 --- a/crates/bevy_ecs/src/world/entity_ref.rs +++ b/crates/bevy_ecs/src/world/entity_ref.rs @@ -4926,6 +4926,7 @@ mod tests { change_detection::{MaybeLocation, MutUntyped}, component::ComponentId, prelude::*, + resource::IsResource, system::{assert_is_system, RunSystemOnce as _}, world::{error::EntityComponentError, DeferredWorld, FilteredEntityMut, FilteredEntityRef}, }; @@ -5374,7 +5375,7 @@ mod tests { world.spawn(TestComponent(0)).insert(TestComponent2(0)); - let mut query = world.query::>(); + let mut query = world.query::>(); let mut found = false; for entity_ref in query.iter_mut(&mut world) { @@ -5432,7 +5433,10 @@ mod tests { world.run_system_once(system).unwrap(); - fn system(_: Query<&mut TestComponent>, query: Query>) { + fn system( + _: Query<&mut TestComponent>, + query: Query>, + ) { for entity_ref in query.iter() { assert!(matches!( entity_ref.get::(), @@ -5449,7 +5453,7 @@ mod tests { let mut world = World::new(); world.spawn(TestComponent(0)).insert(TestComponent2(0)); - let mut query = world.query::>(); + let mut query = world.query::>(); let mut found = false; for mut entity_mut in query.iter_mut(&mut world) { @@ -5514,7 +5518,10 @@ mod tests { world.run_system_once(system).unwrap(); - fn system(_: Query<&mut TestComponent>, mut query: Query>) { + fn system( + _: Query<&mut TestComponent>, + mut query: Query>, + ) { for mut entity_mut in query.iter_mut() { assert!(entity_mut .get_mut::() diff --git a/crates/bevy_ecs/src/world/mod.rs b/crates/bevy_ecs/src/world/mod.rs index f6e3a197ef..458dac99ec 100644 --- a/crates/bevy_ecs/src/world/mod.rs +++ b/crates/bevy_ecs/src/world/mod.rs @@ -22,6 +22,7 @@ use crate::{ event::BufferedEvent, lifecycle::{ComponentHooks, ADD, DESPAWN, INSERT, REMOVE, REPLACE}, prelude::{Add, Despawn, Insert, Remove, Replace}, + resource::{ResourceEntity, TypeErasedResource}, }; pub use bevy_ecs_macros::FromWorld; use bevy_utils::prelude::DebugName; @@ -216,12 +217,20 @@ impl World { &mut self.entities } - /// Retrieves the number of [`Entities`] in the world. + /// Retrieves the number of [`Entities`] in the world. This count does not include resource entities. /// /// This is helpful as a diagnostic, but it can also be used effectively in tests. #[inline] pub fn entity_count(&self) -> u32 { - self.entities.len() + self.entities + .len() + .saturating_sub(self.components.resource_entities.len() as u32) + } + + /// Retrieves the number of [`Resource`]s in the world. + #[inline] + pub fn resource_count(&self) -> u32 { + self.components.resource_entities.len() as u32 } /// Retrieves this world's [`Archetypes`] collection. @@ -1701,6 +1710,18 @@ impl World { pub fn init_resource(&mut self) -> ComponentId { let caller = MaybeLocation::caller(); let component_id = self.components_registrator().register_resource::(); + + if !self + .components + .resource_entities + .contains_key(&component_id) + { + let entity = self.spawn(ResourceEntity::::default()).id(); + self.components + .resource_entities + .insert(component_id, entity); + } + if self .storages .resources @@ -1738,6 +1759,17 @@ impl World { caller: MaybeLocation, ) { let component_id = self.components_registrator().register_resource::(); + if !self + .components + .resource_entities + .contains_key(&component_id) + { + let entity = self.spawn(ResourceEntity::::default()).id(); + self.components + .resource_entities + .insert(component_id, entity); + } + OwningPtr::make(value, |ptr| { // SAFETY: component_id was just initialized and corresponds to resource of type R. unsafe { @@ -1805,6 +1837,10 @@ impl World { #[inline] pub fn remove_resource(&mut self) -> Option { let component_id = self.components.get_valid_resource_id(TypeId::of::())?; + if let Some(entity) = self.components.resource_entities.remove(&component_id) { + self.despawn(entity); + } + let (ptr, _, _) = self.storages.resources.get_mut(component_id)?.remove()?; // SAFETY: `component_id` was gotten via looking up the `R` type unsafe { Some(ptr.read::()) } @@ -2711,6 +2747,20 @@ impl World { ) { let change_tick = self.change_tick(); + if !self + .components + .resource_entities + .contains_key(&component_id) + { + // Since we don't know the type, we use a placeholder type. + let entity = self + .spawn(ResourceEntity::::default()) + .id(); + self.components + .resource_entities + .insert(component_id, entity); + } + let resource = self.initialize_resource_internal(component_id); // SAFETY: `value` is valid for `component_id`, ensured by caller unsafe { @@ -3403,6 +3453,10 @@ impl World { /// **You should prefer to use the typed API [`World::remove_resource`] where possible and only /// use this in cases where the actual types are not known at compile time.** pub fn remove_resource_by_id(&mut self, component_id: ComponentId) -> Option<()> { + if let Some(entity) = self.components.resource_entities.remove(&component_id) { + self.despawn(entity); + } + self.storages .resources .get_mut(component_id)? @@ -3676,7 +3730,7 @@ mod tests { entity::EntityHashSet, entity_disabling::{DefaultQueryFilters, Disabled}, ptr::OwningPtr, - resource::Resource, + resource::{IsResource, Resource}, world::{error::EntityMutableFetchError, DeferredWorld}, }; use alloc::{ @@ -4108,8 +4162,10 @@ mod tests { let iterate_and_count_entities = |world: &World, entity_counters: &mut HashMap<_, _>| { entity_counters.clear(); for entity in world.iter_entities() { - let counter = entity_counters.entry(entity.id()).or_insert(0); - *counter += 1; + if !entity.contains::() { + let counter = entity_counters.entry(entity.id()).or_insert(0); + *counter += 1; + } } }; @@ -4206,9 +4262,9 @@ mod tests { let mut entities = world.iter_entities_mut().collect::>(); entities.sort_by_key(|e| e.get::().map(|a| a.0).or(e.get::().map(|b| b.0))); - let (a, b) = entities.split_at_mut(2); + let (a, b) = entities.split_at_mut(3); core::mem::swap( - &mut a[1].get_mut::().unwrap().0, + &mut a[2].get_mut::().unwrap().0, &mut b[0].get_mut::().unwrap().0, ); assert_eq!(world.entity(a1).get(), Some(&A(0))); diff --git a/crates/bevy_scene/src/lib.rs b/crates/bevy_scene/src/lib.rs index 9b0845f80f..b2936aae39 100644 --- a/crates/bevy_scene/src/lib.rs +++ b/crates/bevy_scene/src/lib.rs @@ -49,7 +49,13 @@ pub mod prelude { use bevy_app::prelude::*; #[cfg(feature = "serialize")] -use {bevy_asset::AssetApp, bevy_ecs::schedule::IntoScheduleConfigs}; +use { + bevy_asset::AssetApp, + bevy_ecs::schedule::IntoScheduleConfigs, + bevy_ecs::{ + entity_disabling::DefaultQueryFilters, resource::IsResource, resource::ResourceEntity, + }, +}; /// Plugin that provides scene functionality to an [`App`]. #[derive(Default)] @@ -64,6 +70,8 @@ impl Plugin for ScenePlugin { .init_resource::() .register_type::() .register_type::() + .register_type::() + .register_type::>() .add_systems(SpawnScene, (scene_spawner, scene_spawner_system).chain()); // Register component hooks for DynamicSceneRoot diff --git a/crates/bevy_scene/src/scene_spawner.rs b/crates/bevy_scene/src/scene_spawner.rs index 13713fe64c..1181c65f45 100644 --- a/crates/bevy_scene/src/scene_spawner.rs +++ b/crates/bevy_scene/src/scene_spawner.rs @@ -610,7 +610,7 @@ mod tests { assert_eq!(scene_component_a.y, 4.0); assert_eq!( app.world().entity(entity).get::().unwrap().len(), - 1 + 3 // two resources-as-entities are also counted ); // let's try to delete the scene diff --git a/crates/bevy_scene/src/serde.rs b/crates/bevy_scene/src/serde.rs index 84badb5850..cd16c4b824 100644 --- a/crates/bevy_scene/src/serde.rs +++ b/crates/bevy_scene/src/serde.rs @@ -516,9 +516,11 @@ mod tests { }; use bevy_ecs::{ entity::{Entity, EntityHashMap}, + entity_disabling::DefaultQueryFilters, prelude::{Component, ReflectComponent, ReflectResource, Resource, World}, query::{With, Without}, reflect::AppTypeRegistry, + resource::{IsResource, ResourceEntity}, world::FromWorld, }; use bevy_reflect::{Reflect, ReflectDeserialize, ReflectSerialize}; @@ -611,6 +613,8 @@ mod tests { registry.register::(); registry.register::(); registry.register::(); + registry.register::(); + registry.register::>(); } world.insert_resource(registry); world @@ -638,20 +642,20 @@ mod tests { ), }, entities: { - 4294967293: ( + 4294967291: ( components: { "bevy_scene::serde::tests::Bar": (345), "bevy_scene::serde::tests::Baz": (789), "bevy_scene::serde::tests::Foo": (123), }, ), - 4294967294: ( + 4294967292: ( components: { "bevy_scene::serde::tests::Bar": (345), "bevy_scene::serde::tests::Foo": (123), }, ), - 4294967295: ( + 4294967293: ( components: { "bevy_scene::serde::tests::Foo": (123), }, @@ -757,7 +761,7 @@ mod tests { .write_to_world(&mut dst_world, &mut map) .unwrap(); - assert_eq!(2, deserialized_scene.entities.len()); + assert_eq!(4, deserialized_scene.entities.len()); assert_scene_eq(&scene, &deserialized_scene); let bar_to_foo = dst_world @@ -785,7 +789,7 @@ mod tests { let (scene, deserialized_scene) = roundtrip_ron(&world); - assert_eq!(1, deserialized_scene.entities.len()); + assert_eq!(3, deserialized_scene.entities.len()); assert_scene_eq(&scene, &deserialized_scene); let mut world = create_world(); @@ -815,10 +819,19 @@ mod tests { assert_eq!( vec![ - 0, 1, 255, 255, 255, 255, 15, 1, 37, 98, 101, 118, 121, 95, 115, 99, 101, 110, 101, + 0, 3, 253, 255, 255, 255, 15, 1, 37, 98, 101, 118, 121, 95, 115, 99, 101, 110, 101, 58, 58, 115, 101, 114, 100, 101, 58, 58, 116, 101, 115, 116, 115, 58, 58, 77, 121, 67, 111, 109, 112, 111, 110, 101, 110, 116, 1, 2, 3, 102, 102, 166, 63, 205, 204, - 108, 64, 1, 12, 72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100, 33 + 108, 64, 1, 12, 72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100, 33, 254, 255, + 255, 255, 15, 1, 30, 98, 101, 118, 121, 95, 101, 99, 115, 58, 58, 114, 101, 115, + 111, 117, 114, 99, 101, 58, 58, 73, 115, 82, 101, 115, 111, 117, 114, 99, 101, 255, + 255, 255, 255, 15, 2, 30, 98, 101, 118, 121, 95, 101, 99, 115, 58, 58, 114, 101, + 115, 111, 117, 114, 99, 101, 58, 58, 73, 115, 82, 101, 115, 111, 117, 114, 99, 101, + 83, 98, 101, 118, 121, 95, 101, 99, 115, 58, 58, 114, 101, 115, 111, 117, 114, 99, + 101, 58, 58, 82, 101, 115, 111, 117, 114, 99, 101, 69, 110, 116, 105, 116, 121, 60, + 98, 101, 118, 121, 95, 101, 99, 115, 58, 58, 101, 110, 116, 105, 116, 121, 95, 100, + 105, 115, 97, 98, 108, 105, 110, 103, 58, 58, 68, 101, 102, 97, 117, 108, 116, 81, + 117, 101, 114, 121, 70, 105, 108, 116, 101, 114, 115, 62 ], serialized_scene ); @@ -830,7 +843,7 @@ mod tests { .deserialize(&mut postcard::Deserializer::from_bytes(&serialized_scene)) .unwrap(); - assert_eq!(1, deserialized_scene.entities.len()); + assert_eq!(3, deserialized_scene.entities.len()); assert_scene_eq(&scene, &deserialized_scene); } @@ -856,11 +869,21 @@ mod tests { assert_eq!( vec![ - 146, 128, 129, 206, 255, 255, 255, 255, 145, 129, 217, 37, 98, 101, 118, 121, 95, + 146, 128, 131, 206, 255, 255, 255, 253, 145, 129, 217, 37, 98, 101, 118, 121, 95, 115, 99, 101, 110, 101, 58, 58, 115, 101, 114, 100, 101, 58, 58, 116, 101, 115, 116, 115, 58, 58, 77, 121, 67, 111, 109, 112, 111, 110, 101, 110, 116, 147, 147, 1, 2, 3, 146, 202, 63, 166, 102, 102, 202, 64, 108, 204, 205, 129, 165, 84, 117, 112, - 108, 101, 172, 72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100, 33 + 108, 101, 172, 72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100, 33, 206, 255, + 255, 255, 254, 145, 129, 190, 98, 101, 118, 121, 95, 101, 99, 115, 58, 58, 114, + 101, 115, 111, 117, 114, 99, 101, 58, 58, 73, 115, 82, 101, 115, 111, 117, 114, 99, + 101, 144, 206, 255, 255, 255, 255, 145, 130, 190, 98, 101, 118, 121, 95, 101, 99, + 115, 58, 58, 114, 101, 115, 111, 117, 114, 99, 101, 58, 58, 73, 115, 82, 101, 115, + 111, 117, 114, 99, 101, 144, 217, 83, 98, 101, 118, 121, 95, 101, 99, 115, 58, 58, + 114, 101, 115, 111, 117, 114, 99, 101, 58, 58, 82, 101, 115, 111, 117, 114, 99, + 101, 69, 110, 116, 105, 116, 121, 60, 98, 101, 118, 121, 95, 101, 99, 115, 58, 58, + 101, 110, 116, 105, 116, 121, 95, 100, 105, 115, 97, 98, 108, 105, 110, 103, 58, + 58, 68, 101, 102, 97, 117, 108, 116, 81, 117, 101, 114, 121, 70, 105, 108, 116, + 101, 114, 115, 62, 144 ], buf ); @@ -874,7 +897,7 @@ mod tests { .deserialize(&mut rmp_serde::Deserializer::new(&mut reader)) .unwrap(); - assert_eq!(1, deserialized_scene.entities.len()); + assert_eq!(3, deserialized_scene.entities.len()); assert_scene_eq(&scene, &deserialized_scene); } @@ -899,13 +922,23 @@ mod tests { assert_eq!( vec![ - 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 255, 255, 255, 255, 0, 0, 0, 0, 1, + 0, 0, 0, 0, 0, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, 253, 255, 255, 255, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 37, 0, 0, 0, 0, 0, 0, 0, 98, 101, 118, 121, 95, 115, 99, 101, 110, 101, 58, 58, 115, 101, 114, 100, 101, 58, 58, 116, 101, 115, 116, 115, 58, 58, 77, 121, 67, 111, 109, 112, 111, 110, 101, 110, 116, 1, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, 102, 102, 166, 63, 205, 204, 108, 64, 1, 0, 0, 0, 12, 0, 0, 0, 0, 0, 0, 0, 72, 101, 108, 108, 111, 32, 87, 111, 114, 108, - 100, 33 + 100, 33, 254, 255, 255, 255, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 30, 0, 0, 0, 0, 0, + 0, 0, 98, 101, 118, 121, 95, 101, 99, 115, 58, 58, 114, 101, 115, 111, 117, 114, + 99, 101, 58, 58, 73, 115, 82, 101, 115, 111, 117, 114, 99, 101, 255, 255, 255, 255, + 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 30, 0, 0, 0, 0, 0, 0, 0, 98, 101, 118, 121, 95, + 101, 99, 115, 58, 58, 114, 101, 115, 111, 117, 114, 99, 101, 58, 58, 73, 115, 82, + 101, 115, 111, 117, 114, 99, 101, 83, 0, 0, 0, 0, 0, 0, 0, 98, 101, 118, 121, 95, + 101, 99, 115, 58, 58, 114, 101, 115, 111, 117, 114, 99, 101, 58, 58, 82, 101, 115, + 111, 117, 114, 99, 101, 69, 110, 116, 105, 116, 121, 60, 98, 101, 118, 121, 95, + 101, 99, 115, 58, 58, 101, 110, 116, 105, 116, 121, 95, 100, 105, 115, 97, 98, 108, + 105, 110, 103, 58, 58, 68, 101, 102, 97, 117, 108, 116, 81, 117, 101, 114, 121, 70, + 105, 108, 116, 101, 114, 115, 62 ], serialized_scene ); @@ -918,7 +951,7 @@ mod tests { bincode::serde::seed_decode_from_slice(scene_deserializer, &serialized_scene, config) .unwrap(); - assert_eq!(1, deserialized_scene.entities.len()); + assert_eq!(3, deserialized_scene.entities.len()); assert_scene_eq(&scene, &deserialized_scene); } diff --git a/release-content/migration-guides/resources_as_components.md b/release-content/migration-guides/resources_as_components.md new file mode 100644 index 0000000000..919dbfbaf7 --- /dev/null +++ b/release-content/migration-guides/resources_as_components.md @@ -0,0 +1,25 @@ +--- +title: Resources as Components +pull_requests: [19711] +--- + +Resources are very similar to Components: they are both data that can be stored in the ECS and queried. +The only real difference between them is that querying a resource will return either one or zero resources, whereas querying for a component can return any number of entities that match it. + +Even so, resources and components have always been separate concepts within the ECS. +This leads to some annoying restrictions. +While components have [`ComponentHooks`](https://docs.rs/bevy/latest/bevy/ecs/component/struct.ComponentHooks.html), it's not possible to add lifecycle hooks to resources. +Moreover, the engine internals contain a lot of duplication because of it. + +This motivates us to transition resources to components, and while most of the public API will stay the same, some breaking changes are inevitable. + +This PR adds a dummy entity alongside every resource. This entity is inserted and removed alongside resources and doesn't do anything (yet). + +This changes `World::entities().len()` as there are more entities than you might expect there to be. For example, a new world, no longer contains zero entities. This is mostly important for unit tests. + +Two methods have been added `World::entity_count()` and `World::resource_count()`. The former returns the number of entities without the resource entities, while the latter returns the number of resources in the world. + +While the marker component `IsResource` is added to [default query filters](https://docs.rs/bevy/latest/bevy/ecs/entity_disabling/struct.DefaultQueryFilters.html), during world creation, resource entities might still show up in broad queries with [`EntityMutExcept`](https://docs.rs/bevy/latest/bevy/ecs/world/struct.EntityMutExcept.html) and [`EntityRefExcept`](https://docs.rs/bevy/latest/bevy/ecs/world/struct.EntityRefExcept.html). +They also show up in `World::iter_entities()`, `World::iter_entities_mut()` and [`QueryBuilder`](https://docs.rs/bevy/latest/bevy/ecs/prelude/struct.QueryBuilder.html). + +Lastly, because of the entity bump, the input and output of the `bevy_scene` crate is not equivalent to the previous version, meaning that it's unadvisable to read in scenes from the previous version into the current one.