ECS (“Entity Component System”) describes a design approach which promotes code reusability by separating data from behaviour. Data is often stored in cache-friendly ways which benefits performance. An ECS has the following characteristics:

  • It has entities, which are unique identifiers
  • It has components, which are plain datatypes without behaviour
  • Entities can contain zero or more components
  • Entities can change components dynamically
  • It has systems, which are functions matched with entities that have a certain set of components.

One of the most important problem it solves is by keeping all of the data that will be iterated upon regularly packed tightly into memory so that an entire cache line’s worth of data can be loaded at once, and when the next item is iterated upon, its data is already in the cache. This is solved by defining components as Plain Old Data (POD), essentially a simple struct with only the relevant data included.

Components of same type are now contiguous, and we store pointers to them.

an entity is just an index that lets you look up components assigned to that entity. The logic then moves out, and it operates on the components. An entity becomes a loose concept at this point as most systems only operate on subsets of components. This gives us massive opportunity to optimize our data access for the specific problem the system is trying to solve, this is the actual beauty of this architecture

which components an entity “has”

This problem is solved differently on each different ECS libraries like flecs and EnTT

and that’s where this whole Archetypes (Tables) vs Sparse Sets debate comes.

New research paper shows Archetype ECS has better framerate and frame stability than alternative designs.

most tutorial blogs on ECS solves this by using bitset or bitmask for simplicity.

ECS component management:

The core of an ECS is two data-structure problems: (1) how component data is stored, and (2) how systems find the entities that have the components they need. There are four common approaches.

  • Bitset / sparse array (EntityX, David Colson’s tutorial): one array per component indexed directly by entity ID, plus a bitmask per entity marking which components it has. Holes are allowed.
  • Sparse set (EnTT): per component, a packed dense array plus a sparse entity→index array. No holes.
  • Archetype (Flecs, Unity DOTS, Bevy): one table per unique component combination, with components as columns and entities as rows.
  • Indexed / reactive (Entitas, Austin Morlan’s tutorial): any of the storages above, plus a cached entity list per system that’s updated whenever an entity’s components change.

Data operations on different approaches

OperationBitsetSparse setArchetypeIndexed
Add/remove componentO(1), flip bit, nothing movesO(1), push / swap-and-popMove the entity’s whole row to another table (expensive)Storage op + re-check against every system
Has / GetBit test / pool[e]dense[sparse[e]]entity → (table, row) → columnSame as its storage
Iterate A+BScan all entities, test each maskWalk smallest pool, probe the othersWalk matching tables, no per-entity checksWalk precomputed list, lookup per component
MemoryEvery pool sized to max entitiesOnly live components (+ sparse index)Tight, but fragments if many combinationsStorage + one list per system

How systems interact with the storage

  • Matching: bitset and sparse set find matches every frame. Archetype and indexed cache the matches, so iteration is cheaper but add/remove costs more.
  • Access pattern: archetypes give aligned contiguous columns, which are the most cache- and SIMD-friendly. Sparse sets are contiguous only for the lead component and use indirect access for the rest. Bitset arrays are contiguous but full of holes and need a mask check per entity.
  • Structural changes mid-iteration: archetypes must defer them (command queue + sync point). Sparse sets can tolerate them by iterating backwards. Bitset slots never move, but a freed ID can get reused in the same frame. A deferred command buffer is the safe default for all four.
  • Entity handles: use generational IDs (index + version) regardless of approach, so stale handles are detected.
  • Sorting / threading: sparse sets are easy to sort (useful for draw order). Archetypes are hard to sort but easy to split across threads.

My plan

Bitset storage + generational handles + a View<A, B> iterator that hides the storage (same way as Colson’s blog), plus a deferred command buffer for structural changes (add/remove/destroy), a concept borrowed from archetype ECSes like Flecs. Our entity counts are small, so the simplicity is worth it. If profiling shows iteration cost matters later, I can swap the pools for sparse sets behind the same interface.

command buffer psudo code:

CommandBuffer
    cmds: list of actions

    Add(entity, component)
        cmds.push( if IsAlive(entity): attach component )

    Destroy(entity)
        cmds.push( if IsAlive(entity): destroy entity )

    Flush(scene)
        pending = cmds
        cmds = empty          // new commands can be queued while flushing
        for each action in pending (in order)
            run action (adding, removing, destroying components)

Reference:

ECS faq: https://github.com/SanderMertens/ecs-faq#what-is-ecs https://austinmorlan.com/posts/entity_component_system/#what-is-an-ecs

https://austinmorlan.com/posts/entity_component_system/

https://www.david-colson.com/2020/02/09/making-a-simple-ecs.html

https://arxiv.org/html/2606.14919v1