Tainted\\Coders

Bevy Relationships

Bevy version: 0.16Last updated:

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:

  1. ChildOf: The Relationship we attach to other entities
  2. Children: The RelationshipTarget 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.