rtecs is a library that implement an optimized Entity Component System for
C++.
It provides a flexible architecture to decouple data (Components) from logic (Systems), allowing for high-performance game development.
- Sparse set storage: High-performance component storage ensuring data locality and O(1) lookups.
- Dynamic bitsets: Efficient bitmasking to handle entity-component associations dynamically.
- Flexible systems: Register and run logic systems globally or individually by ID.
- Group views: Create SparseGroups to iterate efficiently over entities possessing specific subsets of components.
- Safe architecture: Automatic validation of entity existence and component integrity.
| macOS (AppleClang) | Linux (G++) | Windows (MSVC) | |
|---|---|---|---|
| arm64 | ✅ - AppleClang 17.0.0.17000603- CMake 4.1.2 |
☑️ | ☑️ |
| x86_64 | ☑️ | ✅ - GNU 15.2.0- CMake 3.31.6 |
✅ - MSVC 19.50.35718.0- CMake 4.11.1-msvc1 |
✅: Tested on real hardware
☑️: Compiled but not physically tested
The indicated versions are 100% functional. Any older version MIGHT NOT work.
- C++ Compiler that supports C++23 (Clang 10+, GCC 10+, MSVC 19.28+)
- CMake version 3.20 or higher
- Conan package manager version 2.22.2
Since rtecs is a library, you can include it in your project via CMake.
add_subdirectory(<rtecs_dir> rtecs)
target_link_libraries(${PROJECT_NAME}
PRIVATE
rtecs
)rtecs comes with a suite of unit tests (that uses
GTest).
You can build them by following these steps:
- Fetch dependencies with Conan
conan install . --output-folder=build/ --build=missing -s build_type=Debug- Configure the project
cmake -S . -B build/ \
-DCMAKE_TOOLCHAIN_FILE=build/conan_toolchain.cmake \
-DCMAKE_BUILD_TYPE=Debug \
-DRECS_BUILD_TESTS=ON- Build the library
cmake --build build/ # --parallel for faster compilation- Run the unit tests suite
ctest --test-dir build/ --output-on-failure1. Components
2. Systems
3. Entities
A component is a structure that will contain data. Its role is to store data of a single entity.
Component creation
This example shows how to properly define and use a component.
struct Health
{
int hp;
}
struct Profile
{
const std::string name;
}
struct Arrow
{
int[2] direction;
}
struct Transformation2D
{
int x;
int y;
int[2] scale;
int[2] rotation;
}
struct CollideBox2D
{
int top;
int left;
int width;
int height;
}Register a component
#include "rtecs/ECS.hpp"
rtecs::ECS ecs;
// You can register multiple components on a single method call
ecs.registerComponents<Health, Profile, Arrow, Transformation2D, CollideBox2D>();
// Or register only one component by one
ecs.registerComponents<Health>();
ecs.registerComponents<Profile>();
ecs.registerComponents<Arrow>();
ecs.registerComponents<Transformation2D>();
ecs.registerComponents<CollideBox2D>();Get the mask corresponding to multiple components
ecs.getComponentMask<Transformation2D, Health>();Tip
To send a mask through network, there is a DynamicBitSet::serialize method that
returns a std::vector of all the enabled bits. You can use this method to send the mask through the network.
Group creation and manipulation
rtecs::sparse::SparseGroup<Transformation2D, Health, Profile> group = ecs.group<Transformation2D, Health, Profile>
// Manipulate group's instances
group.apply([](rtecs::types::EntityID entityId, Transformation2D& transformation, Health& health, Profile& profile) {
transformation.x += 20;
health.hp -= 1;
LOG_TRACE_R3("Profile : {}", profile.name);
})
// Get a single component instance
rtecs::types::EntityID entityId = 0;
rtecs::types::OptionalRef<Profile> optionalProfile = group.getEntity<Profile>(entityId);
if (optionalProfile.has_value()) {
Profile& profile = optionalProfile.value();
LOG_TRACE_R3("Profile : {}", profile.name);
} else {
LOG_WARNING("Profile not found...");
}
// Get all the component instances
auto& view = group.getAllInstances<Profile>(); // This view will contain all the instances of the Profile component
// Check if an entity is present in the group
if (group.has(entityId)) {
LOG_TRACE_R3("Entity {} is present in the group", entityId);
} else {
LOG_WARNING("Entity {} not found...", entityId);
}A system is a function that will be called at each call of the ECS::applyAllSystems() method. Its role is to manipulate components.
Implement a system
#include "rtecs/systems/ASystem.hpp"
class DamageOnArrowCollision : public systems::ASystem
{
public:
explicit DamageOnArrowCollision():
ASystem("DamageOnArrowCollision") {} // The name of the system will be used for debugging.
void apply(ECS& ecs) override
{
// Retrieve all entities that have at least the Profile component and the CollideBox2D component
sparse::SparseGroup<Health, CollideBox2D> players = ecs.group<Health, CollideBox2D>();
// Retrieve all entities that have at least the Arrow component and the CollideBox2D component
sparse::SparseGroup<Arrow, CollideBox2D> arrows = ecs.group<Arrow, CollideBox2D>();
players.apply([&](rtecs::types::EntityID, Health& playerHealth, const CollideBox2D& playerBox) {
arrows.apply([&playerBox](rtecs::types::EntityID, const Arrow&, const CollideBox2D& arrowBox) {
if (/* Check for collision */) {
playerHealth.hp -= 1;
// Don't kill the player here, create a system that will play an animation if the player's health is lower than 0 !
}
}); // arrows.apply
}); // players.apply
}
};Register a system
#include "rtecs/ECS.hpp"
rtecs::ECS ecs;
// This is the proper way to register a system
ecs.registerSystem(std::make_shared<DamageOnArrowCollision>());
// It is also possible to register a system from a lambda
ecs.registerSystem([](ECS &ecs) {
// Your implementation of the ASystem::apply method goes here...
});
// DO NOT REGISTER SYSTEMS LIKE THAT
auto system = std::make_shared<DamageOnArrowCollision>();
ecs.registerSystem(std::move(system));Apply systems
ecs.applyAllSystems();Important
The order in which the systems are called is the same as the order of registration: First registered, first called.
An entity is represented by a number to which we will associate multiple components.
Warning
Some methods will log a warning if any problem concerning an invalid entity occurs.
Register an entity
To register an entity, you will have to specify its components and a default value for each component.
#include "rtecs/ECS.hpp"
rtecs::ECS ecs;
/* Register your components and your systems first... */
// Specify all the components the entity have
rtecs::types::EntityID entityId = ecs.registerEntity<Profile, Health, CollideBox2D>(
{ "L1x" }, // Profile
{ 20 }, // Health
{ 0, 0, 100, 300 }, // CollideBox2D
{ 0, 0, { 1, 1 }, { 0, 0 } } // Transformation2D
);Add components to an entity
// If you need to add a component later after the entity registration, you can do it easily
ecs.addEntityComponents<Transformation2D>(
entityId, // The ID of the registered Entity
{ 0, 0, { 1, 1 }, { 0, 0 } } // Transformation2D
);Update an entity component instance
// Update an entity component outside of a system
ecs.updateEntityComponent<Health>(entityId, { 25 });Destroy an entity
// Destroy an entity
ecs.destroyEntity(entityId);Important
A destroyed entity can still be present in a SparseGroup, but using it will produce a memory error. This is why you should never store a SparseGroup anywhere.
Get the component mask of an entity
// Get the mask of an entity
const rtecs::bitset::DynamicBitSet& mask = ecs.getEntityMask(entityId);Tip
To send a mask through network, there is a DynamicBitSet::serialize method that
returns a std::vector of all the enabled bits. You can use this method to send the mask through the network.