Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Entity Relations RFC #18

Closed
wants to merge 86 commits into from
Closed
Changes from 16 commits
Commits
Show all changes
86 commits
Select commit Hold shift + click to select a range
771682d
Template
alice-i-cecile Apr 22, 2021
8f450d1
Notes on future work
alice-i-cecile Apr 22, 2021
a45c111
Summary and motivation
alice-i-cecile Apr 22, 2021
6868bc5
Drawbacks
alice-i-cecile Apr 22, 2021
8cffbe8
Prior art in Flecs
alice-i-cecile Apr 22, 2021
49d4f2a
Basic guide-level explanation
alice-i-cecile Apr 23, 2021
1f40033
Improved Future Work section
alice-i-cecile Apr 23, 2021
96065d4
Added `despawn_recursive` to future work
alice-i-cecile Apr 23, 2021
f090896
Relation cleanup note
alice-i-cecile Apr 23, 2021
adfb72e
Minor bug fix in example
alice-i-cecile Apr 23, 2021
94c59d2
Actually get the right lender
alice-i-cecile Apr 23, 2021
5636cec
Improved examples
alice-i-cecile Apr 23, 2021
c4e2af6
boop
BoxyUwU Apr 23, 2021
23b975b
Remove unnecessary filter in all_children example
alice-i-cecile Apr 23, 2021
cf82d3a
merge conflcits sucks :_:
BoxyUwU Apr 23, 2021
8c0171c
Merge remote-tracking branch 'origin/min-relations' into min-relations
BoxyUwU Apr 23, 2021
df96a01
Improved clarity of introduction
alice-i-cecile Apr 23, 2021
55dd988
Relations are not components
alice-i-cecile Apr 24, 2021
a7aff66
Fix example
alice-i-cecile Apr 24, 2021
2bbd838
Reduce optimism on learning burden
alice-i-cecile Apr 24, 2021
e2ce855
Add bikeshed tag
alice-i-cecile Apr 24, 2021
014b33f
Needs more bikeshed
alice-i-cecile Apr 24, 2021
4ef058b
Removed redundant all_children example
alice-i-cecile Apr 24, 2021
ca9d3eb
Removed oudated comment
alice-i-cecile Apr 24, 2021
555a219
Removed weak drawback
alice-i-cecile Apr 24, 2021
2bd4bfe
Clarified that not all problems will be solved by min_relations
alice-i-cecile Apr 24, 2021
c88a592
Noted that Rationale is incomplete
alice-i-cecile Apr 24, 2021
86bfb3e
Fixed hard tabs :(
alice-i-cecile Apr 24, 2021
8727707
Improved target filter API
alice-i-cecile Apr 24, 2021
b504089
Automatic cleanup
alice-i-cecile Apr 24, 2021
91f52b5
Clarity improvements to Future Work
alice-i-cecile Apr 24, 2021
1a121bb
Added change_source and change_target API
alice-i-cecile Apr 24, 2021
82548cb
Entity group use case explanation
alice-i-cecile Apr 24, 2021
40c8ebe
Relations for entity graphs explanation
alice-i-cecile Apr 24, 2021
e5481f0
Clarity
alice-i-cecile Apr 24, 2021
2daaaa2
Advanced relation filters stub
alice-i-cecile Apr 24, 2021
fd261eb
Implementation sketch for compound relation filters
alice-i-cecile Apr 24, 2021
2b280b1
First draft of rationale and alternatives
alice-i-cecile Apr 24, 2021
b384868
Improved examples for changing target and source
alice-i-cecile Apr 24, 2021
0792124
Typo fix
alice-i-cecile Apr 24, 2021
00be482
Typo fix
alice-i-cecile Apr 24, 2021
362ad69
Fixed link
alice-i-cecile Apr 24, 2021
7d63fe8
Fixed link
alice-i-cecile Apr 24, 2021
afe1555
Humans own kittens
alice-i-cecile Apr 24, 2021
cb95234
Fixed backwards relation filter
alice-i-cecile Apr 24, 2021
9ff30f9
Fixed spring query
alice-i-cecile Apr 24, 2021
46a9d0e
Reduced character of writing
alice-i-cecile Apr 24, 2021
bf6bd3e
Phrasing
alice-i-cecile Apr 24, 2021
6bc157f
Clarified spring example
alice-i-cecile Apr 24, 2021
c164e15
Simplified compound relation filters
alice-i-cecile Apr 24, 2021
c0833fa
Added reference to EnTT prior art
alice-i-cecile Apr 24, 2021
ed1694b
Noted that perf work is better done once we can compare perf
alice-i-cecile Apr 24, 2021
bcc3a4d
Improved clarity of advanced relation filtering
alice-i-cecile Apr 24, 2021
523cd59
Noted connection to undirected graphs
alice-i-cecile Apr 24, 2021
07e47db
More detail in future work
alice-i-cecile Apr 24, 2021
17d74d6
Add TODO
alice-i-cecile Apr 24, 2021
1099c39
Builder pattern returns a value
alice-i-cecile Apr 24, 2021
766fb75
Merge remote-tracking branch 'origin/min-relations' into min-relations
alice-i-cecile Apr 24, 2021
09df9a3
More future work
alice-i-cecile Apr 24, 2021
aee4dae
Misc cleanup
alice-i-cecile Apr 24, 2021
af543da
Consistency with RemoveComponents
alice-i-cecile Apr 24, 2021
d1f90c3
Add TODO
alice-i-cecile Apr 24, 2021
8e858c0
Dramatically optimized spring example
alice-i-cecile Apr 24, 2021
4279361
Fixed formatting
alice-i-cecile Apr 24, 2021
674bd5b
Noted bikeshed for command arg order
alice-i-cecile Apr 24, 2021
dd82790
Added get_relation example
alice-i-cecile Apr 25, 2021
7c2cbcf
Example bug fix
alice-i-cecile Apr 25, 2021
c9f0e38
Ideas for reference-level explanation
alice-i-cecile Apr 25, 2021
23f758d
Filtering over groups of entities
alice-i-cecile Apr 25, 2021
2bd87d0
Negative filtering example
alice-i-cecile Apr 25, 2021
2491423
Restructured and shortened guide level explanation a bit
alice-i-cecile Apr 25, 2021
bec0d2c
Frenemies example
alice-i-cecile Apr 25, 2021
9f5f62c
Herbivores example
alice-i-cecile Apr 25, 2021
a8c2f52
rustfmt + minor fix
TheRawMeatball Apr 25, 2021
139baec
Merge pull request #1 from TheRawMeatball/patch-1
alice-i-cecile Apr 25, 2021
add64c0
hotfix
TheRawMeatball Apr 25, 2021
4ac8ab9
Merge pull request #2 from TheRawMeatball/patch-1
alice-i-cecile Apr 25, 2021
7b79ff2
Typo fix
alice-i-cecile Apr 25, 2021
a24d20b
Use .max_by_key for example
alice-i-cecile Apr 25, 2021
90d9436
Added convenience function ideas to Future Work
alice-i-cecile Apr 25, 2021
d84c1d0
tweak example ordering and add some comments
BoxyUwU Apr 27, 2021
4081fe0
sowwy made changes
BoxyUwU Apr 27, 2021
ab0c8bd
Fix Rel -> Relation
BoxyUwU Apr 27, 2021
b1ce346
Fixed typo in example
alice-i-cecile May 2, 2021
85519a3
Add relation event channels idea
alice-i-cecile May 6, 2021
3e06286
Add new FLECS docs to prior art
alice-i-cecile May 11, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
270 changes: 270 additions & 0 deletions rfcs/min-relations.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,270 @@
# Feature Name: `min-relations`

// FIXME
// Make sure to talk about these:
// - Reverse relations.
// - Or support for add_target_filter.

## Summary

Relations connect entities to each other, storing data in these edges. This RFC covers the minimal version of this feature, allowing for basic entity grouping and simple game logic extensions.
alice-i-cecile marked this conversation as resolved.
Show resolved Hide resolved

## Motivation

Entities often need to be aware of each other in complex ways: an attack that targets another unit, groups of entities that are controlled en-masse, or complex parent-child hierarchies that need to move together.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the InFrustum(camera_id) would be especially understandable here. It's easy to see why it can't be handled by simple tag component here.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I worry that multiple camera frustums are bit too obscure of a topic for the average gameplay programmer referring to this RFC. I agree that they're a clear example though!


We *could* simply store an `Entity` in a component, then use `query.get(my_entity)` to access the relevant data.
(In this RFC, we call this the **Entity-in-component pattern**.)
But this quickly starts to balloon in complexity.

- How do we ensure that these components are cleaned up when the entity they're pointing to is?
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
- How do we ensure that these components are cleaned up when the entity they're pointing to is?
- How do we ensure that these components are cleaned up when the entity they're pointing to is removed?

- How do we handle pointing to multiple entities in similar ways?
- How do we look for all entities that point to a particular entity of interest?
- How do we despawn an entity and all of its children (with many types of child-like entities)?
- How do we quickly access data on the entity that we're attached to?
- How do we traverse this graph of entities?
- How do we ensure that our graph is acylic?
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure if this particular point is handled in any way by proposed relations. Afaict cycles are still an issue (in cases where you explicitly don't want them).

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The framing could be clearer. These problems are possible to address in a cohesive way at an engine-level by including relations as a feature. Min-relations won't hit all of these though :(

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay, so it's more of a future prospect. I guess that's fine.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Clarified!


We *could* force our users to solve these problems, over and over again in each individual game.

This powerful and common pattern deserves a proper abstraction that we can make ergonomic, performant and *fearless*.

## Guide-level explanation

**Relations** are components that connect entities together, pointing from the entity they are stored on to the entity that they target.
Just like other components, they can store effectively arbitrary data, or they can be data-less unit structs that serve as **markers**.
alice-i-cecile marked this conversation as resolved.
Show resolved Hide resolved

Each relation has three parts:

1. The **source entity** that it is stored on.
2. The **target entity** (or simply **target**) that it points to.
3. The **relation kind** that defines the data that is stored on it.

Making your first connection is easy, and mirrors the API that you're familiar with from components:

```rust
// Relations might just be markers
struct FriendsWith;

// But they might store data instead
struct Owes(i32);

// Let's make some entities
let world = World::new();
let alice = world.spawn().id();
let boxy = world.spawn().id();

// You can make relations with commands!
commands.entity(alice).insert_relation(FriendsWith, boxy); // uwu:3

// You can remove them with commands!
commands.entity(alice).remove_relation(FriendsWith, boxy) // not uwu :(

// You can use direct World access!
world.entity_mut(alice).insert_relation(FriendsWith, boxy); // uwu:3

// Relations are directed; a relation in one direction does not imply the same in the other direction
commands.entity(boxy).insert_relation(FriendsWith, alice); // one way friendship sad :(

// You can mix different relation kinds on the same entity!
commands.entity(alice).insert_relation(Owes(9999), boxy); // :)))))

// You can add mulitple relations of the same kind to a single source entity!
let cart = world.spawn().id();
commands.entity(alice).insert_relation(FriendsWith, cart); // Hi!
```

Once your relations are in place, you'll want to access them in some form.
Just like with ordinary components, you can request them in your queries:

```rust
// Relations can be queried for immutably
fn friendship_is_magic(mut query: Query<(&mut Magic, &Relation<FriendsWith>), With<Pony>>) {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is &Relation correct? Following relations don't have a reference.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Currently reference is necessary in query directly, but not used in combinators like With<Relation<T>>. It is used to determine if access is read-only or read-write. This is similar to how references are used on component types here.

// Each entity can have their own friends
for (mut magic, friends) in query.iter_mut(){
// Relations return an iterator when unpacked
magic.power += friends.count();
}
}

// Or you can request mutable access to modify the relation's data
fn interest(query: Query<&mut Relation<Owes>>){
for debts in query.iter(){
for (owed_to, amount_owed) in debts {
println!("we owe {} some money", owed_to);
amount_owed *= 1.05;
}
}
}

// You can query for entities that target a specific entity
fn friend_of_dorothy(
mut query: Query<Entity, With<Relation<FriendsWith>>>,
dorothy: Res<Dorothy>,
) {
// Setting relation filters requires that the query is mutable
query.set_relation_filters(
RelationFilters::new()
.add_target_filter::<FriendsWith, _>(dorothy.entity)
);

for source_entity in query.iter(){
println!("{} is friends with Dorothy!", source_entity);
}

// RelationFilters are reset at the end of the system, make sure to set them every time your system runs!
// You can also reset this manually by applying a new `RelationFilters` to the query
// They override existing relation filters, rather than adding to them
}

// Or even look for some combination of targets!
fn caught_in_the_middle(mut query: Query<&mut Stress, With<Relation<FriendsWith>>>,
dorothy: Res<Dorothy>,
elphaba: Res<Elphaba>){

// You can directly chain this as set_relation_filters returns &mut Query
query.set_relation_filters(
// Note that we can set relation filters even if the relation is in a query filter
RelationFilters::new()
// Note: add_target_filter is additive- i.e. entities this query
// matches must be friends with *both* dorothy AND elphaba
.add_target_filter::<FriendsWith, _>(dorothy.entity)
.add_target_filter::<FriendsWith, _>(elphaba.entity)
).for_each(|mut stress| {
println!("{} is friends with Dorothy and Elphaba!", source_entity);
stress.val = 1000;
})
}

/// If we want to combine targets in an "or" fashion, we need a slightly different syntax
TODO: write example

// Query filters work too!
fn not_alone(
commands: mut Commands,
query: Query<Entity, Without<Relation<FriendsWith>>>,
puppy: Res<Puppy>
) {
for lonely_entity in query.iter(){
commands.insert_relation(FriendsWith, puppy.entity);
}
}

// So does change detection with Added and Changed!
fn new_friends(query: Query<&mut Excitement, Added<Relation<FriendsWith>>>){
query.for_each(|mut excitement|{excitement.value += 10});
alice-i-cecile marked this conversation as resolved.
Show resolved Hide resolved
}

```

TODO: mention relation cleanup on despawn.

Sometimes, we may instead want to look for "entities that are targets of some relation".
We can do so by iterating over all entities with that relation, and then filtering by target.

```rust
fn all_children(
BoxyUwU marked this conversation as resolved.
Show resolved Hide resolved
player_query: Query<Entity, With<Player>>,
child_query: Query<&Name, With<Relation<ChildOf>>>,
) {
let player_entity = player_query.single.unwrap();

child_query.set_relation_filters(
RelationFilters::new()
.add_target_filter::<ChildOf, _>(player_entity)
);

for name in child_query.iter() {
println!("{} is one of my children.", name)
}
}
```

Finally, you can use the `Entity` returned by your relations to fetch data from the target's components by combining it with `query::get()` or `query::get_component<C>()`.

```rust
// We need to use a marker component to avoid having multiple mutable references
BoxyUwU marked this conversation as resolved.
Show resolved Hide resolved
fn debts_come_due(
BoxyUwU marked this conversation as resolved.
Show resolved Hide resolved
mut debt_query: Query<(Entity, Relation<Owes>)>,
alice-i-cecile marked this conversation as resolved.
Show resolved Hide resolved
mut money_query: Query<&mut Money>,
) {
for (debtor, debt) in debt.query() {
for (lender, amount_owed) in debt {
*money_query.get(debtor).unwrap()
.checked_sub(amount_owed)
.expect("Nice kneecaps you got there...");
*money_query.get(lender).unwrap() += amount_owed;
}
}
}
```

### Grouping entities

### Entity hierarchies

## Reference-level explanation

TODO: Boxy explains the magic.

## Drawbacks

1. Introduces another core abstraction to users that they will want to learn.
alice-i-cecile marked this conversation as resolved.
Show resolved Hide resolved
Rewriting our parent-child code in terms of relations is important, but will introduce the concept to users quite quickly.
2. Even if relations are more ergonomic than the Entity-in-component approach to accomplish the same tasks, relations will be held to a higher bar for quality.
BoxyUwU marked this conversation as resolved.
Show resolved Hide resolved
3. Heavy use of relations can seriously fragment archetypes. We need to be careful that this doesn't badly impact our performance.

## Rationale and alternatives

- Why is this design the best in the space of possible designs?
- What other designs have been considered and what is the rationale for not choosing them?
- What is the impact of not doing this?
- Why is this important to implement as a feature of Bevy itself, rather than an ecosystem crate?

## Prior art

[`Flecs`](https://github.com/SanderMertens/flecs), an advanced C++ ECS framework, has a similar feature, which they call "relationships".
These are somewhat different, they use an elaborate query domain-specific language along with being more feature rich.
You read more about them in the [corresponding PR](https://github.com/SanderMertens/flecs/pull/358).
alice-i-cecile marked this conversation as resolved.
Show resolved Hide resolved

## Unresolved questions

1. How do we put relations in Bundles?
2. What do we actually call `Noitaler`? Candidates so far are:
alice-i-cecile marked this conversation as resolved.
Show resolved Hide resolved
1. `ReverseRelation`
2. `Inverse`
3. `CoRelation`
4. `TargetOf`
Comment on lines +594 to +597
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I know this isn't exactly the time to bikeshed, but I think we should remove "CoRelation". It's too similar to "corelation", which means totally diffrent thing.

Suggested change
1. `ReverseRelation`
2. `Inverse`
3. `CoRelation`
4. `TargetOf`
1. `ReverseRelation`
2. `Inverse`
3. `ConverseRelation`
4. `TargetOf`

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IMO this is better as a separate option. "Co-relation" is the obvious term for readers with a background in advanced mathematics, even if they are a tiny minority.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I mean, I don't have anything strong to say here. In fact i've made a typo there, I wanted to point out similarity to "correlation" :P If you are not convinced to remove or replace this potential name, ignore this comment.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wanted to point out similarity to "correlation" :P

Yeah, this is the main reason I dislike CoRelation and don't think it's adequate :(. Capitalizing it like CoRelation is better than Corelation for sure, but it doesn't help enough.

3. Do we need a full graph constraint solver in our queries to handle things like "check for unrequited love"?
4. Do we want a macro to make complex calls easier?

```rust
macro_rules Relation {
(this $kind:ty *) => Relation<$kind>,
(* $kind:ty this) => Noitaler<$kind>,
}
```

## Future possibilities

Relation enhancements:

1. Sugar for accessing data on the target entity in a single query. `Relation<T: Component, Q: WorldQuery>`
2. Graph traversals.
3. Arbitrary target types instead of just `Entity` combined with `KindedEntity` proposal to ensure your targets are valid.
4. Automatically symmetric relations.
5. `Noitaler`*
6. Graph shape guarantees using archetype invariants.
7. Streaming iters to allow for queries like: `Query<&mut Money, Relation<Owes, &mut Money>>` and potentially use invariants on graph shape to allow for skipping soundness checks for the aliasing `&mut Money`'s
8. Assorted Performance optimizations.
9. Relation orders to enable things like `Styles`.
10. Generalized `despawn_recursive`.

Relation applications in the engine:

1. Replace existing parent-child.
2. Applications for UI: styling, widget wiring etc.
3. Frustum culling.

AUTHOR'S NOTE: Much like `yeet`, `Noitaler` is the bikeshed-avoidance name for the inverse of `Relation`, and will never be used.
alice-i-cecile marked this conversation as resolved.
Show resolved Hide resolved
EDITOR's NOTE: `Noitaler` may very much be used :3