Skip to content

Commit b34833f

Browse files
alice-i-cecileJondolfMinerSebaskristoff3r
authored
Add an example teaching users about custom relationships (#17443)
# Objective After #17398, Bevy now has relations! We don't teach users how to make / work with these in the examples yet though, but we definitely should. ## Solution - Add a simple abstract example that goes over defining, spawning, traversing and removing a custom relations. - ~~Add `Relationship` and `RelationshipTarget` to the prelude: the trait methods are really helpful here.~~ - this causes subtle ambiguities with method names and weird compiler errors. Not doing it here! - Clean up related documentation that I referenced when writing this example. ## Testing `cargo run --example relationships` ## Notes to reviewers 1. Yes, I know that the cycle detection code could be more efficient. I decided to reduce the caching to avoid distracting from the broader point of "here's how you traverse relationships". 2. Instead of using an `App`, I've decide to use `World::run_system_once` + system functions defined inside of `main` to do something closer to literate programming. --------- Co-authored-by: Joona Aalto <[email protected]> Co-authored-by: MinerSebas <[email protected]> Co-authored-by: Kristoffer Søholm <[email protected]>
1 parent ba5e71f commit b34833f

File tree

5 files changed

+245
-11
lines changed

5 files changed

+245
-11
lines changed

Cargo.toml

+11
Original file line numberDiff line numberDiff line change
@@ -2048,6 +2048,17 @@ description = "Illustrates parallel queries with `ParallelIterator`"
20482048
category = "ECS (Entity Component System)"
20492049
wasm = false
20502050

2051+
[[example]]
2052+
name = "relationships"
2053+
path = "examples/ecs/relationships.rs"
2054+
doc-scrape-examples = true
2055+
2056+
[package.metadata.example.relationships]
2057+
name = "Relationships"
2058+
description = "Define and work with custom relationships between entities"
2059+
category = "ECS (Entity Component System)"
2060+
wasm = false
2061+
20512062
[[example]]
20522063
name = "removal_detection"
20532064
path = "examples/ecs/removal_detection.rs"

crates/bevy_ecs/src/relationship/relationship_query.rs

+15-10
Original file line numberDiff line numberDiff line change
@@ -37,8 +37,9 @@ impl<'w, 's, D: QueryData, F: QueryFilter> Query<'w, 's, D, F> {
3737
/// there are no more related entities, returning the "root entity" of the relationship hierarchy.
3838
///
3939
/// # Warning
40-
/// For relationship graphs that contain loops, this could loop infinitely. Only call this for "hierarchy-style"
41-
/// relationships.
40+
///
41+
/// For relationship graphs that contain loops, this could loop infinitely.
42+
/// If your relationship is not a tree (like Bevy's hierarchy), be sure to stop if you encounter a duplicate entity.
4243
pub fn root_ancestor<R: Relationship>(&'w self, entity: Entity) -> Entity
4344
where
4445
<D as QueryData>::ReadOnly: WorldQuery<Item<'w> = &'w R>,
@@ -53,8 +54,9 @@ impl<'w, 's, D: QueryData, F: QueryFilter> Query<'w, 's, D, F> {
5354
/// Iterates all "leaf entities" as defined by the [`RelationshipTarget`] hierarchy.
5455
///
5556
/// # Warning
56-
/// For relationship graphs that contain loops, this could loop infinitely. Only call this for "hierarchy-style"
57-
/// relationships.
57+
///
58+
/// For relationship graphs that contain loops, this could loop infinitely.
59+
/// If your relationship is not a tree (like Bevy's hierarchy), be sure to stop if you encounter a duplicate entity.
5860
pub fn iter_leaves<S: RelationshipTarget>(
5961
&'w self,
6062
entity: Entity,
@@ -93,8 +95,9 @@ impl<'w, 's, D: QueryData, F: QueryFilter> Query<'w, 's, D, F> {
9395
/// [`RelationshipTarget`].
9496
///
9597
/// # Warning
96-
/// For relationship graphs that contain loops, this could loop infinitely. Only call this for "hierarchy-style"
97-
/// relationships.
98+
///
99+
/// For relationship graphs that contain loops, this could loop infinitely.
100+
/// If your relationship is not a tree (like Bevy's hierarchy), be sure to stop if you encounter a duplicate entity.
98101
pub fn iter_descendants<S: RelationshipTarget>(
99102
&'w self,
100103
entity: Entity,
@@ -109,8 +112,9 @@ impl<'w, 's, D: QueryData, F: QueryFilter> Query<'w, 's, D, F> {
109112
/// [`RelationshipTarget`] in depth-first order.
110113
///
111114
/// # Warning
112-
/// For relationship graphs that contain loops, this could loop infinitely. Only call this for "hierarchy-style"
113-
/// relationships.
115+
///
116+
/// For relationship graphs that contain loops, this could loop infinitely.
117+
/// If your relationship is not a tree (like Bevy's hierarchy), be sure to stop if you encounter a duplicate entity.
114118
pub fn iter_descendants_depth_first<S: RelationshipTarget>(
115119
&'w self,
116120
entity: Entity,
@@ -125,8 +129,9 @@ impl<'w, 's, D: QueryData, F: QueryFilter> Query<'w, 's, D, F> {
125129
/// Iterates all ancestors of the given `entity` as defined by the `R` [`Relationship`].
126130
///
127131
/// # Warning
128-
/// For relationship graphs that contain loops, this could loop infinitely. Only call this for "hierarchy-style"
129-
/// relationships.
132+
///
133+
/// For relationship graphs that contain loops, this could loop infinitely.
134+
/// If your relationship is not a tree (like Bevy's hierarchy), be sure to stop if you encounter a duplicate entity.
130135
pub fn iter_ancestors<R: Relationship>(
131136
&'w self,
132137
entity: Entity,

examples/README.md

+1
Original file line numberDiff line numberDiff line change
@@ -315,6 +315,7 @@ Example | Description
315315
[Observers](../examples/ecs/observers.rs) | Demonstrates observers that react to events (both built-in life-cycle events and custom events)
316316
[One Shot Systems](../examples/ecs/one_shot_systems.rs) | Shows how to flexibly run systems without scheduling them
317317
[Parallel Query](../examples/ecs/parallel_query.rs) | Illustrates parallel queries with `ParallelIterator`
318+
[Relationships](../examples/ecs/relationships.rs) | Define and work with custom relationships between entities
318319
[Removal Detection](../examples/ecs/removal_detection.rs) | Query for entities that had a specific component removed earlier in the current frame
319320
[Run Conditions](../examples/ecs/run_conditions.rs) | Run systems only when one or multiple conditions are met
320321
[Send and receive events](../examples/ecs/send_and_receive_events.rs) | Demonstrates how to send and receive events of the same type in a single system

examples/ecs/hierarchy.rs

+5-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
1-
//! Creates a hierarchy of parents and children entities.
1+
//! Demonstrates techniques for creating a hierarchy of parent and child entities.
2+
//!
3+
//! When [`DefaultPlugins`] are added to your app, systems are automatically added to propagate
4+
//! [`Transform`] and [`Visibility`] from parents to children down the hierarchy,
5+
//! resulting in a final [`GlobalTransform`] and [`InheritedVisibility`] component for each entity.
26
37
use std::f32::consts::*;
48

examples/ecs/relationships.rs

+213
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,213 @@
1+
//! Entities generally don't exist in isolation. Instead, they are related to other entities in various ways.
2+
//! While Bevy comes with a built-in [`ChildOf`]/[`Children`] relationship
3+
//! (which enables transform and visibility propagation),
4+
//! you can define your own relationships using components.
5+
//!
6+
//! We can define a custom relationship by creating two components:
7+
//! one to store the relationship itself, and another to keep track of the reverse relationship.
8+
//! Bevy's [`ChildOf`] component implements the [`Relationship`] trait, serving as the source of truth,
9+
//! while the [`Children`] component implements the [`RelationshipTarget`] trait and is used to accelerate traversals down the hierarchy.
10+
//!
11+
//! In this example we're creating a [`Targeting`]/[`TargetedBy`] relationship,
12+
//! demonstrating how you might model units which target a single unit in combat.
13+
14+
use bevy::ecs::entity::hash_set::EntityHashSet;
15+
use bevy::ecs::system::RunSystemOnce;
16+
use bevy::prelude::*;
17+
18+
/// The entity that this entity is targeting.
19+
///
20+
/// This is the source of truth for the relationship,
21+
/// and can be modified directly to change the target.
22+
#[derive(Component, Debug)]
23+
#[relationship(relationship_target = TargetedBy)]
24+
struct Targeting(Entity);
25+
26+
/// All entities that are targeting this entity.
27+
///
28+
/// This component is updated reactively using the component hooks introduced by deriving
29+
/// the [`Relationship`] trait. We should not modify this component directly,
30+
/// but can safely read its field. In a larger project, we could enforce this through the use of
31+
/// private fields and public getters.
32+
#[derive(Component, Debug)]
33+
#[relationship_target(relationship = Targeting)]
34+
struct TargetedBy(Vec<Entity>);
35+
36+
fn main() {
37+
// Operating on a raw `World` and running systems one at a time
38+
// is great for writing tests and teaching abstract concepts!
39+
let mut world = World::new();
40+
41+
// We're going to spawn a few entities and relate them to each other in a complex way.
42+
// To start, Bob will target Alice, Charlie will target Bob,
43+
// and Alice will target Charlie. This creates a loop in the relationship graph.
44+
//
45+
// Then, we'll spawn Devon, who will target Charlie,
46+
// creating a more complex graph with a branching structure.
47+
fn spawning_entities_with_relationships(mut commands: Commands) {
48+
// Calling .id() after spawning an entity will return the `Entity` identifier of the spawned entity,
49+
// even though the entity itself is not yet instantiated in the world.
50+
// This works because Commands will reserve the entity ID before actually spawning the entity,
51+
// through the use of atomic counters.
52+
let alice = commands.spawn(Name::new("Alice")).id();
53+
// Relations are just components, so we can add them into the bundle that we're spawning.
54+
let bob = commands.spawn((Name::new("Bob"), Targeting(alice))).id();
55+
56+
// The `with_related` helper method on `EntityCommands` can be used to add relations in a more ergonomic way.
57+
let charlie = commands
58+
.spawn((Name::new("Charlie"), Targeting(bob)))
59+
// The `with_related` method will automatically add the `Targeting` component to any entities spawned within the closure,
60+
// targeting the entity that we're calling `with_related` on.
61+
.with_related::<Targeting>(|related_spawner_commands| {
62+
// We could spawn multiple entities here, and they would all target `charlie`.
63+
related_spawner_commands.spawn(Name::new("Devon"));
64+
})
65+
.id();
66+
67+
// Simply inserting the `Targeting` component will automatically create and update the `TargetedBy` component on the target entity.
68+
// We can do this at any point; not just when the entity is spawned.
69+
commands.entity(alice).insert(Targeting(charlie));
70+
}
71+
72+
world
73+
.run_system_once(spawning_entities_with_relationships)
74+
.unwrap();
75+
76+
fn debug_relationships(
77+
// Not all of our entities are targeted by something, so we use `Option` in our query to handle this case.
78+
relations_query: Query<(&Name, &Targeting, Option<&TargetedBy>)>,
79+
name_query: Query<&Name>,
80+
) {
81+
let mut relationships = String::new();
82+
83+
for (name, targeting, maybe_targeted_by) in relations_query.iter() {
84+
let targeting_name = name_query.get(targeting.0).unwrap();
85+
let targeted_by_string = if let Some(targeted_by) = maybe_targeted_by {
86+
let mut vec_of_names = Vec::<&Name>::new();
87+
88+
for entity in &targeted_by.0 {
89+
let name = name_query.get(*entity).unwrap();
90+
vec_of_names.push(name);
91+
}
92+
93+
// Convert this to a nice string for printing.
94+
let vec_of_str: Vec<&str> = vec_of_names.iter().map(|name| name.as_str()).collect();
95+
vec_of_str.join(", ")
96+
} else {
97+
"nobody".to_string()
98+
};
99+
100+
relationships.push_str(&format!(
101+
"{name} is targeting {targeting_name}, and is targeted by {targeted_by_string}\n",
102+
));
103+
}
104+
105+
println!("{}", relationships);
106+
}
107+
108+
world.run_system_once(debug_relationships).unwrap();
109+
110+
// Demonstrates how to correctly mutate relationships.
111+
// Relationship components are immutable! We can't query for the `Targeting` component mutably and modify it directly,
112+
// but we can insert a new `Targeting` component to replace the old one.
113+
// This allows the hooks on the `Targeting` component to update the `TargetedBy` component correctly.
114+
// The `TargetedBy` component will be updated automatically!
115+
fn mutate_relationships(name_query: Query<(Entity, &Name)>, mut commands: Commands) {
116+
// Let's find Devon by doing a linear scan of the entity names.
117+
let devon = name_query
118+
.iter()
119+
.find(|(_entity, name)| name.as_str() == "Devon")
120+
.unwrap()
121+
.0;
122+
123+
let alice = name_query
124+
.iter()
125+
.find(|(_entity, name)| name.as_str() == "Alice")
126+
.unwrap()
127+
.0;
128+
129+
println!("Making Devon target Alice.\n");
130+
commands.entity(devon).insert(Targeting(alice));
131+
}
132+
133+
world.run_system_once(mutate_relationships).unwrap();
134+
world.run_system_once(debug_relationships).unwrap();
135+
136+
// Systems can return errors,
137+
// which can be used to signal that something went wrong during the system's execution.
138+
#[derive(Debug)]
139+
#[expect(
140+
dead_code,
141+
reason = "Rust considers types that are only used by their debug trait as dead code."
142+
)]
143+
struct TargetingCycle {
144+
initial_entity: Entity,
145+
visited: EntityHashSet,
146+
}
147+
148+
/// Bevy's relationships come with all sorts of useful methods for traversal.
149+
/// Here, we're going to look for cycles using a depth-first search.
150+
fn check_for_cycles(
151+
// We want to check every entity for cycles
152+
query_to_check: Query<Entity, With<Targeting>>,
153+
// Fetch the names for easier debugging.
154+
name_query: Query<&Name>,
155+
// The targeting_query allows us to traverse the relationship graph.
156+
targeting_query: Query<&Targeting>,
157+
) -> Result<(), TargetingCycle> {
158+
for initial_entity in query_to_check.iter() {
159+
let mut visited = EntityHashSet::new();
160+
let mut targeting_name = name_query.get(initial_entity).unwrap().clone();
161+
println!("Checking for cycles starting at {targeting_name}",);
162+
163+
// There's all sorts of methods like this; check the `Query` docs for more!
164+
// This would also be easy to do by just manually checking the `Targeting` component,
165+
// and calling `query.get(targeted_entity)` on the entity that it targets in a loop.
166+
for targeting in targeting_query.iter_ancestors(initial_entity) {
167+
let target_name = name_query.get(targeting).unwrap();
168+
println!("{targeting_name} is targeting {target_name}",);
169+
targeting_name = target_name.clone();
170+
171+
if !visited.insert(targeting) {
172+
return Err(TargetingCycle {
173+
initial_entity,
174+
visited,
175+
});
176+
}
177+
}
178+
}
179+
180+
// If we've checked all the entities and haven't found a cycle, we're good!
181+
Ok(())
182+
}
183+
184+
// Calling `world.run_system_once` on systems which return Results gives us two layers of errors:
185+
// the first checks if running the system failed, and the second checks if the system itself returned an error.
186+
// We're unwrapping the first, but checking the output of the system itself.
187+
let cycle_result = world.run_system_once(check_for_cycles).unwrap();
188+
println!("{cycle_result:?} \n");
189+
// We deliberately introduced a cycle during spawning!
190+
assert!(cycle_result.is_err());
191+
192+
// Now, let's demonstrate removing relationships and break the cycle.
193+
fn untarget(mut commands: Commands, name_query: Query<(Entity, &Name)>) {
194+
// Let's find Charlie by doing a linear scan of the entity names.
195+
let charlie = name_query
196+
.iter()
197+
.find(|(_entity, name)| name.as_str() == "Charlie")
198+
.unwrap()
199+
.0;
200+
201+
// We can remove the `Targeting` component to remove the relationship
202+
// and break the cycle we saw earlier.
203+
println!("Removing Charlie's targeting relationship.\n");
204+
commands.entity(charlie).remove::<Targeting>();
205+
}
206+
207+
world.run_system_once(untarget).unwrap();
208+
world.run_system_once(debug_relationships).unwrap();
209+
// Cycle free!
210+
let cycle_result = world.run_system_once(check_for_cycles).unwrap();
211+
println!("{cycle_result:?} \n");
212+
assert!(cycle_result.is_ok());
213+
}

0 commit comments

Comments
 (0)