Skip to content
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

Add an example teaching users about custom relationships #17443

Merged
merged 23 commits into from
Jan 20, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
e3169b2
Expand module docs for `hierarchy` example
alice-i-cecile Jan 19, 2025
4cedb40
Export Relationship and RelationshipTarget in prelude
alice-i-cecile Jan 19, 2025
b008e3f
Add spawning section to example
alice-i-cecile Jan 19, 2025
fe46224
Fix metadata name for example
alice-i-cecile Jan 19, 2025
1a1a9f6
Fix up spawning
alice-i-cecile Jan 19, 2025
94bd55a
Improve warning in traversal methods
alice-i-cecile Jan 19, 2025
fb0dfab
Demonstrate cycle checking
alice-i-cecile Jan 19, 2025
15842f3
Show how to remove relationships
alice-i-cecile Jan 19, 2025
f89777e
Typo
alice-i-cecile Jan 19, 2025
7c25e04
Update examples README
alice-i-cecile Jan 19, 2025
13c193b
Consistently use relationship
alice-i-cecile Jan 19, 2025
a3093a4
Demonstrate how to mutate relationships
alice-i-cecile Jan 19, 2025
fbe500b
Better expect message
alice-i-cecile Jan 19, 2025
240462c
Revise example module docs to be more clear
alice-i-cecile Jan 19, 2025
7a8c66f
Merge remote-tracking branch 'alice-i-cecile/relations-example' into …
alice-i-cecile Jan 19, 2025
24b13d3
Use HashSet::insert more idiomatically
alice-i-cecile Jan 20, 2025
fd6ec8e
Merge branch 'main' into relations-example
alice-i-cecile Jan 20, 2025
4f29b6e
Fix comment about initial targeting setup
alice-i-cecile Jan 20, 2025
222d0e7
Merge branch 'main' into relations-example
alice-i-cecile Jan 20, 2025
8f2212c
Merge branch 'main' into relations-example
alice-i-cecile Jan 20, 2025
492bef4
Parent -> ChildOf rename
alice-i-cecile Jan 20, 2025
8ca1266
Fix EntityHashSet import
alice-i-cecile Jan 20, 2025
54fca60
Remove relationship traits from prelude due to subtle bugs created
alice-i-cecile Jan 20, 2025
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
11 changes: 11 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2048,6 +2048,17 @@ description = "Illustrates parallel queries with `ParallelIterator`"
category = "ECS (Entity Component System)"
wasm = false

[[example]]
name = "relationships"
path = "examples/ecs/relationships.rs"
doc-scrape-examples = true

[package.metadata.example.relationships]
name = "Relationships"
description = "Define and work with custom relationships between entities"
category = "ECS (Entity Component System)"
wasm = false

