Skip to content
Open
Show file tree
Hide file tree
Changes from 14 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
10 changes: 8 additions & 2 deletions assets/scenes/load_scene_example.scn.ron
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
(
resources: {
"scene::ResourceA": (
score: 1,
4294967299: (
components: {
"bevy_ecs::resource::ResourceComponent<scene::ResourceA>": ((
score: 1,
)),
"bevy_ecs::resource::IsResource": (),
"bevy_ecs::entity_disabling::Internal": (),
},
),
},
entities: {
Expand Down
41 changes: 15 additions & 26 deletions crates/bevy_ecs/src/component/info.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ use crate::{
},
lifecycle::ComponentHooks,
query::DebugCheckedUnwrap as _,
resource::Resource,
resource::{Resource, ResourceComponent},
storage::SparseSetIndex,
};

Expand Down Expand Up @@ -290,18 +290,7 @@ impl ComponentDescriptor {
///
/// The [`StorageType`] for resources is always [`StorageType::Table`].
pub fn new_resource<T: Resource>() -> Self {
Self {
name: DebugName::type_name::<T>(),
// PERF: `SparseStorage` may actually be a more
// reasonable choice as `storage_type` for resources.
storage_type: StorageType::Table,
is_send_and_sync: true,
type_id: Some(TypeId::of::<T>()),
layout: Layout::new::<T>(),
drop: needs_drop::<T>().then_some(Self::drop_ptr::<T> as _),
mutable: true,
clone_behavior: ComponentCloneBehavior::Default,
}
Self::new::<ResourceComponent<T>>()
}

pub(super) fn new_non_send<T: Any>(storage_type: StorageType) -> Self {
Expand Down Expand Up @@ -348,7 +337,6 @@ impl ComponentDescriptor {
pub struct Components {
pub(super) components: Vec<Option<ComponentInfo>>,
pub(super) indices: TypeIdMap<ComponentId>,
pub(super) resource_indices: TypeIdMap<ComponentId>,
// This is kept internal and local to verify that no deadlocks can occor.
pub(super) queued: bevy_platform::sync::RwLock<QueuedComponents>,
}
Expand Down Expand Up @@ -587,8 +575,12 @@ impl Components {

/// Type-erased equivalent of [`Components::valid_resource_id()`].
#[inline]
#[deprecated(
Copy link
Contributor

Choose a reason for hiding this comment

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

  1. If I'm understanding this PR correctly, the behaviour of this function changes? So if I had a code get_valid_resource_id(resource_type_id) and upgraded bevy, the function call would start returning unexpected results, correct? In that case, I think it would be better to remove this function completely instead of just deprecating it (users will have to change the code anyways).
  2. If I just have a TypeId and don't know the type R, is there still some way for me to check if it's registered? (for example, I could get the type IDs from iterating over the whole TypeRegistry registrations and checking for ReflectResource type data). If not, one option would be to add these function to ReflectResource.

(the same applies to get_resource_id)

Copy link
Contributor Author

@Trashtalk217 Trashtalk217 Oct 7, 2025

Choose a reason for hiding this comment

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

I think you're right, but I'll leave it to a clean-up PR. I really want to start wrapping this up.

since = "0.18.0",
note = "Use valid_resource_id::<R>() or get_valid_id(TypeId::of::<ResourceComponent<R>>()) for normal resources. Use get_valid_id(TypeId::of::<R>()) for non-send resources."
)]
pub fn get_valid_resource_id(&self, type_id: TypeId) -> Option<ComponentId> {
self.resource_indices.get(&type_id).copied()
self.indices.get(&type_id).copied()
}

/// Returns the [`ComponentId`] of the given [`Resource`] type `T` if it is fully registered.
Expand All @@ -613,7 +605,7 @@ impl Components {
/// * [`Components::get_resource_id()`]
#[inline]
pub fn valid_resource_id<T: Resource>(&self) -> Option<ComponentId> {
self.get_valid_resource_id(TypeId::of::<T>())
self.get_valid_id(TypeId::of::<ResourceComponent<T>>())
}

/// Type-erased equivalent of [`Components::component_id()`].
Expand Down Expand Up @@ -665,15 +657,12 @@ impl Components {

/// Type-erased equivalent of [`Components::resource_id()`].
#[inline]
#[deprecated(
since = "0.18.0",
note = "Use resource_id::<R>() or get_id(TypeId::of::<ResourceComponent<R>>()) instead for normal resources. Use get_id(TypeId::of::<R>()) for non-send resources."
)]
pub fn get_resource_id(&self, type_id: TypeId) -> Option<ComponentId> {
self.resource_indices.get(&type_id).copied().or_else(|| {
self.queued
.read()
.unwrap_or_else(PoisonError::into_inner)
.resources
.get(&type_id)
.map(|queued| queued.id)
})
self.get_id(type_id)
}

/// Returns the [`ComponentId`] of the given [`Resource`] type `T`.
Expand Down Expand Up @@ -705,7 +694,7 @@ impl Components {
/// * [`Components::get_resource_id()`]
#[inline]
pub fn resource_id<T: Resource>(&self) -> Option<ComponentId> {
self.get_resource_id(TypeId::of::<T>())
self.get_id(TypeId::of::<ResourceComponent<T>>())
}

/// # Safety
Expand All @@ -724,7 +713,7 @@ impl Components {
unsafe {
self.register_component_inner(component_id, descriptor);
}
let prev = self.resource_indices.insert(type_id, component_id);
let prev = self.indices.insert(type_id, component_id);
debug_assert!(prev.is_none());
}

Expand Down
76 changes: 47 additions & 29 deletions crates/bevy_ecs/src/component/register.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ use crate::{
Component, ComponentDescriptor, ComponentId, Components, RequiredComponents, StorageType,
},
query::DebugCheckedUnwrap as _,
resource::Resource,
resource::{IsResource, Resource, ResourceComponent},
};

/// Generates [`ComponentId`]s.
Expand Down Expand Up @@ -289,12 +289,7 @@ impl<'w> ComponentsRegistrator<'w> {
/// * [`ComponentsRegistrator::register_resource_with_descriptor()`]
#[inline]
pub fn register_resource<T: Resource>(&mut self) -> ComponentId {
// SAFETY: The [`ComponentDescriptor`] matches the [`TypeId`]
unsafe {
self.register_resource_with(TypeId::of::<T>(), || {
ComponentDescriptor::new_resource::<T>()
})
}
self.register_component::<ResourceComponent<T>>()
}

/// Registers a [non-send resource](crate::system::NonSend) of type `T` with this instance.
Expand All @@ -310,6 +305,37 @@ impl<'w> ComponentsRegistrator<'w> {
}
}

// Adds the necessary resource hooks and required components.
// This ensures that a resource registered with a custom descriptor functions as expected.
// Panics if the component is not registered.
// This has no effect on non-send resources.
fn add_resource_hooks_and_required_components(&mut self, id: ComponentId) {
if self
.get_info(id)
.expect("component was just registered")
.is_send_and_sync()
{
let hooks = self
.components
.get_hooks_mut(id)
.expect("component was just registered");
hooks.on_add(crate::resource::on_add_hook);
hooks.on_remove(crate::resource::on_remove_hook);

let is_resource_id = self.register_component::<IsResource>();
// SAFETY:
// - The IsResource component id matches
// - The constructor constructs an IsResource
unsafe {
let _ = self.components.register_required_components::<IsResource>(
id,
is_resource_id,
|| IsResource,
);
}
}
}

/// Same as [`Components::register_resource_unchecked`] but handles safety.
///
/// # Safety
Expand All @@ -321,7 +347,7 @@ impl<'w> ComponentsRegistrator<'w> {
type_id: TypeId,
descriptor: impl FnOnce() -> ComponentDescriptor,
) -> ComponentId {
if let Some(id) = self.resource_indices.get(&type_id) {
if let Some(id) = self.indices.get(&type_id) {
return *id;
}

Expand All @@ -344,6 +370,10 @@ impl<'w> ComponentsRegistrator<'w> {
self.components
.register_resource_unchecked(type_id, id, descriptor());
}

// registering a resource with this method leaves hooks and required_components empty, so we add them afterwards
self.add_resource_hooks_and_required_components(id);

id
}

Expand All @@ -368,6 +398,10 @@ impl<'w> ComponentsRegistrator<'w> {
unsafe {
self.components.register_component_inner(id, descriptor);
}

// registering a resource with this method leaves hooks and required_components empty, so we add them afterwards
self.add_resource_hooks_and_required_components(id);

id
}

Expand Down Expand Up @@ -619,26 +653,7 @@ impl<'w> ComponentsQueuedRegistrator<'w> {
/// See type level docs for details.
#[inline]
pub fn queue_register_resource<T: Resource>(&self) -> ComponentId {
let type_id = TypeId::of::<T>();
self.get_resource_id(type_id).unwrap_or_else(|| {
// SAFETY: We just checked that this type was not already registered.
unsafe {
self.register_arbitrary_resource(
type_id,
ComponentDescriptor::new_resource::<T>(),
move |registrator, id, descriptor| {
// SAFETY: We just checked that this is not currently registered or queued, and if it was registered since, this would have been dropped from the queue.
// SAFETY: Id uniqueness handled by caller, and the type_id matches descriptor.
#[expect(unused_unsafe, reason = "More precise to specify.")]
unsafe {
registrator
.components
.register_resource_unchecked(type_id, id, descriptor);
}
},
)
}
})
self.queue_register_component::<ResourceComponent<T>>()
}

/// This is a queued version of [`ComponentsRegistrator::register_non_send`].
Expand All @@ -654,7 +669,7 @@ impl<'w> ComponentsQueuedRegistrator<'w> {
#[inline]
pub fn queue_register_non_send<T: Any>(&self) -> ComponentId {
let type_id = TypeId::of::<T>();
self.get_resource_id(type_id).unwrap_or_else(|| {
self.get_id(type_id).unwrap_or_else(|| {
// SAFETY: We just checked that this type was not already registered.
unsafe {
self.register_arbitrary_resource(
Expand Down Expand Up @@ -695,6 +710,9 @@ impl<'w> ComponentsQueuedRegistrator<'w> {
.components
.register_component_inner(id, descriptor);
}

// registering a resource with this method leaves hooks and required_components empty, so we add them afterwards
registrator.add_resource_hooks_and_required_components(id);
})
}
}
10 changes: 5 additions & 5 deletions crates/bevy_ecs/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1236,7 +1236,7 @@ mod tests {

#[test]
fn resource() {
use crate::resource::Resource;
use crate::resource::{Resource, ResourceComponent};

#[derive(Resource, PartialEq, Debug)]
struct Num(i32);
Expand All @@ -1253,7 +1253,7 @@ mod tests {
world.insert_resource(Num(123));
let resource_id = world
.components()
.get_resource_id(TypeId::of::<Num>())
.get_id(TypeId::of::<ResourceComponent<Num>>())
.unwrap();

assert_eq!(world.resource::<Num>().0, 123);
Expand Down Expand Up @@ -1310,7 +1310,7 @@ mod tests {

let current_resource_id = world
.components()
.get_resource_id(TypeId::of::<Num>())
.get_id(TypeId::of::<ResourceComponent<Num>>())
.unwrap();
assert_eq!(
resource_id, current_resource_id,
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(), "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
72 changes: 72 additions & 0 deletions crates/bevy_ecs/src/resource.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,15 @@
//! Resources are unique, singleton-like data types that can be accessed from systems and stored in the [`World`](crate::world::World).

use core::ops::Deref;

use crate::{
entity_disabling::Internal,
lifecycle::HookContext,
prelude::{Component, ReflectComponent},
world::DeferredWorld,
};
use bevy_reflect::{prelude::ReflectDefault, Reflect};

// The derive macro for the `Resource` trait
pub use bevy_ecs_macros::Resource;

Expand Down Expand Up @@ -73,3 +83,65 @@ pub use bevy_ecs_macros::Resource;
note = "consider annotating `{Self}` with `#[derive(Resource)]`"
)]
pub trait Resource: Send + Sync + 'static {}

/// A component that contains the resource of type `T`.
///
/// When creating a resource, a [`ResourceComponent`] is inserted on a new entity in the world.
///
/// 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.
/// Moreover, this component requires both marker components [`IsResource`] and [`Internal`].
/// The former can be used to quickly iterate over all resources through a query,
/// while the latter marks the associated entity as internal, ensuring that it won't show up on broad queries such as
/// `world.query::<Entity>()`.
#[cfg_attr(feature = "bevy_reflect", derive(Reflect), reflect(Component))]
#[derive(Component)]
#[require(IsResource, Internal)]
#[component(on_add = on_add_hook, on_remove = on_remove_hook)]
#[repr(transparent)]
pub struct ResourceComponent<R: Resource>(pub R);

pub(crate) fn on_add_hook(mut deferred_world: DeferredWorld, context: HookContext) {
let world = deferred_world.deref();
if world.resource_entities.contains(context.component_id) {
// the resource already exists and we need to overwrite it
let offending_entity = *world.resource_entities.get(context.component_id).unwrap();
deferred_world.commands().entity(offending_entity).despawn();
}
// we update the cache
// SAFETY: We only update a cache and don't perform any structural changes (component adds / removals)
unsafe {
deferred_world
.as_unsafe_world_cell()
.world_mut()
.resource_entities
.insert(context.component_id, context.entity);
}
}

pub(crate) fn on_remove_hook(mut deferred_world: DeferredWorld, context: HookContext) {
let world = deferred_world.deref();
// If the resource is already linked to a new (different) entity, we don't remove it.
if let Some(entity) = world.resource_entities.get(context.component_id)
&& *entity == context.entity
{
// SAFETY: We only update a cache and don't perform any structural changes (component adds / removals)
unsafe {
deferred_world
.as_unsafe_world_cell()
.world_mut()
.resource_entities
.remove(context.component_id);
}
}
}

/// A marker component for entities which store resources.
#[cfg_attr(
feature = "bevy_reflect",
derive(Reflect),
reflect(Component, Default, Debug)
)]
#[derive(Component, Debug, Default)]
#[require(Internal)]
pub struct IsResource;
Loading