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 <jondolf.dev@gmail.com> Co-authored-by: MinerSebas <66798382+MinerSebas@users.noreply.github.com> Co-authored-by: Kristoffer Søholm <k.soeholm@gmail.com>
This commit is contained in:
parent
ba5e71f53d
commit
b34833f00c
11
Cargo.toml
11
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.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"
|
||||
|
@ -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>,
|
||||
@ -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,
|
||||
@ -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,
|
||||
@ -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,
|
||||
@ -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,
|
||||
|
@ -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
|
||||
|
@ -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::*;
|
||||
|
||||
|
213
examples/ecs/relationships.rs
Normal file
213
examples/ecs/relationships.rs
Normal file
@ -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();
|
||||
// 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…
Reference in New Issue
Block a user