This commit is contained in:
Trashtalk217 2025-07-17 21:11:03 -04:00 committed by GitHub
commit c1a615d15a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
16 changed files with 289 additions and 51 deletions

View File

@ -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::<Table>(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::<Table>(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::<Sparse>(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::<Table>(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::<Sparse>(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) }));

View File

@ -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<Option<ComponentInfo>>,
pub(super) indices: TypeIdMap<ComponentId>,
pub(super) resource_indices: TypeIdMap<ComponentId>,
/// 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<ComponentId, Entity>,
// This is kept internal and local to verify that no deadlocks can occor.
pub(super) queued: bevy_platform::sync::RwLock<QueuedComponents>,
}

View File

@ -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::<Disabled>();
filters.register_disabling_component(disabled_component_id);
let is_resource_component_id = world.register_component::<IsResource>();
filters.register_disabling_component(is_resource_component_id);
filters
}
}

View File

@ -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::<TableStored>(e).is_none());
assert!(world.get::<A>(e).is_none());
assert_eq!(world.get::<TableStored>(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::<TableStored>(e).is_none());
assert!(world.get::<SparseStored>(e).is_none());
assert_eq!(world.get::<TableStored>(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)))];

View File

@ -278,7 +278,7 @@ mod tests {
let mut query = world.query::<NameOrEntity>();
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");

View File

@ -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<A>)>::new(&mut world)
.data::<EntityMut>()
.filter::<Without<IsResource>>()
.build();
// Removing `EntityMutExcept<A>` just leaves A
@ -496,6 +499,7 @@ mod tests {
let mut query = QueryBuilder::<(FilteredEntityMut, EntityRefExcept<A>)>::new(&mut world)
.data::<EntityMut>()
.filter::<Without<IsResource>>()
.build();
// Removing `EntityRefExcept<A>` just leaves A, plus read access

View File

@ -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)));

View File

@ -1850,6 +1850,7 @@ mod tests {
#[test]
fn can_transmute_empty_tuple() {
let mut world = World::new();
world.register_component::<A>();
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::<C>());
world.insert_resource(df);
world.register_disabling_component::<C>();
// Without<C> 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::<Sparse>());
let df = DefaultQueryFilters::from_world(&mut world);
world.insert_resource(df);
world.register_disabling_component::<Sparse>();
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::<Table>());
let df = DefaultQueryFilters::from_world(&mut world);
world.insert_resource(df);
world.register_disabling_component::<Table>();
let mut query = QueryState::<()>::new(&mut world);
// If the filter is instead a table components, the query can still be dense

View File

@ -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<R: Resource>(#[reflect(ignore)] PhantomData<R>);
impl<R: Resource> Default for ResourceEntity<R> {
fn default() -> Self {
ResourceEntity(PhantomData)
}
}
/// A marker component for entities which store resources.
///
/// By contrast, the [`ResourceEntity<R>`] component is used to find the entity that stores a particular resource.
/// This component is required by the [`ResourceEntity<R>`] 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<R>`], 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::<TestResource1>();
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::<TestResource3>();
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::<TestResource1>();
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);
}
}

View File

@ -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]

View File

@ -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::<EntityRefExcept<TestComponent>>();
let mut query = world.query::<EntityRefExcept<(TestComponent, IsResource)>>();
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<EntityRefExcept<TestComponent>>) {
fn system(
_: Query<&mut TestComponent>,
query: Query<EntityRefExcept<(TestComponent, IsResource)>>,
) {
for entity_ref in query.iter() {
assert!(matches!(
entity_ref.get::<TestComponent2>(),
@ -5449,7 +5453,7 @@ mod tests {
let mut world = World::new();
world.spawn(TestComponent(0)).insert(TestComponent2(0));
let mut query = world.query::<EntityMutExcept<TestComponent>>();
let mut query = world.query::<EntityMutExcept<(TestComponent, IsResource)>>();
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<EntityMutExcept<TestComponent>>) {
fn system(
_: Query<&mut TestComponent>,
mut query: Query<EntityMutExcept<(TestComponent, IsResource)>>,
) {
for mut entity_mut in query.iter_mut() {
assert!(entity_mut
.get_mut::<TestComponent2>()

View File

@ -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<R: Resource + FromWorld>(&mut self) -> ComponentId {
let caller = MaybeLocation::caller();
let component_id = self.components_registrator().register_resource::<R>();
if !self
.components
.resource_entities
.contains_key(&component_id)
{
let entity = self.spawn(ResourceEntity::<R>::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::<R>();
if !self
.components
.resource_entities
.contains_key(&component_id)
{
let entity = self.spawn(ResourceEntity::<R>::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<R: Resource>(&mut self) -> Option<R> {
let component_id = self.components.get_valid_resource_id(TypeId::of::<R>())?;
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::<R>()) }
@ -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::<TypeErasedResource>::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::<IsResource>() {
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::<Vec<_>>();
entities.sort_by_key(|e| e.get::<A>().map(|a| a.0).or(e.get::<B>().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::<A>().unwrap().0,
&mut a[2].get_mut::<A>().unwrap().0,
&mut b[0].get_mut::<B>().unwrap().0,
);
assert_eq!(world.entity(a1).get(), Some(&A(0)));

View File

@ -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::<SceneSpawner>()
.register_type::<SceneRoot>()
.register_type::<DynamicSceneRoot>()
.register_type::<IsResource>()
.register_type::<ResourceEntity<DefaultQueryFilters>>()
.add_systems(SpawnScene, (scene_spawner, scene_spawner_system).chain());
// Register component hooks for DynamicSceneRoot

View File

@ -610,7 +610,7 @@ mod tests {
assert_eq!(scene_component_a.y, 4.0);
assert_eq!(
app.world().entity(entity).get::<Children>().unwrap().len(),
1
3 // two resources-as-entities are also counted
);
// let's try to delete the scene

View File

@ -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::<MyEntityRef>();
registry.register::<Entity>();
registry.register::<MyResource>();
registry.register::<IsResource>();
registry.register::<ResourceEntity<DefaultQueryFilters>>();
}
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);
}

View File

@ -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.