[[example]]
name = "removal_detection"
path = "examples/ecs/removal_detection.rs"
Expand Down
25 changes: 15 additions & 10 deletions crates/bevy_ecs/src/relationship/relationship_query.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,9 @@ impl<'w, 's, D: QueryData, F: QueryFilter> Query<'w, 's, D, F> {
/// there are no more related entities, returning the "root entity" of the relationship hierarchy.
///
/// # Warning
/// For relationship graphs that contain loops, this could loop infinitely. Only call this for "hierarchy-style"
/// relationships.
///
/// For relationship graphs that contain loops, this could loop infinitely.
/// If your relationship is not a tree (like Bevy's hierarchy), be sure to stop if you encounter a duplicate entity.
pub fn root_ancestor<R: Relationship>(&'w self, entity: Entity) -> Entity
where
<D as QueryData>::ReadOnly: WorldQuery<Item<'w> = &'w R>,
Expand All @@ -53,8 +54,9 @@ impl<'w, 's, D: QueryData, F: QueryFilter> Query<'w, 's, D, F> {
/// Iterates all "leaf entities" as defined by the [`RelationshipTarget`] hierarchy.
///
/// # Warning
/// For relationship graphs that contain loops, this could loop infinitely. Only call this for "hierarchy-style"
/// relationships.
///
/// For relationship graphs that contain loops, this could loop infinitely.
/// If your relationship is not a tree (like Bevy's hierarchy), be sure to stop if you encounter a duplicate entity.
pub fn iter_leaves<S: RelationshipTarget>(
&'w self,
entity: Entity,
Expand Down Expand Up @@ -93,8 +95,9 @@ impl<'w, 's, D: QueryData, F: QueryFilter> Query<'w, 's, D, F> {
/// [`RelationshipTarget`].
///
/// # Warning
/// For relationship graphs that contain loops, this could loop infinitely. Only call this for "hierarchy-style"
/// relationships.
///
/// For relationship graphs that contain loops, this could loop infinitely.
/// If your relationship is not a tree (like Bevy's hierarchy), be sure to stop if you encounter a duplicate entity.
pub fn iter_descendants<S: RelationshipTarget>(
&'w self,
entity: Entity,
Expand All @@ -109,8 +112,9 @@ impl<'w, 's, D: QueryData, F: QueryFilter> Query<'w, 's, D, F> {
/// [`RelationshipTarget`] in depth-first order.
///
/// # Warning
/// For relationship graphs that contain loops, this could loop infinitely. Only call this for "hierarchy-style"
/// relationships.
///
/// For relationship graphs that contain loops, this could loop infinitely.
/// If your relationship is not a tree (like Bevy's hierarchy), be sure to stop if you encounter a duplicate entity.
pub fn iter_descendants_depth_first<S: RelationshipTarget>(
&'w self,
entity: Entity,
Expand All @@ -125,8 +129,9 @@ impl<'w, 's, D: QueryData, F: QueryFilter> Query<'w, 's, D, F> {
/// Iterates all ancestors of the given `entity` as defined by the `R` [`Relationship`].
///
/// # Warning
/// For relationship graphs that contain loops, this could loop infinitely. Only call this for "hierarchy-style"
/// relationships.
///
/// For relationship graphs that contain loops, this could loop infinitely.
/// If your relationship is not a tree (like Bevy's hierarchy), be sure to stop if you encounter a duplicate entity.
pub fn iter_ancestors<R: Relationship>(
&'w self,
entity: Entity,
Expand Down
1 change: 1 addition & 0 deletions examples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -315,6 +315,7 @@ Example | Description
[Observers](../examples/ecs/observers.rs) | Demonstrates observers that react to events (both built-in life-cycle events and custom events)
[One Shot Systems](../examples/ecs/one_shot_systems.rs) | Shows how to flexibly run systems without scheduling them
[Parallel Query](../examples/ecs/parallel_query.rs) | Illustrates parallel queries with `ParallelIterator`
[Relationships](../examples/ecs/relationships.rs) | Define and work with custom relationships between entities
[Removal Detection](../examples/ecs/removal_detection.rs) | Query for entities that had a specific component removed earlier in the current frame
[Run Conditions](../examples/ecs/run_conditions.rs) | Run systems only when one or multiple conditions are met
[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
Expand Down
6 changes: 5 additions & 1 deletion examples/ecs/hierarchy.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
//! Creates a hierarchy of parents and children entities.
//! Demonstrates techniques for creating a hierarchy of parent and child entities.
//!
//! When [`DefaultPlugins`] are added to your app, systems are automatically added to propagate
//! [`Transform`] and [`Visibility`] from parents to children down the hierarchy,
//! resulting in a final [`GlobalTransform`] and [`InheritedVisibility`] component for each entity.

use std::f32::consts::*;

Expand Down
213 changes: 213 additions & 0 deletions examples/ecs/relationships.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,213 @@
//! Entities generally don't exist in isolation. Instead, they are related to other entities in various ways.
//! While Bevy comes with a built-in [`ChildOf`]/[`Children`] relationship
//! (which enables transform and visibility propagation),
//! you can define your own relationships using components.
//!
//! We can define a custom relationship by creating two components:
//! one to store the relationship itself, and another to keep track of the reverse relationship.
//! Bevy's [`ChildOf`] component implements the [`Relationship`] trait, serving as the source of truth,
//! while the [`Children`] component implements the [`RelationshipTarget`] trait and is used to accelerate traversals down the hierarchy.
//!
//! In this example we're creating a [`Targeting`]/[`TargetedBy`] relationship,
//! demonstrating how you might model units which target a single unit in combat.

use bevy::ecs::entity::hash_set::EntityHashSet;
use bevy::ecs::system::RunSystemOnce;
use bevy::prelude::*;

/// The entity that this entity is targeting.
///
/// This is the source of truth for the relationship,
/// and can be modified directly to change the target.
#[derive(Component, Debug)]
#[relationship(relationship_target = TargetedBy)]
struct Targeting(Entity);

/// All entities that are targeting this entity.
///
/// This component is updated reactively using the component hooks introduced by deriving
/// the [`Relationship`] trait. We should not modify this component directly,
/// but can safely read its field. In a larger project, we could enforce this through the use of
/// private fields and public getters.
#[derive(Component, Debug)]
#[relationship_target(relationship = Targeting)]
struct TargetedBy(Vec<Entity>);

fn main() {
// Operating on a raw `World` and running systems one at a time
// is great for writing tests and teaching abstract concepts!
let mut world = World::new();

// We're going to spawn a few entities and relate them to each other in a complex way.
// To start, Bob will target Alice, Charlie will target Bob,
// and Alice will target Charlie. This creates a loop in the relationship graph.
//
// Then, we'll spawn Devon, who will target Charlie,
// creating a more complex graph with a branching structure.
fn spawning_entities_with_relationships(mut commands: Commands) {
// Calling .id() after spawning an entity will return the `Entity` identifier of the spawned entity,
// even though the entity itself is not yet instantiated in the world.
// This works because Commands will reserve the entity ID before actually spawning the entity,
// through the use of atomic counters.
let alice = commands.spawn(Name::new("Alice")).id();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Crazy question, but where is 'Name' coming from?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a bevy_core component! It's quite nice!

// Relations are just components, so we can add them into the bundle that we're spawning.
let bob = commands.spawn((Name::new("Bob"), Targeting(alice))).id();

// The `with_related` helper method on `EntityCommands` can be used to add relations in a more ergonomic way.
let charlie = commands
.spawn((Name::new("Charlie"), Targeting(bob)))
// The `with_related` method will automatically add the `Targeting` component to any entities spawned within the closure,
// targeting the entity that we're calling `with_related` on.
.with_related::<Targeting>(|related_spawner_commands| {
// We could spawn multiple entities here, and they would all target `charlie`.
related_spawner_commands.spawn(Name::new("Devon"));
})
.id();

// Simply inserting the `Targeting` component will automatically create and update the `TargetedBy` component on the target entity.
// We can do this at any point; not just when the entity is spawned.
commands.entity(alice).insert(Targeting(charlie));
}

world
.run_system_once(spawning_entities_with_relationships)
.unwrap();

fn debug_relationships(
// Not all of our entities are targeted by something, so we use `Option` in our query to handle this case.
relations_query: Query<(&Name, &Targeting, Option<&TargetedBy>)>,
name_query: Query<&Name>,
) {
let mut relationships = String::new();

for (name, targeting, maybe_targeted_by) in relations_query.iter() {
let targeting_name = name_query.get(targeting.0).unwrap();
let targeted_by_string = if let Some(targeted_by) = maybe_targeted_by {
let mut vec_of_names = Vec::<&Name>::new();

for entity in &targeted_by.0 {
let name = name_query.get(*entity).unwrap();
vec_of_names.push(name);
}

// Convert this to a nice string for printing.
let vec_of_str: Vec<&str> = vec_of_names.iter().map(|name| name.as_str()).collect();
vec_of_str.join(", ")
} else {
"nobody".to_string()
};

relationships.push_str(&format!(
"{name} is targeting {targeting_name}, and is targeted by {targeted_by_string}\n",
));
}

println!("{}", relationships);
}

world.run_system_once(debug_relationships).unwrap();

// Demonstrates how to correctly mutate relationships.
// Relationship components are immutable! We can't query for the `Targeting` component mutably and modify it directly,
// but we can insert a new `Targeting` component to replace the old one.
// This allows the hooks on the `Targeting` component to update the `TargetedBy` component correctly.
// The `TargetedBy` component will be updated automatically!
fn mutate_relationships(name_query: Query<(Entity, &Name)>, mut commands: Commands) {
// Let's find Devon by doing a linear scan of the entity names.
let devon = name_query
.iter()
.find(|(_entity, name)| name.as_str() == "Devon")
.unwrap()
.0;

let alice = name_query
.iter()
.find(|(_entity, name)| name.as_str() == "Alice")
.unwrap()
.0;

println!("Making Devon target Alice.\n");
commands.entity(devon).insert(Targeting(alice));
}

world.run_system_once(mutate_relationships).unwrap();
world.run_system_once(debug_relationships).unwrap();

// Systems can return errors,
// which can be used to signal that something went wrong during the system's execution.
#[derive(Debug)]
#[expect(
dead_code,
reason = "Rust considers types that are only used by their debug trait as dead code."
)]
struct TargetingCycle {
initial_entity: Entity,
visited: EntityHashSet,
}

/// Bevy's relationships come with all sorts of useful methods for traversal.
/// Here, we're going to look for cycles using a depth-first search.
fn check_for_cycles(
// We want to check every entity for cycles
query_to_check: Query<Entity, With<Targeting>>,
// Fetch the names for easier debugging.
name_query: Query<&Name>,
// The targeting_query allows us to traverse the relationship graph.
targeting_query: Query<&Targeting>,
) -> Result<(), TargetingCycle> {
for initial_entity in query_to_check.iter() {
let mut visited = EntityHashSet::new();
let mut targeting_name = name_query.get(initial_entity).unwrap().clone();
println!("Checking for cycles starting at {targeting_name}",);

// There's all sorts of methods like this; check the `Query` docs for more!
// This would also be easy to do by just manually checking the `Targeting` component,
// and calling `query.get(targeted_entity)` on the entity that it targets in a loop.
for targeting in targeting_query.iter_ancestors(initial_entity) {
let target_name = name_query.get(targeting).unwrap();
println!("{targeting_name} is targeting {target_name}",);
targeting_name = target_name.clone();

if !visited.insert(targeting) {
return Err(TargetingCycle {
initial_entity,
visited,
});
}
}
}

// If we've checked all the entities and haven't found a cycle, we're good!
Ok(())
}

// Calling `world.run_system_once` on systems which return Results gives us two layers of errors:
// the first checks if running the system failed, and the second checks if the system itself returned an error.
// We're unwrapping the first, but checking the output of the system itself.
let cycle_result = world.run_system_once(check_for_cycles).unwrap();
println!("{cycle_result:?} \n");
// We deliberately introduced a cycle during spawning!
assert!(cycle_result.is_err());

// Now, let's demonstrate removing relationships and break the cycle.
fn untarget(mut commands: Commands, name_query: Query<(Entity, &Name)>) {
// Let's find Charlie by doing a linear scan of the entity names.
let charlie = name_query
.iter()
.find(|(_entity, name)| name.as_str() == "Charlie")
.unwrap()
.0;

// We can remove the `Targeting` component to remove the relationship
// and break the cycle we saw earlier.
println!("Removing Charlie's targeting relationship.\n");
commands.entity(charlie).remove::<Targeting>();
}

world.run_system_once(untarget).unwrap();
world.run_system_once(debug_relationships).unwrap();
// Cycle free!
let cycle_result = world.run_system_once(check_for_cycles).unwrap();
println!("{cycle_result:?} \n");
assert!(cycle_result.is_ok());
}
Loading