EnTT Entity Component System
repository·main·Indexed 11 days ago
https://github.com/skypjack/enttA high-performance, header-only C++ library featuring an Entity Component System (ECS) and optimized core utilities for memory management, type handling, and bit manipulation. Includes specialized containers like dense_map, dense_set, and basic_table, as well as utilities such as entt::any, hashed strings, and type-safe opaque containers.
What's inside EnTT
- EnTT is a header-only, lightweight, and easy-to-use Entity-Component-System (ECS) module written in modern C++. It is primarily designed for architectural patterns used in game development, providing high-performance containers for managing entities, components, and systems.
Use the Table adaptor for columnar data
mainThe
basic_tableis a container adaptor that manages multiple sequential containers as if they were columns in a single table.Key Features:
- Columnar Storage: Internally, it uses a tuple of containers (rather than a container of tuples) to allow efficient access to individual columns.
- Row Access: Accessing a row or iterating over the table returns a
std::tuplecontaining references to the elements of that row. - API: It provides a small set of functions similar to
std::vector.
Usage: You can use the
tablealias to simplify usage, which defaults to usingstd::vectoras the underlying sequential container for each column.Handle type identifier conflicts
mainWhile rare, identifier conflicts can occur if two types have the same hash or if two types from different libraries share the same fully qualified name (resulting in the same
type_name).Solutions:
- Define
ENTT_STANDARD_CPP: This forces runtime identifiers, which typically avoid compile-time hash collisions, though it may not be ideal for plugin systems. - Specialize
entt::type_name: Assign a custom identifier to the conflicting type to ensure uniqueness. - Custom Policy: Implement a fully customized identifier generation policy (e.g., using enum classes or preprocessing).
- Define
How the cooperative scheduler works
mainThe scheduler manages the lifecycle of multiple processes. Each process is invoked once per tick.
Key behaviors:
- Termination: When a process terminates, it is automatically removed from the scheduler.
- Chaining (Children): If a process has a 'child' (defined via
.then()), the parent is replaced by the child only if the parent succeeds. If the parent fails, both the parent and the child are discarded. - Sharing: Processes inherit from
std::enable_shared_from_this. You can obtain a shared pointer to a process to intervene in its lifecycle (likepause()), but you should not use it to manage the process's ownership, as this can compromise the scheduler.
Generate execution graphs with entt::organizer
mainThe
entt::organizerclass template allows you to build an execution graph from a set of functions and their resource requirements. This graph can be used to safely schedule tasks in parallel.Registering Tasks
Functions can be free functions, member functions, or lambdas. Supported parameters include:
entt::registry(orconst entt::registry &)entt::basic_view(any combination of storage classes)- A context variable of type
T(orconst T &)
Resource Management
When a function is registered, the organizer treats its parameters as resources. The
constqualifier determines if access is Read-Only (RO) or Read-Write (RW). You can explicitly declare additional resources or override access modes via template parameters.Workflow
- Emplace tasks into the organizer.
- Generate the graph using
.graph(). - Prepare the registry for execution using the graph's
preparefunction. - Schedule the tasks using your own preferred scheduler.
```cpp entt::organizer organizer; // 1. Register tasks organizer.emplace<&free_function>(Use Signals and Sinks for event handling
mainEnTT provides
entt::sigh(signals) to manage event listeners. To maintain a clean API, it is recommended to keep the signal as a private member and expose aentt::sinkto clients. Thesinkis the tool used to connect and disconnect listeners.Key Concepts:
- Signal (
entt::sigh<Signature>): The object that holds the listeners and triggers them. - Sink (
entt::sink): The interface used to manage connections (connect/disconnect) to a signal. - Connection:
connectreturns aconnectionobject. You can call.release()on it to disconnect, or wrap it in ascoped_connectionto disconnect automatically when it goes out of scope.
Listener Types:
- Free functions.
- Member functions (requires an instance).
- Lambdas and functors.
entt::sigh<void(int, char)> signal; entt::sink sink{signal}; void foo(int, char) { /* ... */ } struct listener { void bar(const int &, char) { /* ... */ } }; listener instance; sink.connect<&foo>(); sink.connect<&listener::bar>(instance); sink.disconnect<&foo>(); sink.disconnect<&listener::bar>(instance); sink.disconnect(&instance); sink.disconnect(); // Discards all listeners- Signal (
Use the Dense Set container
mainThe
dense_setis a hash set implementation based on sparse sets. Like thedense_map, it aims to provide a packed array of elements to improve iteration performance by reducing memory jumps.Its interface is similar to
std::unordered_set, but it adds support for reverse iteration (providingrbegin()andrend()).Use entt::handle to wrap an entity and registry
mainAn
entt::handleis a thin, non-owning wrapper around an entity and a registry. It replicates theentt::registryAPI (e.g.,get,emplace) but implicitly passes the wrapped entity to every call.- Validity: A default-constructed handle is invalid (contains a null registry/entity). Use its implicit conversion to
boolto check validity. - Mutability: Handles are trivially copyable. Because they are non-owning, mutability is part of the type (
entt::handlevsentt::const_handle). - Customization: You can create custom handles for your own identifier types using
entt::basic_handle.
// Standard handles entt::handle handle{registry, entity}; // Custom handles for custom identifiers using my_handle = entt::basic_handle<entt::basic_registry<my_identifier>>; using my_const_handle = entt::basic_handle<const entt::basic_registry<my_identifier>>; // Check validity if (handle) { handle.get<position>(); }- Validity: A default-constructed handle is invalid (contains a null registry/entity). Use its implicit conversion to
Use entt::delegate as a lightweight function invoker
mainAn
entt::delegateis a general-purpose invoker designed to be a lightweight alternative tostd::function. It provides zero memory overhead for free functions, lambdas, and member functions (when provided with an instance).Key Characteristics:
- No Allocations: Unlike
std::function, it avoids hidden memory allocations. - Signature Flexibility: It supports functions with shorter argument lists than the delegate's signature (extra arguments are silently discarded). It also supports "payload" functions where the first argument is a reference to data passed during connection.
- Safety: Invoking an empty delegate results in undefined behavior or a crash. Always check if a delegate is valid using
if(delegate)before calling it. - Resetting: Use
.reset()to clear a delegate; there is nodisconnectmethod for individual delegates.
// Create an empty delegate for a specific signature entt::delegate<int(int)> delegate{}; // Check if it is valid before use if(delegate) { auto ret = delegate(42); } // Clear the delegate delegate.reset();- No Allocations: Unlike
Using a const registry and avoiding dangling views
mainA
constregistry is thread-safe because it does not perform lazy initialization of missing storages. This has implications for how you generate views.The Risk
Because a
constregistry cannot lazily create storage, a view generated from aconstregistry might contain dangling references to non-existing storage if the storage hasn't been explicitly created. If you keep such a view aside for later use, it may misbehave.Solutions
- Immediate Use: Always create views when necessary and discard them immediately after use.
- Pre-announce Storage: Use the
registry.storage<T>()method to explicitly instantiate storage classes for specific types. This 'announces' the type and ensures the storage exists, making views generated from aconstregistry safe to use.
How View iteration order is determined
mainBy default, a view iterates along the pool that contains the smallest number of elements to minimize work. The order of types in the template arguments does not affect the iteration order.
Enforcing Order
If you need to force the view to iterate based on a specific component's pool, use the
.use<T>()method.Reverse Iteration
Single type views support reverse iteration via
rbegin()andrend(). Multi-type views do not support reverse iterators; you must implement this manually or use a single-type view to lead the iteration.// Default: iterates the smallest pool for(auto entity: registry.view<position, velocity>()) { // ... } // Enforce iteration based on position pool auto view = registry.view<position, velocity>(); view.use<position>(); for(auto entity: view) { // ... } // Reverse iteration (Single type views only) auto view = registry.view<position>(); for(auto it = view.rbegin(), last = view.rend(); it != last; ++it) { // ... }Control execution order using fake resources in entt::flow
mainThe
entt::flowbuilder does not have an explicitbefore()orafter()API. Instead, execution order is determined by resource access modes. To force a specific order, you can use fake resources.- To force Task A to run BEFORE Task B: Have Task A request a fake resource in
rw(read-write) mode, and have Task B request the same fake resource inro(read-only) mode. - To force Task B to run AFTER a group of tasks: Have the group of tasks request a fake resource in
romode, and have Task B request that same fake resource inrwmode.
This works because accessing a resource in opposite modes (one
rw, onero) requires sequential scheduling rather than parallel execution.// Force task_1 to run BEFORE task_2 and task_3 builder .bind("task_1"_hs) .ro("resource_1"_hs) .rw("fake"_hs) .bind("task_2"_hs) .ro("resource_2"_hs) .ro("fake"_hs) .bind("task_3"_hs) .ro("resource_2"_hs) .ro("fake"_hs); // Force task_3 to run AFTER task_1 and task_2 builder .bind("task_1"_hs) .ro("resource_1"_hs) .ro("fake"_hs) .bind("task_2"_hs) .ro("resource_1"_hs) .ro("fake"_hs) .bind("task_3"_hs) .ro("resource_2"_hs) .rw("fake"_hs);- To force Task A to run BEFORE Task B: Have Task A request a fake resource in