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: {
"bevy_ecs::resource::ResourceComponent<scene::ResourceA>": ((
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