Impl: ECS - little-dragons/Siedler GitHub Wiki

Generally, the Lini engine works with the well-known concept of Entity-Component-System.

Entities are mostly syntactical elements with little semantic meaning. They are simply a collection of components and have a parent and children. Components are just data aggregations. Each component may be instantiated multiple times and each instantiation is attached to a specific entity. Every entity may only have one component of a given type. Systems operate on entities and components and can make changes to any data. Each system specifies on which components and entities it operates.

Components

Components simply store data and are attached to an entity. Their memory layout is most important as systems often iterate over a specific set of components which needs to happen fast.

Components are blittable types.

Entities

Entities provide a logical grouping of entities through parent-child relationships and component via grouping. Iteration and queries are performed on entities using their attached components.

We need thus a memory design where components are stored adjacent to each other with the order being determined by iteration order of the entities.

Archetypes

For that, each entity is stored per its archetype where the archetype is the combination of all components it has. A system operates on some select archetypes (for example all archetypes containing the components Test and OtherTest). Each archetype stores the components its entities have by using a bitfield.

Systems

Systems are probably the most complex part of the ECS as they have a wide range of what they can do but also a large number of other requirements:

For example, a system should be able to set the active camera, change component data, change the transform of an entity, add and remove entities, and so on. All of this should be done quickly and in parallel. To enable this, a lot of information has to be passed around: For example, to add children to an entity, the entity itself always has to be a parameter to that system. As functions parameters typically live on the stack and are set for each call, this can have a negative performance impact, especially if a system does not need this capability.

It is thus clear that a system should generally have exactly that information it requires: Having more information will always have negative performance. For that, a system needs to define very clearly which information it requires.

This also is important for multi-threading: The scheduler needs to know very precisely which systems can run in parallel.

Some operations have to happen independently, like resource loading: this will be implemented internally in the engine, not in a system like this.

A system can work with:

  • component data of entities
    • by specifying every component which is required
    • and also specifying for each component if it is only read, written, or both
    • this can be done for multiple entities to achieve a join so to say, but this should be avoided as this is always a $O(n^2)$ operation
  • properties of entities
    • manipulating their children
    • deleting entities
    • changing their components
  • layer instance data
    • change the active camera
  • general update data
    • delta time
  • user input
    • read keyboard and mouse input
    • mark those events as handled such that other systems cannot handle it again

Scheduling

We can safely say that operations can be modeled as a dependency tree or dependency graph. The graph stores each system and a system can run if all requirements have successfully finished. The graph is traversed once per frame.

It makes sense to handle input at the beginning of each frame such that other systems can already work with the newly modified data.

To determine an order of systems, it makes sense to use Source Generation: A client uses attributes to set a has-to-happen-before and has-to-happen-after relationship with other systems. This should be handled by a Code Analyzer.

Mutating systems

Mutating components is probably fine to do in a system when it is run mutually exclusive with other systems (for that written component). Mutating entities however is much more tricky, since they can interfer with all other operations. It might make sense to buffer entity modification as commands which are executed at a later point in time. Since this would be a hefty operation because all parallelization had to be disabled for this, this should be restricted and disabled by default.