Skip to content
Open
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
30 changes: 27 additions & 3 deletions crates/bevy_ecs/macros/src/component.rs
Original file line number Diff line number Diff line change
Expand Up @@ -529,6 +529,7 @@ struct Require {

struct Relationship {
relationship_target: Type,
allow_self: bool,
}

struct RelationshipTarget {
Expand Down Expand Up @@ -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<Self> {
input.parse::<kw::relationship_target>()?;
input.parse::<Token![=]>()?;
let mut relationship_target: Option<Type> = None;
let mut allow_self: bool = false;

while !input.is_empty() {
let lookahead = input.lookahead1();
if lookahead.peek(kw::allow_self) {
input.parse::<kw::allow_self>()?;
allow_self = true;
} else if lookahead.peek(kw::relationship_target) {
input.parse::<kw::relationship_target>()?;
input.parse::<Token![=]>()?;
relationship_target = Some(input.parse()?);
} else {
return Err(lookahead.error());
}
if !input.is_empty() {
input.parse::<Token![,]>()?;
}
}
Ok(Relationship {
relationship_target: input.parse::<Type>()?,
relationship_target: relationship_target.ok_or_else(|| {
syn::Error::new(input.span(), "Missing `relationship_target = X` attribute")
})?,
allow_self,
})
}
}
Expand Down Expand Up @@ -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 {
Expand Down
15 changes: 13 additions & 2 deletions crates/bevy_ecs/macros/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Entity>);
/// ```
///
/// 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::<Self>`,
/// or a function call that returns a function that can be turned into
/// a `ComponentHook`, e.g. `get_closure("Hi!")`.
Expand Down
77 changes: 73 additions & 4 deletions crates/bevy_ecs/src/relationship/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -75,11 +75,39 @@ use log::warn;
/// #[relationship_target(relationship = ChildOf, linked_spawn)]
/// pub struct Children(Vec<Entity>);
/// ```
///
/// 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<Entity>);
/// ```
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<Relationship = Self>;

/// 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;

Expand Down Expand Up @@ -120,9 +148,9 @@ pub trait Relationship: Component + Sized {
}
}
let target_entity = world.entity(entity).get::<Self>().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::<Self>(),
DebugName::type_name::<Self>()
Expand Down Expand Up @@ -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;
Expand All @@ -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);
Expand All @@ -589,6 +617,47 @@ mod tests {
assert!(!world.entity(a).contains::<RelTarget>());
}

#[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<Entity>);

let mut world = World::new();
let a = world.spawn_empty().id();
world.entity_mut(a).insert(Rel(a));
assert!(world.entity(a).contains::<Rel>());
assert!(world.entity(a).contains::<RelTarget>());
assert_eq!(world.entity(a).get::<Rel>().unwrap().get(), a);
assert_eq!(&*world.entity(a).get::<RelTarget>().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<Entity>);

let mut world = World::new();
let a = world.spawn_empty().id();
world.entity_mut(a).insert(Rel(a));
assert!(world.entity(a).contains::<Rel>());
assert!(world.entity(a).contains::<RelTarget>());

// Remove the relationship and verify cleanup
world.entity_mut(a).remove::<Rel>();
assert!(!world.entity(a).contains::<Rel>());
assert!(!world.entity(a).contains::<RelTarget>());
}

#[test]
fn relationship_with_missing_target_fails() {
#[derive(Component)]
Expand Down
34 changes: 34 additions & 0 deletions release-content/release-notes/allow_self_relationships.md
Original file line number Diff line number Diff line change
@@ -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<Entity>);
```

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::<LikedBy>());
assert!(world.entity(entity).contains::<PeopleILike>());
```