ECS Design Crossroads

Thomas Schaller edited this page May 27, 2017 · 6 revisions

Target requirements

  • Typical systems should access the data efficiently in a hardware-friendly manner.
  • Non-conflicting systems should be able to work independently in parallel.
  • Component storage should be abstract for each component, allowing some of them to use vectors, trees, hashmaps, or something else entirely.

Decision points

Component updates

  1. Changesets - latency: N cycles are needed for N-length dependency chain - storage: how to efficiently store all component writes - flush: need sync points to apply changes - priority: need to resolve merge conflicts
  2. In-place modification - parallel: need to carefully schedule systems in order to avoid conflicts - probably requires unsafe code in the implementation
  3. World copy - speed: can be expensive to copy - latency
  4. Message passing - speed/latency: messages are not instant - storage: actors require encapsulation, which is not cache-friendly

Partial mixes are possible, like: in-place for modification, and changesets for adding new entities or removing existing ones. Concerns:

  • Parallel processing
  • Latency
  • Speed/performance

Components storage

  1. Vector of components - cache-friendly - allows quick direct access by the recycling part of an entity's ID - has a size of a maximum amount of entities alive during the application's run
  2. HashMap - cache-unfriendly for linear iteration - constant access time, but slower than vector - more compact than vector

These are not mutually exclusive, it is preferable to use different ways of storing components for different types (e.g. Vector for more common ones, such as Position and RenderData, and HashMap for more rarely used components).

Entity representation

  1. Pure ID = u64, incremented for each new entity - no linear access possible
  2. Generational ID = (u32, u32) - must be careful to disallow using the wrong generation accidentally - may need special handling of the generation overflow
  3. Collection of component IDs. Sub-variants: 1. Optional indices = (Option<u64>, Option<u64>, ...)
    • dependends on the total number of components
    • rather inefficient, considering that Option doubles the size of each index
    • allows directly exposing these indices to the user, which can be nice 2. Offset indices = (u64, u64, ...), considering that the next entitie's indices are not less than the previous one
    • depends on the total number of components
    • requires careful arrangement of entities, puts constraints onto how you can delete stuff
    • allows multiple same-typed components per entity natively 3. Anymap = Map<TypeId, u64>
    • nicely handles optional components, doesn't depend on the total known set of them
    • non-copy, making it a little inconvenient
    • slower on access
  4. Collection of components. Not really viable, given here for completeness. Sub-variants: 1. Composition = (Comp1, Comp2, ...)
    • non-copy
    • slower linear access
    • depends on the total number of components 2. Boxed = (Box<Comp1>, Box<Comp2>, ...)
    • non-copy, non-clone, only movable
    • no linear access 3. Shared = (Arc<RwLock<Comp1>>, Arc<RwLock<Comp2>>, ...)
    • non-copy
    • no linear access
    • must be careful with mutation: either unsafe, or much boilerplate with slow access

Concerns:

  • Single component access speed
  • Cache friendly batch processing
  • Size and copy-ability
You can’t perform that action at this time.
You signed in with another tab or window. Reload to refresh your session. You signed out in another tab or window. Reload to refresh your session.
Press h to open a hovercard with more details.