diff --git a/crates/bevy_ecs/macros/src/component.rs b/crates/bevy_ecs/macros/src/component.rs index f37defab0b111..dbc7b3402a704 100644 --- a/crates/bevy_ecs/macros/src/component.rs +++ b/crates/bevy_ecs/macros/src/component.rs @@ -529,6 +529,7 @@ struct Require { struct Relationship { relationship_target: Type, + allow_self: bool, } struct RelationshipTarget { @@ -735,14 +736,35 @@ mod kw { syn::custom_keyword!(relationship_target); syn::custom_keyword!(relationship); syn::custom_keyword!(linked_spawn); + syn::custom_keyword!(allow_self); } impl Parse for Relationship { fn parse(input: syn::parse::ParseStream) -> Result { - input.parse::()?; - input.parse::()?; + let mut relationship_target: Option = None; + let mut allow_self: bool = false; + + while !input.is_empty() { + let lookahead = input.lookahead1(); + if lookahead.peek(kw::allow_self) { + input.parse::()?; + allow_self = true; + } else if lookahead.peek(kw::relationship_target) { + input.parse::()?; + input.parse::()?; + relationship_target = Some(input.parse()?); + } else { + return Err(lookahead.error()); + } + if !input.is_empty() { + input.parse::()?; + } + } Ok(Relationship { - relationship_target: input.parse::()?, + relationship_target: relationship_target.ok_or_else(|| { + syn::Error::new(input.span(), "Missing `relationship_target = X` attribute") + })?, + allow_self, }) } } @@ -807,10 +829,12 @@ fn derive_relationship( let (impl_generics, type_generics, where_clause) = &ast.generics.split_for_impl(); let relationship_target = &relationship.relationship_target; + let allow_self = relationship.allow_self; Ok(Some(quote! { impl #impl_generics #bevy_ecs_path::relationship::Relationship for #struct_name #type_generics #where_clause { type RelationshipTarget = #relationship_target; + const ALLOW_SELF: bool = #allow_self; #[inline(always)] fn get(&self) -> #bevy_ecs_path::entity::Entity { diff --git a/crates/bevy_ecs/macros/src/lib.rs b/crates/bevy_ecs/macros/src/lib.rs index 6936ca4aff3a6..65c0ef1c0ee9e 100644 --- a/crates/bevy_ecs/macros/src/lib.rs +++ b/crates/bevy_ecs/macros/src/lib.rs @@ -629,17 +629,28 @@ pub fn derive_resource(input: TokenStream) -> TokenStream { /// On despawn, also despawn all related entities: /// ```ignore /// #[derive(Component)] -/// #[relationship_target(relationship_target = Children, linked_spawn)] +/// #[relationship_target(relationship = ChildOf, linked_spawn)] /// pub struct Children(Vec); /// ``` /// +/// Allow relationships to point to their own entity: +/// ```ignore +/// #[derive(Component)] +/// #[relationship(relationship_target = PeopleILike, allow_self)] +/// pub struct LikedBy(pub Entity); +/// ``` +/// ## Warning +/// +/// When `allow_self` is enabled, be careful when using recursive traversal methods +/// like `iter_ancestors` or `root_ancestor`, as they will loop infinitely if an entity points to itself. +/// /// ## Hooks /// ```ignore /// #[derive(Component)] /// #[component(hook_name = function)] /// struct MyComponent; /// ``` -/// where `hook_name` is `on_add`, `on_insert`, `on_replace` or `on_remove`; +/// where `hook_name` is `on_add`, `on_insert`, `on_replace` or `on_remove`; /// `function` can be either a path, e.g. `some_function::`, /// or a function call that returns a function that can be turned into /// a `ComponentHook`, e.g. `get_closure("Hi!")`. diff --git a/crates/bevy_ecs/src/relationship/mod.rs b/crates/bevy_ecs/src/relationship/mod.rs index 62849f4977675..6624ea40020f1 100644 --- a/crates/bevy_ecs/src/relationship/mod.rs +++ b/crates/bevy_ecs/src/relationship/mod.rs @@ -75,11 +75,39 @@ use log::warn; /// #[relationship_target(relationship = ChildOf, linked_spawn)] /// pub struct Children(Vec); /// ``` +/// +/// By default, relationships cannot point to their own entity. If you want to allow self-referential +/// relationships, you can use the `allow_self` attribute: +/// +/// ``` +/// # use bevy_ecs::component::Component; +/// # use bevy_ecs::entity::Entity; +/// #[derive(Component)] +/// #[relationship(relationship_target = PeopleILike, allow_self)] +/// pub struct LikedBy(pub Entity); +/// +/// #[derive(Component)] +/// #[relationship_target(relationship = LikedBy)] +/// pub struct PeopleILike(Vec); +/// ``` pub trait Relationship: Component + Sized { /// The [`Component`] added to the "target" entities of this [`Relationship`], which contains the list of all "source" /// entities that relate to the "target". type RelationshipTarget: RelationshipTarget; + /// If `true`, a relationship is allowed to point to its own entity. + /// + /// Set this to `true` when self-relationships are semantically valid for your use case, + /// such as `Likes(self)`, `EmployedBy(self)`, or a `ColliderOf` relationship where + /// a collider can be attached to its own entity. + /// + /// # Warning + /// + /// When `ALLOW_SELF` is `true`, be careful when using recursive traversal methods + /// like `iter_ancestors` or `root_ancestor`, as they will loop infinitely if an entity + /// points to itself. + const ALLOW_SELF: bool = false; + /// Gets the [`Entity`] ID of the related entity. fn get(&self) -> Entity; @@ -120,9 +148,9 @@ pub trait Relationship: Component + Sized { } } let target_entity = world.entity(entity).get::().unwrap().get(); - if target_entity == entity { + if !Self::ALLOW_SELF && target_entity == entity { warn!( - "{}The {}({target_entity:?}) relationship on entity {entity:?} points to itself. The invalid {} relationship has been removed.", + "{}The {}({target_entity:?}) relationship on entity {entity:?} points to itself. The invalid {} relationship has been removed.\nIf this is intended behavior self-referential relations can be enabled with the allow_self attribute: #[relationship(allow_self)]", caller.map(|location|format!("{location}: ")).unwrap_or_default(), DebugName::type_name::(), DebugName::type_name::() @@ -550,7 +578,7 @@ mod tests { use core::marker::PhantomData; use crate::prelude::{ChildOf, Children}; - use crate::relationship::RelationshipAccessor; + use crate::relationship::{Relationship, RelationshipAccessor}; use crate::world::World; use crate::{component::Component, entity::Entity}; use alloc::vec::Vec; @@ -573,7 +601,7 @@ mod tests { } #[test] - fn self_relationship_fails() { + fn self_relationship_fails_by_default() { #[derive(Component)] #[relationship(relationship_target = RelTarget)] struct Rel(Entity); @@ -589,6 +617,47 @@ mod tests { assert!(!world.entity(a).contains::()); } + #[test] + fn self_relationship_succeeds_with_allow_self() { + #[derive(Component)] + #[relationship(relationship_target = RelTarget, allow_self)] + struct Rel(Entity); + + #[derive(Component)] + #[relationship_target(relationship = Rel)] + struct RelTarget(Vec); + + let mut world = World::new(); + let a = world.spawn_empty().id(); + world.entity_mut(a).insert(Rel(a)); + assert!(world.entity(a).contains::()); + assert!(world.entity(a).contains::()); + assert_eq!(world.entity(a).get::().unwrap().get(), a); + assert_eq!(&*world.entity(a).get::().unwrap().0, &[a]); + } + + #[test] + fn self_relationship_removal_with_allow_self() { + #[derive(Component)] + #[relationship(relationship_target = RelTarget, allow_self)] + struct Rel(Entity); + + #[derive(Component)] + #[relationship_target(relationship = Rel)] + struct RelTarget(Vec); + + let mut world = World::new(); + let a = world.spawn_empty().id(); + world.entity_mut(a).insert(Rel(a)); + assert!(world.entity(a).contains::()); + assert!(world.entity(a).contains::()); + + // Remove the relationship and verify cleanup + world.entity_mut(a).remove::(); + assert!(!world.entity(a).contains::()); + assert!(!world.entity(a).contains::()); + } + #[test] fn relationship_with_missing_target_fails() { #[derive(Component)] diff --git a/release-content/release-notes/allow_self_relationships.md b/release-content/release-notes/allow_self_relationships.md new file mode 100644 index 0000000000000..cc3f13e5df4d5 --- /dev/null +++ b/release-content/release-notes/allow_self_relationships.md @@ -0,0 +1,34 @@ +--- +title: Self-Referential Relationships +authors: ["@mrchantey"] +pull_requests: [22269] +--- + +Relationships can now optionally point to their own entity by setting the `allow_self` attribute on the `#[relationship]` macro. + +By default pointing a relationship to its own entity will log a warning and remove the component. However, self-referential relationships are semantically valid in many cases: `Likes(self)`, `EmployedBy(self)`, `TalkingTo(self)`, `Healing(self)`, and many more. + +## Usage + +To allow a relationship to point to its own entity, add the `allow_self` attribute: + +```rust +#[derive(Component)] +#[relationship(relationship_target = PeopleILike, allow_self)] +pub struct LikedBy(pub Entity); + +#[derive(Component)] +#[relationship_target(relationship = LikedBy)] +pub struct PeopleILike(Vec); +``` + +Now entities can have relationships that point to themselves: + +```rust +let entity = world.spawn_empty().id(); +world.entity_mut(entity).insert(LikedBy(entity)); + +// The relationship is preserved +assert!(world.entity(entity).contains::()); +assert!(world.entity(entity).contains::()); +```