Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
33 changes: 33 additions & 0 deletions crates/bevy_ecs/macros/src/component.rs
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,35 @@ 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(
// Safety: we pass valid offset of a field containing Entity (obtained via offset_off!)
unsafe {
#bevy_ecs_path::relationship::ComponentRelationshipAccessor::<Self>::relationship(
core::mem::offset_of!(Self, #relationship_member)
)
}
)
}
} else {
quote! {
Some(#bevy_ecs_path::relationship::ComponentRelationshipAccessor::<Self>::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! {
Expand All @@ -241,6 +270,10 @@ pub fn derive_component(input: TokenStream) -> TokenStream {
}

#map_entities

fn relationship_accessor() -> Option<#bevy_ecs_path::relationship::ComponentRelationshipAccessor<Self>> {
#relationship_accessor
}
}

#relationship
Expand Down
14 changes: 14 additions & 0 deletions crates/bevy_ecs/src/component/info.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ use crate::{
},
lifecycle::ComponentHooks,
query::DebugCheckedUnwrap as _,
relationship::RelationshipAccessor,
resource::Resource,
storage::SparseSetIndex,
};
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -219,6 +225,7 @@ pub struct ComponentDescriptor {
drop: Option<for<'a> unsafe fn(OwningPtr<'a>)>,
mutable: bool,
clone_behavior: ComponentCloneBehavior,
relationship_accessor: Option<RelationshipAccessor>,
}

// We need to ignore the `drop` field in our `Debug` impl
Expand All @@ -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()
}
}
Expand All @@ -258,6 +266,7 @@ impl ComponentDescriptor {
drop: needs_drop::<T>().then_some(Self::drop_ptr::<T> as _),
mutable: T::Mutability::MUTABLE,
clone_behavior: T::clone_behavior(),
relationship_accessor: T::relationship_accessor().map(|v| v.accessor),
}
}

Expand All @@ -266,13 +275,15 @@ 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<Cow<'static, str>>,
storage_type: StorageType,
layout: Layout,
drop: Option<for<'a> unsafe fn(OwningPtr<'a>)>,
mutable: bool,
clone_behavior: ComponentCloneBehavior,
relationship_accessor: Option<RelationshipAccessor>,
) -> Self {
Self {
name: name.into().into(),
Expand All @@ -283,6 +294,7 @@ impl ComponentDescriptor {
drop,
mutable,
clone_behavior,
relationship_accessor,
}
}

Expand All @@ -301,6 +313,7 @@ impl ComponentDescriptor {
drop: needs_drop::<T>().then_some(Self::drop_ptr::<T> as _),
mutable: true,
clone_behavior: ComponentCloneBehavior::Default,
relationship_accessor: None,
}
}

Expand All @@ -314,6 +327,7 @@ impl ComponentDescriptor {
drop: needs_drop::<T>().then_some(Self::drop_ptr::<T> as _),
mutable: true,
clone_behavior: ComponentCloneBehavior::Default,
relationship_accessor: None,
}
}

Expand Down
8 changes: 8 additions & 0 deletions crates/bevy_ecs/src/component/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ pub use tick::*;
use crate::{
entity::EntityMapper,
lifecycle::ComponentHook,
relationship::ComponentRelationshipAccessor,
system::{Local, SystemParam},
world::{FromWorld, World},
};
Expand Down Expand Up @@ -625,6 +626,13 @@ pub trait Component: Send + Sync + 'static {
/// You can use the turbofish (`::<A,B,C>`) to specify parameters when a function is generic, using either M or _ for the type of the mapper parameter.
#[inline]
fn map_entities<E: EntityMapper>(_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<ComponentRelationshipAccessor<Self>> {
None
}
}

