From e3169b2bfb4007ca7b9020d09259ffc7da218950 Mon Sep 17 00:00:00 2001 From: Alice Cecile Date: Sun, 19 Jan 2025 12:57:49 -0800 Subject: [PATCH 01/19] Expand module docs for `hierarchy` example --- examples/ecs/hierarchy.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/examples/ecs/hierarchy.rs b/examples/ecs/hierarchy.rs index e302ab6e27d82..6f746601994e2 100644 --- a/examples/ecs/hierarchy.rs +++ b/examples/ecs/hierarchy.rs @@ -1,4 +1,8 @@ -//! Creates a hierarchy of parents and children entities. +//! Demonstrates techniques for creating a hierarchy of parents and children 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::*; From 4cedb40e7edaa741ae072903442ee416ac00d7c1 Mon Sep 17 00:00:00 2001 From: Alice Cecile Date: Sun, 19 Jan 2025 13:24:35 -0800 Subject: [PATCH 02/19] Export Relationship and RelationshipTarget in prelude --- crates/bevy_ecs/src/lib.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/bevy_ecs/src/lib.rs b/crates/bevy_ecs/src/lib.rs index f6b9bba576871..f9244893b0d81 100644 --- a/crates/bevy_ecs/src/lib.rs +++ b/crates/bevy_ecs/src/lib.rs @@ -79,6 +79,7 @@ pub mod prelude { name::{Name, NameOrEntity}, observer::{CloneEntityWithObserversExt, Observer, Trigger}, query::{Added, AnyOf, Changed, Has, Or, QueryBuilder, QueryState, With, Without}, + relationship::{Relationship, RelationshipTarget}, removal_detection::RemovedComponents, result::{Error, Result}, schedule::{ From b008e3f43a9a19365f8ff5880cf5c85ca1cf1d83 Mon Sep 17 00:00:00 2001 From: Alice Cecile Date: Sun, 19 Jan 2025 13:25:18 -0800 Subject: [PATCH 03/19] Add spawning section to example --- Cargo.toml | 11 +++++ examples/ecs/relationships.rs | 86 +++++++++++++++++++++++++++++++++++ 2 files changed, 97 insertions(+) create mode 100644 examples/ecs/relationships.rs diff --git a/Cargo.toml b/Cargo.toml index fe1695c58f936..c1481f10649ea 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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.relations] +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" diff --git a/examples/ecs/relationships.rs b/examples/ecs/relationships.rs new file mode 100644 index 0000000000000..8d35a602b6945 --- /dev/null +++ b/examples/ecs/relationships.rs @@ -0,0 +1,86 @@ +//! Entities generally don't exist in isolation. Instead, they are related to other entities in various ways. +//! While Bevy comes with a built-in [`Parent`]/[`Children`] relationship +//! (which enables transform and visibility propagation), +//! you can define your own relationships using components. +//! +//! Every relation has two sides: the source and the target. +//! The source is the entity that has the relationship with the target, +//! while the target keeps track of all the entities that have a relationship with it. +//! For the standard hierarchy, the source is stored in the [`Parent`] component, +//! while the target is stored in the [`Children`] component. +//! +//! We can define a custom relationship by creating two components: +//! one to store "what is being targeted" and another to store "what is targeting." +//! In this example we're using the literal names [`Targeting`] and [`TargetedBy`], +//! as games often have units that target other units in combat. + +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); + +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, Alice will target Bob, Bob will target Charlie, + // and Charlie will target Alice. 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(); + // 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(); + + // 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(bob)); + + // 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::(|related_spawner_commands| { + related_spawner.spawn(Name::new("Devon")); + }) + .id(); + } + + world.run_system(spawning_entities_with_relationships); + + fn debug_relationships(query: Query<(&Name, &Targeting, &TargetedBy)>) { + let mut relationships = String::new(); + + for (name, targeting, targeted_by) in query.iter() { + relationships.push_str(&format!( + "{} is targeting {:?}, and is targeted by {:?}\n", + name.0, targeting.0, targeted_by.0 + )); + } + } + + world.run_system(debug_relationships); +} From fe46224027f9ceaea4be66616e55627b56b1b5fb Mon Sep 17 00:00:00 2001 From: Alice Cecile Date: Sun, 19 Jan 2025 13:27:34 -0800 Subject: [PATCH 04/19] Fix metadata name for example --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index c1481f10649ea..4d749aee880ce 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2053,7 +2053,7 @@ name = "relationships" path = "examples/ecs/relationships.rs" doc-scrape-examples = true -[package.metadata.example.relations] +[package.metadata.example.relationships] name = "Relationships" description = "Define and work with custom relationships between entities" category = "ECS (Entity Component System)" From 1a1a9f6b47f42507ced207cbd00fc624e470271a Mon Sep 17 00:00:00 2001 From: Alice Cecile Date: Sun, 19 Jan 2025 13:53:21 -0800 Subject: [PATCH 05/19] Fix up spawning --- examples/ecs/relationships.rs | 49 ++++++++++++++++++++++++++--------- 1 file changed, 37 insertions(+), 12 deletions(-) diff --git a/examples/ecs/relationships.rs b/examples/ecs/relationships.rs index 8d35a602b6945..7bb265cea83dc 100644 --- a/examples/ecs/relationships.rs +++ b/examples/ecs/relationships.rs @@ -14,6 +14,7 @@ //! In this example we're using the literal names [`Targeting`] and [`TargetedBy`], //! as games often have units that target other units in combat. +use bevy::ecs::system::RunSystemOnce; use bevy::prelude::*; /// The entity that this entity is targeting. @@ -50,37 +51,61 @@ fn main() { // 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(); + let alice = commands.spawn(Name::new("Alice")).id(); // 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(); - // 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(bob)); - // 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::(|related_spawner_commands| { - related_spawner.spawn(Name::new("Devon")); + // 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(spawning_entities_with_relationships); + world + .run_system_once(spawning_entities_with_relationships) + .unwrap(); - fn debug_relationships(query: Query<(&Name, &Targeting, &TargetedBy)>) { + 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, targeted_by) in query.iter() { + 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!( - "{} is targeting {:?}, and is targeted by {:?}\n", - name.0, targeting.0, targeted_by.0 + "{name} is targeting {targeting_name}, and is targeted by {targeted_by_string}\n", )); } + + println!("{}", relationships); } - world.run_system(debug_relationships); + world.run_system_once(debug_relationships).unwrap(); } From 94bd55a590ff44bf890c608f0563d44c602f9c93 Mon Sep 17 00:00:00 2001 From: Alice Cecile Date: Sun, 19 Jan 2025 14:01:39 -0800 Subject: [PATCH 06/19] Improve warning in traversal methods --- .../src/relationship/relationship_query.rs | 25 +++++++++++-------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/crates/bevy_ecs/src/relationship/relationship_query.rs b/crates/bevy_ecs/src/relationship/relationship_query.rs index f47b6c14caa14..082daaa6ce0a7 100644 --- a/crates/bevy_ecs/src/relationship/relationship_query.rs +++ b/crates/bevy_ecs/src/relationship/relationship_query.rs @@ -35,8 +35,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(&'w self, entity: Entity) -> Entity where ::ReadOnly: WorldQuery = &'w R>, @@ -51,8 +52,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( &'w self, entity: Entity, @@ -90,8 +92,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( &'w self, entity: Entity, @@ -106,8 +109,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( &'w self, entity: Entity, @@ -121,8 +125,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( &'w self, entity: Entity, From fb0dfabc8101a4bdbadd2872181a2ad2ebf044e7 Mon Sep 17 00:00:00 2001 From: Alice Cecile Date: Sun, 19 Jan 2025 14:22:13 -0800 Subject: [PATCH 07/19] Demonstrate cycle checking --- examples/ecs/relationships.rs | 53 +++++++++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) diff --git a/examples/ecs/relationships.rs b/examples/ecs/relationships.rs index 7bb265cea83dc..0eca86116d072 100644 --- a/examples/ecs/relationships.rs +++ b/examples/ecs/relationships.rs @@ -14,6 +14,7 @@ //! In this example we're using the literal names [`Targeting`] and [`TargetedBy`], //! as games often have units that target other units in combat. +use bevy::ecs::entity::EntityHashSet; use bevy::ecs::system::RunSystemOnce; use bevy::prelude::*; @@ -108,4 +109,56 @@ fn main() { } 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)] + 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, &Name), With>, + // The targeting_query allows us to traverse the relationship graph. + targeting_query: Query<&Targeting>, + ) -> Result<(), TargetingCycle> { + for (initial_entity, initial_entity_name) in query_to_check.iter() { + println!("Checking for cycles starting from {initial_entity_name}...",); + let mut visited = EntityHashSet::new(); + let mut targeting_name = initial_entity_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 = query_to_check.get(targeting).unwrap().1; + println!("{targeting_name} is targeting {target_name}",); + targeting_name = target_name; + + if visited.contains(&targeting) { + return Err(TargetingCycle { + initial_entity, + visited, + }); + } else { + visited.insert(targeting); + } + } + } + + // 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); + // We deliberately introduced a cycle during spawning! + assert!(cycle_result.is_err()); } From 15842f331a9a2a59414408bed9eada1a8f8ada64 Mon Sep 17 00:00:00 2001 From: Alice Cecile Date: Sun, 19 Jan 2025 14:38:02 -0800 Subject: [PATCH 08/19] Show how to remove relationships --- examples/ecs/relationships.rs | 42 +++++++++++++++++++++++++++++------ 1 file changed, 35 insertions(+), 7 deletions(-) diff --git a/examples/ecs/relationships.rs b/examples/ecs/relationships.rs index 0eca86116d072..adf517f980e79 100644 --- a/examples/ecs/relationships.rs +++ b/examples/ecs/relationships.rs @@ -113,6 +113,10 @@ fn main() { // 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 = "This error struct is only used for debugging in this example." + )] struct TargetingCycle { initial_entity: Entity, visited: EntityHashSet, @@ -122,22 +126,24 @@ fn main() { /// 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, &Name), With>, + query_to_check: Query>, + // 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, initial_entity_name) in query_to_check.iter() { - println!("Checking for cycles starting from {initial_entity_name}...",); + for initial_entity in query_to_check.iter() { let mut visited = EntityHashSet::new(); - let mut targeting_name = initial_entity_name; + 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 = query_to_check.get(targeting).unwrap().1; + let target_name = name_query.get(targeting).unwrap(); println!("{targeting_name} is targeting {target_name}",); - targeting_name = target_name; + targeting_name = target_name.clone(); if visited.contains(&targeting) { return Err(TargetingCycle { @@ -158,7 +164,29 @@ fn main() { // 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); + 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::(); + } + + 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()); } From f89777e03141d8ba665c2c1a60d1d9939e9ba217 Mon Sep 17 00:00:00 2001 From: Alice Cecile Date: Sun, 19 Jan 2025 17:52:36 -0500 Subject: [PATCH 09/19] Typo Co-authored-by: Joona Aalto --- examples/ecs/hierarchy.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/ecs/hierarchy.rs b/examples/ecs/hierarchy.rs index 6f746601994e2..be3bbcd49bde8 100644 --- a/examples/ecs/hierarchy.rs +++ b/examples/ecs/hierarchy.rs @@ -1,4 +1,4 @@ -//! Demonstrates techniques for creating 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, From 7c25e047fa93dbc20dc28cd2805a006181833cc4 Mon Sep 17 00:00:00 2001 From: Alice Cecile Date: Sun, 19 Jan 2025 14:53:37 -0800 Subject: [PATCH 10/19] Update examples README --- examples/README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/examples/README.md b/examples/README.md index c7a5d6b86dbd0..b102de55c0814 100644 --- a/examples/README.md +++ b/examples/README.md @@ -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 From 13c193b700d7a92af972f1292e5d8f035a0eb5bb Mon Sep 17 00:00:00 2001 From: Alice Cecile Date: Sun, 19 Jan 2025 17:54:05 -0500 Subject: [PATCH 11/19] Consistently use relationship Co-authored-by: Joona Aalto --- examples/ecs/relationships.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/ecs/relationships.rs b/examples/ecs/relationships.rs index adf517f980e79..adc43baa9ab91 100644 --- a/examples/ecs/relationships.rs +++ b/examples/ecs/relationships.rs @@ -3,7 +3,7 @@ //! (which enables transform and visibility propagation), //! you can define your own relationships using components. //! -//! Every relation has two sides: the source and the target. +//! Every relationship has two sides: the source and the target. //! The source is the entity that has the relationship with the target, //! while the target keeps track of all the entities that have a relationship with it. //! For the standard hierarchy, the source is stored in the [`Parent`] component, From a3093a422c74c46a2767f6205cfc0fcd2f34b32a Mon Sep 17 00:00:00 2001 From: Alice Cecile Date: Sun, 19 Jan 2025 15:35:22 -0800 Subject: [PATCH 12/19] Demonstrate how to mutate relationships --- examples/ecs/relationships.rs | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/examples/ecs/relationships.rs b/examples/ecs/relationships.rs index adc43baa9ab91..eca925c2b5307 100644 --- a/examples/ecs/relationships.rs +++ b/examples/ecs/relationships.rs @@ -110,6 +110,32 @@ fn main() { 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)] From fbe500b99515b2c814d952709afcefdc5e3731f7 Mon Sep 17 00:00:00 2001 From: Alice Cecile Date: Sun, 19 Jan 2025 18:52:57 -0500 Subject: [PATCH 13/19] Better expect message Co-authored-by: MinerSebas <66798382+MinerSebas@users.noreply.github.com> --- examples/ecs/relationships.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/ecs/relationships.rs b/examples/ecs/relationships.rs index eca925c2b5307..15f64917d40d9 100644 --- a/examples/ecs/relationships.rs +++ b/examples/ecs/relationships.rs @@ -141,7 +141,7 @@ fn main() { #[derive(Debug)] #[expect( dead_code, - reason = "This error struct is only used for debugging in this example." + reason = "Rust considers types that are only used by their debug trait as dead code." )] struct TargetingCycle { initial_entity: Entity, From 240462cc242e2d20899e75c9954340f1bf5cc738 Mon Sep 17 00:00:00 2001 From: Alice Cecile Date: Sun, 19 Jan 2025 15:58:45 -0800 Subject: [PATCH 14/19] Revise example module docs to be more clear --- examples/ecs/relationships.rs | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/examples/ecs/relationships.rs b/examples/ecs/relationships.rs index eca925c2b5307..0dd694ce38cc4 100644 --- a/examples/ecs/relationships.rs +++ b/examples/ecs/relationships.rs @@ -3,16 +3,13 @@ //! (which enables transform and visibility propagation), //! you can define your own relationships using components. //! -//! Every relationship has two sides: the source and the target. -//! The source is the entity that has the relationship with the target, -//! while the target keeps track of all the entities that have a relationship with it. -//! For the standard hierarchy, the source is stored in the [`Parent`] component, -//! while the target is stored in the [`Children`] component. -//! //! We can define a custom relationship by creating two components: -//! one to store "what is being targeted" and another to store "what is targeting." -//! In this example we're using the literal names [`Targeting`] and [`TargetedBy`], -//! as games often have units that target other units in combat. +//! one to store the relationship itself, and another to keep track of the reverse relationship. +//! Bevy's [`Parent`] 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::EntityHashSet; use bevy::ecs::system::RunSystemOnce; From 24b13d36a553f8bfc4b18f5a6a3f268ad290fb01 Mon Sep 17 00:00:00 2001 From: Alice Cecile Date: Mon, 20 Jan 2025 13:56:04 -0500 Subject: [PATCH 15/19] Use HashSet::insert more idiomatically MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Kristoffer Søholm --- examples/ecs/relationships.rs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/examples/ecs/relationships.rs b/examples/ecs/relationships.rs index c3b6bd322f3b8..9fcd450ce2c95 100644 --- a/examples/ecs/relationships.rs +++ b/examples/ecs/relationships.rs @@ -168,13 +168,11 @@ fn main() { println!("{targeting_name} is targeting {target_name}",); targeting_name = target_name.clone(); - if visited.contains(&targeting) { + if !visited.insert(targeting) { return Err(TargetingCycle { initial_entity, visited, }); - } else { - visited.insert(targeting); } } } From 4f29b6e74dbd758f7a1681fdf4bcf86a8f13a893 Mon Sep 17 00:00:00 2001 From: Alice Cecile Date: Mon, 20 Jan 2025 14:13:04 -0800 Subject: [PATCH 16/19] Fix comment about initial targeting setup --- examples/ecs/relationships.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/ecs/relationships.rs b/examples/ecs/relationships.rs index 9fcd450ce2c95..f212c9bbe9c32 100644 --- a/examples/ecs/relationships.rs +++ b/examples/ecs/relationships.rs @@ -39,8 +39,8 @@ fn main() { 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, Alice will target Bob, Bob will target Charlie, - // and Charlie will target Alice. This creates a loop in the relationship graph. + // 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. From 492bef433fd5e51d029a6ad9b0b2414924ec90ff Mon Sep 17 00:00:00 2001 From: Alice Cecile Date: Mon, 20 Jan 2025 17:36:11 -0500 Subject: [PATCH 17/19] Parent -> ChildOf rename --- examples/ecs/relationships.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/ecs/relationships.rs b/examples/ecs/relationships.rs index f212c9bbe9c32..955c1fe35e004 100644 --- a/examples/ecs/relationships.rs +++ b/examples/ecs/relationships.rs @@ -1,11 +1,11 @@ //! Entities generally don't exist in isolation. Instead, they are related to other entities in various ways. -//! While Bevy comes with a built-in [`Parent`]/[`Children`] relationship +//! 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 [`Parent`] component implements the [`Relationship`] trait, serving as the source of truth, +//! 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, From 8ca12662257fbeb8400b64565619f3c70601000d Mon Sep 17 00:00:00 2001 From: Alice Cecile Date: Mon, 20 Jan 2025 14:50:50 -0800 Subject: [PATCH 18/19] Fix EntityHashSet import --- examples/ecs/relationships.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/ecs/relationships.rs b/examples/ecs/relationships.rs index 955c1fe35e004..675cd78c2d807 100644 --- a/examples/ecs/relationships.rs +++ b/examples/ecs/relationships.rs @@ -11,7 +11,7 @@ //! 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::EntityHashSet; +use bevy::ecs::entity::hash_set::EntityHashSet; use bevy::ecs::system::RunSystemOnce; use bevy::prelude::*; From 54fca60939409082b0eba2db82f4e52923643b2f Mon Sep 17 00:00:00 2001 From: Alice Cecile Date: Mon, 20 Jan 2025 14:58:45 -0800 Subject: [PATCH 19/19] Remove relationship traits from prelude due to subtle bugs created --- crates/bevy_ecs/src/lib.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/crates/bevy_ecs/src/lib.rs b/crates/bevy_ecs/src/lib.rs index dcaec34f8c1d0..d0b93f0592676 100644 --- a/crates/bevy_ecs/src/lib.rs +++ b/crates/bevy_ecs/src/lib.rs @@ -80,7 +80,6 @@ pub mod prelude { name::{Name, NameOrEntity}, observer::{CloneEntityWithObserversExt, Observer, Trigger}, query::{Added, AnyOf, Changed, Has, Or, QueryBuilder, QueryState, With, Without}, - relationship::{Relationship, RelationshipTarget}, removal_detection::RemovedComponents, result::{Error, Result}, schedule::{