diff --git a/crates/bevy_ecs/src/component/info.rs b/crates/bevy_ecs/src/component/info.rs index 0e222692d7fb1..99fcd419ed2c0 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::{hash::FixedHasher, sync::PoisonError}; +use bevy_platform::{collections::HashMap, hash::FixedHasher, sync::PoisonError}; use bevy_ptr::OwningPtr; #[cfg(feature = "bevy_reflect")] use bevy_reflect::Reflect; @@ -19,6 +19,7 @@ use crate::{ RequiredComponents, StorageType, }, lifecycle::ComponentHooks, + prelude::Entity, query::DebugCheckedUnwrap as _, resource::Resource, storage::SparseSetIndex, @@ -349,6 +350,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/lib.rs b/crates/bevy_ecs/src/lib.rs index 974c371bf31d0..84bb489155033 100644 --- a/crates/bevy_ecs/src/lib.rs +++ b/crates/bevy_ecs/src/lib.rs @@ -1794,7 +1794,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)))]; @@ -1818,7 +1818,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 317c8f5017bb5..2887475cc648f 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/resource.rs b/crates/bevy_ecs/src/resource.rs index 7da4f31113f4f..a8e63565bfcdb 100644 --- a/crates/bevy_ecs/src/resource.rs +++ b/crates/bevy_ecs/src/resource.rs @@ -1,5 +1,11 @@ //! Resources are unique, singleton-like data types that can be accessed from systems and stored in the [`World`](crate::world::World). +use crate::entity_disabling::Internal; +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 +79,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(Internal, 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/world/entity_ref.rs b/crates/bevy_ecs/src/world/entity_ref.rs index 5bdd86282efe7..ac324ecfdcdbe 100644 --- a/crates/bevy_ecs/src/world/entity_ref.rs +++ b/crates/bevy_ecs/src/world/entity_ref.rs @@ -5029,6 +5029,7 @@ mod tests { use crate::{ change_detection::{MaybeLocation, MutUntyped}, component::ComponentId, + entity_disabling::Internal, prelude::*, system::{assert_is_system, RunSystemOnce as _}, world::{error::EntityComponentError, DeferredWorld, FilteredEntityMut, FilteredEntityRef}, @@ -5478,7 +5479,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) { @@ -5536,7 +5537,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::(), @@ -5553,7 +5557,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) { @@ -5618,7 +5622,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 9e36484cc5141..96f6cba3462fb 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; @@ -1695,6 +1696,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 @@ -1732,6 +1745,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 { @@ -1799,6 +1823,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::()) } @@ -2705,6 +2733,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 { @@ -3397,6 +3439,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)? @@ -3668,7 +3714,7 @@ mod tests { change_detection::{DetectChangesMut, MaybeLocation}, component::{ComponentCloneBehavior, ComponentDescriptor, ComponentInfo, StorageType}, entity::EntityHashSet, - entity_disabling::{DefaultQueryFilters, Disabled}, + entity_disabling::{DefaultQueryFilters, Disabled, Internal}, ptr::OwningPtr, resource::Resource, world::{error::EntityMutableFetchError, DeferredWorld}, @@ -4102,7 +4148,7 @@ mod tests { let iterate_and_count_entities = |world: &World, entity_counters: &mut HashMap<_, _>| { entity_counters.clear(); #[expect(deprecated, reason = "remove this test in in 0.17.0")] - for entity in world.iter_entities() { + for entity in world.iter_entities().filter(|e| !e.contains::()) { let counter = entity_counters.entry(entity.id()).or_insert(0); *counter += 1; } @@ -4202,7 +4248,10 @@ mod tests { assert_eq!(world.entity(b2).get(), Some(&B(4))); #[expect(deprecated, reason = "remove this test in in 0.17.0")] - let mut entities = world.iter_entities_mut().collect::>(); + let mut entities = world + .iter_entities_mut() + .filter(|e| !e.contains::()) + .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); core::mem::swap( diff --git a/crates/bevy_scene/src/lib.rs b/crates/bevy_scene/src/lib.rs index 8908fc466ff64..af0037ff4c708 100644 --- a/crates/bevy_scene/src/lib.rs +++ b/crates/bevy_scene/src/lib.rs @@ -49,7 +49,15 @@ 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, Internal}, + resource::IsResource, + resource::ResourceEntity, + }, +}; /// Plugin that provides scene functionality to an [`App`]. #[derive(Default)] @@ -62,6 +70,11 @@ impl Plugin for ScenePlugin { .init_asset::() .init_asset_loader::() .init_resource::() + .register_type::() + .register_type::() + .register_type::() + .register_type::() + .register_type::>() .add_systems(SpawnScene, (scene_spawner, scene_spawner_system).chain()); // Register component hooks for DynamicSceneRoot @@ -120,9 +133,7 @@ mod tests { use bevy_ecs::{ component::Component, entity::Entity, - entity_disabling::Internal, hierarchy::{ChildOf, Children}, - query::Allow, reflect::{AppTypeRegistry, ReflectComponent}, world::World, }; @@ -309,11 +320,7 @@ mod tests { scene .world .insert_resource(world.resource::().clone()); - let entities: Vec = scene - .world - .query_filtered::>() - .iter(&scene.world) - .collect(); + let entities: Vec = scene.world.query::().iter(&scene.world).collect(); DynamicSceneBuilder::from_world(&scene.world) .extract_entities(entities.into_iter()) .build() diff --git a/crates/bevy_scene/src/scene_spawner.rs b/crates/bevy_scene/src/scene_spawner.rs index f3a9ab7e149de..a15e4465a313c 100644 --- a/crates/bevy_scene/src/scene_spawner.rs +++ b/crates/bevy_scene/src/scene_spawner.rs @@ -764,7 +764,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 84badb5850bef..cd16c4b824e8a 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/internal_entities.md b/release-content/migration-guides/internal_entities.md index 3ede75f09a3f6..02d67ef800949 100644 --- a/release-content/migration-guides/internal_entities.md +++ b/release-content/migration-guides/internal_entities.md @@ -1,10 +1,11 @@ --- title: Internal Entities -pull_requests: [20204] +pull_requests: [20204, 19711] --- Bevy 0.17 introduces internal entities. Entities tagged by the `Internal` component that are hidden from most queries using [`DefaultQueryFilters`](https://docs.rs/bevy/latest/bevy/ecs/entity_disabling/index.html). -Currently, both [`Observer`s](https://docs.rs/bevy/latest/bevy/ecs/observer/struct.Observer.html) and systems that are registered through [`World::register_system`](https://docs.rs/bevy/latest/bevy/prelude/struct.World.html#method.register_system) are considered internal entities. +Currently, [`Observer`s](https://docs.rs/bevy/latest/bevy/ecs/observer/struct.Observer.html) and systems that are registered through [`World::register_system`](https://docs.rs/bevy/latest/bevy/prelude/struct.World.html#method.register_system) are considered internal entities. +The resource entities part of the resources-as-components worked are also marked internal. If you queried them before, add the `Allow` filter to the query to bypass the default filter. 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 0000000000000..32895e04881bc --- /dev/null +++ b/release-content/migration-guides/resources_as_components.md @@ -0,0 +1,28 @@ +--- +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 matching entities. + +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. +If there is any place you are currently using `world.entities().len()`, we recommend you instead use a query `world.query().query(&world).count()`. + +Meanwhile, resource entities are also tagged with `IsResource` and `Internal` marker components. +For more information, checkout the migration guide for internal entities. +In summary, internal entities are added to [default query filters](https://docs.rs/bevy/latest/bevy/ecs/entity_disabling/struct.DefaultQueryFilters.html), and will not show up in most queries. + +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.