![]() |
Flecs v4.1
A fast entity component system (ECS) for C & C++
|
Entities can be arranged in parent/child hierarchies using the built-in hierarchy implementation. Hierarchies are useful for:
The following example shows how to create a simple hierarchy:
C
C++
C#
Rust
The following sections cover the different features of Flecs hierarchies.
Applications can get the parent and children of an entity using the following APIs:
C
C++
C#
Rust
When a parent is deleted, all of its children will also be deleted in depth-last order (children will be deleted before their parents). An example:
C
C++
C#
Rust
Hierarchies can be traversed depth-first by iterating the children of a parent recursively. For an example, see:
Hierarchies can be traversed breadth-first by using the cascade feature of queries. For an example, see:
Entities are named, which allows them to be looked up in the world by name. Names are namespaced according to the entity hierarchy. For example, an entity named "Cockpit" with a parent called "SpaceShip" will have to be looked up as "SpaceShip.Cockpit" (C) or "SpaceShip::Cockpit" (C++).
For more details, see the names section in the Entities and Components manual.
The OrderedChildren trait can be added to entities to indicate that creation order or a custom order should be preserved.
When this trait is added to a parent, the entity ids returned by the ecs_children / entity::children operations will be in creation or custom order. Children of a parent with the OrderedChildren trait are guaranteed to be returned in a single result.
The trait does not affect the order in which entities are returned by queries.
C
C++
C#
Rust
The stored order can be modified by an application with the ecs_set_child_order / entity::set_child_order operation.
Flecs implements two hierarchy storages that are optimized for different use cases: ChildOf hierarchies and Parent hierarchies. The following sections go over the differences between the two storages, and when to use which one.
ChildOf hierarchies are optimized for large, "unstructured" hierarchies. An example of a large unstructured hierarchy is a scene. A scene root can have many (think thousands of) children that are dynamically created and deleted while the game is running. Which entities exist at any point in time is often not known beforehand, which is why this is an unstructured hierarchy.
Parent hierarchies are optimized for small structured hierarchies. An example of a small structured hierarchy is a prefab. A prefab typically has a small number (think dozens) of children that are created when a prefab is instantiated, and deleted when an instance is deleted. At any point in the game it is usually known which entities exist.
Use ChildOf hierarchies when:
Use Parent hierarchies when:
ChildOf hierarchies are created by adding a (ChildOf, parent) relationship pair to an entity. Parent hierarchies are created by setting a Parent component on an entity. An example:
C
C++
C#
Rust
A single parent can contain children that use both ChildOf and Parent hierarchies. A single entity cannot both have a ChildOf pair and a Parent component. For example:
C
C++
C#
Rust
The behavior of ChildOf hierarchies and Parent hierarchies is mostly the same:
ChildOf terms work with both storages.Position(up) or Position(cascade)).parent.lookup("child_name")).ecs_get_parent / e.parent() can be used to obtain a parent with both storages.ecs_children / e.children() can be used to iterate children with both storages.There are some differences between ChildOf and Parent hierarchies:
Parent hierarchies, the parent will not show up in the entity's type (ecs_get_type() / e.type())Parent hierarchies are built on top of the OrderedChildren feature, which means that children are stored in well defined orderParent hierarchy, instance children will inherit from the prefab children. Instantiating a prefab with a ChildOf hierarchy copies the prefab child components to the instance child.Parent hierarchy, the names of the prefab children are not copied over to the instance children.Or and Not query operators do not yet work with Parent hierarchies.The following two sections describe in more detail how the different hierarchy storages are implemented.
ChildOf hierarchies are implemented as regular "fragmenting" Flecs relationships, where each unique (ChildOf, parent) pair is conceptually similar to a component. As a result the performance of ChildOf hierarchies is predictable, as almost all component operations have O(1) time complexity. Additionally, it also means that querying for components of children for a specific parent is fast, as components can be accessed as contiguous plain arrays. For example:
These benefits are most noticeable if a parent has many children, or when an application only needs to access children of a specific parent. For example, an application may load multiple scenes into memory, where only one scene is rendered and actively updated. With ChildOf hierarchies, children for different parents are stored separately from each other, which means that filtering out entities for a different scene can be a near zero cost operation.
When an application has both many different parents and a need to access children of different parents simultaneously however, ChildOf hierarchies can become expensive. There are several factors contributing to that cost:
These downsides are most noticeable when an application has many parents where each parent only has a small number of children. In the degenerate case, an application ends up with a single child per table, which can happen if each child of a parent has a different set of components.
When deciding whether ChildOf hierarchies are a good fit, consider:
When the answer to one or more is "no", consider using Parent hierarchies.
Parent hierarchies are enabled by setting a Parent component with as value the parent. Unlike ChildOf hierarchies, children for multiple parents can be stored in the same table when using Parent hierarchies.. Children in a Parent hierarchy are stored in a vector<ecs_entity_t> on the component index record for (ChildOf, parent). This is the same vector that is used for the OrderedChildren storage.
When a Parent component is set, a (ParentDepth, depth) pair is automatically added to the child. When inspecting the type of a child, this will show up as:
This means that the child is stored 3 levels deep in the hierarchy. Hierarchy depth is used by queries to efficiently implement the cascade/group_by features for Parent hierarchies. The @ indicates that this is a value pair, as opposed to 3 specifying an entity id. User defined queries can also take advantage of ParentDepth. For example, the following query iterates entities breadth-first:
C
C++
C#
Rust
The main benefit of Parent hierarchies is that they do not significantly change the memory layout of components in the ECS storage. A query that does not interact with the hierarchy will perform the same whether entities are organized in a hierarchy or not. This can be a significant benefit over ChildOf hierarchies, especially if the application contains many small (prefab) hierarchies. Switching to Parent hierarchies can improve performance and reduce memory footprint by over an order of magnitude.
In addition to not fragmenting the storage, Parent hierarchies have additional performance benefits when used in combination with prefabs. This is because:
Parent hierarchies.Parent hierarchies build a "TreeSpawner", which caches the structure and tables of the prefab hierarchy. This is not possible for ChildOf hierarchies, because tables are not the same across instances.In most cases Parent hierarchies will outperform ChildOf hierarchies, but there are a few exceptions:
Parent hierarchy are stored in a vector, which means that deleting a child is an O(n) operation. If a parent has many (think thousands) children,this can be an expensive operation. If this is the case, consider using a ChildOf hierarchy.ChildOf term may perform worse. This is largely due to children of multiple parents being stored in the same table, which means a query needs to do more work for ChildOf terms to filter out matching entities.The O(n) cost of deleting children does not apply when deleting the parent. In that case children are deleted in bulk, which does not require scanning the children vector.
How a query will perform with the new hierarchy storage highly depends on the kind of query. This section provides an overview of the different kinds of queries, and how their performance compares to the ChildOf hierarchy.
Queries such as these that do not interact with the hierarchy will almost always perform better with Parent hierarchies. ChildOf hierarchies result in more tables, which decreases query performance. When using Parent hierarchies it is much more likely that components are densely packed in memory.
Queries in this form will typically perform the same or better for Parent hierarchies. The reason for this is that the query returns the children vector of a parent directly. This means that it never has to iterate multiple tables, which is something that can occur for ChildOf hierarchies.
This query will perform worse for Parent hierarchies. The reason is that this query cannot be evaluated at the table level. Instead the query has to individually check whether each child of my_parent has Position.
This query will perform worse for Parent hierarchies. The reason for this is that this query will first find all tables with either a ChildOf pair or a Parent component, and then check if the resulting table has Position. For tables with the Parent component, the query then has to return each individual entity, because it needs to return the actual pair matched by the wildcard. This is to support code that uses ecs_field_id, it.id() or it.pair():
Both storages will perform better if the terms are reversed. The first term is likely to match significantly more tables than the second term, which increases the cost of uncached queries, and the cost of populating a query cache.
This query will perform worse for Parent hierarchies. This query will first find all tables with Position, and then has to check for each entity in that table whether it is a child of my_parent. If the table contains many entities, this can be a very expensive operation.
If the table only contains a single child for my_parent, the query uses a faster path that does not require scanning each entity, but the query will still perform worse than with ChildOf hierarchies.
This query will perform worse for Parent hierarchies. This query will first find all tables with Position, and then check if the table has the Parent component. If so, the query now knows that each entity in the table matches the query. However, it has to return entities one by one because the matched parent needs to be communicated to the application (see above).
For a more efficient way to do this with Parent hierarchies, see the next query:
This query will perform the same for Parent hierarchies. This query will first find all tables with Position, and then check if the table has the Parent component. If so, the query now knows that each entity in the table matches the query. Because the any (_) wildcard does not require communicating the parent to the application, the entire table can be returned.
To also find the entities' parent, an application can add the flecs::Parent component to the query:
This query will perform worse for Parent queries. This query will first find all tables with Position. If it finds a table with a Parent component (as opposed to one with a ChildOf pair) the query will have to iterate each entity, and traverse the hierarchy upwards for that entity. The result of this cannot be cached, which further hurts performance.
The reason up traversal is supported for Parent hierarchies is mostly to provide an easier migration path from ChildOf hierarchies. Performance critical queries should generally not use up traversal in combination with Parent hierarchies. See below on how to migrate to alternative queries.
This query will perform worse for Parent queries. Though not as bad as the previous query, this query will also perform worse with Parent hierarchies than for ChildOf hierarchies, because the results that use the Parent hierarchy cannot be cached.
As outlined in the previous section, queries that use relationship traversal can be slower for entities with Parent hierarchies. To get around this, an application can split up a query that uses relationship traversal into two queries, one for ChildOf hierarchies, and one for Parent hierarchies. Consider the following query:
This can be split up into two queries like so:
Then in the iteration logic, manually fetch the Position component from the parent. For example:
C
C++
C#
Rust
For queries that use cascade, applications can use group_by(flecs::ParentDepth) instead, which will iterate entities ordered by hierarchy depth. The following code shows how to do breadth-first traversal with a query per storage kind. Note how the implementation iterates each depth individually; this prevents deeper ChildOf levels from being processed before higher-level Parent entities.
C
C++
C#
Rust