Skip to content

Add entities alongside resources #19711

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 22 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion crates/bevy_ecs/src/component/info.rs
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -18,6 +18,7 @@ use crate::{
RequiredComponents, StorageType,
},
lifecycle::ComponentHooks,
prelude::Entity,
query::DebugCheckedUnwrap as _,
resource::Resource,
storage::SparseSetIndex,
Expand Down Expand Up @@ -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>,
}
Expand Down
4 changes: 2 additions & 2 deletions crates/bevy_ecs/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1799,7 +1799,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)))];

Expand All @@ -1823,7 +1823,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)))];

Expand Down
2 changes: 1 addition & 1 deletion crates/bevy_ecs/src/name.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand Down
92 changes: 92 additions & 0 deletions crates/bevy_ecs/src/resource.rs
Original file line number Diff line number Diff line change
@@ -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;

Expand Down Expand Up @@ -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<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);
}
}
15 changes: 11 additions & 4 deletions crates/bevy_ecs/src/world/entity_ref.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5041,6 +5041,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},
Expand Down Expand Up @@ -5490,7 +5491,7 @@ mod tests {

world.spawn(TestComponent(0)).insert(TestComponent2(0));

let mut query = world.query::<EntityRefExcept<TestComponent>>();
let mut query = world.query::<EntityRefExcept<(TestComponent, Internal)>>();

let mut found = false;
for entity_ref in query.iter_mut(&mut world) {
Expand Down Expand Up @@ -5548,7 +5549,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, Internal)>>,
) {
for entity_ref in query.iter() {
assert!(matches!(
entity_ref.get::<TestComponent2>(),
Expand All @@ -5565,7 +5569,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, Internal)>>();

let mut found = false;
for mut entity_mut in query.iter_mut(&mut world) {
Expand Down Expand Up @@ -5630,7 +5634,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, Internal)>>,
) {
for mut entity_mut in query.iter_mut() {
assert!(entity_mut
.get_mut::<TestComponent2>()
Expand Down
55 changes: 52 additions & 3 deletions crates/bevy_ecs/src/world/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -1695,6 +1696,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
Expand Down Expand Up @@ -1732,6 +1745,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 {
Expand Down Expand Up @@ -1799,6 +1823,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>()) }
Expand Down Expand Up @@ -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::<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 {
Expand Down Expand Up @@ -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)?
Expand Down Expand Up @@ -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},
Expand Down Expand Up @@ -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::<Internal>()) {
let counter = entity_counters.entry(entity.id()).or_insert(0);
*counter += 1;
}
Expand Down Expand Up @@ -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::<Vec<_>>();
let mut entities = world
.iter_entities_mut()
.filter(|e| !e.contains::<Internal>())
.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);
core::mem::swap(
Expand Down
21 changes: 13 additions & 8 deletions crates/bevy_scene/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)]
Expand All @@ -64,6 +72,9 @@ impl Plugin for ScenePlugin {
.init_resource::<SceneSpawner>()
.register_type::<SceneRoot>()
.register_type::<DynamicSceneRoot>()
.register_type::<IsResource>()
.register_type::<Internal>()
.register_type::<ResourceEntity<DefaultQueryFilters>>()
.add_systems(SpawnScene, (scene_spawner, scene_spawner_system).chain());

// Register component hooks for DynamicSceneRoot
Expand Down Expand Up @@ -122,9 +133,7 @@ mod tests {
use bevy_ecs::{
component::Component,
entity::Entity,
entity_disabling::Internal,
hierarchy::{ChildOf, Children},
query::Allows,
reflect::{AppTypeRegistry, ReflectComponent},
world::World,
};
Expand Down Expand Up @@ -305,11 +314,7 @@ mod tests {
scene
.world
.insert_resource(world.resource::<AppTypeRegistry>().clone());
let entities: Vec<Entity> = scene
.world
.query_filtered::<Entity, Allows<Internal>>()
.iter(&scene.world)
.collect();
let entities: Vec<Entity> = scene.world.query::<Entity>().iter(&scene.world).collect();
DynamicSceneBuilder::from_world(&scene.world)
.extract_entities(entities.into_iter())
.build()
Expand Down
2 changes: 1 addition & 1 deletion crates/bevy_scene/src/scene_spawner.rs
Original file line number Diff line number Diff line change
Expand Up @@ -696,7 +696,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
Expand Down
Loading
Loading