Graphs

The relation system allows creating Entity hierarchies and graphs.

These graphs can be traversed in different ways through the use of a Query.

The default child_of relation provides a mutually exclusive parent-child relation, which is perfect for transform hierarchies or trees, and is trivial to construct using the EntityBuilder::attach method.

#![allow(unused)]

fn main() {
    let root = Entity::builder()
        .set(name(), "root".into())
        .attach(
            child_of,
            Entity::builder().set(name(), "root.child.1".into()).attach(
                child_of,
                Entity::builder().set(name(), "root.child.1.1".into()),
            ),
        )
        .attach(
            child_of,
            Entity::builder().set(name(), "root.child.2".into()),
        )
        .attach(
            child_of,
            Entity::builder().set(name(), "root.child.3".into()),
        )
        .spawn(&mut world);

}

Likewise, attach_with can be used for stateful relations

Depth First Iteration

Dfs allows traversing hierarchies in depth-first order.

#![allow(unused)]

fn main() {
        let mut query = Query::new((entity_ids(), name())).with_strategy(Dfs::new(child_of));

        tracing::info!("Dfs:");
        for (id, name) in query.borrow(&world).iter() {
            tracing::info!(?id, ?name);
        }

}

Traversal

For modifying a graph through a value which is passed through the parents, such as updating a UI layout, there is Dfs::traverse which provides an easy recursion based visiting, which can for example be used for updating transform hierarchies.

Example:

#![allow(unused)]

fn main() {
    let update_world_position = System::builder()
        .with_query(
            Query::new((world_position().as_mut(), position())).with_strategy(Dfs::new(child_of)),
        )
        .build(
            |mut query: DfsBorrow<(Mutable<Vec3>, Component<Vec3>), All, ()>| {
                query.traverse(&Vec3::ZERO, |(world_pos, &pos), _, &parent_pos| {
                    *world_pos = pos + parent_pos;
                    *world_pos
                });
            },
        );

    let mut buf = String::new();
    let print_hierarchy = System::builder()
        .with_query(
            Query::new((name(), position(), world_position())).with_strategy(Dfs::new(child_of)),
        )
        .build(move |mut query: DfsBorrow<_, _, _>| {
            query.traverse(&0usize, |(name, pos, world_pos), _, depth| {
                let indent = depth * 4;
                writeln!(
                    buf,
                    "{:indent$}{name}: {pos} {world_pos}",
                    "",
                    indent = indent,
                )
                .unwrap();
                depth + 1
            });

            tracing::info!("{buf}");
        });

    let mut schedule = Schedule::new()
        .with_system(update_world_position)
        .with_system(print_hierarchy);

    schedule.execute_seq(&mut world).unwrap();

}

See: transform

Topological Iteration

In addition to depth first traversal, queries offer iteration in topological ordering through Topo.

Topological ordering is less constrained as it only ensures that each node's parents are visited before the children, but not that the children are visited immediately after the parent.

More specifically, each node is visited in the ordered of their maximum recursion depth. I.e, first all roots are visited, then all children, then all 2nd level children and so on.

This allows far greater cache locality and is more similar in memory access patterns to the non-relation aware Planar strategy.