mod private {
Expand Down
1 change: 1 addition & 0 deletions crates/bevy_ecs/src/entity/clone_entities.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2099,6 +2099,7 @@ mod tests {
None,
true,
ComponentCloneBehavior::Custom(test_handler),
None,
)
};
let component_id = world.register_component_with_descriptor(descriptor);
Expand Down
115 changes: 115 additions & 0 deletions crates/bevy_ecs/src/relationship/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -478,11 +480,78 @@ 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.
///
/// 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`].
RelationshipTarget {
/// 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<dyn Iterator<Item = Entity> + 'a>,
/// Value of [`RelationshipTarget::LINKED_SPAWN`] of this [`RelationshipTarget`].
linked_spawn: bool,
},
}

/// A type-safe convenience wrapper over [`RelationshipAccessor`].
pub struct ComponentRelationshipAccessor<C: ?Sized> {
pub(crate) accessor: RelationshipAccessor,
phantom: PhantomData<C>,
}

impl<C> ComponentRelationshipAccessor<C> {
/// 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,
linked_spawn: C::RelationshipTarget::LINKED_SPAWN,
},
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::<C>())) },
linked_spawn: C::LINKED_SPAWN,
},
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;
Expand Down Expand Up @@ -697,4 +766,50 @@ mod tests {
assert!(world.get::<ChildOf>(child).is_some());
assert!(world.get::<Children>(parent).is_some());
}

#[test]
fn dynamically_traverse_hierarchy() {
let mut world = World::new();
let child_of_id = world.register_component::<ChildOf>();
let children_id = world.register_component::<Children>();

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

Choose a reason for hiding this comment

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

Should a helper method be provided to do this?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

There could be, but I'm not sure how it would look like since it's an enum. I guess we can add RelationshipAccessor::get_relationship_entity(relationship_component: Ptr<'_>) -> Option<Entity>, but I'm not sure if it's any more ergonomic than matching.

Copy link
Contributor

Choose a reason for hiding this comment

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

Yes, I've started using the API to dynamically traverse relations in multi-source queries and I was tripped by the fact that is an enum. I think it might be better to split it into 2 separate structs?

Or maybe this would be resolved if we had
Also maybe we could have some more user friendly wrappers like:
UnsafeEntityCell.get_relationship_by_id(relationship_id) which would under the hood fetch the correct RelationshipAccessor.

So that we could do world.get(source)?.get_relationship_by_id(relationship_id)?

I can try adding that

Copy link
Contributor

@urben1680 urben1680 Oct 23, 2025

Choose a reason for hiding this comment

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

RelationshipAccessor could have a mapper method that returns this enum:

enum RelationshipAccessorDeref<'a> {
  Relationship {
    entity: Entity,
    linked_spawn: bool,
  },
  RelationshipTarget {
    iter: Box<dyn Iterator<Item = Entity> + 'a>,
    linked_spawn: bool,
  }
}

The mapper needs to be unsafe though with notice that RelationshipAccessor must originate from ComponentInfo or constructed with the given unsafe constructor.

assert_eq!(child_of_entity, parent);
}
}
1 change: 1 addition & 0 deletions crates/bevy_ecs/src/world/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4001,6 +4001,7 @@ mod tests {
}),
true,
ComponentCloneBehavior::Default,
None,
)
};

Expand Down
1 change: 1 addition & 0 deletions examples/ecs/dynamic.rs
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ fn main() {
None,
true,
ComponentCloneBehavior::Default,
None,
)
});
let Some(info) = world.components().get_info(id) else {
Expand Down
1 change: 1 addition & 0 deletions examples/ecs/immutable_components.rs
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,7 @@ fn demo_3(world: &mut World) {
None,
false,
ComponentCloneBehavior::Default,
None,
)
};

Expand Down
1 change: 1 addition & 0 deletions examples/stress_tests/many_components.rs
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ fn stress_test(num_entities: u32, num_components: u32, num_systems: u32) {
None,
true, // is mutable
ComponentCloneBehavior::Default,
None,
)
},
)
Expand Down
9 changes: 9 additions & 0 deletions release-content/migration-guides/dynamic_relationships_api.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
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 code creating `ComponentDescriptors`.