Fără Descriere

Michele Caini b0b8ee7aea init 9 ani în urmă
build b0b8ee7aea init 9 ani în urmă
cmake b0b8ee7aea init 9 ani în urmă
deps b0b8ee7aea init 9 ani în urmă
src b0b8ee7aea init 9 ani în urmă
test b0b8ee7aea init 9 ani în urmă
.gitignore b0b8ee7aea init 9 ani în urmă
.gitmodules b0b8ee7aea init 9 ani în urmă
AUTHORS b0b8ee7aea init 9 ani în urmă
CMakeLists.txt b0b8ee7aea init 9 ani în urmă
LICENSE b0b8ee7aea init 9 ani în urmă
README.md b0b8ee7aea init 9 ani în urmă
deps.sh b0b8ee7aea init 9 ani în urmă

README.md

EnTT - entity-component system in modern C++

Introduction

EnTT is a header-only, tiny and easy to use entity-component system in modern C++.
ECS is an architectural pattern used mostly in game development. For further details:

Code Example

#include <iostream>
#include <registry.hpp>

struct Position {
    float x;
    float y;
};

struct Velocity {
    float dx;
    float dy;
};

using ECS = entt::DefaultRegistry<Position, Velocity>;

int main() {
    ECS ecs;

    for(auto i = 0; i < 10; ++i) {
        auto entity = ecs.create();
        ecs.assign<Position>(entity, i * 1.f, i * 1.f);
        if(i % 2 == 0) { ecs.assign<Velocity>(entity, i * .1f, i * .1f); }
    }

    std::cout << "single component view" << std::endl;

    for(auto entity: ecs.view<Position>()) {
        auto &position = ecs.get<Position>(entity);
        std::cout << position.x << "," << position.y << std::endl;
    }

    std::cout << "multi component view" << std::endl;

    for(auto entity: ecs.view<Position, Velocity>()) {
        auto &position = ecs.get<Position>(entity);
        auto &velocity = ecs.get<Velocity>(entity);
        std::cout << position.x << "," << position.y << " - " << velocity.dx << "," << velocity.dy << std::endl;
        if(entity % 4) { ecs.remove<Velocity>(entity); }
        else { ecs.destroy(entity); }
    }

    std::cout << "single component view" << std::endl;

    for(auto entity: ecs.view<Position>()) {
        auto &position = ecs.get<Position>(entity);
        std::cout << position.x << "," << position.y << std::endl;
    }

    std::cout << "multi component view" << std::endl;

    for(auto entity: ecs.view<Position, Velocity>()) {
        auto &position = ecs.get<Position>(entity);
        auto &velocity = ecs.get<Velocity>(entity);
        std::cout << position.x << "," << position.y << " - " << velocity.dx << "," << velocity.dy << std::endl;
        if(entity % 4) { ecs.remove<Velocity>(entity); }
        else { ecs.destroy(entity); }
    }

    ecs.reset();
}

Motivation

I started using another well known Entity Component System named entityx.
While I was playing with it, I found that I didn't like that much the way it manages its memory. Moreover, I was pretty sure that one could achieve better performance with a slightly modified pool under the hood.
That's also the reason for which the interface is quite similar to the one of entityx, so that EnTT can be used as a drop-in replacement for it with a minimal effort.

Performance

As it stands right now, EnTT is just fast enough for my requirements if compared to my first choice (that was already amazingly fast):

Benchmark EntityX (master) EntityX (experimental/compile_time) EnTT
Creating 10M entities 0.281481 0.213988s 0.00542235s
Destroying 10M entities 0.166156 0.0673857s 0.0582367s
Iterating over 10M entities, unpacking one component 0.047039 0.0297941s 9.3e-08s
Iterating over 10M entities, unpacking two components 0.0701693 0.0412988 0.0206747s

See benchmark.cpp for further details.
Of course, I'll try to get out of it more features and better performance anyway in the future, mainly for fun.
If you want to contribute and have any suggestion, feel free to make a PR or open an issue to discuss them.

Build Instructions

Requirements

To be able to use EnTT, users must provide a full-featured compiler that supports at least C++14.
CMake version 3.4 or later is mandatory to compile the tests, you don't have to install it otherwise.

Library

EnTT is a header-only library.
This means that including the registry.hpp header is enough to use it.
It's a matter of adding the following line at the top of a file:

#include <registry.hpp>

Then pass the proper -I argument to the compiler to add the src directory to the include paths.

Documentation

API Reference

EnTT contains three main actors: the registry, the view and the pool.
Unless you have specific requirements of memory management, the default registry (that used the pool provided with EnTT) should be good enough for any use. Customization is an option anyway, so that you can use your own pool as long as it offers the expected interface.

The Registry

There are two options to instantiate your own registry:

  • By using the default one:

    auto registry = entt::DefaultRegistry<Components...>{args...};
    

That is, you must provide the whole list of components to be registered with the default registry.

  • By using your own pool:

    auto registry = entt::Registry<YourOwnPool<Components...>{args...};
    

Note that the registry expects a class template where the template parameters are the components to be managed.

In both cases, args... parameters are forwarded to the underlying pool during the construction.
There are no requirements for the components but to be moveable, therefore POD types are just fine.

