Skip to content

Commit a264dd3

Browse files
authored
API for traversing Relationships and RelationshipTargets in dynamic contexts (#21601)
# 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.
1 parent 9a9817b commit a264dd3

File tree

10 files changed

+184
-0
lines changed

10 files changed

+184
-0
lines changed

crates/bevy_ecs/macros/src/component.rs

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -216,6 +216,35 @@ pub fn derive_component(input: TokenStream) -> TokenStream {
216216
)
217217
};
218218

219+
let relationship_accessor = if (relationship.is_some() || relationship_target.is_some())
220+
&& let Data::Struct(DataStruct {
221+
fields,
222+
struct_token,
223+
..
224+
}) = &ast.data
225+
&& let Ok(field) = relationship_field(fields, "Relationship", struct_token.span())
226+
{
227+
let relationship_member = field.ident.clone().map_or(Member::from(0), Member::Named);
228+
if relationship.is_some() {
229+
quote! {
230+
Some(
231+
// Safety: we pass valid offset of a field containing Entity (obtained via offset_off!)
232+
unsafe {
233+
#bevy_ecs_path::relationship::ComponentRelationshipAccessor::<Self>::relationship(
234+
core::mem::offset_of!(Self, #relationship_member)
235+
)
236+
}
237+
)
238+
}
239+
} else {
240+
quote! {
241+
Some(#bevy_ecs_path::relationship::ComponentRelationshipAccessor::<Self>::relationship_target())
242+
}
243+
}
244+
} else {
245+
quote! {None}
246+
};
247+
219248
// This puts `register_required` before `register_recursive_requires` to ensure that the constructors of _all_ top
220249
// level components are initialized first, giving them precedence over recursively defined constructors for the same component type
221250
TokenStream::from(quote! {
@@ -241,6 +270,10 @@ pub fn derive_component(input: TokenStream) -> TokenStream {
241270
}
242271

243272
#map_entities
273+
274+
fn relationship_accessor() -> Option<#bevy_ecs_path::relationship::ComponentRelationshipAccessor<Self>> {
275+
#relationship_accessor
276+
}
244277
}
245278

246279
#relationship

crates/bevy_ecs/src/component/info.rs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ use crate::{
2020
},
2121
lifecycle::ComponentHooks,
2222
query::DebugCheckedUnwrap as _,
23+
relationship::RelationshipAccessor,
2324
resource::Resource,
2425
storage::SparseSetIndex,
2526
};
@@ -140,6 +141,11 @@ impl ComponentInfo {
140141
pub fn required_components(&self) -> &RequiredComponents {
141142
&self.required_components
142143
}
144+
145+
/// Returns [`RelationshipAccessor`] for this component if it is a [`Relationship`](crate::relationship::Relationship) or [`RelationshipTarget`](crate::relationship::RelationshipTarget) , `None` otherwise.
146+
pub fn relationship_accessor(&self) -> Option<&RelationshipAccessor> {
147+
self.descriptor.relationship_accessor.as_ref()
148+
}
143149
}
144150

145151
/// A value which uniquely identifies the type of a [`Component`] or [`Resource`] within a
@@ -219,6 +225,7 @@ pub struct ComponentDescriptor {
219225
drop: Option<for<'a> unsafe fn(OwningPtr<'a>)>,
220226
mutable: bool,
221227
clone_behavior: ComponentCloneBehavior,
228+
relationship_accessor: Option<RelationshipAccessor>,
222229
}
223230

224231
// We need to ignore the `drop` field in our `Debug` impl
@@ -232,6 +239,7 @@ impl Debug for ComponentDescriptor {
232239
.field("layout", &self.layout)
233240
.field("mutable", &self.mutable)
234241
.field("clone_behavior", &self.clone_behavior)
242+
.field("relationship_accessor", &self.relationship_accessor)
235243
.finish()
236244
}
237245
}
@@ -258,6 +266,7 @@ impl ComponentDescriptor {
258266
drop: needs_drop::<T>().then_some(Self::drop_ptr::<T> as _),
259267
mutable: T::Mutability::MUTABLE,
260268
clone_behavior: T::clone_behavior(),
269+
relationship_accessor: T::relationship_accessor().map(|v| v.accessor),
261270
}
262271
}
263272

