From 88615734ba0b86312e533169ec1d235750bed8f0 Mon Sep 17 00:00:00 2001 From: eugineerd Date: Sun, 19 Oct 2025 11:21:32 +0000 Subject: [PATCH 1/7] Dynamic relationships api --- crates/bevy_ecs/macros/src/component.rs | 30 ++++++ crates/bevy_ecs/src/component/info.rs | 14 +++ crates/bevy_ecs/src/component/mod.rs | 8 ++ crates/bevy_ecs/src/entity/clone_entities.rs | 1 + crates/bevy_ecs/src/relationship/mod.rs | 104 +++++++++++++++++++ crates/bevy_ecs/src/world/mod.rs | 1 + examples/ecs/dynamic.rs | 1 + examples/ecs/immutable_components.rs | 1 + examples/stress_tests/many_components.rs | 1 + 9 files changed, 161 insertions(+) diff --git a/crates/bevy_ecs/macros/src/component.rs b/crates/bevy_ecs/macros/src/component.rs index 576ce15a5f4f9..3a26eb674e274 100644 --- a/crates/bevy_ecs/macros/src/component.rs +++ b/crates/bevy_ecs/macros/src/component.rs @@ -216,6 +216,32 @@ pub fn derive_component(input: TokenStream) -> TokenStream { ) }; + let relationship_accessor = if (relationship.is_some() || relationship_target.is_some()) + && let Data::Struct(DataStruct { + fields, + struct_token, + .. + }) = &ast.data + && let Ok(field) = relationship_field(fields, "Relationship", struct_token.span()) + { + let relationship_member = field.ident.clone().map_or(Member::from(0), Member::Named); + if relationship.is_some() { + quote! { + Some(unsafe { + #bevy_ecs_path::relationship::ComponentRelationshipAccessor::::relationship( + core::mem::offset_of!(Self, #relationship_member) + ) + }) + } + } else { + quote! { + Some(#bevy_ecs_path::relationship::ComponentRelationshipAccessor::::relationship_target()) + } + } + } else { + quote! {None} + }; + // This puts `register_required` before `register_recursive_requires` to ensure that the constructors of _all_ top // level components are initialized first, giving them precedence over recursively defined constructors for the same component type TokenStream::from(quote! { @@ -241,6 +267,10 @@ pub fn derive_component(input: TokenStream) -> TokenStream { } #map_entities + + fn relationship_accessor() -> Option<#bevy_ecs_path::relationship::ComponentRelationshipAccessor> { + #relationship_accessor + } } #relationship diff --git a/crates/bevy_ecs/src/component/info.rs b/crates/bevy_ecs/src/component/info.rs index 0e222692d7fb1..50691d990d282 100644 --- a/crates/bevy_ecs/src/component/info.rs +++ b/crates/bevy_ecs/src/component/info.rs @@ -20,6 +20,7 @@ use crate::{ }, lifecycle::ComponentHooks, query::DebugCheckedUnwrap as _, + relationship::RelationshipAccessor, resource::Resource, storage::SparseSetIndex, }; @@ -140,6 +141,11 @@ impl ComponentInfo { pub fn required_components(&self) -> &RequiredComponents { &self.required_components } + + /// Returns [`RelationshipAccessor`] for this component if it is a [`Relationship`](crate::relationship::Relationship) or [`RelationshipTarget`](crate::relationship::RelationshipTarget) , `None` otherwise. + pub fn relationship_accessor(&self) -> Option<&RelationshipAccessor> { + self.descriptor.relationship_accessor.as_ref() + } } /// A value which uniquely identifies the type of a [`Component`] or [`Resource`] within a @@ -219,6 +225,7 @@ pub struct ComponentDescriptor { drop: Option unsafe fn(OwningPtr<'a>)>, mutable: bool, clone_behavior: ComponentCloneBehavior, + relationship_accessor: Option, } // We need to ignore the `drop` field in our `Debug` impl @@ -232,6 +239,7 @@ impl Debug for ComponentDescriptor { .field("layout", &self.layout) .field("mutable", &self.mutable) .field("clone_behavior", &self.clone_behavior) + .field("relationship_accessor", &self.relationship_accessor) .finish() } } @@ -258,6 +266,7 @@ impl ComponentDescriptor { drop: needs_drop::().then_some(Self::drop_ptr:: as _), mutable: T::Mutability::MUTABLE, clone_behavior: T::clone_behavior(), + relationship_accessor: T::relationship_accessor().map(|v| v.accessor), } } @@ -266,6 +275,7 @@ impl ComponentDescriptor { /// # Safety /// - the `drop` fn must be usable on a pointer with a value of the layout `layout` /// - the component type must be safe to access from any thread (Send + Sync in rust terms) + /// - `relationship_accessor` must be valid for this component type if not `None` pub unsafe fn new_with_layout( name: impl Into>, storage_type: StorageType, @@ -273,6 +283,7 @@ impl ComponentDescriptor { drop: Option unsafe fn(OwningPtr<'a>)>, mutable: bool, clone_behavior: ComponentCloneBehavior, + relationship_accessor: Option, ) -> Self { Self { name: name.into().into(), @@ -283,6 +294,7 @@ impl ComponentDescriptor { drop, mutable, clone_behavior, + relationship_accessor, } } @@ -301,6 +313,7 @@ impl ComponentDescriptor { drop: needs_drop::().then_some(Self::drop_ptr:: as _), mutable: true, clone_behavior: ComponentCloneBehavior::Default, + relationship_accessor: None, } } @@ -314,6 +327,7 @@ impl ComponentDescriptor { drop: needs_drop::().then_some(Self::drop_ptr:: as _), mutable: true, clone_behavior: ComponentCloneBehavior::Default, + relationship_accessor: None, } } diff --git a/crates/bevy_ecs/src/component/mod.rs b/crates/bevy_ecs/src/component/mod.rs index 1d808f0e1d1ee..3286e7a1eebf3 100644 --- a/crates/bevy_ecs/src/component/mod.rs +++ b/crates/bevy_ecs/src/component/mod.rs @@ -15,6 +15,7 @@ pub use tick::*; use crate::{ entity::EntityMapper, lifecycle::ComponentHook, + relationship::ComponentRelationshipAccessor, system::{Local, SystemParam}, world::{FromWorld, World}, }; @@ -625,6 +626,13 @@ pub trait Component: Send + Sync + 'static { /// You can use the turbofish (`::`) to specify parameters when a function is generic, using either M or _ for the type of the mapper parameter. #[inline] fn map_entities(_this: &mut Self, _mapper: &mut E) {} + + /// Returns [`ComponentRelationshipAccessor`] required for working with relationships in dynamic contexts. + /// + /// If component is not a [`Relationship`](crate::relationship::Relationship) or [`RelationshipTarget`](crate::relationship::RelationshipTarget), this should return `None`. + fn relationship_accessor() -> Option> { + None + } } mod private { diff --git a/crates/bevy_ecs/src/entity/clone_entities.rs b/crates/bevy_ecs/src/entity/clone_entities.rs index 4c110d0057c9a..3b68f854beb96 100644 --- a/crates/bevy_ecs/src/entity/clone_entities.rs +++ b/crates/bevy_ecs/src/entity/clone_entities.rs @@ -2099,6 +2099,7 @@ mod tests { None, true, ComponentCloneBehavior::Custom(test_handler), + None, ) }; let component_id = world.register_component_with_descriptor(descriptor); diff --git a/crates/bevy_ecs/src/relationship/mod.rs b/crates/bevy_ecs/src/relationship/mod.rs index b234318ea8947..a2a753ad16eb1 100644 --- a/crates/bevy_ecs/src/relationship/mod.rs +++ b/crates/bevy_ecs/src/relationship/mod.rs @@ -4,6 +4,8 @@ mod related_methods; mod relationship_query; mod relationship_source_collection; +use alloc::boxed::Box; +use bevy_ptr::Ptr; use core::marker::PhantomData; use alloc::format; @@ -478,11 +480,68 @@ impl RelationshipTargetCloneBehaviorHierarchy } } +/// This enum describes a way to access the entities of [`Relationship`] and [`RelationshipTarget`] components +/// in a type-erased context. +#[derive(Debug, Clone, Copy)] +pub enum RelationshipAccessor { + /// This component is a `Relationship`. + Relationship { + /// Offset of the field containing `Entity` from the base of the component. + entity_field_offset: usize, + }, + /// This component is a `RelationshipTarget`. + RelationshipTarget { + /// Function that returns and iterator over all Entities of this relationship target. + /// # Safety + /// Passed pointer must point to the value of the same component as the one that this accessor was registered to. + iter: for<'a> unsafe fn(Ptr<'a>) -> Box + 'a>, + }, +} + +/// A type-safe convince wrapper over [`RelationshipAccessor`]. +pub struct ComponentRelationshipAccessor { + pub(crate) accessor: RelationshipAccessor, + phantom: PhantomData, +} + +impl ComponentRelationshipAccessor { + /// Create a new [`ComponentRelationshipAccessor`] for a [`Relationship`] component. + /// # Safety + /// `entity_field_offset` should be the offset from the base of this component and point to a field that stores value of type `Entity`. + /// This value can be obtained using the [`core::mem::offset_of`] macro. + pub unsafe fn relationship(entity_field_offset: usize) -> Self + where + C: Relationship, + { + Self { + accessor: RelationshipAccessor::Relationship { + entity_field_offset, + }, + phantom: Default::default(), + } + } + + /// Create a new [`ComponentRelationshipAccessor`] for a [`RelationshipTarget`] component. + pub fn relationship_target() -> Self + where + C: RelationshipTarget, + { + Self { + accessor: RelationshipAccessor::RelationshipTarget { + // Safety: caller ensures that `ptr` is of type `C`. + iter: |ptr| unsafe { Box::new(RelationshipTarget::iter(ptr.deref::())) }, + }, + phantom: Default::default(), + } + } +} + #[cfg(test)] mod tests { use core::marker::PhantomData; use crate::prelude::{ChildOf, Children}; + use crate::relationship::RelationshipAccessor; use crate::world::World; use crate::{component::Component, entity::Entity}; use alloc::vec::Vec; @@ -697,4 +756,49 @@ mod tests { assert!(world.get::(child).is_some()); assert!(world.get::(parent).is_some()); } + + #[test] + fn dynamically_traverse_hierarchy() { + let mut world = World::new(); + let child_of_id = world.register_component::(); + let children_id = world.register_component::(); + + let parent = world.spawn_empty().id(); + let child = world.spawn_empty().id(); + world.entity_mut(child).insert(ChildOf(parent)); + world.flush(); + + let children_ptr = world.get_by_id(parent, children_id).unwrap(); + let RelationshipAccessor::RelationshipTarget { iter } = world + .components() + .get_info(children_id) + .unwrap() + .relationship_accessor() + .unwrap() + else { + unreachable!() + }; + // Safety: `children_ptr` contains value of the same type as the one this accessor was registered for. + let children: Vec<_> = unsafe { iter(children_ptr).collect() }; + assert_eq!(children, alloc::vec![child]); + + let child_of_ptr = world.get_by_id(child, child_of_id).unwrap(); + let RelationshipAccessor::Relationship { + entity_field_offset, + } = world + .components() + .get_info(child_of_id) + .unwrap() + .relationship_accessor() + .unwrap() + else { + unreachable!() + }; + // Safety: + // - offset is in bounds, aligned and has the same lifetime as the original pointer. + // - value at offset is guaranteed to be a valid Entity + let child_of_entity: Entity = + unsafe { *child_of_ptr.byte_add(*entity_field_offset).deref() }; + assert_eq!(child_of_entity, parent); + } } diff --git a/crates/bevy_ecs/src/world/mod.rs b/crates/bevy_ecs/src/world/mod.rs index 024c127aab8b2..55b47c56def8f 100644 --- a/crates/bevy_ecs/src/world/mod.rs +++ b/crates/bevy_ecs/src/world/mod.rs @@ -4001,6 +4001,7 @@ mod tests { }), true, ComponentCloneBehavior::Default, + None, ) }; diff --git a/examples/ecs/dynamic.rs b/examples/ecs/dynamic.rs index 35eb8a8ea6c00..e5dd3c40b50a2 100644 --- a/examples/ecs/dynamic.rs +++ b/examples/ecs/dynamic.rs @@ -97,6 +97,7 @@ fn main() { None, true, ComponentCloneBehavior::Default, + None, ) }); let Some(info) = world.components().get_info(id) else { diff --git a/examples/ecs/immutable_components.rs b/examples/ecs/immutable_components.rs index c0ee241923931..a4e282af61967 100644 --- a/examples/ecs/immutable_components.rs +++ b/examples/ecs/immutable_components.rs @@ -154,6 +154,7 @@ fn demo_3(world: &mut World) { None, false, ComponentCloneBehavior::Default, + None, ) }; diff --git a/examples/stress_tests/many_components.rs b/examples/stress_tests/many_components.rs index 0250f6e3c1fc2..60ace001f5333 100644 --- a/examples/stress_tests/many_components.rs +++ b/examples/stress_tests/many_components.rs @@ -97,6 +97,7 @@ fn stress_test(num_entities: u32, num_components: u32, num_systems: u32) { None, true, // is mutable ComponentCloneBehavior::Default, + None, ) }, ) From 2f317c2ab180039694380e8fff19f18daac2e05b Mon Sep 17 00:00:00 2001 From: eugineerd Date: Sun, 19 Oct 2025 11:51:28 +0000 Subject: [PATCH 2/7] support `LINKED_SPAWN` --- crates/bevy_ecs/src/relationship/mod.rs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/crates/bevy_ecs/src/relationship/mod.rs b/crates/bevy_ecs/src/relationship/mod.rs index a2a753ad16eb1..bb2b419a567d3 100644 --- a/crates/bevy_ecs/src/relationship/mod.rs +++ b/crates/bevy_ecs/src/relationship/mod.rs @@ -488,6 +488,8 @@ pub enum RelationshipAccessor { Relationship { /// Offset of the field containing `Entity` from the base of the component. entity_field_offset: usize, + /// Value of [`RelationshipTarget::LINKED_SPAWN`] for the [`Relationship::RelationshipTarget`] of this [`Relationship`]. + linked_spawn: bool, }, /// This component is a `RelationshipTarget`. RelationshipTarget { @@ -495,6 +497,8 @@ pub enum RelationshipAccessor { /// # Safety /// Passed pointer must point to the value of the same component as the one that this accessor was registered to. iter: for<'a> unsafe fn(Ptr<'a>) -> Box + 'a>, + /// Value of [`RelationshipTarget::LINKED_SPAWN`] of this [`RelationshipTarget`]. + linked_spawn: bool, }, } @@ -516,6 +520,7 @@ impl ComponentRelationshipAccessor { Self { accessor: RelationshipAccessor::Relationship { entity_field_offset, + linked_spawn: C::RelationshipTarget::LINKED_SPAWN, }, phantom: Default::default(), } @@ -530,6 +535,7 @@ impl ComponentRelationshipAccessor { accessor: RelationshipAccessor::RelationshipTarget { // Safety: caller ensures that `ptr` is of type `C`. iter: |ptr| unsafe { Box::new(RelationshipTarget::iter(ptr.deref::())) }, + linked_spawn: C::LINKED_SPAWN, }, phantom: Default::default(), } @@ -769,7 +775,7 @@ mod tests { world.flush(); let children_ptr = world.get_by_id(parent, children_id).unwrap(); - let RelationshipAccessor::RelationshipTarget { iter } = world + let RelationshipAccessor::RelationshipTarget { iter, .. } = world .components() .get_info(children_id) .unwrap() @@ -785,6 +791,7 @@ mod tests { let child_of_ptr = world.get_by_id(child, child_of_id).unwrap(); let RelationshipAccessor::Relationship { entity_field_offset, + .. } = world .components() .get_info(child_of_id) From 70458c5f69736aa80d9be6af61aae53284fd55a9 Mon Sep 17 00:00:00 2001 From: eugineerd Date: Sun, 19 Oct 2025 12:36:50 +0000 Subject: [PATCH 3/7] doc fixes --- crates/bevy_ecs/src/relationship/mod.rs | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/crates/bevy_ecs/src/relationship/mod.rs b/crates/bevy_ecs/src/relationship/mod.rs index bb2b419a567d3..2778568dead6e 100644 --- a/crates/bevy_ecs/src/relationship/mod.rs +++ b/crates/bevy_ecs/src/relationship/mod.rs @@ -484,16 +484,20 @@ impl RelationshipTargetCloneBehaviorHierarchy /// in a type-erased context. #[derive(Debug, Clone, Copy)] pub enum RelationshipAccessor { - /// This component is a `Relationship`. + /// This component is a [`Relationship`]. Relationship { - /// Offset of the field containing `Entity` from the base of the component. + /// Offset of the field containing [`Entity`] from the base of the component. + /// + /// Dynamic equivalent of [`Relationship::get`]. entity_field_offset: usize, /// Value of [`RelationshipTarget::LINKED_SPAWN`] for the [`Relationship::RelationshipTarget`] of this [`Relationship`]. linked_spawn: bool, }, - /// This component is a `RelationshipTarget`. + /// This component is a [`RelationshipTarget`]. RelationshipTarget { - /// Function that returns and iterator over all Entities of this relationship target. + /// Function that returns an iterator over all [`Entity`]s of this [`RelationshipTarget`]'s collection. + /// + /// Dynamic equivalent of [`RelationshipTarget::iter`]. /// # Safety /// Passed pointer must point to the value of the same component as the one that this accessor was registered to. iter: for<'a> unsafe fn(Ptr<'a>) -> Box + 'a>, @@ -502,7 +506,7 @@ pub enum RelationshipAccessor { }, } -/// A type-safe convince wrapper over [`RelationshipAccessor`]. +/// A type-safe convenience wrapper over [`RelationshipAccessor`]. pub struct ComponentRelationshipAccessor { pub(crate) accessor: RelationshipAccessor, phantom: PhantomData, @@ -511,7 +515,7 @@ pub struct ComponentRelationshipAccessor { impl ComponentRelationshipAccessor { /// Create a new [`ComponentRelationshipAccessor`] for a [`Relationship`] component. /// # Safety - /// `entity_field_offset` should be the offset from the base of this component and point to a field that stores value of type `Entity`. + /// `entity_field_offset` should be the offset from the base of this component and point to a field that stores value of type [`Entity`]. /// This value can be obtained using the [`core::mem::offset_of`] macro. pub unsafe fn relationship(entity_field_offset: usize) -> Self where From 34882e77be2e79604b406d286a0027720afb5e1d Mon Sep 17 00:00:00 2001 From: eugineerd Date: Sun, 19 Oct 2025 12:42:00 +0000 Subject: [PATCH 4/7] add migration guide --- .../migration-guides/dynamic_relationships_api.md | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 release-content/migration-guides/dynamic_relationships_api.md diff --git a/release-content/migration-guides/dynamic_relationships_api.md b/release-content/migration-guides/dynamic_relationships_api.md new file mode 100644 index 0000000000000..21bce13f4fb78 --- /dev/null +++ b/release-content/migration-guides/dynamic_relationships_api.md @@ -0,0 +1,8 @@ +--- +title: API for working with `Relationships` and `RelationshipTargets` in type-erased contexts +pull_requests: [21601] +--- + +`ComponentDescriptor` now stores additional data for working with relationships in dynamic contexts. +This resulted in changes to `ComponentDescriptor::new_with_layout`: +- Now requires additional parameter `relationship_accessor`, which should be set to `None` for all existing components. From 2b7b92dfb944dc8b807cbad4f1f07bab1651f15b Mon Sep 17 00:00:00 2001 From: eugineerd Date: Sun, 19 Oct 2025 12:47:12 +0000 Subject: [PATCH 5/7] markdownlint --- release-content/migration-guides/dynamic_relationships_api.md | 1 + 1 file changed, 1 insertion(+) diff --git a/release-content/migration-guides/dynamic_relationships_api.md b/release-content/migration-guides/dynamic_relationships_api.md index 21bce13f4fb78..9b9bf73628717 100644 --- a/release-content/migration-guides/dynamic_relationships_api.md +++ b/release-content/migration-guides/dynamic_relationships_api.md @@ -5,4 +5,5 @@ pull_requests: [21601] `ComponentDescriptor` now stores additional data for working with relationships in dynamic contexts. This resulted in changes to `ComponentDescriptor::new_with_layout`: + - Now requires additional parameter `relationship_accessor`, which should be set to `None` for all existing components. From b457bdc5754e181a506c778a03fb6a35a5f82c86 Mon Sep 17 00:00:00 2001 From: eugineerd Date: Mon, 20 Oct 2025 14:20:52 +0000 Subject: [PATCH 6/7] add safety comment --- crates/bevy_ecs/macros/src/component.rs | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/crates/bevy_ecs/macros/src/component.rs b/crates/bevy_ecs/macros/src/component.rs index 3a26eb674e274..3ba48175d6f90 100644 --- a/crates/bevy_ecs/macros/src/component.rs +++ b/crates/bevy_ecs/macros/src/component.rs @@ -227,11 +227,14 @@ pub fn derive_component(input: TokenStream) -> TokenStream { let relationship_member = field.ident.clone().map_or(Member::from(0), Member::Named); if relationship.is_some() { quote! { - Some(unsafe { - #bevy_ecs_path::relationship::ComponentRelationshipAccessor::::relationship( - core::mem::offset_of!(Self, #relationship_member) - ) - }) + Some( + // Safety: we pass valid offset of a field containing Entity (obtained via offset_off!) + unsafe { + #bevy_ecs_path::relationship::ComponentRelationshipAccessor::::relationship( + core::mem::offset_of!(Self, #relationship_member) + ) + } + ) } } else { quote! { From ac51e0529effddac00185e077bf25f66351a3b3f Mon Sep 17 00:00:00 2001 From: eugineerd Date: Mon, 20 Oct 2025 14:22:07 +0000 Subject: [PATCH 7/7] clarify migration guide --- release-content/migration-guides/dynamic_relationships_api.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/release-content/migration-guides/dynamic_relationships_api.md b/release-content/migration-guides/dynamic_relationships_api.md index 9b9bf73628717..9317ad8a32d4f 100644 --- a/release-content/migration-guides/dynamic_relationships_api.md +++ b/release-content/migration-guides/dynamic_relationships_api.md @@ -6,4 +6,4 @@ pull_requests: [21601] `ComponentDescriptor` now stores additional data for working with relationships in dynamic contexts. This resulted in changes to `ComponentDescriptor::new_with_layout`: -- Now requires additional parameter `relationship_accessor`, which should be set to `None` for all existing components. +- Now requires additional parameter `relationship_accessor`, which should be set to `None` for all existing code creating `ComponentDescriptors`.