-
-
Notifications
You must be signed in to change notification settings - Fork 3.7k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
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]>
- Loading branch information
1 parent
ba5e71f
commit b34833f
Showing
5 changed files
with
245 additions
and
11 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | ||
// 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()); | ||
} |