@@ -266,13 +275,15 @@ impl ComponentDescriptor {
266275
/// # Safety
267276
/// - the `drop` fn must be usable on a pointer with a value of the layout `layout`
268277
/// - the component type must be safe to access from any thread (Send + Sync in rust terms)
278+
/// - `relationship_accessor` must be valid for this component type if not `None`
269279
pub unsafe fn new_with_layout(
270280
name: impl Into<Cow<'static, str>>,
271281
storage_type: StorageType,
272282
layout: Layout,
273283
drop: Option<for<'a> unsafe fn(OwningPtr<'a>)>,
274284
mutable: bool,
275285
clone_behavior: ComponentCloneBehavior,
286+
relationship_accessor: Option<RelationshipAccessor>,
276287
) -> Self {
277288
Self {
278289
name: name.into().into(),
@@ -283,6 +294,7 @@ impl ComponentDescriptor {
283294
drop,
284295
mutable,
285296
clone_behavior,
297+
relationship_accessor,
286298
}
287299
}
288300

@@ -301,6 +313,7 @@ impl ComponentDescriptor {
301313
drop: needs_drop::<T>().then_some(Self::drop_ptr::<T> as _),
302314
mutable: true,
303315
clone_behavior: ComponentCloneBehavior::Default,
316+
relationship_accessor: None,
304317
}
305318
}
306319

@@ -314,6 +327,7 @@ impl ComponentDescriptor {
314327
drop: needs_drop::<T>().then_some(Self::drop_ptr::<T> as _),
315328
mutable: true,
316329
clone_behavior: ComponentCloneBehavior::Default,
330+
relationship_accessor: None,
317331
}
318332
}
319333

crates/bevy_ecs/src/component/mod.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ pub use tick::*;
1515
use crate::{
1616
entity::EntityMapper,
1717
lifecycle::ComponentHook,
18+
relationship::ComponentRelationshipAccessor,
1819
system::{Local, SystemParam},
1920
world::{FromWorld, World},
2021
};
@@ -625,6 +626,13 @@ pub trait Component: Send + Sync + 'static {
625626
/// 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.
626627
#[inline]
627628
fn map_entities<E: EntityMapper>(_this: &mut Self, _mapper: &mut E) {}
629+
630+
/// Returns [`ComponentRelationshipAccessor`] required for working with relationships in dynamic contexts.
631+
///
632+
/// If component is not a [`Relationship`](crate::relationship::Relationship) or [`RelationshipTarget`](crate::relationship::RelationshipTarget), this should return `None`.
633+
fn relationship_accessor() -> Option<ComponentRelationshipAccessor<Self>> {
634+
None
635+
}
628636
}
629637

630638
mod private {

crates/bevy_ecs/src/entity/clone_entities.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2099,6 +2099,7 @@ mod tests {
20992099
None,
21002100
true,
21012101
ComponentCloneBehavior::Custom(test_handler),
2102+
None,
21022103
)
21032104
};
21042105
let component_id = world.register_component_with_descriptor(descriptor);

crates/bevy_ecs/src/relationship/mod.rs

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ mod related_methods;
44
mod relationship_query;
55
mod relationship_source_collection;
66

7+
use alloc::boxed::Box;
8+
use bevy_ptr::Ptr;
79
use core::marker::PhantomData;
810

911
use alloc::format;
@@ -478,11 +480,78 @@ impl RelationshipTargetCloneBehaviorHierarchy
478480
}
479481
}
480482

