Wednesday, April 15, 2020

TECS - Tight Entity Component System

Hi readers!

 

I have just finished my Master's so I had a little extra time to work on side projects...

 

It's been a while since my last post regarding BitEngine. The framework is still in development, but I found a few things that I wanted to improve before resuming development. That is a new Entity Component System.

 

Today I present Tight Entity Component System (TECS), a small header-only library that manages entity and components in a memory-efficient way. This development of this library was motivated by the new design decisions made for BitEngine, which includes decoupling more from the standard OOP approach and take a more data-oriented design. 

 

Before implementing TECS I studied how other implementations work, especially EnTT and EntityX and read many articles about the subject around the internet. There are many libraries out there, however, many seem no be now dead or they use resources in a different way from my current intentions. Also, some libraries have coupled the entity and component management with systems, which I also don't like. EnTT is an awesome library, but it has the same memory allocation problem, although there was even a discussion about the subject (here), I don't think it was implemented yet. I could probably just used one existing library and find workarounds for it to work in my case, or even modify the libraries so they would work with arena allocations. However, by doing that I would be missing part of the fun on implementing such an interesting system!

 

The library requires the user to provide a pointer of memory and size and specifies a few other parameters such as entity and component limits and a TypeProvider to enumerate the component types. The library has a type-safe interface, however, it also aims to expose an API to be used without types. Making it capable of dynamic run-time component manipulation without known types.

 

Some interesting features:

    • All allocations are constrained to a tight memory arena. Want to clear everything? just drop the arena and initiate the ECS again. This also helps in keeping your ECS working across boundaries.
    • Component references are guaranteed to be valid, independently if you add or remove more entities. Of course, if the entity or the component is removed, that reference no longer makes sense (you can still write data to it, but it might affect other entities) or components.
    • Components are tightly packed in memory (as much as possible) in order to be cache-friendly when iterating over them.
    • Compile-time type-safe API. Run-time types are planned to be supported as well.

     

    The library is currently functional and I have a few test cases to ensure it's behaviour. My initial goal was to make it Arena Allocator friendly and later performance efficient. Hopefully, it is already fast enough for relevant cases but I still need to perform some actual benchmarks to assess that.

     

    The library is very simple to use, first, we need to include the library and define some components:


     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    #include <iostream>
    
    #include <tecs/tecs.h>
    
    struct Position {
        float x;
        float y;
    };
    
    struct Velocity {
        float x;
        float y;
    };
    
    CREATE_COMPONENT_TYPES(TypeProvider);
    REGISTER_COMPONENT_TYPE(TypeProvider, Position, 1);
    REGISTER_COMPONENT_TYPE(TypeProvider, Velocity, 2);
    

     

    Then we just need to allocate some memory and start using the ECS:


     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    int main()
    {
        // Feel free to allocate/manage the memory anyway you want.
        constexpr u32 MEMORY_SIZE = 1024 * 1024 * 8;
        char* memory = new char[MEMORY_SIZE]; // Allocate 8MB
    
        const u32 maxEntities = 1000;
    
        tecs::Ecs<TypeProvider, 8> ecs(tecs::ArenaAllocator(memory, MEMORY_SIZE), maxEntities);
    
        auto e = ecs.newEntity();
        ecs.addComponent<Position>(e) = {1, 1};
        ecs.addComponent<Velocity>(e) = {1, 1};
    
        e = ecs.newEntity();
        ecs.addComponent<Position>(e) = {1, 1};
        ecs.addComponent<Velocity>(e) = {2, 2};
    
        ecs.forEach<Position, Velocity>([](tecs::EntityHandle e, Position& pos, Velocity& vel) {
            pos.x += vel.x;
            pos.y += vel.y;
        });
    
        // Show entity positions:
        ecs.forEach<Position>([](tecs::EntityHandle e, Position& pos) {
            std::cout << "Entity: " << e << ", Position: " << pos.x << ", " << pos.y
                      << std::endl;
        });
    
        delete[] memory;
    }
    

     

    The next step is to replace the current Entity System used in BitEngine by TECS.

     

    Library on GitHub: MateusMP/TightECS