diff --git a/crates/bevy_ecs/src/component.rs b/crates/bevy_ecs/src/component.rs index cfcde29ab2..ef1e4a4c5e 100644 --- a/crates/bevy_ecs/src/component.rs +++ b/crates/bevy_ecs/src/component.rs @@ -1734,6 +1734,9 @@ pub struct Components { components: Vec>, indices: TypeIdMap, resource_indices: TypeIdMap, + /// A lookup for the entities on which resources are stored. + /// It uses ComponentIds instead of TypeIds for untyped APIs + pub(crate) resource_entities: HashMap, // This is kept internal and local to verify that no deadlocks can occor. queued: bevy_platform::sync::RwLock, } diff --git a/crates/bevy_ecs/src/entity_disabling.rs b/crates/bevy_ecs/src/entity_disabling.rs index 5d62011174..3c5523e4fd 100644 --- a/crates/bevy_ecs/src/entity_disabling.rs +++ b/crates/bevy_ecs/src/entity_disabling.rs @@ -207,6 +207,7 @@ mod tests { use crate::{ prelude::World, query::{Has, With}, + resource::IsResource, }; use alloc::{vec, vec::Vec}; @@ -278,6 +279,9 @@ mod tests { let mut world = World::new(); world.register_disabling_component::(); + // We don't want to query resources for this test. + world.register_disabling_component::(); + world.spawn_empty(); world.spawn(Disabled); world.spawn(CustomDisabled); diff --git a/crates/bevy_ecs/src/query/builder.rs b/crates/bevy_ecs/src/query/builder.rs index b545caad8f..cbef579fbc 100644 --- a/crates/bevy_ecs/src/query/builder.rs +++ b/crates/bevy_ecs/src/query/builder.rs @@ -332,6 +332,9 @@ mod tests { #[test] fn builder_or() { let mut world = World::new(); + // We don't want to query resources for this test. + world.register_disabling_component::(); + world.spawn((A(0), B(0))); world.spawn(B(0)); world.spawn(C(0)); diff --git a/crates/bevy_ecs/src/query/iter.rs b/crates/bevy_ecs/src/query/iter.rs index eb49204434..0b8a8b9a91 100644 --- a/crates/bevy_ecs/src/query/iter.rs +++ b/crates/bevy_ecs/src/query/iter.rs @@ -2659,6 +2659,7 @@ mod tests { use crate::component::Component; use crate::entity::Entity; use crate::prelude::World; + use crate::resource::IsResource; #[derive(Component, Debug, PartialEq, PartialOrd, Clone, Copy)] struct A(f32); @@ -2669,6 +2670,9 @@ mod tests { #[test] fn query_iter_sorts() { let mut world = World::new(); + // We don't want to query resources for this test. + world.register_disabling_component::(); + 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..e9188ba0bd 100644 --- a/crates/bevy_ecs/src/query/state.rs +++ b/crates/bevy_ecs/src/query/state.rs @@ -1850,6 +1850,9 @@ mod tests { #[test] fn can_transmute_empty_tuple() { let mut world = World::new(); + // We don't want to query resources for this test. + world.register_disabling_component::(); + world.register_component::(); let entity = world.spawn(A(10)).id(); @@ -2207,6 +2210,7 @@ mod tests { #[test] fn query_default_filters_updates_is_dense() { let mut world = World::new(); + let num_resources = world.components().num_resources(); world.spawn((Table, Sparse)); world.spawn(Table); world.spawn(Sparse); @@ -2214,7 +2218,7 @@ mod tests { let mut query = QueryState::<()>::new(&mut world); // There are no sparse components involved thus the query is dense assert!(query.is_dense); - assert_eq!(3, query.iter(&world).count()); + assert_eq!(3, query.iter(&world).count() - num_resources); let mut df = DefaultQueryFilters::empty(); df.register_disabling_component(world.register_component::()); diff --git a/crates/bevy_ecs/src/resource.rs b/crates/bevy_ecs/src/resource.rs index 7da4f31113..27ce87cffa 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; + +#[cfg(test)] +#[expect(clippy::print_stdout, reason = "Allowed in tests.")] +mod tests { + use crate::change_detection::MaybeLocation; + use crate::ptr::PtrMut; + use crate::resource::Resource; + use crate::world::World; + use bevy_platform::prelude::String; + use core::mem::ManuallyDrop; + + #[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); + unsafe { + // SAFETY + // * + world.insert_resource_by_id( + id, + PtrMut::from(&mut ManuallyDrop::new(20 as u8)).promote(), + 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/world/mod.rs b/crates/bevy_ecs/src/world/mod.rs index 63e5d85435..9b27c32eab 100644 --- a/crates/bevy_ecs/src/world/mod.rs +++ b/crates/bevy_ecs/src/world/mod.rs @@ -216,12 +216,14 @@ 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 num_entities(&self) -> u32 { - self.entities.len() + pub fn entity_count(&self) -> u32 { + self.entities + .len() + .saturating_sub(self.components.resource_entities.len() as u32) } /// Retrieves this world's [`Archetypes`] collection. 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 71cd848751..bbd967527c 100644 --- a/crates/bevy_scene/src/scene_spawner.rs +++ b/crates/bevy_scene/src/scene_spawner.rs @@ -609,7 +609,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 cb8206d3dd..734f444986 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); }