Skip to content

Conversation

eugineerd
Copy link
Contributor

Objective

Currently there is no way to traverse relationships in type-erased contexts or to define dynamic relationship components, which is a hole in the current relationships api.

Solution

Introduce RelationshipAccessor to describe a way to get Entity values from any registered relationships in dynamic contexts and store it on ComponentDescriptor. This allows to traverse relationships without knowing their type, which is useful for working with entity hierarchies using non-default components.

Testing

Added a simple test/example of how to use this api to traverse hierarchies in a type-erased context.

@alice-i-cecile alice-i-cecile added C-Feature A new feature, making something new possible A-ECS Entities, components, systems, and events X-Uncontroversial This work is generally agreed upon D-Modest A "normal" level of difficulty; suitable for simple features or challenging fixes D-Unsafe Touches with unsafe code in some way S-Needs-Review Needs reviewer attention (from anyone!) to move forward labels Oct 19, 2025
Copy link
Contributor

@urben1680 urben1680 left a comment

Choose a reason for hiding this comment

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

I really like this feature and already see me using this. 👍

Comment on lines +230 to +234
Some(unsafe {
#bevy_ecs_path::relationship::ComponentRelationshipAccessor::<Self>::relationship(
core::mem::offset_of!(Self, #relationship_member)
)
})
Copy link
Contributor

Choose a reason for hiding this comment

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

I never really reviewed derive macro source code because it scares me, but where does one write safety comments of this if not here? Is the CI not caring about safety comments in macros?

Comment on lines +520 to +531
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(),
}
}
Copy link
Contributor

@urben1680 urben1680 Oct 20, 2025

Choose a reason for hiding this comment

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

It is a bit weird to see one information (LINKED_SPAWN) be taken from C, but another (entity_field_offset) is not. I guess it is not worth the hassle to bring another offset method to RelationShip though but it would make this method safe in case users want to directly use this. That would also not require the user to have access to the field for offset_of!.
If this had no parameter, one could use this for a generic API that needs this RelationshipAccessor but has no Components access.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Getting offset is inherently unsafe as there is no way to know that it is actually the correct offset, we just have to trust that it is.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

If what you mean is that it can store the get function of Relationship similar to RelationshipTarget::iter, that can be done, but would introduce unnecessary overhead when we can get the value without having to call a function.

Copy link
Contributor

Choose a reason for hiding this comment

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

I was thinking of something like

pub trait Relationship {
  const ENTITY_FIELD_OFFSET: usize = core::mem::offset_of!(Self, #relationship_member);

... that this method here picks up like LINKED_SPAWN.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

That would work, yes, although in both cases this requires the derive macro to set it, at which point there is little difference in how it actually gets to RelationshipAccessor. In fact, the entire ComponentRelationshipAccessor is a bit redundant - it exists mostly to avoid having to mark Component trait as unsafe.

`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.
Copy link
Contributor

Choose a reason for hiding this comment

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

"for all existing components" seems to not fit really? The user calls ComponentDescriptor::new_with_layout for components that do not yet exist to pass the descriptor to World::register_component_with_descriptor. This should instead point out that new relationship components in dynamic contexts are not supported and thus None should be passed in here.

Suggested change
- 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` as relationships need further registrations that is only covered by the derive macro.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

What I meant to say here is that all existing code can just put None here and it would be correct, relationship_accessor is only relevant for new code that would want to register relationships.

Copy link
Contributor

Choose a reason for hiding this comment

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

Ah yes from that perspective it makes sense. Maybe phrase it like "all existing code"?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

A-ECS Entities, components, systems, and events C-Feature A new feature, making something new possible D-Modest A "normal" level of difficulty; suitable for simple features or challenging fixes D-Unsafe Touches with unsafe code in some way S-Needs-Review Needs reviewer attention (from anyone!) to move forward X-Uncontroversial This work is generally agreed upon

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants