fecs is a minimal and efficient Entity Component System (ECS) library for C++, designed to be simple, fast, and easy to integrate into any project.
๐ง Work in Progress: This library is still in early development and not yet feature complete.
โ๏ธ In this small documentation I tried to explain, among other things, how and why some parts of the system work the way they do.
- ๐งฑ Entity creation โ lightweight
uint32_t-based entities - ๐งฉ Component management โ add/remove with optional constructor args
- ๐งน Component cleanup โ auto-removal on entity destruction
- ๐ Entity builder โ simplifies adding multiple components
- ๐น๏ธ Component management โ reservation
- ๐ Simple queues โ
view,runner,direct_for_eachfor lightweight iteration - โก Fast owning queues โ
group,group_slicefor cache-friendly iteration - ๐ View support in groups โ combine owned + viewed components
- ๐ซ Excluder โ filter out specific component types from iteration
Clone this GitHub repository to a desired folder:
git clone https://github.com/foured/fecs.gitAdd the repository to your CMake project:
add_subdirectory("somefolder/fecs")Link fecs to your target:
target_link_libraries(${CMAKE_PROJECT_NAME} PRIVATE fecs)To begin using fecs, you need to create the main object โ the registry:
#include <fecs/core/registry.h>
int main() {
fecs::registry registry;
return 0;
}This registry will manage all your entities and components.
fecs::entity_t e = registry.create_entity();Entities in fecs are just uint32_t, making them lightweight and efficient.
๐ก It's recommended to pass entities by value, since they are only 4 bytes, whereas pointers or references typically take 8 bytes.
registry.destroy_entity(e);This will automatically remove all components attached to the entity.
struct component_1 {
int bullets;
float lifetime;
};registry.add_component<component_1>(e);If your component has a constructor, you can pass arguments to add_component:
struct component_2 {
component_2(const std::string& s)
: val(std::atoi(s.c_str())) {}
int val;
};
registry.add_component<component_2>(e, "123456789");If you need to add a lot of components and you don't want to pass entity every time
fecs::entity_builder builder(registry);
builder.add_component<component_1>(1, 0.1f);
fecs::entity_t e = builder.get();Or simpler
fecs::entity_t e = fecs::entity_builder(registry).with<component_1>(1, 0.1f).get();std::vector<fecs::entity_t> entities;
registry.add_component<component_1>(entities);registry.remove_component<component_1>(e);
registry.remove_component<component_2>(entities);if (registry.has_component<component_1>(e)) {
// ...
}fecs does not enforce a strict "systems" abstraction, as optimal processing patterns vary by project.
Instead, it provides a flexible set of queues to iterate over and process components.
โ Queues are helper classes that simplify iteration and logic application on entities and their components.
There are two main categories of queues:
runnerdirect_for_each
groupgroup_sliceview
Because fecs stores each component type in a separate sparse set โ a cache-friendly data structure that enables fast iteration and lookup.
โน๏ธ Sparse sets are similar to maps, but store data contiguously in memory for better performance.
When iterating over one component type, it's straightforward and fast.
But when working with multiple components, there are two strategies:
Align all sparse sets so that component data for entity X is stored at the same index Y across sets.
This allows fast access via shared index.
- Implemented via:
group,group_slice - Requires owned components
โ ๏ธ Only one group can own a given component type.
Look up each component individually before applying logic.
- Implemented via:
view - Slower, but works in all cases
| Feature | runner |
direct_for_each |
|---|---|---|
| Type | Class | Registry method |
| Caching | โ Yes | โ No |
| Suitable for | Repeated logic | One-time initialization |
| Under the hood | Uses direct_for_each |
Simple loop |
Guideline:
- Use
direct_for_eachfor simple initialization logic. - Use
runnerwhen executing the same logic repeatedly โ it caches iteration data for better performance.
Both allow multi-component iteration with fast access to owned and viewed components, but differ in flexibility:
| Feature | group |
group_slice |
|---|---|---|
| Iteration scope | All owned components | Only a selected subset of owned components |
| Filtering capability | โ No filtering โ uses all owned | โ Select only specific owned components |
| Viewed components | โ Supported | โ Supported |
| Performance tuning | โ Less flexible | โ More granular and optimized |
| Use case | Full group logic | Targeted or partial logic within the same group setup |
๐ก
group_slicecan only be used after agroupthat owns all of the sliceโs owned components has been created.
Guideline:
- Use
groupfor full access to all components in the group. - Use
group_slicewhen you only need to operate on a few components from the group โ it avoids unnecessary unpacking.
๐ก All
for_eachfunctions can optionally takefecs::entity_tas the first parameter.
registry.direct_for_each<component_1>([](component_1& c) {
// ...
});
registry.direct_for_each<component_1>([](fecs::entity_t e, component_1& c) {
// ...
});auto r = registry.runner<component_1>();
r.for_each([](fecs::entity_t e, component_1& c) {
// ...
});Only owning group:
registry.create_group<component_1, component_2>();
auto g = registry.group<component_1, component_2>();
g->for_each([](component_1& c1, component_2& c2) {
// ...
});Owning + viewed components:
registry.create_group<component_1, component_2>(fecs::view_part<component_3>{});
auto g = registry.group<component_1, component_2>(fecs::view_part<component_3>{});
g->for_each([](component_1& c1, component_2& c2, component_3& c3) {
// ...
});Shorter with descriptor:
using group_desc = fecs::queue_args_descriptor<
fecs::pack_part<component_1, component_2>,
fecs::view_part<component_3>
>;
registry.create_group(group_desc{});
auto g = registry.group(group_desc{});
g->for_each([](component_1& c1, component_2& c2, component_3& c3) {
// ...
});group_slice is used when you want to iterate over only some of the owned components from a group:
registry.create_group<component_1, component_2, component_3>(fecs::view_part<component_4>{});
auto gs = registry.group_slice<component_1, component_3>(fecs::view_part<component_5>{});
gs.for_each([](fecs::entity_t e, component_1& c1, component_3& c3, component_5& c5) {
// ...
});You can also construct it via queue_args_descriptor:
using slice_desc = fecs::queue_args_descriptor<
fecs::pack_part<component_1, component_3>,
fecs::view_part<component_5>
>;
auto gs = registry.group_slice(slice_desc{});
gs.for_each([](fecs::entity_t e, component_1& c1, component_3& c3, component_5& c5) {
// ...
});