- Introduction
- Quick Start
- Usage
- CMake Integration
- Manager
- Entities
- Components
- Views
- Hooks
- Manager Utility Functions
- Serialization (Advanced)
- Acknowledgements
This C++ Entity Component System (ECS) library is designed to be cache-friendly by storing components contiguously in memory and supports runtime addition of new component types. It uses a hybrid of dense and sparse sets along with an indexing table to efficiently associate entities with their components.
#include "ecs/ecs.h"
struct Position {
float x, y;
};
struct Velocity {
float dx, dy;
};
int main() {
ecs::Manager manager;
auto entity = manager.CreateEntity();
entity.Add<Position>(0.f, 0.f);
entity.Add<Velocity>(1.f, 1.f);
manager.Refresh();
for (auto [e, pos, vel] : manager.EntitiesWith<Position, Velocity>()) {
pos.x += vel.dx;
pos.y += vel.dy;
}
}- Download or include the
include/ecs/ecs.hfile from this repository manually or via CMake. - Add
#include "ecs/ecs.h"in your C++ source files that use the ECS library.
Add the following to your CMakeLists.txt (minimum version: 3.14) to fetch and include the ECS library:
include(FetchContent)
FetchContent_Declare(
ecs
GIT_REPOSITORY https://github.com/martinstarkov/ecs.git
GIT_TAG main
)
FetchContent_MakeAvailable(ecs)
set(ECS_INCLUDE_DIR "${ecs_SOURCE_DIR}/include")
target_include_directories(<target_name> PRIVATE ${ECS_INCLUDE_DIR})Replace <target_name> with your actual CMake target.
The ecs::Manager is the core class responsible for storing and managing all entities and their components.
#include "ecs/ecs.h"
ecs::Manager manager;Entities are lightweight handles to component data stored in the manager.
auto entity = manager.CreateEntity();When you create an entity, you can add components to it, but the entity won't be considered "alive" by the manager until you call manager.Refresh(). This means the entity won't show up in entity iteration functions like manager.Entities() or manager.EntitiesWith<T>(). This behavior prevents iterator invalidation if entities are created or destroyed while looping through them, such as when destroying entities with an expired lifetime component.
auto entity = manager.CreateEntity();
entity.Add<LifetimeComponent>(5.0f); // add a component to the entity
// entity is not yet considered "alive" for iteration
for (auto e : manager.Entities()) {
// this loop will not include the newly created entity yet
}
// now call Refresh to update the manager's internal state
manager.Refresh(); // the entity is now considered "alive" and will be included
// destroy the entity
entity.Destroy();
// the entity is still considered alive until we call Refresh
for (auto e : manager.Entities()) {
// the entity will still be part of the iteration, because it hasn't been refreshed yet
}
// call Refresh again to update the state, removing destroyed entities
manager.Refresh();
// now the entity is excluded from future entity loopsIn this example, the entity is only considered "alive" in the manager and part of the iteration after calling manager.Refresh(). Similarly, destroyed entities won't be removed from the loop until Refresh() is called again, ensuring there is no iterator invalidation while modifying the entity list.
To represent a null entity, you can use a default-constructed ecs::Entity{}.
ecs::Entity null_entity{}; // using default-constructed entity- Use
==and!=to compare entity handles. - Use
entity.IsIdenticalTo(other)to compare entity components (not just handles). Entity::operator bool()returns true only if the entity is alive.
Entity handles are hashable and usable in std::unordered_map.
You can retrieve the parent manager of an entity using the GetManager() function. This gives you a reference to the manager that owns the entity.
auto& manager_ref = entity.GetManager();This allows one to interact with or manipulate the manager directly from the entity handle.
Components represent the data or properties of an entity. They are stored in contiguous memory, which can improve cache efficiency. Components must have a valid constructor, destructor, and move constructor.
struct HumanComponent {
HumanComponent(int age, double height) : age{ age }, height{ height } {}
int age;
double height;
};auto& human = entity.Add<HumanComponent>(22, 180.5);
human.height += 0.5;Adding a component replaces the existing one if it already exists.
⚠️ Warning: Adding a component to an entity may invalidate previously saved references to other entity components of the same type. This is because adding a new component to the contiguous container may cause it to expand or be relocated in memory.
auto& pos = entity.TryAdd<Position>(0.f, 0.f);Try add only adds a component if the entity does not already have it. It returns a reference to the new / existing component.
bool is_human = entity.Has<HumanComponent>();
bool is_cyborg = entity.Has<HumanComponent, RobotComponent>();
bool is_either = entity.HasAny<HumanComponent, RobotComponent>();auto& human = entity.Get<HumanComponent>();
auto [robot, alien] = entity.Get<RobotComponent, AlienComponent>();
⚠️ Accessing a missing component throughGet<T>()triggers a debug assertion. Advisable to check withHas<T>()first or useTryGet<T>().
if (auto* pos = entity.TryGet<Position>()) {
pos->x += 1.f;
}Returns a component pointer or nullptr if the component does not exist.
entity.Remove<HumanComponent>();
entity.Remove<RobotComponent, AlienComponent>();No-op if the component doesn't exist.
Views provide filtered access to entities and optionally their components.
for (auto entity : manager.Entities()) {
// all alive entities
}
for (auto [entity, pos, vel] : manager.EntitiesWith<Position, Velocity>()) {
// entities that have Position and Velocity
}
for (auto entity : manager.EntitiesWithout<Sleeping>()) {
// entities missing Sleeping
}Entities() yields entity handles.
EntitiesWith<T...>() yields entity, component....
EntitiesWithout<T...>() yields entity handles.
Modifying components during iteration may affect which entities are yielded later in the loop. This is safe, but order-dependent (based on entity ID iteration).
Views also provide a few utility helpers in addition to range-based iteration.
// save the view
auto view = manager.EntitiesWith<Position, Velocity>();std::size_t count = view.Count();
auto entities = view.GetVector();// order follows internal entity iteration order
auto entities = view.GetVector();
// these are null entities if the view is empty
auto first = view.Front();
auto last = view.Back();
bool contains = view.Contains(entity);Predicates to range functions can take in either just the entity or the entity together with the view components.
bool any_fast = view.AnyOf([](auto entity, const auto& pos, const auto& vel) {
return vel.dx != 0.0f || vel.dy != 0.0f;
});
bool all_valid = view.AllOf([](auto entity) {
return entity.IsAlive();
});
std::size_t moving = view.CountIf([](auto entity, const auto& pos, const auto& vel) {
return vel.dx != 0.0f || vel.dy != 0.0f;
});
// returns null entity if nothing matches
auto found = view.FindIf([](auto entity, const auto& pos) {
return pos.x > 100.f;
});view.ForEach([](auto entity, auto& pos, auto& vel) {
pos.x += vel.dx;
pos.y += vel.dy;
});Transform projects the view into a std::vector of values.
auto ids = view.Transform([](auto entity) {
return entity.GetId();
});
// std::vector<result>, result is deduced from the function return type
auto speeds = view.Transform([](auto entity, const auto& pos, const auto& vel) {
return vel.dx * vel.dx + vel.dy * vel.dy;
});Hooks allow you to react to component lifecycle events.
auto& hook = manager.OnConstruct<Position>();
hook.Connect<&OnPositionCreated>();You can register hooks for:
OnConstruct<T>()OnDestruct<T>()OnUpdate<T>()
void OnPositionCreated(ecs::Entity e) {}
manager.OnConstruct<Position>().Connect<&OnPositionCreated>();struct System {
void OnUpdate(ecs::Entity e) {}
};
System sys;
manager.OnUpdate<Position>().Connect<System, &System::OnUpdate>(&sys);manager.OnDestruct<Position>().Connect([](ecs::Entity e) {
// your cleanup logic
});manager.RemoveOnConstruct<Position>(hook);You must keep the returned hook instance if you wish to remove it later.
if (manager.HasOnUpdate<Position>(hook)) {
// hook is registered
}| Event | Trigger |
|---|---|
| Construct | Add<T>() (new component) |
| Update | Add<T>() (replace existing) or manual entity.Update<T>() |
| Destruct | Remove<T>(), entity destruction, or manager reset |
⚠️ The update hook does not fire if the user changes a component directly through a reference. One solution is to provide set/get functions and manually trigger the hook viaentity.Update<T>().
auto new_entity = entity.Copy();
// or:
auto new_entity = manager.CopyEntity(entity);
// or copy to existing:
manager.CopyEntity(source_entity, destination_entity);You can also copy specific components:
auto new_entity = manager.CopyEntity<FoodComponent, HealthComponent>(entity);auto new_manager = manager; // copy constructs a new manager (expensive)
// Note: operator== compares identity (same instance), not deep equality
assert(new_manager != manager);bool is_empty = manager.IsEmpty();
std::size_t size = manager.Size();
std::size_t capacity = manager.Capacity();manager.Reserve(100); // preallocate for 100 entities
manager.Clear(); // remove all entities, keep capacity
manager.Reset(); // remove all entities and free memoryThe ECS supports passing in custom archivers via BaseManager<TArchiver>.
An archiver must implement methods like:
WriteComponent<T>()HasComponent<T>()ReadComponent<T>()WriteComponents<T>()SetDenseSet<T>()SetSparseSet<T>()ReadComponents<T>()GetDenseSet<T>()GetSparseSet<T>()
The default VoidArchiver disables serialization.
⚠️ Components must be default-constructible to support deserialization.
Thanks to the following for their ideas and inspiration:
- Vittorio Romeo — CppCon 2015 ECS talk
- Michele Caini (skypjack) — ECS blog series
- Adam (T-Machine) — Article on data structures for ECS
If you find this library useful or have suggestions, please feel free to contribute or open issues on GitHub!