-
-
Notifications
You must be signed in to change notification settings - Fork 3.7k
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
alice-i-cecile
merged 23 commits into
bevyengine:main
from
alice-i-cecile:relations-example
Jan 20, 2025
+245
−11
Merged
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 4cedb40
Export Relationship and RelationshipTarget in prelude
alice-i-cecile b008e3f
Add spawning section to example
alice-i-cecile fe46224
Fix metadata name for example
alice-i-cecile 1a1a9f6
Fix up spawning
alice-i-cecile 94bd55a
Improve warning in traversal methods
alice-i-cecile fb0dfab
Demonstrate cycle checking
alice-i-cecile 15842f3
Show how to remove relationships
alice-i-cecile f89777e
Typo
alice-i-cecile 7c25e04
Update examples README
alice-i-cecile 13c193b
Consistently use relationship
alice-i-cecile a3093a4
Demonstrate how to mutate relationships
alice-i-cecile fbe500b
Better expect message
alice-i-cecile 240462c
Revise example module docs to be more clear
alice-i-cecile 7a8c66f
Merge remote-tracking branch 'alice-i-cecile/relations-example' into …
alice-i-cecile 24b13d3
Use HashSet::insert more idiomatically
alice-i-cecile fd6ec8e
Merge branch 'main' into relations-example
alice-i-cecile 4f29b6e
Fix comment about initial targeting setup
alice-i-cecile 222d0e7
Merge branch 'main' into relations-example
alice-i-cecile 8f2212c
Merge branch 'main' into relations-example
alice-i-cecile 492bef4
Parent -> ChildOf rename
alice-i-cecile 8ca1266
Fix EntityHashSet import
alice-i-cecile 54fca60
Remove relationship traits from prelude due to subtle bugs created
alice-i-cecile File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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()); | ||
} |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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!