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) { for entity_count in RANGE.map(|i| i * 10_000) {
group.bench_function(format!("{entity_count}_entities"), |bencher| { group.bench_function(format!("{entity_count}_entities"), |bencher| {
let world = setup::<Table>(entity_count); let world = setup::<Table>(entity_count);
let offset = world.resource_count();
bencher.iter(|| { bencher.iter(|| {
for i in 0..entity_count { for i in offset..(entity_count + offset) {
let entity = let entity =
// SAFETY: Range is exclusive. // SAFETY: Range is exclusive.
Entity::from_raw(EntityRow::new(unsafe { NonMaxU32::new_unchecked(i) })); 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) { for entity_count in RANGE.map(|i| i * 10_000) {
group.bench_function(format!("{entity_count}_entities_table"), |bencher| { group.bench_function(format!("{entity_count}_entities_table"), |bencher| {
let world = setup::<Table>(entity_count); let world = setup::<Table>(entity_count);
let offset = world.resource_count();
bencher.iter(|| { bencher.iter(|| {
for i in 0..entity_count { for i in offset..(entity_count + offset) {
let entity = let entity =
// SAFETY: Range is exclusive. // SAFETY: Range is exclusive.
Entity::from_raw(EntityRow::new(unsafe { NonMaxU32::new_unchecked(i) })); 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| { group.bench_function(format!("{entity_count}_entities_sparse"), |bencher| {
let world = setup::<Sparse>(entity_count); let world = setup::<Sparse>(entity_count);
let offset = world.resource_count();
bencher.iter(|| { bencher.iter(|| {
for i in 0..entity_count { for i in offset..(entity_count + offset) {
let entity = let entity =
// SAFETY: Range is exclusive. // SAFETY: Range is exclusive.
Entity::from_raw(EntityRow::new(unsafe { NonMaxU32::new_unchecked(i) })); 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) { for entity_count in RANGE.map(|i| i * 10_000) {
group.bench_function(format!("{entity_count}_entities_table"), |bencher| { group.bench_function(format!("{entity_count}_entities_table"), |bencher| {
let mut world = setup::<Table>(entity_count); let mut world = setup::<Table>(entity_count);
let offset = world.resource_count();
let mut query = world.query::<&Table>(); let mut query = world.query::<&Table>();
bencher.iter(|| { bencher.iter(|| {
for i in 0..entity_count { for i in offset..(entity_count + offset) {
let entity = let entity =
// SAFETY: Range is exclusive. // SAFETY: Range is exclusive.
Entity::from_raw(EntityRow::new(unsafe { NonMaxU32::new_unchecked(i) })); 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<4>,
&WideTable<5>, &WideTable<5>,
)>(); )>();
let offset = world.resource_count();
bencher.iter(|| { bencher.iter(|| {
for i in 0..entity_count { for i in offset..(entity_count + offset) {
let entity = let entity =
// SAFETY: Range is exclusive. // SAFETY: Range is exclusive.
Entity::from_raw(EntityRow::new(unsafe { NonMaxU32::new_unchecked(i) })); 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| { group.bench_function(format!("{entity_count}_entities_sparse"), |bencher| {
let mut world = setup::<Sparse>(entity_count); let mut world = setup::<Sparse>(entity_count);
let offset = world.resource_count();
let mut query = world.query::<&Sparse>(); let mut query = world.query::<&Sparse>();
bencher.iter(|| { bencher.iter(|| {
for i in 0..entity_count { for i in offset..(entity_count + offset) {
let entity = let entity =
// SAFETY: Range is exclusive. // SAFETY: Range is exclusive.
Entity::from_raw(EntityRow::new(unsafe { NonMaxU32::new_unchecked(i) })); 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<4>,
&WideSparse<5>, &WideSparse<5>,
)>(); )>();
let offset = world.resource_count();
bencher.iter(|| { bencher.iter(|| {
for i in 0..entity_count { for i in offset..(entity_count + offset) {
// SAFETY: Range is exclusive. // SAFETY: Range is exclusive.
let entity = let entity =
Entity::from_raw(EntityRow::new(unsafe { NonMaxU32::new_unchecked(i) })); Entity::from_raw(EntityRow::new(unsafe { NonMaxU32::new_unchecked(i) }));

View File

@ -1,5 +1,5 @@
use alloc::{borrow::Cow, vec::Vec}; 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; use bevy_ptr::OwningPtr;
#[cfg(feature = "bevy_reflect")] #[cfg(feature = "bevy_reflect")]
use bevy_reflect::Reflect; use bevy_reflect::Reflect;
@ -18,6 +18,7 @@ use crate::{
RequiredComponents, StorageType, RequiredComponents, StorageType,
}, },
lifecycle::ComponentHooks, lifecycle::ComponentHooks,
prelude::Entity,
query::DebugCheckedUnwrap as _, query::DebugCheckedUnwrap as _,
resource::Resource, resource::Resource,
storage::SparseSetIndex, storage::SparseSetIndex,
@ -346,6 +347,9 @@ pub struct Components {
pub(super) components: Vec<Option<ComponentInfo>>, pub(super) components: Vec<Option<ComponentInfo>>,
pub(super) indices: TypeIdMap<ComponentId>, pub(super) indices: TypeIdMap<ComponentId>,
pub(super) resource_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. // This is kept internal and local to verify that no deadlocks can occor.
pub(super) queued: bevy_platform::sync::RwLock<QueuedComponents>, pub(super) queued: bevy_platform::sync::RwLock<QueuedComponents>,
} }

View File

@ -70,6 +70,7 @@
use crate::{ use crate::{
component::{ComponentId, Components, StorageType}, component::{ComponentId, Components, StorageType},
query::FilteredAccess, query::FilteredAccess,
resource::IsResource,
world::{FromWorld, World}, world::{FromWorld, World},
}; };
use bevy_ecs_macros::{Component, Resource}; use bevy_ecs_macros::{Component, Resource};
@ -143,6 +144,8 @@ impl FromWorld for DefaultQueryFilters {
let mut filters = DefaultQueryFilters::empty(); let mut filters = DefaultQueryFilters::empty();
let disabled_component_id = world.register_component::<Disabled>(); let disabled_component_id = world.register_component::<Disabled>();
filters.register_disabling_component(disabled_component_id); 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 filters
} }
} }

View File

@ -378,9 +378,9 @@ mod tests {
let mut world = World::new(); let mut world = World::new();
let e = world.spawn((TableStored("abc"), A(123))).id(); let e = world.spawn((TableStored("abc"), A(123))).id();
let f = world.spawn((TableStored("def"), A(456))).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!(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::<TableStored>(e).is_none());
assert!(world.get::<A>(e).is_none()); assert!(world.get::<A>(e).is_none());
assert_eq!(world.get::<TableStored>(f).unwrap().0, "def"); 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 e = world.spawn((TableStored("abc"), SparseStored(123))).id();
let f = world.spawn((TableStored("def"), SparseStored(456))).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!(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::<TableStored>(e).is_none());
assert!(world.get::<SparseStored>(e).is_none()); assert!(world.get::<SparseStored>(e).is_none());
assert_eq!(world.get::<TableStored>(f).unwrap().0, "def"); assert_eq!(world.get::<TableStored>(f).unwrap().0, "def");
@ -1786,7 +1786,7 @@ mod tests {
fn try_insert_batch() { fn try_insert_batch() {
let mut world = World::default(); let mut world = World::default();
let e0 = world.spawn(A(0)).id(); 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)))]; 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() { fn try_insert_batch_if_new() {
let mut world = World::default(); let mut world = World::default();
let e0 = world.spawn(A(0)).id(); 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)))]; 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 mut query = world.query::<NameOrEntity>();
let d1 = query.get(&world, e1).unwrap(); let d1 = query.get(&world, e1).unwrap();
// NameOrEntity Display for entities without a Name should be {index}v{generation} // 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(); let d2 = query.get(&world, e2).unwrap();
// NameOrEntity Display for entities with a Name should be the Name // NameOrEntity Display for entities with a Name should be the Name
assert_eq!(d2.to_string(), "MyName"); assert_eq!(d2.to_string(), "MyName");

View File

@ -275,6 +275,7 @@ impl<'w, D: QueryData, F: QueryFilter> QueryBuilder<'w, D, F> {
mod tests { mod tests {
use crate::{ use crate::{
prelude::*, prelude::*,
resource::IsResource,
world::{EntityMutExcept, EntityRefExcept, FilteredEntityMut, FilteredEntityRef}, world::{EntityMutExcept, EntityRefExcept, FilteredEntityMut, FilteredEntityRef},
}; };
use std::dbg; use std::dbg;
@ -332,6 +333,7 @@ mod tests {
#[test] #[test]
fn builder_or() { fn builder_or() {
let mut world = World::new(); let mut world = World::new();
world.spawn((A(0), B(0))); world.spawn((A(0), B(0)));
world.spawn(B(0)); world.spawn(B(0));
world.spawn(C(0)); world.spawn(C(0));
@ -485,6 +487,7 @@ mod tests {
let mut query = QueryBuilder::<(FilteredEntityMut, EntityMutExcept<A>)>::new(&mut world) let mut query = QueryBuilder::<(FilteredEntityMut, EntityMutExcept<A>)>::new(&mut world)
.data::<EntityMut>() .data::<EntityMut>()
.filter::<Without<IsResource>>()
.build(); .build();
// Removing `EntityMutExcept<A>` just leaves A // Removing `EntityMutExcept<A>` just leaves A
@ -496,6 +499,7 @@ mod tests {
let mut query = QueryBuilder::<(FilteredEntityMut, EntityRefExcept<A>)>::new(&mut world) let mut query = QueryBuilder::<(FilteredEntityMut, EntityRefExcept<A>)>::new(&mut world)
.data::<EntityMut>() .data::<EntityMut>()
.filter::<Without<IsResource>>()
.build(); .build();
// Removing `EntityRefExcept<A>` just leaves A, plus read access // Removing `EntityRefExcept<A>` just leaves A, plus read access

View File

@ -2669,6 +2669,7 @@ mod tests {
#[test] #[test]
fn query_iter_sorts() { fn query_iter_sorts() {
let mut world = World::new(); let mut world = World::new();
for i in 0..100 { for i in 0..100 {
world.spawn(A(i as f32)); world.spawn(A(i as f32));
world.spawn((A(i as f32), Sparse(i))); world.spawn((A(i as f32), Sparse(i)));

View File

@ -1850,6 +1850,7 @@ mod tests {
#[test] #[test]
fn can_transmute_empty_tuple() { fn can_transmute_empty_tuple() {
let mut world = World::new(); let mut world = World::new();
world.register_component::<A>(); world.register_component::<A>();
let entity = world.spawn(A(10)).id(); let entity = world.spawn(A(10)).id();
@ -2172,9 +2173,7 @@ mod tests {
world.spawn((B(0), C(0))); world.spawn((B(0), C(0)));
world.spawn(C(0)); world.spawn(C(0));
let mut df = DefaultQueryFilters::empty(); world.register_disabling_component::<C>();
df.register_disabling_component(world.register_component::<C>());
world.insert_resource(df);
// Without<C> only matches the first entity // Without<C> only matches the first entity
let mut query = QueryState::<()>::new(&mut world); let mut query = QueryState::<()>::new(&mut world);
@ -2216,9 +2215,9 @@ mod tests {
assert!(query.is_dense); assert!(query.is_dense);
assert_eq!(3, query.iter(&world).count()); assert_eq!(3, query.iter(&world).count());
let mut df = DefaultQueryFilters::empty(); let df = DefaultQueryFilters::from_world(&mut world);
df.register_disabling_component(world.register_component::<Sparse>());
world.insert_resource(df); world.insert_resource(df);
world.register_disabling_component::<Sparse>();
let mut query = QueryState::<()>::new(&mut world); let mut query = QueryState::<()>::new(&mut world);
// The query doesn't ask for sparse components, but the default filters adds // The query doesn't ask for sparse components, but the default filters adds
@ -2226,9 +2225,9 @@ mod tests {
assert!(!query.is_dense); assert!(!query.is_dense);
assert_eq!(1, query.iter(&world).count()); assert_eq!(1, query.iter(&world).count());
let mut df = DefaultQueryFilters::empty(); let df = DefaultQueryFilters::from_world(&mut world);
df.register_disabling_component(world.register_component::<Table>());
world.insert_resource(df); world.insert_resource(df);
world.register_disabling_component::<Table>();
let mut query = QueryState::<()>::new(&mut world); let mut query = QueryState::<()>::new(&mut world);
// If the filter is instead a table components, the query can still be dense // 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). //! 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 // The derive macro for the `Resource` trait
pub use bevy_ecs_macros::Resource; pub use bevy_ecs_macros::Resource;
@ -73,3 +78,89 @@ pub use bevy_ecs_macros::Resource;
note = "consider annotating `{Self}` with `#[derive(Resource)]`" note = "consider annotating `{Self}` with `#[derive(Resource)]`"
)] )]
pub trait Resource: Send + Sync + 'static {} 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] #[test]
fn command_processing() { fn command_processing() {
let mut world = World::new(); let mut world = World::new();
assert_eq!(world.entities.len(), 0); assert_eq!(world.entity_count(), 0);
world.run_system_once(spawn_entity).unwrap(); world.run_system_once(spawn_entity).unwrap();
assert_eq!(world.entities.len(), 1); assert_eq!(world.entity_count(), 1);
} }
#[test] #[test]

View File

@ -4926,6 +4926,7 @@ mod tests {
change_detection::{MaybeLocation, MutUntyped}, change_detection::{MaybeLocation, MutUntyped},
component::ComponentId, component::ComponentId,
prelude::*, prelude::*,
resource::IsResource,
system::{assert_is_system, RunSystemOnce as _}, system::{assert_is_system, RunSystemOnce as _},
world::{error::EntityComponentError, DeferredWorld, FilteredEntityMut, FilteredEntityRef}, world::{error::EntityComponentError, DeferredWorld, FilteredEntityMut, FilteredEntityRef},
}; };
@ -5374,7 +5375,7 @@ mod tests {
world.spawn(TestComponent(0)).insert(TestComponent2(0)); 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; let mut found = false;
for entity_ref in query.iter_mut(&mut world) { for entity_ref in query.iter_mut(&mut world) {
@ -5432,7 +5433,10 @@ mod tests {
world.run_system_once(system).unwrap(); 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() { for entity_ref in query.iter() {
assert!(matches!( assert!(matches!(
entity_ref.get::<TestComponent2>(), entity_ref.get::<TestComponent2>(),
@ -5449,7 +5453,7 @@ mod tests {
let mut world = World::new(); let mut world = World::new();
world.spawn(TestComponent(0)).insert(TestComponent2(0)); 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; let mut found = false;
for mut entity_mut in query.iter_mut(&mut world) { for mut entity_mut in query.iter_mut(&mut world) {
@ -5514,7 +5518,10 @@ mod tests {
world.run_system_once(system).unwrap(); 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() { for mut entity_mut in query.iter_mut() {
assert!(entity_mut assert!(entity_mut
.get_mut::<TestComponent2>() .get_mut::<TestComponent2>()

View File

@ -22,6 +22,7 @@ use crate::{
event::BufferedEvent, event::BufferedEvent,
lifecycle::{ComponentHooks, ADD, DESPAWN, INSERT, REMOVE, REPLACE}, lifecycle::{ComponentHooks, ADD, DESPAWN, INSERT, REMOVE, REPLACE},
prelude::{Add, Despawn, Insert, Remove, Replace}, prelude::{Add, Despawn, Insert, Remove, Replace},
resource::{ResourceEntity, TypeErasedResource},
}; };
pub use bevy_ecs_macros::FromWorld; pub use bevy_ecs_macros::FromWorld;
use bevy_utils::prelude::DebugName; use bevy_utils::prelude::DebugName;
@ -216,12 +217,20 @@ impl World {
&mut self.entities &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. /// This is helpful as a diagnostic, but it can also be used effectively in tests.
#[inline] #[inline]
pub fn entity_count(&self) -> u32 { 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. /// Retrieves this world's [`Archetypes`] collection.
@ -1701,6 +1710,18 @@ impl World {
pub fn init_resource<R: Resource + FromWorld>(&mut self) -> ComponentId { pub fn init_resource<R: Resource + FromWorld>(&mut self) -> ComponentId {
let caller = MaybeLocation::caller(); let caller = MaybeLocation::caller();
let component_id = self.components_registrator().register_resource::<R>(); 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 if self
.storages .storages
.resources .resources
@ -1738,6 +1759,17 @@ impl World {
caller: MaybeLocation, caller: MaybeLocation,
) { ) {
let component_id = self.components_registrator().register_resource::<R>(); 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| { OwningPtr::make(value, |ptr| {
// SAFETY: component_id was just initialized and corresponds to resource of type R. // SAFETY: component_id was just initialized and corresponds to resource of type R.
unsafe { unsafe {
@ -1805,6 +1837,10 @@ impl World {
#[inline] #[inline]
pub fn remove_resource<R: Resource>(&mut self) -> Option<R> { pub fn remove_resource<R: Resource>(&mut self) -> Option<R> {
let component_id = self.components.get_valid_resource_id(TypeId::of::<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()?; let (ptr, _, _) = self.storages.resources.get_mut(component_id)?.remove()?;
// SAFETY: `component_id` was gotten via looking up the `R` type // SAFETY: `component_id` was gotten via looking up the `R` type
unsafe { Some(ptr.read::<R>()) } unsafe { Some(ptr.read::<R>()) }
@ -2711,6 +2747,20 @@ impl World {
) { ) {
let change_tick = self.change_tick(); 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); let resource = self.initialize_resource_internal(component_id);
// SAFETY: `value` is valid for `component_id`, ensured by caller // SAFETY: `value` is valid for `component_id`, ensured by caller
unsafe { unsafe {
@ -3403,6 +3453,10 @@ impl World {
/// **You should prefer to use the typed API [`World::remove_resource`] where possible and only /// **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.** /// 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<()> { 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 self.storages
.resources .resources
.get_mut(component_id)? .get_mut(component_id)?
@ -3676,7 +3730,7 @@ mod tests {
entity::EntityHashSet, entity::EntityHashSet,
entity_disabling::{DefaultQueryFilters, Disabled}, entity_disabling::{DefaultQueryFilters, Disabled},
ptr::OwningPtr, ptr::OwningPtr,
resource::Resource, resource::{IsResource, Resource},
world::{error::EntityMutableFetchError, DeferredWorld}, world::{error::EntityMutableFetchError, DeferredWorld},
}; };
use alloc::{ use alloc::{
@ -4108,9 +4162,11 @@ mod tests {
let iterate_and_count_entities = |world: &World, entity_counters: &mut HashMap<_, _>| { let iterate_and_count_entities = |world: &World, entity_counters: &mut HashMap<_, _>| {
entity_counters.clear(); entity_counters.clear();
for entity in world.iter_entities() { for entity in world.iter_entities() {
if !entity.contains::<IsResource>() {
let counter = entity_counters.entry(entity.id()).or_insert(0); let counter = entity_counters.entry(entity.id()).or_insert(0);
*counter += 1; *counter += 1;
} }
}
}; };
// Adding one entity and validating iteration // Adding one entity and validating iteration
@ -4206,9 +4262,9 @@ mod tests {
let mut entities = world.iter_entities_mut().collect::<Vec<_>>(); 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))); 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( 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, &mut b[0].get_mut::<B>().unwrap().0,
); );
assert_eq!(world.entity(a1).get(), Some(&A(0))); assert_eq!(world.entity(a1).get(), Some(&A(0)));

View File

@ -49,7 +49,13 @@ pub mod prelude {
use bevy_app::prelude::*; use bevy_app::prelude::*;
#[cfg(feature = "serialize")] #[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`]. /// Plugin that provides scene functionality to an [`App`].
#[derive(Default)] #[derive(Default)]
@ -64,6 +70,8 @@ impl Plugin for ScenePlugin {
.init_resource::<SceneSpawner>() .init_resource::<SceneSpawner>()
.register_type::<SceneRoot>() .register_type::<SceneRoot>()
.register_type::<DynamicSceneRoot>() .register_type::<DynamicSceneRoot>()
.register_type::<IsResource>()
.register_type::<ResourceEntity<DefaultQueryFilters>>()
.add_systems(SpawnScene, (scene_spawner, scene_spawner_system).chain()); .add_systems(SpawnScene, (scene_spawner, scene_spawner_system).chain());
// Register component hooks for DynamicSceneRoot // Register component hooks for DynamicSceneRoot

View File

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

View File

@ -516,9 +516,11 @@ mod tests {
}; };
use bevy_ecs::{ use bevy_ecs::{
entity::{Entity, EntityHashMap}, entity::{Entity, EntityHashMap},
entity_disabling::DefaultQueryFilters,
prelude::{Component, ReflectComponent, ReflectResource, Resource, World}, prelude::{Component, ReflectComponent, ReflectResource, Resource, World},
query::{With, Without}, query::{With, Without},
reflect::AppTypeRegistry, reflect::AppTypeRegistry,
resource::{IsResource, ResourceEntity},
world::FromWorld, world::FromWorld,
}; };
use bevy_reflect::{Reflect, ReflectDeserialize, ReflectSerialize}; use bevy_reflect::{Reflect, ReflectDeserialize, ReflectSerialize};
@ -611,6 +613,8 @@ mod tests {
registry.register::<MyEntityRef>(); registry.register::<MyEntityRef>();
registry.register::<Entity>(); registry.register::<Entity>();
registry.register::<MyResource>(); registry.register::<MyResource>();
registry.register::<IsResource>();
registry.register::<ResourceEntity<DefaultQueryFilters>>();
} }
world.insert_resource(registry); world.insert_resource(registry);
world world
@ -638,20 +642,20 @@ mod tests {
), ),
}, },
entities: { entities: {
4294967293: ( 4294967291: (
components: { components: {
"bevy_scene::serde::tests::Bar": (345), "bevy_scene::serde::tests::Bar": (345),
"bevy_scene::serde::tests::Baz": (789), "bevy_scene::serde::tests::Baz": (789),
"bevy_scene::serde::tests::Foo": (123), "bevy_scene::serde::tests::Foo": (123),
}, },
), ),
4294967294: ( 4294967292: (
components: { components: {
"bevy_scene::serde::tests::Bar": (345), "bevy_scene::serde::tests::Bar": (345),
"bevy_scene::serde::tests::Foo": (123), "bevy_scene::serde::tests::Foo": (123),
}, },
), ),
4294967295: ( 4294967293: (
components: { components: {
"bevy_scene::serde::tests::Foo": (123), "bevy_scene::serde::tests::Foo": (123),
}, },
@ -757,7 +761,7 @@ mod tests {
.write_to_world(&mut dst_world, &mut map) .write_to_world(&mut dst_world, &mut map)
.unwrap(); .unwrap();
assert_eq!(2, deserialized_scene.entities.len()); assert_eq!(4, deserialized_scene.entities.len());
assert_scene_eq(&scene, &deserialized_scene); assert_scene_eq(&scene, &deserialized_scene);
let bar_to_foo = dst_world let bar_to_foo = dst_world
@ -785,7 +789,7 @@ mod tests {
let (scene, deserialized_scene) = roundtrip_ron(&world); 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); assert_scene_eq(&scene, &deserialized_scene);
let mut world = create_world(); let mut world = create_world();
@ -815,10 +819,19 @@ mod tests {
assert_eq!( assert_eq!(
vec![ 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, 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, 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 serialized_scene
); );
@ -830,7 +843,7 @@ mod tests {
.deserialize(&mut postcard::Deserializer::from_bytes(&serialized_scene)) .deserialize(&mut postcard::Deserializer::from_bytes(&serialized_scene))
.unwrap(); .unwrap();
assert_eq!(1, deserialized_scene.entities.len()); assert_eq!(3, deserialized_scene.entities.len());
assert_scene_eq(&scene, &deserialized_scene); assert_scene_eq(&scene, &deserialized_scene);
} }
@ -856,11 +869,21 @@ mod tests {
assert_eq!( assert_eq!(
vec![ 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, 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, 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, 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 buf
); );
@ -874,7 +897,7 @@ mod tests {
.deserialize(&mut rmp_serde::Deserializer::new(&mut reader)) .deserialize(&mut rmp_serde::Deserializer::new(&mut reader))
.unwrap(); .unwrap();
assert_eq!(1, deserialized_scene.entities.len()); assert_eq!(3, deserialized_scene.entities.len());
assert_scene_eq(&scene, &deserialized_scene); assert_scene_eq(&scene, &deserialized_scene);
} }
@ -899,13 +922,23 @@ mod tests {
assert_eq!( assert_eq!(
vec![ 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, 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, 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, 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, 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, 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 serialized_scene
); );
@ -918,7 +951,7 @@ mod tests {
bincode::serde::seed_decode_from_slice(scene_deserializer, &serialized_scene, config) bincode::serde::seed_decode_from_slice(scene_deserializer, &serialized_scene, config)
.unwrap(); .unwrap();
assert_eq!(1, deserialized_scene.entities.len()); assert_eq!(3, deserialized_scene.entities.len());
assert_scene_eq(&scene, &deserialized_scene); 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.