Once you have created a registry, the followings are the exposed member functions:

  • size: returns the number of entities still alive.
  • capacity: returns the maximum number of entities created till now.
  • empty: returns true if all the entities have been destroyed, false otherwise.
  • create: creates a new entity and returns it, no components assigned.
  • create<Components...>: creates a new entity and assigns it the given components, then returns the entity.
  • destroy: destroys the entity and all its components.
  • assign<Component>(entity, args...): assigns the given component to the entity and uses args... to initialize it.
  • remove<Component>(entity): removes the given component from the entity.
  • has<Component>(entity): returns true if the entity has the given component, false otherwise.
  • get<Component>(entity): returns a reference to the given component for the entity (undefined behaviour if the entity has not the component).
  • replace<Component>(entity, args...): replaces the given component for the entity, using args... to create the new component.
  • clone(entity): clones an entity and all its components, then returns the new entity identifier.
  • copy<Component>(from, to): copies a component from an entity to another one (both the entities must already have been assigned the component, undefined behaviour otherwise).
  • copy(from, to): copies all the components and their contents from an entity to another one (comoonents are created or destroyed if needed).
  • reset(): resets the pool and destroys all the entities and their components.
  • view<Components...>(): gets a view of the entities that have the given components (see below for further details).

Note that entities are numbers and nothing more. They are not classes and they have no memeber functions at all.

The View

There are two different kinds of view, each one with a slighlty different interface: the single component view and the multi component view.
Both of them are iterable, that is both of them have begin and end member functions that are suitable for a range-based for loop:

auto view = registry.view<Position, Velocity>();

for(auto entity: view) {
    // do whatever you want with your entities
}

Iterators are extremely poor, they are meant exclusively to be used to iterate over a set of entities.
Exposed member functions are: operator++(), operator++(int), operator==(), operator!=() and operator*().

The single component view has also a member function named size that returns the exact number of expected entities.
The multi component view has also a member function named reset that reorganizes internal data to create optimized iterators. Use it whenever the data within the registry are known to be changed.
Both the views can be used more than once, for they return newly created and correctly initialized iterators whenever begin or end is invoked. Anyway views and iterators are tiny objects and the time to construct them can be safely ignored. I'd suggest not to store them anywhere and to invoke the Registry::view member function at each iteration to get a properly initialized view over which to iterate.

Note.
An important feature (usually missed by other well known ECS) is that users can create and destroy entities, as well as assign or remove components while iterating and neither the views nor the iterators will be invalidated.
Therefore, unless one tries to access a destroyed entity through an iterator that hasn't been advanced (in this case, of course, it's an undefined behaviour), users can freely interact with the registry and keep views and iterators consistent.
On the other side, iterators aren't thread safe, thus do no try to concurrently iterate over and modify a set of components at the same time. That being said, as long as a thread iterates over the entities that have the component X or assign and removes that component from a set of entities and another thread does something similar with components Y and Z, it shouldn't be a problem at all.
As an example, that means that users can freely run the render system over the renderable entities and update the physics concurrently on a separate thread if needed.

The Pool

Custom pools for a given component can be defined as a specialization of the class template ComponentPool.
In particular:

template<>
struct ComponentPool<MyComponent> final {
    // ...
};

A custom pool should expose at least the following member functions:

  • bool empty() const noexcept;
  • size_type capacity() const noexcept;
  • size_type size() const noexcept;
  • const entity_type * entities() const noexcept;
  • bool has(entity_type entity) const noexcept;
  • const component_type & get(entity_type entity) const noexcept;
  • component_type & get(entity_type entity) noexcept;
  • template component_type & construct(entity_type entity, Args&&... args);`
  • void destroy(entity_type entity);
  • void reset();
  • This is a fast and easy way to define a custom pool specialization for a given component (as an example, if the component X requires to be ordered internally somehow during construction or destruction operations) and to use the default pool for all the other components.
    It's a mattrer of including the given specialization along with the registry, so that it can find it during the instantiation.
    In this case, users are not required to use the more explicit Registry class. Instead, they can still use entt::DefaultRegistry.

    In cases when the per-component pools are not good enough, the registry can be initialized with a custom pool.
    In other terms, entt::Registry has a template template parameter that can be used to provide both the pool and the list of components:

    auto registry = entt::Registry<MyCustomPool<Component1, Component2>>{};
    

    Even thoug the underlying pool doesn't store the components separately, the registry must know them to be able to do specific actions (like destroy or copy). That's why they must be explicitly specified.
    A generic pool should expose at least the following memeber functions:

    • template<typename Comp> bool empty() const noexcept;
    • template<typename Comp> size_type capacity() const noexcept;
    • template<typename Comp> size_type size() const noexcept;
    • template<typename Comp> const entity_type * entities() const noexcept;
    • template<typename Comp> bool has(entity_type entity) const noexcept;
    • template<typename Comp> const Comp & get(entity_type entity) const noexcept;
    • template<typename Comp> Comp & get(entity_type entity) noexcept;
    • template<typename Comp, typename... Args> Comp & construct(entity_type entity, Args&&... args);
    • template<typename Comp> void destroy(entity_type entity);
    • void reset();

    Good luck. If you come out with a more performant components pool, do not forget to make a PR so that I can add it to the list of available ones. I would be glad to have such a contribution to the project!!

    Tests

    To compile and run the tests, EnTT requires googletest.
    Run the script deps.sh to download it. It is good practice to do it every time one pull the project.

    Then, to build the tests:

    • $ cd build
    • $ cmake ..
    • $ make
    • $ make test

    Contributors

    If you want to contribute, please send patches as pull requests against the branch master.
    Check the contributors list to see who has partecipated so far.

    License

    Code and documentation Copyright (c) 2017 Michele Caini.
    Code released under the MIT license.