483+
/// This enum describes a way to access the entities of [`Relationship`] and [`RelationshipTarget`] components
484+
/// in a type-erased context.
485+
#[derive(Debug, Clone, Copy)]
486+
pub enum RelationshipAccessor {
487+
/// This component is a [`Relationship`].
488+
Relationship {
489+
/// Offset of the field containing [`Entity`] from the base of the component.
490+
///
491+
/// Dynamic equivalent of [`Relationship::get`].
492+
entity_field_offset: usize,
493+
/// Value of [`RelationshipTarget::LINKED_SPAWN`] for the [`Relationship::RelationshipTarget`] of this [`Relationship`].
494+
linked_spawn: bool,
495+
},
496+
/// This component is a [`RelationshipTarget`].
497+
RelationshipTarget {
498+
/// Function that returns an iterator over all [`Entity`]s of this [`RelationshipTarget`]'s collection.
499+
///
500+
/// Dynamic equivalent of [`RelationshipTarget::iter`].
501+
/// # Safety
502+
/// Passed pointer must point to the value of the same component as the one that this accessor was registered to.
503+
iter: for<'a> unsafe fn(Ptr<'a>) -> Box<dyn Iterator<Item = Entity> + 'a>,
504+
/// Value of [`RelationshipTarget::LINKED_SPAWN`] of this [`RelationshipTarget`].
505+
linked_spawn: bool,
506+
},
507+
}
508+
509+
/// A type-safe convenience wrapper over [`RelationshipAccessor`].
510+
pub struct ComponentRelationshipAccessor<C: ?Sized> {
511+
pub(crate) accessor: RelationshipAccessor,
512+
phantom: PhantomData<C>,
513+
}
514+
515+
impl<C> ComponentRelationshipAccessor<C> {
516+
/// Create a new [`ComponentRelationshipAccessor`] for a [`Relationship`] component.
517+
/// # Safety
518+
/// `entity_field_offset` should be the offset from the base of this component and point to a field that stores value of type [`Entity`].
519+
/// This value can be obtained using the [`core::mem::offset_of`] macro.
520+
pub unsafe fn relationship(entity_field_offset: usize) -> Self
521+
where
522+
C: Relationship,
523+
{
524+
Self {
525+
accessor: RelationshipAccessor::Relationship {
526+
entity_field_offset,
527+
linked_spawn: C::RelationshipTarget::LINKED_SPAWN,
528+
},
529+
phantom: Default::default(),
530+
}
531+
}
532+
533+
/// Create a new [`ComponentRelationshipAccessor`] for a [`RelationshipTarget`] component.
534+
pub fn relationship_target() -> Self
535+
where
536+
C: RelationshipTarget,
537+
{
538+
Self {
539+
accessor: RelationshipAccessor::RelationshipTarget {
540+
// Safety: caller ensures that `ptr` is of type `C`.
541+
iter: |ptr| unsafe { Box::new(RelationshipTarget::iter(ptr.deref::<C>())) },
542+
linked_spawn: C::LINKED_SPAWN,
543+
},
544+
phantom: Default::default(),
545+
}
546+
}
547+
}
548+
481549
#[cfg(test)]
482550
mod tests {
483551
use core::marker::PhantomData;
484552

485553
use crate::prelude::{ChildOf, Children};
554+
use crate::relationship::RelationshipAccessor;
486555
use crate::world::World;
487556
use crate::{component::Component, entity::Entity};
488557
use alloc::vec::Vec;
@@ -697,4 +766,50 @@ mod tests {
697766
assert!(world.get::<ChildOf>(child).is_some());
698767
assert!(world.get::<Children>(parent).is_some());
699768
}
769+
770+
#[test]
771+
fn dynamically_traverse_hierarchy() {
772+
let mut world = World::new();
773+
let child_of_id = world.register_component::<ChildOf>();
774+
let children_id = world.register_component::<Children>();
775+
776+
let parent = world.spawn_empty().id();
777+
let child = world.spawn_empty().id();
778+
world.entity_mut(child).insert(ChildOf(parent));
779+
world.flush();
780+
781+
let children_ptr = world.get_by_id(parent, children_id).unwrap();
782+
let RelationshipAccessor::RelationshipTarget { iter, .. } = world
783+
.components()
784+
.get_info(children_id)
785+
.unwrap()
786+
.relationship_accessor()
787+
.unwrap()
788+
else {
789+
unreachable!()
790+
};
791+
// Safety: `children_ptr` contains value of the same type as the one this accessor was registered for.
792+
let children: Vec<_> = unsafe { iter(children_ptr).collect() };
793+
assert_eq!(children, alloc::vec![child]);
794+
795+
let child_of_ptr = world.get_by_id(child, child_of_id).unwrap();
796+
let RelationshipAccessor::Relationship {
797+
entity_field_offset,
798+
..
799+
} = world
800+
.components()
801+
.get_info(child_of_id)
802+
.unwrap()
803+
.relationship_accessor()
804+
.unwrap()
805+
else {
806+
unreachable!()
807+
};
808+
// Safety:
809+
// - offset is in bounds, aligned and has the same lifetime as the original pointer.
810+
// - value at offset is guaranteed to be a valid Entity
811+
let child_of_entity: Entity =
812+
unsafe { *child_of_ptr.byte_add(*entity_field_offset).deref() };
813+
assert_eq!(child_of_entity, parent);
814+
}
700815
}

crates/bevy_ecs/src/world/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3997,6 +3997,7 @@ mod tests {
39973997
}),
39983998
true,
39993999
ComponentCloneBehavior::Default,
4000+
None,
40004001
)
40014002
};
40024003

examples/ecs/dynamic.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,7 @@ fn main() {
9797
None,
9898
true,
9999
ComponentCloneBehavior::Default,
100+
None,
100101
)
101102
});
102103
let Some(info) = world.components().get_info(id) else {

examples/ecs/immutable_components.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,7 @@ fn demo_3(world: &mut World) {
154154
None,
155155
false,
156156
ComponentCloneBehavior::Default,
157+
None,
157158
)
158159
};
159160

examples/stress_tests/many_components.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,7 @@ fn stress_test(num_entities: u32, num_components: u32, num_systems: u32) {
9797
None,
9898
true, // is mutable
9999
ComponentCloneBehavior::Default,
100+
None,
100101
)
101102
},
102103
)
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
---
2+
title: API for working with `Relationships` and `RelationshipTargets` in type-erased contexts
3+
pull_requests: [21601]
4+
---
5+
6+
`ComponentDescriptor` now stores additional data for working with relationships in dynamic contexts.
7+
This resulted in changes to `ComponentDescriptor::new_with_layout`:
8+
9+
- Now requires additional parameter `relationship_accessor`, which should be set to `None` for all existing code creating `ComponentDescriptors`.

0 commit comments

Comments
 (0)