Repository navigation
Description
What problem does this solve or what need does it fill?
Several common entity hierarchy based systems have started to crop up in multiple upcoming and current focus areas.
- Current:
Transformpropagation is reliant on a depth-first traversal of theTransformhierarchy. - Current: Make
Visibilityaffect children entities #3874 -ComputedVisbilityshould be inherited from parents/ancestors. - Upcoming: Animation bone binding maintenance require querying a path of named descendants. For example,
root/hips/chest/shoulder_Lis a top-down hierarchy query based on theNamecomponents of each entity.
There are also several existing cases where the current Transform based hierarchy is either buggy or unwieldy to manage the need to wait for the consistency of the system to settle.
GlobalTransformnot being updated #3329- Ensure
GlobalTransformconsistency whenParentis spawned withoutChildren#3340 - Child's Transform component ignored when parent does not have the GlobalTransform/Transform components #2277
Other issues brought up about the hierarchy:
What solution would you like?
This will require additional design but it'd be best to have a hierarchy structure that is:
- Agnostic to the ECS data that is being stored at each Entity, but enables querying for ECS components as well as it's position within a hierarchy.
- Performant to traverse: primarily in both a top-down depth first but also linear manner.
- Minimize deferred/time based delays between hierarchy updates and the results appearing in the final output.
- Constant time updates to local changes in the hierarchy itself, primarily these operations:
- Parenting an Entity to a child
- Unparenting an Entity to a child.
- Re-parenting an Entity between two different parents.
- These should not require a secondary maintenance system to keep it correct.
- Easy to detect, deduplicate, and propagate dirtying. Think transitive change detection at the hierarchy level.
- Optionally: dedicated hierarchy-optimized ECS storage. This may be a huge stretch goal, but it would speed up any hierarchy traversals by cutting out some of the cache misses.
What alternative(s) have you considered?
Using the current Parent and Children components as is.
Possible Ideas
One of the potential options that doesn't rock the boat that hard is using a tree-based linked list approach.
#[derive(Component)]
pub struct HierarchicalRelations {
first_child: Option<Entity>,
prev_sibling: Option<Entity>,
next_sibling: Option<Entity>,
parent: Option<Entity>,
}This has a few benefits/drawbacks over the current Parent/Children component approach:
- This component exposes an interface that would encapsulate both
ParentandChildrenas well as let users query for the siblings
of a givenEntitywithout a reference to the parent. - This creates a tree/forest of linked entities where each set of children forms a doubly-linked list. This can be used to traverse the tree in many different ways.
- This could be split into 4 different components if we need the cache locality, though the random access may negate any cache based perf gains seen there.
- Custom iterators and SystemParams can be built on top of queries for this component(s) to more easily query for ECS data based on the hierarchy (i.e.
HierarchyQuery<T>::iter_all_in_descendantsorHierarchyQuery<T>::iter_all_in_ancestors) - Updating these pointers just requires
Query<&mut HierarchicalRelations>.Query::get_multipleor something similar can be used to update multiple relations at the same time to "atomically" update each of them. Removes the reliance on a secondary system to keepChildrenup to date. - Ignoring change detection, this would actually have a smaller memory footprint as both
ChildrenandParent, assuming we haveOption<Entity>niched.SmallVec<[Entity; 8]>is 64 bytes in ECS storage, and more if it overflows, andParentis 8 bytes. This single component is only 32 bytes. - The main downside to this is that counting the number of children of an entity is O(n), though this could be fixed via caching.
- This loses cache locality of child references, becoming a linked list with random lookups. However, since both this component and the current
Childrensolution are both more or less useless without a secondaryQuery::getto fetch corresponding components, this might not be a big loss. - This loses the ergonomics of using a for loop over the children of a given Entity. Could be handled with a custom iterator though.
This is an approach seen in bitsquid http://bitsquid.blogspot.com/2014/10/building-data-oriented-entity-system.html, where dedicated storage for the hierarchy was also used.
Additional context
- This likely requires a lot of design and RFC around this if we decide to go down this route.
- Is it possible to handle this with ECS relations?
Activity
alice-i-cecile commentedon Mar 7, 2022
Related discussion of current limitations, with a focus on bugs and usability: #2572.
This is effectively a more-scoped version of #3742.
alice-i-cecile commentedon Mar 7, 2022
Yes: this is one of the core use cases. However, as you see, there's a huge level of design and benchmarking required. This may be a good starting point to allow us to build-out a generalizable solution.
IMO this is required.
alice-i-cecile commentedon Mar 7, 2022
Alternative proposal: Hierarchy as a resource
Rather than storing
ParentandChildcomponents, simply store aHierarchyresource.Benefits
Drawbacks
Limitations
Transform) will still require fragmentedquery.getcalljames7132 commentedon Mar 7, 2022
One downsidde to this is that this blocks any concurrent editing of the hierarchy, at least not without a
RwLockof some kind. Though that might be desirable for determinism purposes, and arguablyQuery<&mut HierarhicalRelations>when usingQuery::get_multipleis in a similar boat.However, mutation, beyond the local scope is probably not something we should be optimizing for here, since the heaviest operations involve traversing and reading the hierarchy. It's probably best to think of the ways we want to be querying this structure what other non-mutative operations we want on it.
Namecomponents), to find one or more descendants of a ancestor.Some additional observations:
Query::getcalls are unavoidable. Even then the question of hierarchy maintenance requires quite a bit of swapping and shifting of storage, so the gains here might be quickly negated outside of simple toy examples.alice-i-cecile commentedon Mar 7, 2022
I will note that this is currently the case with commands, but this is a limitation.
Yep: however by storing e.g. a
Transformin this hierarchical storage we're losing performance on linear iteration instead (which is also quite common), unless we're duplicating data across both storages.This is correct for some but not all of the potential designs :) The index-backed solutions can be custom-optimized for these situations.
cart commentedon Mar 7, 2022
@alice-i-cecile another big drawback to "hierarchy as a resource" is that it is not particularly scene friendly. It assumes (1) that we support loading resources from scenes and (2) that each time we load a scene's instance of the hierarchy resource, we "merge" that with the global version.
I do think a Hierarchy / left+right pointer based approach is worth considering (note that I read the bitsquid article linked above way back when I was first deciding how hierarchy would work in Bevy). The ability to update the hierarchy "transactionally" is really nice (although this could be accomplished with split components, with the right scoping). I also really like the O(1) entity deparenting.
The main reasons I opted for split components vs "Hierarchy left+right":
Query<Entity, Without<Parent>>), things with parents (With<Parent>), and things with children (With<Children>). Doing a full scan of all Hierarchy components every time a root is needed (ex: propagating transforms, updating UI layouts, etc) isn't ideal.Changed<Children>to detect when a child has been added / removed from a specific entity.However during this early decision making process, I very clearly undervalued "always consistent" hierarchies. Given how central hierarchy can be to game logic, being able to trust it at a given point in time (and not just after the Hierarchy sync) is pretty important. The "consistency problem" must be solved, but I do think we lose a lot of valuable functionality by moving to the "Hierarchy left/right/parent" approach.
james7132 commentedon Mar 7, 2022
As suggested by the bitsquid article, which has been referenced several times in this thread already, you can keep what is effectively a
Vec<Transform>around. Iteration is still 100% linear, and better yet, it enforces that parents are iterated before their children. The main issue here is that such a storage aggressively swaps the data stored within it upon dirtying any of the components held within: could be a very write-heavy solution for large pieces of data and should generally be used in cases only when mutations are a very small subset of the available entities.cart commentedon Mar 7, 2022
Also for "hierarchy as a resource", it creates a "garbage collection" problem. Every time an entity is despawned, we now need to check if it exists in the hierarchy. You can't know ahead of time, so you need to check every despawned entity.
james7132 commentedon Mar 7, 2022
Is this something we can't deal with using either Resources that wrap HashSet (i.e.
Root(HashSet<Entity>)andLeaves(HashSet<Entity>)) or marker ZSTs? Both of which can be enforced in O(1) time and can have linear iteration time. For cases where we load/unload large masses of entities, we can have a one-shot system patch up these resources afterwards to enforce correctness.Wouldn't a inverted filter work here too (i.e.
Changed<Parent>whereParent(Option<Entity>)and a corresponding query for the parent's components) also suffice? This is less ergonomic, but IMO this is resolvable with a custom SystemParam.Can you explain this more? Read-only queries should be easy enough to work with without clashes.
Overall, I don't think the underlying representation here is something we should encourage users to directly query, much like ECS itself, but rather provide solid wrappers and abstractions (i.e. custom SystemParams, dedicated hierarchical query types) that provide the ergonomics needed.
8 remaining items
james7132 commentedon Mar 8, 2022
Arguably, the same parenting structure for transforms should be used for the others, since it's the logical expectation here. Not too keen on the idea of multiple overlapping and conflicting hierarchies, particularly when it comes to showing it to users in an editor. We could split it out so that the hierarchy exists separately from a "f32-based Transform system operating in Euclidean space" that bevy_transform currently provides. It would allow others to implement their own transform systems like a
f64backed one, a fixed point transform system, a UI-focused rect-based transform system, or internal efforts for a dedicated 2D one too, but also leverage the parent-sibling-child relationships that seem endemic to all of them.maniwani commentedon Mar 8, 2022
Honestly, I would parameterize the types "just for the heck of it" since it would help clarify how the components are used. That way, coupling other stuff to the transform hierarchy becomes an intentional choice, instead of users just accidentally using the same components not knowing they're for transforms (bike-shedding, but renaming the types to something like
IsParentTransformandIsChildTransformis also an option).And there are clearly hierarchies people want to express that have nothing to do with transforms.
alice-i-cecile commentedon Mar 8, 2022
This was @cart's take: we should try to have a single hierarchy that we use for everything (if feasible).
I'm not sure I personally agree, but I'm willing to explore the direction the design takes. However, I think it's critical to support the inheritance of other properties (hopefully even user-defined properties), and to be able to control what is and isn't inherited (translation? scale? visibility?) at an entity-level.
james7132 commentedon Mar 8, 2022
I think I would rather just provide the tools for managing, traversing, and querying the hierarchy. Operations like dirtying, propagation, and inheritance may be too use case specific to make generic systems for.
maniwani commentedon Mar 8, 2022
Getting mixed signals here. Is this issue about hierarchies in general or specifically the transform hierarchy (since you mentioned animation, visibility, and working in an editor)?
A tree of linked nodes would be useful in general (in the same way ECS relations would be), but I see no reason to discuss splitting
bevy_transformhere if we're only going to talk about the transform hierarchy and properties that have to inherit from it.Anyway, I was just saying that people misuse
ParentandChildrenfor non-transform-related things, so we should do something to clarify (edit: when) components are coupled to the transform hierarchy.alice-i-cecile commentedon Mar 8, 2022
Agreed: we should either do this, or allow people to opt-out of this coupling.
cart commentedon Mar 8, 2022
Not fully caught up yet: but I'd like to first state that hierarchy is already decoupled from Transforms. Transforms just choose to use it for transform propagation. The only real coupling is "the crate these things live in". Opting out does seem like a reasonable feature request, but I'd want real motivating use cases before adding that complexity.
I also agree that how Transforms relate to the "generic hierarchy" should be a separate conversation.
alice-i-cecile commentedon Mar 9, 2022
@cart are you okay with me redoing #2789 and splitting the crates apart? Regardless of the rest of the details, I think this is a necessary split and will seriously reduce user and developer confusion about the intended design.
james7132 commentedon Mar 9, 2022
Whipped up a summary of the discussions here as an RFC: bevyengine/rfcs/pull/53. Please take a look!
cart commentedon Mar 10, 2022
Yeah I'm cool with this. Thanks for checking.
Split bevy_hierarchy out from bevy_transform (#4168)
Split bevy_hierarchy out from bevy_transform (bevyengine#4168)
Shatur commentedon Sep 17, 2022
What I found inconvenient about the current system is that when I store a scene, I have to store
ParentandChildrenin order to restore the hierarchy later. And the same for replication over the network.Split bevy_hierarchy out from bevy_transform (bevyengine#4168)
Split bevy_hierarchy out from bevy_transform (#4168)