Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
042337f
add back all changes
Trashtalk217 Jul 10, 2025
47c20a5
Merge branch 'main' of https://github.com/bevyengine/bevy into resour…
Trashtalk217 Jul 10, 2025
64eac9a
cargo fmt
Trashtalk217 Jul 10, 2025
956c1a6
cargo clippy
Trashtalk217 Jul 10, 2025
073df4b
add entities with resources, auto disabled IsResource
Trashtalk217 Jul 12, 2025
6c8281d
fixed and tests
Trashtalk217 Jul 12, 2025
f9dc9f1
fixed more tests
Trashtalk217 Jul 12, 2025
07723ac
fixed moore tests
Trashtalk217 Jul 12, 2025
1ba7af2
fix ci
Trashtalk217 Jul 12, 2025
b89c042
fix mooore tests (benches)
Trashtalk217 Jul 12, 2025
9b33933
fix docs
Trashtalk217 Jul 12, 2025
c7b4f1d
add migration guide
Trashtalk217 Jul 12, 2025
96aef45
fixed spelling errors to prove I'm not AI
Trashtalk217 Jul 12, 2025
e3ccd1b
Merge branch 'main' of https://github.com/bevyengine/bevy into resour…
Trashtalk217 Jul 12, 2025
5758134
addressed comments
Trashtalk217 Jul 13, 2025
9824c3a
testing robustness
Trashtalk217 Jul 14, 2025
9acf43f
Merge branch 'main' of https://github.com/bevyengine/bevy into resour…
Trashtalk217 Jul 14, 2025
da1d203
merge upstream
Trashtalk217 Jul 23, 2025
4d4e914
cleanup
Trashtalk217 Jul 23, 2025
78a0672
fix more stuff
Trashtalk217 Jul 23, 2025
4a2416d
update migration guides
Trashtalk217 Jul 24, 2025
3942c56
spelling
Trashtalk217 Jul 24, 2025
724a4c4
merge
Trashtalk217 Aug 22, 2025
f442ff6
fix imports
Trashtalk217 Aug 22, 2025
acb539e
second attempt at fixing a test
Trashtalk217 Aug 22, 2025
a0d76c3
merge
Trashtalk217 Aug 30, 2025
c99df9c
Merge branch 'resource_entity_lookup' of github.com:trashtalk217/bevy…
Trashtalk217 Aug 30, 2025
12383a3
Merge branch 'main' of https://github.com/bevyengine/bevy into resour…
Trashtalk217 Sep 2, 2025
d1b9b56
Merge branch 'main' of https://github.com/bevyengine/bevy into resour…
Trashtalk217 Sep 8, 2025
2261f8b
Merge branch 'main' of https://github.com/bevyengine/bevy into resour…
Trashtalk217 Sep 14, 2025
7f224be
address comments
Trashtalk217 Sep 14, 2025
6246539
Update crates/bevy_ecs/src/resource.rs
Trashtalk217 Sep 14, 2025
b83a617
Update crates/bevy_ecs/src/resource.rs
Trashtalk217 Sep 14, 2025
e98bcd8
Update crates/bevy_ecs/src/resource.rs
Trashtalk217 Sep 14, 2025
09e5ff8
Update crates/bevy_ecs/src/name.rs
Trashtalk217 Sep 14, 2025
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::{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;
Expand All @@ -19,6 +19,7 @@ use crate::{
RequiredComponents, StorageType,
},
lifecycle::ComponentHooks,
prelude::Entity,
query::DebugCheckedUnwrap as _,
resource::Resource,
storage::SparseSetIndex,
Expand Down Expand Up @@ -349,6 +350,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
8 changes: 4 additions & 4 deletions crates/bevy_ecs/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1553,8 +1553,8 @@ mod tests {
let mut world_a = World::new();
let world_b = World::new();
let mut query = world_a.query::<&A>();
let _ = query.get(&world_a, Entity::from_raw_u32(0).unwrap());
let _ = query.get(&world_b, Entity::from_raw_u32(0).unwrap());
let _ = query.get(&world_a, Entity::from_raw_u32(10_000).unwrap());
let _ = query.get(&world_b, Entity::from_raw_u32(10_000).unwrap());
}

#[test]
Expand Down Expand Up @@ -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(10_000).unwrap();

let values = vec![(e0, (A(1), B(0))), (e1, (A(0), B(1)))];

Expand All @@ -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(10_000).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(), std::format!("{e1}"));
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
97 changes: 97 additions & 0 deletions crates/bevy_ecs/src/resource.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
//! 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 crate::prelude::ReflectResource;
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 +80,93 @@ 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`.
/// Note that until [#20934](https://github.com/bevyengine/bevy/pull/20934) is merged, this does not actually store any data.
///
/// 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.
///
/// Note that because [`Internal`] is a required component, this entity will not appear in queries by default.
#[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 as the `R` generic of [`ResourceEntity<R>`], when no type information is available.
/// This is used by [`World::insert_resource_by_id`](crate::world::World).
#[cfg_attr(feature = "bevy_reflect", derive(Reflect), reflect(Resource, Debug))]
#[derive(Resource, Debug)]
pub 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 @@ -5056,6 +5056,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 @@ -5497,7 +5498,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)>>();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

while it doesn't hurt, Internal here shouldn't be needed anymore since #20163. same for the rest of this file.


let mut found = false;
for entity_ref in query.iter_mut(&mut world) {
Expand Down Expand Up @@ -5555,7 +5556,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 @@ -5572,7 +5576,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 @@ -5637,7 +5641,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
56 changes: 52 additions & 4 deletions crates/bevy_ecs/src/world/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ use crate::{
prelude::{Add, Despawn, Insert, Remove, Replace},
query::{DebugCheckedUnwrap, QueryData, QueryFilter, QueryState},
relationship::RelationshipHookMode,
resource::Resource,
resource::{Resource, ResourceEntity, TypeErasedResource},
schedule::{Schedule, ScheduleLabel, Schedules},
storage::{ResourceData, Storages},
system::Commands,
Expand Down Expand Up @@ -1702,6 +1702,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 @@ -1739,6 +1751,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 @@ -1806,6 +1829,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 @@ -2722,6 +2749,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 @@ -3434,6 +3475,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 @@ -3705,7 +3750,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 @@ -4139,7 +4184,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 @@ -4239,7 +4284,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
23 changes: 15 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 @@ -62,6 +70,11 @@ impl Plugin for ScenePlugin {
.init_asset::<Scene>()
.init_asset_loader::<SceneLoader>()
.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 @@ -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,
};
Expand Down Expand Up @@ -309,11 +320,7 @@ mod tests {
scene
.world
.insert_resource(world.resource::<AppTypeRegistry>().clone());
let entities: Vec<Entity> = scene
.world
.query_filtered::<Entity, Allow<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 @@ -773,7 +773,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