-
-
Notifications
You must be signed in to change notification settings - Fork 4.2k
API for traversing Relationships and RelationshipTargets in dynamic contexts
#21601
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
8861573
2f317c2
70458c5
34882e7
2b7b92d
b457bdc
ac51e05
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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,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; | ||
|
|
@@ -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() }; | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should a helper method be provided to do this? There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 So that we could do I can try adding that There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
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 |
||
| assert_eq!(child_of_entity, parent); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -4001,6 +4001,7 @@ mod tests { | |
| }), | ||
| true, | ||
| ComponentCloneBehavior::Default, | ||
| None, | ||
| ) | ||
| }; | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -154,6 +154,7 @@ fn demo_3(world: &mut World) { | |
| None, | ||
| false, | ||
| ComponentCloneBehavior::Default, | ||
| None, | ||
| ) | ||
| }; | ||
|
|
||
|
|
||
| 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`. |
Uh oh!
There was an error while loading. Please reload this page.