Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
15 changes: 15 additions & 0 deletions crates/bevy_ecs/macros/src/component.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,9 +40,24 @@ pub fn derive_resource(input: TokenStream) -> TokenStream {
let struct_name = &ast.ident;
let (impl_generics, type_generics, where_clause) = &ast.generics.split_for_impl();

let map_entities_impl = map_entities(
&ast.data,
&bevy_ecs_path,
Ident::new("self", Span::call_site()),
false,
false,
None,
);

TokenStream::from(quote! {
impl #impl_generics #bevy_ecs_path::resource::Resource for #struct_name #type_generics #where_clause {
}

impl #impl_generics #bevy_ecs_path::entity::MapEntities for #struct_name #type_generics #where_clause {
fn map_entities<MAPENT: #bevy_ecs_path::entity::EntityMapper>(&mut self, mapper: &mut MAPENT) {
#map_entities_impl
}
}
})
}

Expand Down
4 changes: 2 additions & 2 deletions crates/bevy_ecs/macros/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -600,7 +600,7 @@ pub fn derive_message(input: TokenStream) -> TokenStream {
}

/// Implement the `Resource` trait.
#[proc_macro_derive(Resource)]
#[proc_macro_derive(Resource, attributes(entities))]
pub fn derive_resource(input: TokenStream) -> TokenStream {
component::derive_resource(input)
}
Expand Down Expand Up @@ -677,7 +677,7 @@ pub fn derive_resource(input: TokenStream) -> TokenStream {
/// #[component(hook_name = function)]
/// struct MyComponent;
/// ```
/// where `hook_name` is `on_add`, `on_insert`, `on_replace` or `on_remove`;
/// where `hook_name` is `on_add`, `on_insert`, `on_replace` or `on_remove`;
/// `function` can be either a path, e.g. `some_function::<Self>`,
/// or a function call that returns a function that can be turned into
/// a `ComponentHook`, e.g. `get_closure("Hi!")`.
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
82 changes: 53 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,43 @@ 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.
//
// Panics if the id isn't registered or valid.
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>();

assert!(self.is_id_valid(id));

// SAFETY:
// - The IsResource component id matches
// - The constructor constructs an IsResource
// - The id is valid, otherwise the assert would have panicked
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 +353,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 +376,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 +404,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 +659,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 +675,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 +716,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
Loading