Skip to content
Open
Show file tree
Hide file tree
Changes from 14 commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
dd53970
Resources as components with ResourceComponent wrapper
Trashtalk217 Oct 1, 2025
449b132
deprecate get_valid_resource_id and get_resource_id
Trashtalk217 Oct 2, 2025
8c4269c
last simplification:
Trashtalk217 Oct 2, 2025
1d62f80
Revert "last simplification:"
Trashtalk217 Oct 2, 2025
96e758c
fix queued registration
Trashtalk217 Oct 2, 2025
54e5f10
catch bevy_scene up (not the tests)
Trashtalk217 Oct 2, 2025
a990c99
fixed most bevy_scene tests
Trashtalk217 Oct 2, 2025
1c27389
fixed bevy_render
Trashtalk217 Oct 2, 2025
233f967
fixed various tests behind feature flags
Trashtalk217 Oct 2, 2025
3e93e00
changed a test: MapEntities on resources is not possible right now
Trashtalk217 Oct 2, 2025
3f68bdc
updated example
Trashtalk217 Oct 2, 2025
ff679fe
removed internal import example
Trashtalk217 Oct 2, 2025
1a517f8
Apply suggestions from code review
Trashtalk217 Oct 3, 2025
d206bf8
made changes from code review
Trashtalk217 Oct 4, 2025
7298223
resource derives MapEntities by default
Trashtalk217 Oct 6, 2025
5718b34
avoid generic parameter name collision
Trashtalk217 Oct 6, 2025
4025a5e
fix tests
Trashtalk217 Oct 6, 2025
610d9ae
put resource entities behind a SyncUnsafeCell
Trashtalk217 Oct 6, 2025
4297457
Update crates/bevy_ecs/src/resource.rs
Trashtalk217 Oct 7, 2025
4098c11
Update crates/bevy_ecs/src/resource.rs
Trashtalk217 Oct 8, 2025
f0b91c9
adressed comments from review
Trashtalk217 Oct 9, 2025
2fa342e
add a migration guide
Trashtalk217 Oct 10, 2025
7faca83
added a release-note
Trashtalk217 Oct 10, 2025
e4b53dc
Apply suggestions from code review
Trashtalk217 Oct 10, 2025
fcb593e
fixed markdown
Trashtalk217 Oct 10, 2025
37c7f80
Merge branch 'resource-as-components-v2' of github.com:trashtalk217/b…
Trashtalk217 Oct 10, 2025
136e930
fixed markdown
Trashtalk217 Oct 10, 2025
0706222
delete dead/duplicate code
Trashtalk217 Oct 11, 2025
2a5da87
last review suggestions
Trashtalk217 Oct 11, 2025
9220980
Merge branch 'main' of https://github.com/bevyengine/bevy into resour…
Trashtalk217 Oct 11, 2025
5e65a4f
review comments
Trashtalk217 Oct 11, 2025
cc33a19
add an extra check
Trashtalk217 Oct 12, 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
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: {
Copy link
Member

Choose a reason for hiding this comment

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

One of the motivators for this change is that we get "free" functionality for resources, in the context of things like entity inspectors. However I think this is a great example of why that "free" functionality would require additional custom handling for resources to make it "good".

An entity inspector would (logically) display this as something like:

entity_id: 11v0,
components: [
    ResourceComponent<ResourceA>(ResourceA {
        score: 1
    }),
    IsResource
    Internal
]

And notably this would be hidden by default because it is Internal.

Is this actually better than having a separate resource section of the inspector that displays it as:

ResourceA {
    score: 1
}

without needing to disable the Internal filter and manually filter down to IsResource?

Of course, you could build that functionality on top of the entity representation. But the ideal UX is very different from what we get by default, and the default UX doesn't really win us much in the majority of cases. That is also true for the scene case (see my other comment).

"bevy_ecs::resource::ResourceComponent<scene::ResourceA>": ((
Copy link
Member

Choose a reason for hiding this comment

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

It is worth pointing out that from a user-facing perspective, this is strictly worse UX by a pretty wide margin. Harder to read, harder to write, harder to understand, takes up more space, error prone (any additional data you attach here will be replaced the second another scene tries to set the resource). Special casing resources in scenes makes a lot of sense I think.

Copy link
Member

Choose a reason for hiding this comment

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

Agreed: ideally we can avoid any breakage to the representation of resources in scenes.

Copy link
Contributor Author

@Trashtalk217 Trashtalk217 Oct 15, 2025

Choose a reason for hiding this comment

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

That might be possible, but I would still like to support the entity notation. If / when people start attaching extra data to resource entities, that should be serializable / deserializable through scenes. The only solution I can think of is to store resources twice (both in entities and in resources). It would look something like this:

// valid
resources: {
  "scene::ResourceA": (
        score: 1,
      ),
  },
},
entities: {
  ... // this does not contain a ResourceComponent<ResourceA> entity
}

// also valid
resources: {
  ... // this does not contain a ResourceA resource
},
entities: {
  4294967291: (
    components: {
      "bevy_ecs::entity_disabling::Internal": (),
      "bevy_ecs::resource::IsResource": (),
      "bevy_ecs::resource::ResourceComponent<scene::ResourceA>": ((
        score: 1,
      )),
      "replicon::Replicate": (), // useful additional components that wouldn't fit in `resources { ... }`
    },
  ),
}

// this would be invalid
resources: {
  "scene::ResourceA": (
        score: 1,
      ),
  },
},
entities: {
  4294967291: (
    components: {
      "bevy_ecs::entity_disabling::Internal": (),
      "bevy_ecs::resource::IsResource": (),
      "bevy_ecs::resource::ResourceComponent<scene::ResourceA>": ((
        score: 1,
      )),
      "replicon::Replicate": (),
    },
  ),
}

What do you think of this?

Copy link
Member

Choose a reason for hiding this comment

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

How would you reconcile the entity-notation version with this comment?

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.

Copy link
Contributor

Choose a reason for hiding this comment

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

This seems a pretty big footgun IMO and not something to clean up later on.

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)]
Copy link
Member

Choose a reason for hiding this comment

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

I'm not in love with the ResourceComponent wrapper. Why not implement Component directly for the resource? This would make it appear nicely (without a wrapper type) in scenes / brp / inspectors, it would make the component type id match the type of the resource, it would make it look nicer in queries, easier to consume when being queried for, would result in less codegen, fewer reflected types, etc, etc, etc.

Copy link
Member

Choose a reason for hiding this comment

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

This was previously attempted, and caused problems with type safety and open questions around overlap between ReflectResource and ReflectComponent. I'm open to reverting back to that design: I didn't consider the codegen concerns.

Copy link
Contributor Author

@Trashtalk217 Trashtalk217 Oct 15, 2025

Choose a reason for hiding this comment

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

If you prefer an approach without the wrapper, you can review the first version (#20934).

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();
Copy link
Member

@cart cart Oct 15, 2025

Choose a reason for hiding this comment

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

I don't think silently despawning an existing resource entity / replacing it is something we should allow as part of normal flows. This would break any existing references to the resource entity, including things like relationships. It would also erase any additional context we might add to the entity (ex: marker components). I personally think we are better off despawning the new entity, losing the new value, and logging an error. We lose many of the benefits of representing resources as entities if we can't rely on the stability of their entity id or their components.

(I am aware of how this behavior would interact with scenes, see my other comment)

}
// 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