-
-
Notifications
You must be signed in to change notification settings - Fork 3.7k
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
Relationships (non-fragmenting, one-to-many) #17398
base: main
Are you sure you want to change the base?
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
omg its happening
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Excellent to see this landing! Mostly comments, but I do have one question around the need for Component<Mutability = Mutable>
on RelationshipSource
.
@@ -207,6 +210,7 @@ pub const ON_ADD: &str = "on_add"; | |||
pub const ON_INSERT: &str = "on_insert"; | |||
pub const ON_REPLACE: &str = "on_replace"; | |||
pub const ON_REMOVE: &str = "on_remove"; | |||
pub const ON_DESPAWN: &str = "on_despawn"; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
An ON_DESPAWN
hook is also a great outcome form this PR in of itself. I assume there's negligible performance impacts from the addition of this hook?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We can benchmark, but I don't expect significant degradation.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I wouldn't expect any problems either, agreed.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Didn't read everything, added a couple comments.
Is there a test for the commands.remove::<Parent>().despawn()
doesn't despawn descendants? I think that would be good to have
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've updated my repo to pick up the latest changes (using Component derive) and everything still works.
Co-authored-by: Alice Cecile <[email protected]>
@cart I have no concerns about this at this stage and we have plenty of community review; feel free to merge when you feel it's ready. |
This adds support for one-to-many non-fragmenting relationships (with planned paths for fragmenting and non-fragmenting many-to-many relationships). "Non-fragmenting" means that entities with the same relationship type, but different relationship targets, are not forced into separate tables (which would cause "table fragmentation").
Functionally, this fills a similar niche as the current Parent/Children system. The biggest differences are:
REQUEST TO REVIEWERS: please don't leave top level comments and instead comment on specific lines of code. That way we can take advantage of threaded discussions. Also dont leave comments simply pointing out CI failures as I can read those just fine.
Built on top of what we have
Relationships are implemented on top of the Bevy ECS features we already have: components, immutability, and hooks. This makes them immediately compatible with all of our existing (and future) APIs for querying, spawning, removing, scenes, reflection, etc. The fewer specialized APIs we need to build, maintain, and teach, the better.
Why focus on one-to-many non-fragmenting first?
Flecs is heavily considering a switch to non-fragmenting relations after careful considerations of the performance tradeoffs.(Correction from @SanderMertens: Flecs is implementing non-fragmenting storage specialized for asset hierarchies, where asset hierarchies are many instances of small trees that have a well defined structure)The changes
This PR does the following:
bevy_ecs::hierarchy
. The oldbevy_hierarchy
crate has been removed.entity.despawn()
is called. The built in Parent/Children hierarchies enable this behavior, andentity.despawn_recursive()
has been removed.world.spawn
now applies commands after spawning. This ensures that relationship bookkeeping happens immediately and removes the need to manually flush. This is in line with the equivalent behaviors recently added to the other APIs (ex: insert).validate_parent_has_component
hook.Using Relationships
The
Relationship
trait looks like this:A relationship is a component that:
RelationshipSources
component, which is a simple wrapper over a collection of entities. Every "target entity" targeted by a "source entity" with aRelationship
has aRelationshipSources
component, which contains every "source entity" that targets it.For example, the
Parent
component (as it currently exists in Bevy) is theRelationship
component and the entity containing the Parent is the "source entity". The entity inside theParent(Entity)
component is the "target entity". And that target entity has aChildren
component (which implementsRelationshipSources
).In practice, the Parent/Children relationship looks like this:
The Relationship and RelationshipSources derives automatically implement Component with the relevant configuration (namely, the hooks necessary to keep everything in sync).
The most direct way to add relationships is to spawn entities with relationship components:
There are also convenience APIs for spawning more than one entity with the same relationship:
The existing
with_children
API is now a simpler wrapper overwith_related
. This makes this change largely non-breaking for existing spawn patterns.There are also other relationship APIs, such as
add_related
anddespawn_related
.Automatic recursive despawn via the new on_despawn hook
RelationshipSources
can opt-in to "despawn descendants" behavior, which will despawn all related entities in the relationship hierarchy:This means that
entity.despawn_recursive()
is no longer required. Instead, just useentity.despawn()
and the relevant related entities will also be despawned.To despawn an entity without despawning its parent/child descendants, you should remove the
Children
component first, which will also remove the relatedParent
components:This builds on the on_despawn hook introduced in this PR, which is fired when an entity is despawned (before other hooks).
Relationships are the source of truth
Relationship
is the single source of truth component.RelationshipSources
is merely a reflection of what all theRelationship
components say. By embracing this, we are able to significantly improve the performance of the system as a whole. We can rely on component lifecycles to protect us against duplicates, rather than needing to scan at runtime to ensure entities don't already exist (which results in quadratic runtime). A single source of truth gives us constant-time inserts. This does mean that we cannot directly spawn populatedChildren
components (or directly add or remove entities from those components). I personally think this is a worthwhile tradeoff, both because it makes the performance much better and because it means theres exactly one way to do things (which is a philosophy we try to employ for Bevy APIs).As an aside: treating both sides of the relationship as "equivalent source of truth relations" does enable building simple and flexible many-to-many relationships. But this introduces an inherent need to scan (or hash) to protect against duplicates.
evergreen_relations
has a very nice implementation of the "symmetrical many-to-many" approach. Unfortunately I think the performance issues inherent to that approach make it a poor choice for Bevy's default relationship system.Followup Work
Parent
toChildOf
. I refrained from doing that in this PR to keep the diff reasonable, but I'm personally biased toward this change (and using that naming pattern generally for relationships).ChildOf(Entity)
as a "value component",ChildOf(e1)
andChildOf(e2)
would be considered "different components". This makes the transition between fragmenting and non-fragmenting a single flag, and everything else continues to work as expected.Fixes #3742 (If this PR is merged, I think we should open more targeted followup issues for the work above, with a fresh tracking issue free of the large amount of less-directed historical context)
Fixes #17301
Fixes #12235
Fixes #15299
Fixes #15308
Migration Guide
ChildBuilder
withChildSpawnerCommands
..set_parent(parent_id)
with.insert(Parent(parent_id))
..replace_children()
with.remove::<Children>()
followed by.add_children()
. Note that you'll need to manually despawn any children that are not carried over..despawn_recursive()
with.despawn()
..despawn_descendants()
with.despawn_related::<Children>()
..despawn()
which depend on the children being preserved, you'll need to remove theChildren
component first.