text// Internal structure
include/darkmoon/ecs/
├── entity_manager.hpp ← EntityManager<CMPList, TAGList>
├── slotmap.hpp ← Slotmap<T, Capacity><T> (component storage)
└── meta_program.hpp ← MP:: namespace (TypeList, traits, masks)
// User structure
game/src/
├── utils/
│ └── types.hpp ← user defines CMPList, TAGList, using EM = ...
├── components/
│ ├── render_component.hpp
│ └── physics_component.hpp
└── systems/
├── render_system.hpp
└── physics_system.hpp
texttypes.hpp defines CMPList, TAGList, EM = EntityManager<CMPList, TAGList>
│
▼
user owns EM m_em {}
│
├── m_em.newEntity() --> Entity&
├── m_em.addComponent(e) --> T&
├── m_em.getComponent(e) --> T&
└── m_em.forEach() --> lambda(Entity&, T&...)
types.hpp
c++#pragma once
#include <darkmoon/ecs.hpp>
// Include your component headers
#include "../components/render_component.hpp"
#include "../components/physics_component.hpp"
// List all components here (order matters for the bitmask)
using GameCMPs = MP::TypeList<
RenderComponent,
PhysicsComponent
>;
// Tags: empty structs used as boolean flags, no data, no SlotMap slot
struct TagPlayer {};
struct TagEnemy {};
// List all tags here
using GameTAGs = MP::TypeList<
TagPlayer,
TagEnemy
>;
// Instantiate the EntityManager with your types
using EM = EntityManager<GameCMPs, GameTAGs>;
using Entity = EM::Entity;
C++// components/physics_component.hpp
#pragma once
struct PhysicsComponent {
int positionX {}, positionY {};
};
// components/render_component.hpp
#pragma once
struct RenderComponent {
char character {};
RenderComponent(const char* ch) : character(*ch) {}
};
// Adding a component to an entity
auto& e = EM.newEntity();
EM.addComponent<PhysicsComponent>(e);
// Checking a component in a system
if (e.hasComponent<PhysicsComponent>()) { /* ... */ }
// Iterating only entities with a specific component
EM.forEach<MP::TypeList<PhysicsComponent>, MP::TypeList<>>([&](Entity& e){
// only PhysicsComponent entities
});
C++// In types.hpp
struct TagPlayer {};
struct TagEnemy {};
using GameTAGs = MP::TypeList<TagPlayer, TagEnemy>;
// Adding a tag to an entity
auto& e = EM.newEntity();
EM.addTag<TagPlayer>(e);
// Checking a tag in a system
if (e.hasTag<TagPlayer>()) { /* ... */ }
// Iterating only entities with a specific tag
EM.forEach<MP::TypeList<>, MP::TypeList<TagPlayer>>([&](Entity& e){
// only TagPlayer entities
});
C++// systems/render_system.hpp
#pragma once
#include "../utils/types.hpp"
struct RenderSystem {
// Declare which components and tags this system needs
using SYSCMPs = MP::TypeList<RenderComponent>;
using SYSTAGs = MP::TypeList<>;
void update(EM& em) {
em.forEach<SYSCMPs, SYSTAGs>([&](Entity& e, RenderComponent& r) {
// Optionally access other components on the same entity
if (e.hasComponent<PhysicsComponent>()) {
auto& phys = em.getComponent<PhysicsComponent>(e);
// do something
}
std::cout << r.character << "\n";
});
}
};
// main.cpp
#include "./systems/render_system.hpp"
int main() {
EM em {};
Entity& e = em.newEntity();
em.addComponent<RenderComponent>(e);
RenderSystem rm {};
while (true) {
rm.update(em);
}
}
| newEntity() | Entity& |
|
| destroyEntityByID(id) | void |
|
| destroyEntities(deathSet) | void |
|
| destroyAll() | void |
|
| getEntityByID(id) | Entity* |
|
| getEntities() | std::span<Entity> |
|
| aliveCount() | std::size_t |
|
| freeEntities() | std::size_t |
| getID() | std::size_t |
|
| hasComponent<T>() | bool |
|
| hasTag<T>() | bool |
| addComponent<T>(e, args...) | T& |
|
| getComponent<T>(e) | T& |
|
| destroyComponent<T>(e) | void |
|
| addTag<T>(e) | void |
|
| destroyTag<T>(e) | void |
| forEach<CMPs, TAGs>(func) | void |
|
| forEachAny<CMPs, TAGs>(func) | void |
C++// Safe pattern for destroying entities during iteration
EM::deathSet toDestroy {};
EM.forEach<SYSCMPs, SYSTAGs>([&](Entity& e, PhysicsComponent& p) {
if (p.positionX > 100)
toDestroy.insert(e.getID());
});
EM.destroyEntities(toDestroy); // destroy after the loop
C++// include/darkmoon/ecs/slotmap.hpp
template <
typename T,
std::size_t Capacity = 10,
typename INDEXT = std::uint32_t
>
struct Slotmap;
| T | - | |
| Capacity | 10 |
|
| INDEXT | uint32_t |
textm_indices [ ] -> indirection table. Each slot holds:
.id --> position in m_data (when occupied)
--> next free slot index (when free, forms the freelist)
.gen --> generation counter; incremented on every alloc/free
m_data [ ] -> packed array of T. Elements 0..m_size-1 are always valid.
Gaps never exist; erase uses swap-and-pop to stay compact.
m_erase [ ] -> reverse map: m_erase[dataIdx] = slotIdx that points here.
Used to fix up m_indices when swap-and-pop relocates the last element.
C++using key_type = struct { index_type id; gen_type gen; };
// push_back returns a key
auto key = slotmap.push_back(MyComponent{});
// Access via key asserts in debug if stale
MyComponent& cmp = slotmap[key];
// Safe validity check before access
if (slotmap.is_valid(key)) {
slotmap[key].value = 42;
}
// Erase increments generation, invalidates all copies of this key
slotmap.erase(key); // returns false if key was already stale
| push_back(T&&) | key_type |
O(1) | |
| push_back(const T&) | key_type |
O(1) | |
| operator[](key) | T& |
O(1) | |
| erase(key) | bool |
O(1) | |
| is_valid(key) | bool |
O(1) | |
| clear() | void |
O(N) | |
| begin() / end() | T* |
O(1) | |
| size() | std::size_t |
O(1) | |
| capacity() | std::size_t |
O(1) |
textBefore erase(key B): After erase(key B):
m_data: [ A | B | C ] m_data: [ A | C ]
m_erase: [ a | b | c ] m_erase: [ a | c ]
↑ C moved here; m_indices[c].id updated
C++// Compile-time constant wrapper
template <typename T, T VAL>
struct constant { static constexpr T value{ VAL }; };
struct true_type : constant<bool, true> {};
struct false_type : constant<bool, false> {};
// Type identity helper used as base for "this struct IS type T"
template <typename T>
struct type_id { using type = T; };
// Type equality
template <typename T, typename U> struct is_same : false_type {};
template <typename T> struct is_same<T, T> : true_type {};
template <typename T, typename U>
constexpr bool is_same_v{ is_same<T, U>::value };
C++// Base case: N == 0 --> the type is the first in the pack
template <typename T, typename... Ts>
struct nth_type<0, T, Ts...> : type_id<T> {};
// Recursive case: strip the head, decrement N
template <std::size_t N, typename T, typename... Ts>
struct nth_type<N, T, Ts...> : type_id<nth_type_t<N - 1, Ts...>> {};
// Convenience alias
template <std::size_t N, typename... Ts>
using nth_type_t = typename nth_type<N, TypeList<Ts...>>::type;
C++// Match found at head --> position 0
template <typename T, typename... Ts>
struct pos<T, T, Ts...> : constant<std::size_t, 0> {};
// No match yet --> 1 + position in the rest
template <typename T, typename U, typename... Ts>
struct pos<T, U, Ts...> : constant<std::size_t, 1 + pos_v<T, Ts...>> {};
C++template <bool Condition, typename T, typename F> struct IFT : type_id<F> {};
template <typename T, typename F> struct IFT<true, T, F> : type_id<T> {};
// Alias for readability
template <bool Condition, typename T, typename F>
using IFT_t = typename IFT<Condition, T, F>::type;
TypeList
C++template <typename... Ts>
struct TypeList {
// Number of types in the list
consteval static std::size_t size() noexcept { return sizeof...(Ts); }
// True if T appears anywhere in the list
template <typename T>
consteval static bool contains() noexcept {
return (false || ... || is_same_v<T, Ts>);
}
// Zero-based index of T; static_assert if not found
template <typename T>
consteval static std::size_t pos() noexcept {
static_assert(contains<T>(), "Type not found");
return pos_v<T, Ts...>;
}
};
// Usage
using GameCMPs = MP::TypeList<RenderComponent, PhysicsComponent>;
constexpr std::size_t idx = GameCMPs::pos<PhysicsComponent>(); // --> 1
constexpr bool has = GameCMPs::contains<AIComponent>(); // --> false
C++template <typename LIST>
using smallest_type =
IFT_t<LIST::size() <= 8, std::uint8_t,
IFT_t<LIST::size() <= 16, std::uint16_t,
IFT_t<LIST::size() <= 32, std::uint32_t,
std::uint64_t>>>;
// Example: 3 components + 2 tags = 5 total --> uint8_t (fits in 8 bits)
// Example: 20 total types --> uint32_t
C++template <typename TLIST>
struct cmp_tag_traits {
static_assert(TLIST::size() <= 64, "Component tag list is too large");
// Narrowest uint that fits one bit per type
using mask_type = smallest_type<TLIST>;
// Number of types registered
consteval static uint8_t size() noexcept { return TLIST::size(); }
// Bit position of a single component or tag
template <typename CMPTAG>
consteval static uint8_t id() noexcept {
static_assert(TLIST::template contains<CMPTAG>(), "Component or Tag not found");
return TLIST::template pos<CMPTAG>();
}
// OR together the bits for all types in the query
template <typename... Ts>
consteval static mask_type mask() noexcept {
return (0 | ... | (1LL << id<Ts>()));
}
};
// forEach uses it like this (pseudocode):
using Traits = cmp_tag_traits<MergedList>;
constexpr auto queryMask = Traits::mask<PhysicsComponent, TagPlayer>();
// compiled down to a single integer constant zero runtime cost
if ((entity.mask & queryMask) == queryMask) { /* visit */ }
C++// replace_t: reuse the types in a TypeList with a different variadic template
// TypeList<A,B,C> --> std::tuple<A,B,C>
using SlotmapTuple = replace_t<std::tuple, GameCMPs>;
// forall_insert_template_t: wrap every type in a TypeList with another template
// TypeList<A,B,C> --> TypeList<Slotmap<A>, Slotmap<B>, Slotmap<C>>
using SlotmapList = forall_insert_template_t<Slotmap, GameCMPs>;
C++// Both const and non-const overloads are provided
// Base case (I == tuple size): do nothing
// Recursive case: call f on element I, then advance
MP::for_each_in_tuple(m_slotmaps, [](auto& slotmap) {
slotmap.clear(); // resets every component storage in one call
});
| constant<T, V> | struct | |
| true_type / false_type | struct | |
| is_same<T,U> | trait | |
| nth_type<N, Ts...> | trait | |
| pos<T, Ts...> | trait | |
| IFT<Cond, T, F> | trait | |
| TypeList<Ts...> | struct | |
| smallest_type<TL> | alias | |
| cmp_tag_traits<TL> | struct | |
| replace_t<New, TL> | alias | |
| forall_insert_template_t<W, TL> | alias | |
| for_each_in_tuple(t, f) | function |