4 Introduction
Dawid Masiukiewicz edited this page 2021-03-07 17:13:26 +00:00

Core

This page will introduce to you main idea behind Bubel ECS (BECS).

Bubel ECS (as name stands) is a library which implements Entity-component-system architectural pattern. This page will be about library itself, so if you don't know what is ECS pattern you can find information here Wikipedia.

The core idea behind BECS technology is the way how it is managing memory and systems. Entities are grouped by types (entity Type). Each entity type consists of components. Component duplicates are not allowed which means that e.g. you can't have two CVelocity components (assuming that "CVelocity" is component name). Entities are allocated in memory chunks, each entity type has list of chunks with his entities. Each block has following memory layout:

Meta data EntityIDs Components 1 Components 2 ...

This layout provides fast memory access thanks to good cache locality.

Library provides a possibility to add/remove entity components. That operation means that entity changes its entity type and memory needs to be copied to different location (into chunk of new entity type). This operation is quick enough to be used for infrequent logic changes.

Systems execution is managed by library itself. Developer gives the BECS information about system priority, components and dependencies. Systems are grouped into passes. The purpose for passes is to give possibility to run different systems with different frequency (e.g. default pass run once per frame, physics pass called multiple times per frame (fixed time step) and then rendering pass). Passes call all systems assigned to them in order designated by priorities. This type of systems execution model gives library a lot information which helps to improve performance.

Systems have access to components instead of entities directly. They iterate over selected components for all existing entities. Entities are filtered by contained components. There are several properties for components in system:

  • Required - it's default attribute for component. System don't iterate over entities which doesn't have these components.
  • Optional - it's not required but system can use that components data too
  • Excluded - system will not process entities which have these components Besides that attributes you can use filterEntity callback to filter entities as you like (filtering is done ahead of time in system registration process).

Next important thing is how entity is referenced. System work on raw components data, but sometimes you need to get entity data outside of system or from different entity that is executed right now. In this kind of situation you can use EntityID. This ID consists of two values: index and counter. First value is and index into linear array of pointer to entities. Counter is incrementing every time when entity is destroyed, it's used to determine if index is still valid. This gives you possibility to safely check if entity behind EntityID still exists. Using different methods like simple pointer to memory is disallowed as position in memory is valid only during single pass. Entity position in memory is position of Entity which contain only EntityID. Because memory chunks are aligned to some value, library is able to calculate meta data of block from pointer to entity and then pointer to each component. Using direct access by EntityID is considered to be fast, but a lot slower than access from system.

Adding new entities requires using templates. Template is assigned to entity type. You can use single template for adding entities on multiple threads.

Multithreading

BECS has system to automatically generate jobs for multithreaded execution. Task of developer is to tag components inside systems as readonly or writable and setting proper priorities for systems. Library will automatically generate jobs and dependencies in lockless fashion. There is also possibility to specify external dependencies (readonly and writable) which can be used to synchronize non-ECS memory access (e.g. octree, renderer).

Adding new entities is thread-safe which means you can add entities from multiple threads at the same time. Adding entities and assigning their IDs is managed using atomic operations so it is fast (but don't over use that, if you want only to spawn hundreds of entities in one system without any logic behind that, it can be slower that singlethreaded code). Entities can be accessed immediately after being added.

Removing entities and adding/removing components from entities operations are deferred. If you remove entity component it would be removed after pass being done.

Event handling

Entity-component-system pattern is very good when entities are processed without accessing memory of different entities, but in real case you want entities to interact together. In single-threaded mode you can safely use direct access through EntityID but in multithreading it's not the case if you need data coherency in access. Event system fixes not only problem of multithreading interaction but also gives more universal way of modeling game logic. In Bubel ECS systems don't have to know anything more than components they are processing. That means that you can easily separate systems from each others. It makes code cleaner and building plugins simpler.

Events are types and can be handled by systems. Event consists of EntityID and custom data. System doesn't have to know who spawned event, it only knows about components and events which it's processing.