Bevy Relationships
ECS is a relatively flat data structure, which makes it awkward to represent parent/child relationships directly.
Before Bevy 0.16 we only had the Parent and Children components, which we could use to express one-way relationships.
After 0.16, Parent became ChildOf, and relationships are now built on the generic Relationship and RelationshipTarget components. We got full on bidirectional relationships that we can use to create our own dynamic relationships.
Parents and children
Bevy has a builtin relationship it provides for parent/child relationships that is made up of two components:
ChildOf: TheRelationshipwe attach to other entitiesChildren: TheRelationshipTargetthat is kept in sync
The built-in hierarchy is wired into Bevy's transform and visibility propagation, so a parent's Transform and Visibility are inherited by its children automatically.
When you despawn the parent (the entity holding the Children) then all of its children and their descendants are despawned automatically. This happens because Children is declared with linked_spawn. If you would rather keep the children alive, detach them first with detach_children or detach_all_children.
fn spawn_ship(mut commands: Commands) {
let fleet = commands.spawn((Fleet, Name::new("Fleet A"))).id();
commands.spawn((Ship, Name::new("Ship A"), ChildOf(fleet)));
}
We can spawn children directly while spawning a parent with the with_children method on your EntityCommands:
fn spawn_fleet(mut commands: Commands) {
commands
.spawn((Fleet, Name::new("Fleet A")))
.with_children(|parent| {
parent.spawn((Ship, Name::new("Ship 1")));
parent.spawn((Ship, Name::new("Ship 2")));
});
}
Instead of the closure we can pass a bundle of children to the children! macro.
fn spawn_fleet_with_sugar(mut commands: Commands) {
commands.spawn((
Fleet,
Name::new("Fleet B"),
children![(Ship, Name::new("Ship 3")), (Ship, Name::new("Ship 4"))],
));
}
And alternatively we could use Bevy Scene Notation (BSN) to define the template of our fleet and spawn it with that:
fn fleet() -> impl Scene {
bsn! {
Fleet
Name::new("Fleet C")
Children [
(Ship #Ship5),
(Ship #Ship6),
]
}
}
Creating a relationship
Imagine you want to represent a relationship of a Ship and its various attachments such as a GunTurret:
// The main entity
#[derive(Component, Clone, Default)]
struct Ship;
// The child entity
#[derive(Component, Clone, Default)]
struct GunTurret;
To represent this relationship we need to invent two components. One for each direction of the relationship.
The source of truth is the Relationship component. This is the component we will be adding to other entities to specify the relationship. It must contain a reference to the entity we will be attaching ourselves to.
// This is the relationship FROM child TO parent
#[derive(Component)]
#[relationship(relationship_target = ShipAttachments)]
struct AttachedToShip(Entity);
It is considered the source of truth because it's how we affect the other side of the relationship, which is the RelationshipTarget.
This RelationshipTarget is the component that will automatically be kept in sync with all our AttachedToShip components. It must contain a collection of entities, using any type that implements RelationshipSourceCollection, such as Vec<Entity>, an entity hash set, or even a single Entity for a one-to-one relationship.
// This is the relationship target on the parent
#[derive(Component)]
#[relationship_target(relationship = AttachedToShip, linked_spawn)]
struct ShipAttachments(Vec<Entity>);
The linked_spawn attribute means that despawning the entity holding the ShipAttachments will also despawn the entities that have an AttachedToShip component. Even without it, despawning the ship would still remove the AttachedToShip components from the turrets (via the relationship target's discard hook), but it would leave the turret entities alive.
To create the relationship we can then spawn this Relationship on other entities.
fn spawn_attached_to_ship(mut commands: Commands) {
let ship = commands.spawn(Ship).id();
commands.spawn((GunTurret, AttachedToShip(ship), Name::new("GunTurret 1")));
commands.spawn((GunTurret, AttachedToShip(ship), Name::new("GunTurret 2")));
}
This can be shortened by using the related! macro to specify the relationships from the parent entity instead:
fn build_ship(mut commands: Commands) {
commands.spawn((
Ship,
Name::new("Ship A"),
related!(ShipAttachments[
(GunTurret, Name::new("GunTurret 1")),
(GunTurret, Name::new("GunTurret 2")),
]),
));
}
Which can be expressed even more simply through some BSN:
fn ship() -> impl Scene {
bsn! {
Ship
Name::new("Ship A")
ShipAttachments [
(GunTurret #GunTurret1),
(GunTurret #GunTurret2)
]
}
}
Once we've spawned the turrets, because of the AttachedToShip component, Bevy runs some component hooks which add these entities to a ShipAttachments component that was added to our Ship automatically.
Querying relationships
We can now query for those attachments for a specific Ship:
fn log_ship_report(
ships: Query<(&Name, &ShipAttachments), With<Ship>>,
turrets: Query<&Name, With<GunTurret>>,
) {
for (ship_name, attachments) in &ships {
info!("{} has the following attachments:", ship_name.as_str());
for &attachment in &attachments.0 {
if let Ok(child_name) = turrets.get(attachment) {
info!(" - {}", child_name.as_str());
}
}
}
}
It is convenient to grab the ShipAttachments and in most cases we would want to query the synced relationship directly. However, we can also enumerate from the relationship itself using iter_ancestors.
fn iterate_from_turrets_to_ships(
ships: Query<Entity, With<Ship>>,
turrets: Query<Entity, With<AttachedToShip>>,
attachments: Query<&AttachedToShip>,
) {
for turret in &turrets {
for attached in attachments.iter_ancestors(turret) {
let ship = ships.get(attached);
info!("Turret {:?} is attached to Ship {:?}", turret, ship);
}
}
}
We are starting with the turrets, the relationship sources that we attached to the ships, and finding which ship each one targets. The traversal helpers use hierarchy terminology, so walking from a source to its target is iterating to an ancestor.
We did this by querying the AttachedToShip relationship, but this works the same way with the ChildOf builtin relationship Bevy provides:
fn log_ships_in_fleet(
fleets: Query<&Name, With<Fleet>>,
ships: Query<(Entity, &Name), With<Ship>>,
children: Query<&ChildOf>,
) {
for (ship_entity, ship_name) in &ships {
for ancestor in children.iter_ancestors(ship_entity) {
if let Ok(fleet_name) = fleets.get(ancestor) {
info!("{} is in fleet {}", ship_name.as_str(), fleet_name.as_str());
}
}
}
}
To iterate from the other direction, that is from the parent to the children we can use iter_descendants. So we can ask each ship which turrets it has:
fn iterate_from_ships_to_turrets(
ships: Query<Entity, With<Ship>>,
turrets: Query<Entity, With<GunTurret>>,
ship_attachments: Query<&ShipAttachments>,
) {
for ship in &ships {
for attachment in ship_attachments.iter_descendants(ship) {
let turret_entity = turrets.get(attachment);
info!("Ship {:?} has Turret {:?}", ship, turret_entity);
}
}
}
You must be careful not to use this if your relationships contain loops, as it will run infinitely.
Other useful traversal helpers include:
root_ancestorto jump straight to the top of a hierarchyiter_descendants_depth_firstanditer_leavesfor treesiter_siblingsto walk entities that share a target.
Self referential relationships
By default, relationships are expected to be traversed and therefore should not contain loops. Bevy will give us a warning and remove the relationship if we try to add one that points to the same Entity. Note that only these self-loops are checked: a longer cycle such as A -> B -> A is not detected, so it is up to you to avoid those.
We can opt out of this behavior and use it to define self referential relationships:
#[derive(Component)]
#[relationship(relationship_target = PeopleILike, allow_self_referential)]
pub struct LikedBy(pub Entity);
#[derive(Component)]
#[relationship_target(relationship = LikedBy)]
pub struct PeopleILike(Vec<Entity>);
Bevy will no longer warn us, but then it's up to us to either avoid traversing these relationships or write our own custom logic to handle the loops.
Limitations
Currently there are some limitations to relationships that are being worked on.
We don't currently have a native way of representing many-to-many relationships. ChildOf can only point to a single entity.
Relationships are currently only fragmented by component type, not by target value. So even if two entities have the same relationship pointing at different targets, they will still be in the same archetype. Value-based fragmentation is planned as an opt-in optimization.