Skip to content

Commit

Permalink
Merge pull request #10 from ten3roberts/relations
Browse files Browse the repository at this point in the history
Clarify documentation and examples for relations
  • Loading branch information
ten3roberts authored Oct 28, 2023
2 parents 584b51e + 53184f2 commit 47a3b0f
Show file tree
Hide file tree
Showing 26 changed files with 543 additions and 146 deletions.
5 changes: 5 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,11 @@ name = "systems"
path = "./examples/guide/systems.rs"
required-features = ["std", "rayon"]

[[example]]
name = "springs"
path = "./examples/guide/springs.rs"
required-features = ["std"]

[[bench]]
name = "benchmarks"
harness = false
Expand Down
79 changes: 52 additions & 27 deletions examples/guide/relations.rs
Original file line number Diff line number Diff line change
@@ -1,22 +1,23 @@
use flax::{
components::{child_of, name},
relation::RelationExt,
*,
};
use flax::{components::name, relation::RelationExt, *};
use itertools::Itertools;
use tracing_subscriber::{prelude::*, registry};
use tracing_tree::HierarchicalLayer;

fn main() -> anyhow::Result<()> {
registry().with(HierarchicalLayer::default()).init();
basic()?;
exclusive()?;
Ok(())
}

fn basic() -> anyhow::Result<()> {
let mut world = World::new();

// ANCHOR: relation_basic
component! {
spring_joint(other): f32 => [Debuggable],
child_of(id): (),
}

let mut world = World::new();

let parent = Entity::builder()
.set(name(), "Parent".into())
.spawn(&mut world);
Expand All @@ -30,7 +31,6 @@ fn main() -> anyhow::Result<()> {
.set(name(), "Child2".into())
.set_default(child_of(parent))
.spawn(&mut world);

// ANCHOR_END: relation_basic

// ANCHOR: many_to_many
Expand All @@ -42,49 +42,41 @@ fn main() -> anyhow::Result<()> {

tracing::info!("World: {world:#?}");

// Connect child1 with two entities via springs of different strength
world.set(child1, spring_joint(child2), 1.5)?;
world.set(child1, spring_joint(parent2), 7.4)?;
// Give child1 yet one more parent
world.set(child1, child_of(parent2), ())?;

tracing::info!(
"Connections from child1({child1}): {:?}",
Query::new(relations_like(spring_joint))
Query::new(relations_like(child_of))
.borrow(&world)
.get(child1)?
.collect_vec()
);

// ANCHOR_END: many_to_many
// ANCHOR: query

let children_of_parent = Query::new(entity_ids())
// Mathes a relation exactly
let children_of_parent: Vec<Entity> = Query::new(entity_ids())
.with(child_of(parent))
.borrow(&world)
.iter()
.collect_vec();
.collect_vec(&world);

tracing::info!("Children: {children_of_parent:?}");

let all_children = Query::new(entity_ids())
// Matches a relation with any parent
let all_children: Vec<Entity> = Query::new(entity_ids())
.filter(child_of.with_relation())
.borrow(&world)
.iter()
.collect_vec();
.collect_vec(&world);

tracing::info!("Children: {all_children:?}");

let roots = Query::new(entity_ids())
.filter(child_of.without_relation())
.borrow(&world)
.iter()
.collect_vec();
.collect_vec(&world);

tracing::info!("Roots: {roots:?}");

// ANCHOR_END: query

// ANCHOR: lifetime

tracing::info!(
"has relation to: {parent2}: {}",
world.has(child1, child_of(parent2))
Expand All @@ -105,3 +97,36 @@ fn main() -> anyhow::Result<()> {

Ok(())
}

fn exclusive() -> anyhow::Result<()> {
let mut world = World::new();

// ANCHOR: exclusive
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()
);
// ANCHOR_END: exclusive
Ok(())
}
68 changes: 68 additions & 0 deletions examples/guide/springs.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
use flax::{component, components::name, entity_ids, Dfs, Entity, FetchExt, Query, World};
use glam::{vec2, Vec2};
use tracing_subscriber::{prelude::*, registry};
use tracing_tree::HierarchicalLayer;

fn main() {
registry().with(HierarchicalLayer::default()).init();

let mut world = World::new();

// ANCHOR: 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))
});
// ANCHOR_END: main
}
2 changes: 1 addition & 1 deletion guide/src/diving_deeper/dynamic_components.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,5 +43,5 @@ entity as the generation is not needed.
{{ #include ../../../examples/guide/dynamic_components.rs:relation }}
```

When despawning either the relation component or object entity, the "parent",
When despawning either the relation component or target entity, the "parent",
the component is removed from all entities.
52 changes: 40 additions & 12 deletions guide/src/fundamentals/relations.md
Original file line number Diff line number Diff line change
@@ -1,26 +1,31 @@
# Relations

A relation is a component which *links* to another `Entity`, similar to a foreign key in a database.
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 linked entity is referred to as the `object` of a relation, while the entity the component is attached to is called the `subject`.
The links between entities are managed by the ECS itself and will always be valid, see [Lifetime](#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.

A relation is used as a *parameterized* component, which requires an `Entity` to be fully instantiated.
See the [`child_of`](https://docs.rs/flax/latest/flax/components/fn.child_of.html) 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](https://docs.rs/flax/latest/flax/macro.component.html) macro.
[component](https://docs.rs/flax/latest/flax/macro.component.html) macro, but can be constructed dynamically as well. See [dynamic_components](../diving_deeper/dynamic_components.md)

For example, declaring a child relationship that connects to a parent can be done like so:

```rust
{{ #include ../../../examples/guide/relations.rs:relation_basic }}
```

Important to note is that the same `child_of` component with different `object`
arguments are distinct, and can as such exist on an entity at the same time,
allowing many-many relationships between entities;
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`

There is no limitation of the number of relations an entity can have. As such,
an entity can have multiple relations to other entities, allowing for any kind of graphs inside the ecs.
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.

```rust
{{ #include ../../../examples/guide/relations.rs:many_to_many }}
Expand All @@ -36,12 +41,35 @@ See the [Graphs](../query/graphs.md) chapter in queries.
```rust
{{ #include ../../../examples/guide/relations.rs:query }}
```
## 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.

```rust
{{ #include ../../../examples/guide/springs.rs:main }}
```

# 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`](https://docs.rs/flax/latest/flax/components/fn.child_of.html) relation.

```rust
{{ #include ../../../examples/guide/relations.rs:exclusive }}
```

## Lifetime

When an entity is despawned, all relations to it present on other components
will be removed and dropped. As such, no entity will have a relation to an
entity which does not exist.
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.

```rust
{{ #include ../../../examples/guide/relations.rs:lifetime }}
Expand Down
2 changes: 1 addition & 1 deletion guide/src/introduction.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ and new functionality can be added to existing entities and components.

In Flax, there are 3 fundamental building blocks.

[Entity](https://docs.rs/flax/latest/flax/struct.Entity.html). A unique identifier for the objects of the program. Has a managed lifecycle.
[Entity](https://docs.rs/flax/latest/flax/struct.Entity.html). A unique identifier for the entities of the program. Has a managed lifecycle.

[Component](https://docs.rs/flax/latest/flax/struct.Component.html), data which
can be added to an Entity. Has a unique Id, which works as the key for storing
Expand Down
2 changes: 1 addition & 1 deletion recipes.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
"kind": "term"
},
"test-miri": {
"cmd": "cargo miri nextest run -j 8 --no-default-features --features std,serde,flume,derive"
"cmd": "cargo +nightly miri nextest run -j 8 --no-default-features --features std,serde,flume,derive"
},
"doc": {
"cmd": "cargo doc --all-features --open"
Expand Down
5 changes: 5 additions & 0 deletions src/archetypes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,11 @@ impl Archetypes {
let dst = self.get_mut(dst_id);
dst.remove_link(component);
}

for (key, &dst_id) in &arch.outgoing {
self.get_mut(dst_id).incoming.remove(key);
}

self.gen = self.gen.wrapping_add(1);

arch
Expand Down
6 changes: 3 additions & 3 deletions src/cascade/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -89,15 +89,15 @@ fn get_ordered_archetypes(
},
}

// Find relations to other objects, and visit them as well
// Find relations to other targets, and visit them as well
let relations = arch.relations_like(relation);
let mut is_reachable = false;
let mut is_root = true;
for (key, _) in relations {
is_root = false;
let parent = key.object.unwrap();
let target = key.target.unwrap();

let loc = world.location(parent).unwrap();
let loc = world.location(target).unwrap();
// Part of the visited set
if let Some(arch) = archetypes.get(&loc.arch_id) {
if get_ordered_archetypes(world, archetypes, arch_id, arch, relation, visited, ordered)
Expand Down
Loading

0 comments on commit 47a3b0f

Please sign in to comment.