Relations
A relation is a component which links to another Entity
, similar to a foreign key in a database. This can be used to construct different kinds of graphs and trees inside the ECS.
The links between entities are managed by the ECS itself and will always be valid, see Lifetime.
The linked entity is referred to as the target
of a relation, while the entity the component is attached to is called the subject
.
This allows forming hierarchies such as parent-child relations for transforms and UI, as well as arbitrary graphs.
See the child_of
relation for an example of a parent-child relation which uses the parent entity as the relation's target.
Relations are most easily declared using the component macro, but can be constructed dynamically as well. See dynamic_components
For example, declaring a child relationship that connects to a parent can be done like so:
#![allow(unused)] fn main() { component! { child_of(id): (), } let parent = Entity::builder() .set(name(), "Parent".into()) .spawn(&mut world); let child1 = Entity::builder() .set(name(), "Child1".into()) .set_default(child_of(parent)) .spawn(&mut world); let child2 = Entity::builder() .set(name(), "Child2".into()) .set_default(child_of(parent)) .spawn(&mut world); }
The parameter to the component function determines the target entity of the relation.
Since the value of the relation in this case is ()
, set_default
can be used as a shorthand over set
Two relations of the same type but with different targets behave like two separate components and will not interfere. This allows having many-to-many relationships between entities, if so desired.
This allows constructing many different kinds of graphs inside the ECS.
#![allow(unused)] fn main() { let parent2 = Entity::builder() .set(name(), "Parent2".into()) .spawn(&mut world); world.set(child1, child_of(parent2), ())?; tracing::info!("World: {world:#?}"); // Give child1 yet one more parent world.set(child1, child_of(parent2), ())?; tracing::info!( "Connections from child1({child1}): {:?}", Query::new(relations_like(child_of)) .borrow(&world) .get(child1)? .collect_vec() ); }
Queries
Since relations are normal components, they can be used in a query as normal, or used to exclude components.
See the Graphs chapter in queries.
#![allow(unused)] fn main() { // Mathes a relation exactly let children_of_parent: Vec<Entity> = Query::new(entity_ids()) .with(child_of(parent)) .collect_vec(&world); tracing::info!("Children: {children_of_parent:?}"); // Matches a relation with any parent let all_children: Vec<Entity> = Query::new(entity_ids()) .with_filter(child_of.with_relation()) .collect_vec(&world); tracing::info!("Children: {all_children:?}"); let roots = Query::new(entity_ids()) .with_filter(child_of.without_relation()) .collect_vec(&world); tracing::info!("Roots: {roots:?}"); }
Associated values
In addition to linking between entities, a relation can also store additional data just like a component. This can be used to create weighted graphs or storing other additional information such as physical joint parameters.
Since relations behave like separate components, each value on a relation is specific to that link, and as such saves you the hassle of managing a separate list of values for each connection on an entity.
The following shows a more complete example of how to traverse and calculate the forces between entities connected via springs using hook's law.
#![allow(unused)] fn main() { struct Spring { strength: f32, length: f32, } impl Spring { fn new(strength: f32, length: f32) -> Self { Self { strength, length } } } component! { spring_joint(id): Spring, position: Vec2, } let id1 = Entity::builder() .set(name(), "a".into()) .set(position(), vec2(1.0, 4.0)) .spawn(&mut world); // Connect id2 to id1 with a spring of strength 2.0 let id2 = Entity::builder() .set(name(), "b".into()) .set(spring_joint(id1), Spring::new(2.0, 1.0)) .set(position(), vec2(2.0, 0.0)) .spawn(&mut world); let _id3 = Entity::builder() .set(name(), "c".into()) .set(spring_joint(id1), Spring::new(2.0, 3.0)) .set(position(), vec2(2.0, 3.0)) .spawn(&mut world); let _id4 = Entity::builder() .set(name(), "d".into()) .set(spring_joint(id2), Spring::new(5.0, 0.5)) .set(position(), vec2(1.0, 0.0)) .spawn(&mut world); let mut query = Query::new((entity_ids(), name().cloned(), position())) .with_strategy(Dfs::new(spring_joint)); query .borrow(&world) .traverse(&None, |(id, name, &pos), strength, parent| { if let (Some(spring), Some((parent_name, parent_pos))) = (strength, parent) { let distance = pos.distance(*parent_pos) - spring.length; let force = distance * spring.strength; tracing::info!("spring acting with {force:.1}N between {parent_name} and {name}"); } else { tracing::info!(%id, name, "root"); } Some((name, pos)) }); }
Exclusive relations
Relations can be declared as exclusive, which means that only one relation of that type can exist on an entity at a time. This is useful for cases where you want to have a single parent or outgoing connection.
Note: This does not prevent multiple entities from referencing the same entity, but rather an entity referencing multiple entities.
When a new relation is added to an entity, any existing relation of the same type will be removed.
This is the case for the included child_of
relation.
#![allow(unused)] fn main() { component! { child_of(parent): () => [ Exclusive ], } let id1 = Entity::builder().spawn(&mut world); let id2 = Entity::builder().spawn(&mut world); let id3 = Entity::builder() .set_default(child_of(id1)) .spawn(&mut world); let entity = world.entity_mut(id3).unwrap(); tracing::info!( "relations of {id3}: {:?}", entity.relations(child_of).map(|v| v.0).collect_vec() ); world.set(id3, child_of(id2), ()).unwrap(); let entity = world.entity_mut(id3).unwrap(); tracing::info!( "relations of {id3}: {:?}", entity.relations(child_of).map(|v| v.0).collect_vec() ); }
Lifetime
Relations are managed by the ECS and will automatically be cleaned up. When an entity is despawned all relations which reference it will be removed from the ECS. As such, a relation will never point to an invalid entity.
#![allow(unused)] fn main() { tracing::info!( "has relation to: {parent2}: {}", world.has(child1, child_of(parent2)) ); world.despawn(parent2)?; tracing::info!( "has relation to: {parent2}: {}", world.has(child1, child_of(parent2)) ); tracing::info!("World: {world:#?}"); world.despawn_recursive(parent, child_of)?; tracing::info!("World: {world:#?}"); }