From 042337f787e99612085e606e4ffdb20bb5f64db9 Mon Sep 17 00:00:00 2001 From: Trashtalk Date: Thu, 10 Jul 2025 22:23:13 +0000 Subject: [PATCH 01/14] add back all changes --- crates/bevy_ecs/src/component.rs | 3 + crates/bevy_ecs/src/entity_disabling.rs | 4 ++ crates/bevy_ecs/src/query/builder.rs | 3 + crates/bevy_ecs/src/query/iter.rs | 4 ++ crates/bevy_ecs/src/query/state.rs | 6 +- crates/bevy_ecs/src/resource.rs | 91 +++++++++++++++++++++++++ crates/bevy_ecs/src/world/mod.rs | 8 ++- crates/bevy_scene/src/lib.rs | 10 ++- crates/bevy_scene/src/scene_spawner.rs | 2 +- crates/bevy_scene/src/serde.rs | 61 +++++++++++++---- 10 files changed, 172 insertions(+), 20 deletions(-) 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); } From 64eac9a9a563c4477d5e5cd20d4db135d16a9cec Mon Sep 17 00:00:00 2001 From: Trashtalk Date: Thu, 10 Jul 2025 22:39:22 +0000 Subject: [PATCH 02/14] cargo fmt --- crates/bevy_ecs/src/component/info.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/bevy_ecs/src/component/info.rs b/crates/bevy_ecs/src/component/info.rs index 9f122dbd82..0ccd681964 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, collections::HashMap}; +use bevy_platform::{collections::HashMap, collections::HashSet, sync::PoisonError}; use bevy_ptr::OwningPtr; #[cfg(feature = "bevy_reflect")] use bevy_reflect::Reflect; From 956c1a669aaf483c7101cbccf40f0d779dd9c76b Mon Sep 17 00:00:00 2001 From: Trashtalk Date: Thu, 10 Jul 2025 22:48:11 +0000 Subject: [PATCH 03/14] cargo clippy --- crates/bevy_ecs/src/component/info.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/bevy_ecs/src/component/info.rs b/crates/bevy_ecs/src/component/info.rs index 0ccd681964..c9aa707a74 100644 --- a/crates/bevy_ecs/src/component/info.rs +++ b/crates/bevy_ecs/src/component/info.rs @@ -348,7 +348,7 @@ pub struct Components { pub(super) indices: TypeIdMap, pub(super) resource_indices: TypeIdMap, /// A lookup for the entities on which resources are stored. - /// It uses ComponentIds instead of TypeIds for untyped APIs + /// 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, From 073df4bf9df054318b304469093fb1f47ff13602 Mon Sep 17 00:00:00 2001 From: Trashtalk Date: Sat, 12 Jul 2025 09:12:20 +0000 Subject: [PATCH 04/14] add entities with resources, auto disabled IsResource --- crates/bevy_ecs/src/entity_disabling.rs | 4 -- crates/bevy_ecs/src/query/builder.rs | 2 - crates/bevy_ecs/src/query/iter.rs | 3 -- crates/bevy_ecs/src/query/state.rs | 4 +- crates/bevy_ecs/src/resource.rs | 5 +++ crates/bevy_ecs/src/world/mod.rs | 53 +++++++++++++++++++++++++ 6 files changed, 59 insertions(+), 12 deletions(-) diff --git a/crates/bevy_ecs/src/entity_disabling.rs b/crates/bevy_ecs/src/entity_disabling.rs index 3c5523e4fd..5d62011174 100644 --- a/crates/bevy_ecs/src/entity_disabling.rs +++ b/crates/bevy_ecs/src/entity_disabling.rs @@ -207,7 +207,6 @@ mod tests { use crate::{ prelude::World, query::{Has, With}, - resource::IsResource, }; use alloc::{vec, vec::Vec}; @@ -279,9 +278,6 @@ 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 cbef579fbc..b7705694c9 100644 --- a/crates/bevy_ecs/src/query/builder.rs +++ b/crates/bevy_ecs/src/query/builder.rs @@ -332,8 +332,6 @@ 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)); diff --git a/crates/bevy_ecs/src/query/iter.rs b/crates/bevy_ecs/src/query/iter.rs index 0b8a8b9a91..1771a464f9 100644 --- a/crates/bevy_ecs/src/query/iter.rs +++ b/crates/bevy_ecs/src/query/iter.rs @@ -2659,7 +2659,6 @@ 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); @@ -2670,8 +2669,6 @@ 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)); diff --git a/crates/bevy_ecs/src/query/state.rs b/crates/bevy_ecs/src/query/state.rs index e9188ba0bd..fe37a76fe8 100644 --- a/crates/bevy_ecs/src/query/state.rs +++ b/crates/bevy_ecs/src/query/state.rs @@ -1850,8 +1850,6 @@ 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(); @@ -2210,7 +2208,7 @@ mod tests { #[test] fn query_default_filters_updates_is_dense() { let mut world = World::new(); - let num_resources = world.components().num_resources(); + let num_resources = world.resource_count() as usize; world.spawn((Table, Sparse)); world.spawn(Table); world.spawn(Sparse); diff --git a/crates/bevy_ecs/src/resource.rs b/crates/bevy_ecs/src/resource.rs index 27ce87cffa..de32334298 100644 --- a/crates/bevy_ecs/src/resource.rs +++ b/crates/bevy_ecs/src/resource.rs @@ -112,6 +112,11 @@ impl Default for ResourceEntity { #[derive(Component, Default, Debug)] pub struct IsResource; +/// Used in conjunction with [`ResourceEntity`], when no type information is available. +/// This is used by [`insert_resource_by_id`]. +#[derive(Resource)] +pub(crate) struct TypeErasedResource; + #[cfg(test)] #[expect(clippy::print_stdout, reason = "Allowed in tests.")] mod tests { diff --git a/crates/bevy_ecs/src/world/mod.rs b/crates/bevy_ecs/src/world/mod.rs index e6c505462d..170a0157ad 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::{IsResource, ResourceEntity, TypeErasedResource}, }; pub use bevy_ecs_macros::FromWorld; use bevy_utils::prelude::DebugName; @@ -169,6 +170,7 @@ impl World { // This sets up `Disabled` as a disabling component, via the FromWorld impl self.init_resource::(); + self.register_disabling_component::(); } /// Creates a new empty [`World`]. /// @@ -226,6 +228,12 @@ impl World { .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. #[inline] pub fn archetypes(&self) -> &Archetypes { @@ -1702,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 @@ -1739,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 { @@ -1806,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::()) } @@ -2709,6 +2744,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 { @@ -3401,6 +3450,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)? From 6c8281d568c8b51cfad8c540b49a6c39f2a396d2 Mon Sep 17 00:00:00 2001 From: Trashtalk Date: Sat, 12 Jul 2025 09:48:53 +0000 Subject: [PATCH 05/14] fixed and tests --- crates/bevy_ecs/src/world/entity_ref.rs | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/crates/bevy_ecs/src/world/entity_ref.rs b/crates/bevy_ecs/src/world/entity_ref.rs index 90438b8d6e..40e5944532 100644 --- a/crates/bevy_ecs/src/world/entity_ref.rs +++ b/crates/bevy_ecs/src/world/entity_ref.rs @@ -4867,6 +4867,7 @@ mod tests { change_detection::{MaybeLocation, MutUntyped}, component::ComponentId, prelude::*, + resource::IsResource, system::{assert_is_system, RunSystemOnce as _}, world::{error::EntityComponentError, DeferredWorld, FilteredEntityMut, FilteredEntityRef}, }; @@ -5253,7 +5254,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) { @@ -5311,7 +5312,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::(), @@ -5328,7 +5332,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) { @@ -5393,7 +5397,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::() From f9dc9f1619eb76e3f9e62d705fa8c5ad09af8f88 Mon Sep 17 00:00:00 2001 From: Trashtalk Date: Sat, 12 Jul 2025 10:10:34 +0000 Subject: [PATCH 06/14] fixed more tests --- crates/bevy_ecs/src/name.rs | 2 +- crates/bevy_ecs/src/query/builder.rs | 3 +++ crates/bevy_ecs/src/query/state.rs | 7 +++++-- 3 files changed, 9 insertions(+), 3 deletions(-) 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 b7705694c9..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; @@ -486,6 +487,7 @@ mod tests { let mut query = QueryBuilder::<(FilteredEntityMut, EntityMutExcept)>::new(&mut world) .data::() + .filter::>() .build(); // Removing `EntityMutExcept` just leaves A @@ -497,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/state.rs b/crates/bevy_ecs/src/query/state.rs index fe37a76fe8..a89c4c77ca 100644 --- a/crates/bevy_ecs/src/query/state.rs +++ b/crates/bevy_ecs/src/query/state.rs @@ -1777,6 +1777,7 @@ mod tests { component::Component, entity_disabling::DefaultQueryFilters, prelude::*, + resource::IsResource, system::{QueryLens, RunSystemOnce}, world::{FilteredEntityMut, FilteredEntityRef}, }; @@ -2176,6 +2177,7 @@ mod tests { 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); @@ -2208,7 +2210,6 @@ mod tests { #[test] fn query_default_filters_updates_is_dense() { let mut world = World::new(); - let num_resources = world.resource_count() as usize; world.spawn((Table, Sparse)); world.spawn(Table); world.spawn(Sparse); @@ -2216,11 +2217,12 @@ 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() - num_resources); + assert_eq!(3, query.iter(&world).count()); let mut df = DefaultQueryFilters::empty(); df.register_disabling_component(world.register_component::()); 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 @@ -2231,6 +2233,7 @@ mod tests { let mut df = DefaultQueryFilters::empty(); df.register_disabling_component(world.register_component::()); 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 From 07723acf392df8ce903be065f365bbb1857016d7 Mon Sep 17 00:00:00 2001 From: Trashtalk Date: Sat, 12 Jul 2025 11:03:09 +0000 Subject: [PATCH 07/14] fixed moore tests --- crates/bevy_ecs/src/lib.rs | 12 ++++++------ crates/bevy_ecs/src/system/system.rs | 4 ++-- crates/bevy_ecs/src/world/mod.rs | 12 +++++++----- 3 files changed, 15 insertions(+), 13 deletions(-) diff --git a/crates/bevy_ecs/src/lib.rs b/crates/bevy_ecs/src/lib.rs index 8a07cdc8e1..ce566e96d6 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/system/system.rs b/crates/bevy_ecs/src/system/system.rs index aad37c09d0..0e90bf2201 100644 --- a/crates/bevy_ecs/src/system/system.rs +++ b/crates/bevy_ecs/src/system/system.rs @@ -494,9 +494,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/mod.rs b/crates/bevy_ecs/src/world/mod.rs index 170a0157ad..1cd215e157 100644 --- a/crates/bevy_ecs/src/world/mod.rs +++ b/crates/bevy_ecs/src/world/mod.rs @@ -3727,7 +3727,7 @@ mod tests { entity::EntityHashSet, entity_disabling::{DefaultQueryFilters, Disabled}, ptr::OwningPtr, - resource::Resource, + resource::{IsResource, Resource}, world::{error::EntityMutableFetchError, DeferredWorld}, }; use alloc::{ @@ -4159,8 +4159,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; + } } }; @@ -4257,9 +4259,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))); From 1ba7af2b28baf3c3901d643c33aa8b0952460fc2 Mon Sep 17 00:00:00 2001 From: Trashtalk Date: Sat, 12 Jul 2025 11:16:11 +0000 Subject: [PATCH 08/14] fix ci --- crates/bevy_ecs/src/resource.rs | 19 +++++++------------ 1 file changed, 7 insertions(+), 12 deletions(-) diff --git a/crates/bevy_ecs/src/resource.rs b/crates/bevy_ecs/src/resource.rs index de32334298..fda419c434 100644 --- a/crates/bevy_ecs/src/resource.rs +++ b/crates/bevy_ecs/src/resource.rs @@ -118,14 +118,12 @@ pub struct IsResource; pub(crate) struct TypeErasedResource; #[cfg(test)] -#[expect(clippy::print_stdout, reason = "Allowed in tests.")] mod tests { use crate::change_detection::MaybeLocation; - use crate::ptr::PtrMut; + use crate::ptr::OwningPtr; use crate::resource::Resource; use crate::world::World; use bevy_platform::prelude::String; - use core::mem::ManuallyDrop; #[test] fn unique_resource_entities() { @@ -150,15 +148,12 @@ mod tests { // 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(), - ); - } + 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); From b89c0420faa25350735a11ab8d0120d5bb724a2c Mon Sep 17 00:00:00 2001 From: Trashtalk Date: Sat, 12 Jul 2025 11:32:54 +0000 Subject: [PATCH 09/14] fix mooore tests (benches) --- benches/benches/bevy_ecs/world/world_get.rs | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) 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) })); From 9b33933fc5d60d3bf7236e11e8aaf5708de3e323 Mon Sep 17 00:00:00 2001 From: Trashtalk Date: Sat, 12 Jul 2025 11:48:51 +0000 Subject: [PATCH 10/14] fix docs --- crates/bevy_ecs/src/resource.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/bevy_ecs/src/resource.rs b/crates/bevy_ecs/src/resource.rs index fda419c434..06c5a05c30 100644 --- a/crates/bevy_ecs/src/resource.rs +++ b/crates/bevy_ecs/src/resource.rs @@ -113,7 +113,7 @@ impl Default for ResourceEntity { pub struct IsResource; /// Used in conjunction with [`ResourceEntity`], when no type information is available. -/// This is used by [`insert_resource_by_id`]. +/// This is used by [`World::insert_resource_by_id`](crate::world::World). #[derive(Resource)] pub(crate) struct TypeErasedResource; From c7b4f1d0b1f3e650daec76736136c83dff870938 Mon Sep 17 00:00:00 2001 From: Trashtalk Date: Sat, 12 Jul 2025 12:28:51 +0000 Subject: [PATCH 11/14] add migration guide --- .../resources_as_components.md | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 release-content/migration-guides/resources_as_components.md 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..109a48d809 --- /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 similair 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 seperate 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. From 96aef455696ce0d2d767207c0c81c81e95449973 Mon Sep 17 00:00:00 2001 From: Trashtalk Date: Sat, 12 Jul 2025 12:31:14 +0000 Subject: [PATCH 12/14] fixed spelling errors to prove I'm not AI --- release-content/migration-guides/resources_as_components.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/release-content/migration-guides/resources_as_components.md b/release-content/migration-guides/resources_as_components.md index 109a48d809..919dbfbaf7 100644 --- a/release-content/migration-guides/resources_as_components.md +++ b/release-content/migration-guides/resources_as_components.md @@ -3,10 +3,10 @@ title: Resources as Components pull_requests: [19711] --- -Resources are very similair to Components: they are both data that can be stored in the ECS and queried. +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 seperate concepts within the ECS. +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. From 57581349b984732360cc43ff52a83a5bb6e0509d Mon Sep 17 00:00:00 2001 From: Trashtalk Date: Sun, 13 Jul 2025 18:38:50 +0000 Subject: [PATCH 13/14] addressed comments --- crates/bevy_ecs/src/entity_disabling.rs | 3 +++ crates/bevy_ecs/src/query/state.rs | 5 +---- crates/bevy_ecs/src/world/mod.rs | 3 +-- 3 files changed, 5 insertions(+), 6 deletions(-) diff --git a/crates/bevy_ecs/src/entity_disabling.rs b/crates/bevy_ecs/src/entity_disabling.rs index 5d62011174..5c412ca75e 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/query/state.rs b/crates/bevy_ecs/src/query/state.rs index a89c4c77ca..afb3f4e022 100644 --- a/crates/bevy_ecs/src/query/state.rs +++ b/crates/bevy_ecs/src/query/state.rs @@ -2174,10 +2174,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::(); + world.register_disabling_component::(); // Without only matches the first entity let mut query = QueryState::<()>::new(&mut world); diff --git a/crates/bevy_ecs/src/world/mod.rs b/crates/bevy_ecs/src/world/mod.rs index 1cd215e157..4ccd1a1574 100644 --- a/crates/bevy_ecs/src/world/mod.rs +++ b/crates/bevy_ecs/src/world/mod.rs @@ -22,7 +22,7 @@ use crate::{ event::BufferedEvent, lifecycle::{ComponentHooks, ADD, DESPAWN, INSERT, REMOVE, REPLACE}, prelude::{Add, Despawn, Insert, Remove, Replace}, - resource::{IsResource, ResourceEntity, TypeErasedResource}, + resource::{ResourceEntity, TypeErasedResource}, }; pub use bevy_ecs_macros::FromWorld; use bevy_utils::prelude::DebugName; @@ -170,7 +170,6 @@ impl World { // This sets up `Disabled` as a disabling component, via the FromWorld impl self.init_resource::(); - self.register_disabling_component::(); } /// Creates a new empty [`World`]. /// From 9824c3a5df04a05889a7423e0ecbeb00c90f5584 Mon Sep 17 00:00:00 2001 From: Trashtalk Date: Mon, 14 Jul 2025 22:37:34 +0000 Subject: [PATCH 14/14] testing robustness --- crates/bevy_ecs/src/query/state.rs | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/crates/bevy_ecs/src/query/state.rs b/crates/bevy_ecs/src/query/state.rs index afb3f4e022..35251fe9d5 100644 --- a/crates/bevy_ecs/src/query/state.rs +++ b/crates/bevy_ecs/src/query/state.rs @@ -1777,7 +1777,6 @@ mod tests { component::Component, entity_disabling::DefaultQueryFilters, prelude::*, - resource::IsResource, system::{QueryLens, RunSystemOnce}, world::{FilteredEntityMut, FilteredEntityRef}, }; @@ -2216,10 +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::(); + world.register_disabling_component::(); let mut query = QueryState::<()>::new(&mut world); // The query doesn't ask for sparse components, but the default filters adds @@ -2227,10 +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::(); + 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