-
A truly component based object is nothing more than the sum of its parts. This means the definition of a component based object is also nothing more than an inventory with some construction arguments. This object or definition agnostic approach makes refactoring and redesigning a much simpler exercise.
-
When you introduce component based entities, you have an opportunity to turn the idea of how you define an object on its head. The normal approach to defining an object in object-oriented design is to name it, then fill out the details as and when they become necessary. For example, your car object is defined as a Car, if not extending Vehicle, then at least including some data about what physics and meshes are needed, with construction arguments for wheels and body shell model assets etc, possibly changing class dependent on whether it's an AI or player car. In component-oriented design, objects aren't so rigidly defined, and don't so much become defined after they are named, as much as a definition is selected or compiled, and then tagged with a name if necessary. For example, instancing a physics component with four-wheel physics, instancing a renderable for each part (wheels, shell, suspension) adding an AI or player component to control the inputs for the physics component, all adds up to something which we can tag as a Car, or leave as is and it becomes something implicit rather than explicit and immutable.
Storage Models
-
Problem :
-
-
In this case, it's used an
union, so the data can be either a value or nil. -
There are a lot of empty spots inside those vectors, causing memory bloat; the data is not tightly packed.
-
-
Frameworks :
-
"Entt uses a sparse set ECS, Flecs uses archetypes".
-
| Strategy | Iteration | Removal | Complexity |
| -------------- | --------- | ------- | ---------- |
| Sparse Set | Fast | O(1) | Medium |
| Archetype | Fastest | Medium | High |
Sparse Set
-
Sparse-sets organize data by component type (one dense array per component + a sparse entityโindex table).
-
Make structural changes cheap and queries flexible.
-
Manage components individually, using a dense array for each component type and a sparse array for existence checks. It's generally better in scenarios where components are added or removed frequently because you don't need to move all components an entity has, but iteration is more expensive as it requires checks to ensure each entity has the requested components.
-
Splits one list into a Sparse List and a Dense List.
-
We don't care about the cache friendliness of the Sparse List, only for the Dense List.
-
Dense List:
-
Contains the actual component data.
-
-
Sparse List:
-
It's what maps its Entity ID to its corresponding Dense List index.
-
-
.
-
.
-
-
It's also possible to use the Sparse List as a HashMap:
-
.
-
This has trade-offs in performance, depending on the amount of entities there are stored.
-
Cient video : "For what I've seem, the HashMap is usually faster for fewer entities (100), but a sparse list with Pagination is faster with a higher entity count (100k)".
-
Component :: struct($T: typeid) {
sparse: [COMPONENT_MAX_ENTITIES]Maybe(u32), // Maybe(dense_idx) = sparse[ent]
dense: [dynamic]Entity_ID, // ent = dense[dense_idx]
data: [dynamic]T, // T = data[dense_idx]
}
-
Get :
-
O(1). -
.
-
-
Add :
-
O(1). -
.
-
-
Remove :
-
O(1). -
.
-
Swap the element we want to delete in the Dense List, with the last element of the Dense List.
-
Update the Sparse List, so the swapped elements from the Dense List point to the correct elements.
-
The deleted element from the Dense List should be set to a nil value, so it doesn't point to anything.
-
Pop the Dense List.
-
-
For very large entity numbers :
-
Pagination :
-
The Sparse Set can be divided into a "array of arrays".
-
.
-
.
-
"Allocate pages only when needed".
-
-
Entity ID Recycling :
-
Generation counter.
-
-
-
Bitmask :
-
Only answers โis the component present?โ; it does not give you the
dense_idxyou need to locate the componentTin your data array.
-
-
Speeding multi-component queries :
-
Archetypes is a way to get the speed up, at its costs.
-
Iterate the best order.
-
Always drive the query with the smallest component set (sparse-set dense array with fewest entries).
-
Complexity: O(min_set_size) + O(1) membership checks for each candidate.
-
Cheap, no extra memory, often good enough if one component is small.
-
-
Etc, do it later.
-
Archetypes
-
An archetype is a grouping of entities that all share the exact same set of component types. Each archetype stores component data in contiguous, component-oriented arrays so that iteration over entities that have a particular set of components is very fast.
-
Maximize per-system data locality and remove membership checks at the cost of entity migration bookkeeping.
-
Archetypes seems like a specialization, just like oop is a specialization, so it comes at a cost of less flexibility.
-
Group entities with identical sets of components together in memory, allowing for contiguous memory storage and fast access patterns during iteration.
-
You don't need to check whether a given entity has certain components on each iteration.
-
Adding or removing components is generally more expensive than with sparse sets, as you need to transfer all components for an entity from one pool to another.
-
A chosen set of components that an entity of a certain type will have
-
.
-
Each archetype has one contiguous array per component type. Rows are entities.
Entities: Pos.x Pos.y | Vel.x Vel.y | Health.hp
Entity 42 [ 1.2 ] [ 3.4 ]| [ 0.1 ] [ 0.0 ]| [ 100 ]
Entity 73 [ 5.6 ] [ 2.2 ]| [ -1. ] [ 0.3 ]| [ 80 ]
Entity 11 [ ... ] [ ... ]| [ ... ] [ ... ]| [ 120 ]
-
.
-
.
-
Archetypes are created lazily, else we run into a combinatorial of creating new tables for each theoretical component combination, even tho that combination might not be useful as an entity type in the world.
-
Because every entity in a table must have every component, there can't be any gaps.
-
Problem :
-
If we want to loop over everything with a position and velocity components, we would have to loop over 2 different tables.
-
Solution:
-
.
-
Take the intersection of these arrays, giving us the final set of archetypes, where each archetype contains all of the requested components.
-
-
-
Add/Remove :
-
Move entity from one archetype to another (copy component values to new archetype arrays, remove from old archetype).
-
-
In Unity :
-
It's not a container for anything. It doesn't contain the entity or any data associated with them. It's just an unique identifier.
-
"Unique combination of components types for an entity".
-
We can have multiple archetypes for an entity query.
-