Bevy Relationships
Bevy stores both entities and components in completely flat arrays and uses the entity's ID as an index to get all the related components. This flat structure can make it difficult to represent parent/child relationships.
In other engines like Godot these relationships are expressed through the fact that everything is a node with some kind of parent right down to the root scene.
Before Bevy 0.16
we only had Parent
and Child
components we could use to express one way relationships.
After 0.16
these were replaced with ChildOf
and Children
components. We got full on bidirectional relationships with the Relationship
and RelationshipTarget
components 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
: TheRelationship
we attach to other entitiesChildren
: TheRelationshipTarget
that is kept in sync
These are slightly more powerful than custom relationships in that the Transform
and GlobalTransform
of the parent will be propagated to the children.
When you despawn the parent (the entity holding the Children
) then all the ChildOf
components are removed automatically.
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) {
// Spawn a Fleet entity
commands
.spawn((Fleet, Name::new("Fleet")))
.with_children(|parent| {
// Spawn a Ship entity as a child of the Fleet
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")),
]
));
}
Creating a relationship
Relationships are expressed as part of the derive
macro for your components.
Imagine you want to represent a relationship of a Ship
and its various attachments such as a GunTurret
:
#[derive(Component)]
struct Ship;
#[derive(Component)]
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.
#[derive(Component)]
#[relationship(relationship_target = ShipAttachments)]
struct AttachedToShip(pub Entity)
It is considered the source of truth because its 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 list of entities to store them.
#[derive(Component)]
#[relationship_target(relationship = AttachedToShip, linked_spawn)]
struct ShipAttachments(Vec<Entity>);
The linked_spawn
will allow us to remove the ShipAttachments
and Bevy will automatically despawn any AttachedToShip
components on our other entities.
To create the relationship we can then spawn this Relationship
on other entities.
fn spawn_ship(mut commands: Commands) {
// Spawn the parent Ship
let ship = commands.spawn((Ship, Name::new("Ship"))).id();
// Spawn a GunTurret and attach it to the Ship using the new Relationship
// component
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) {
// Spawn a Ship entity
commands.spawn((
Ship,
Name::new("Ship A"),
related!(ShipAttachments[
// Attach GunTurrets to the Ship using the relationship
(GunTurret, Name::new("GunTurret 1")),
(GunTurret, Name::new("GunTurret 2")),
]),
));
}
Once we spawned the turrets, because of the AttachedToShip
component, Bevy ran some component hooks which added 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>>,
turrents: 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) = turrents.get(attachment) {
info!(" - {}", child_name.as_str());
}
}
}
}
There is a more idiomatic way to parse these relationships with 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);
}
}
}
To iterate from the other direction we can use iter_descendants
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 this will run infinitely.
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 not currently fragmented the same as other component combinations so even if relationships point to different entities they will be in the same archetype.
It is possible to have the components that store the Relationship
to have other data if its immutable, but there are currently issues.