Examples¶
Practical patterns for using ECSS. Each snippet is self‑contained and focuses on one aspect of the API.
More real‑world usage: explore the test suite in this repository (covers edge cases, iteration modes, defrag, threading) and my pet engine using ECSS: https://github.com/wagnerks/StelForge
1. Basic Movement View¶
struct Position { float x,y; };
struct Velocity { float dx,dy; };
ecss::Registry<false> reg; // non thread-safe
auto e = reg.takeEntity();
reg.addComponent<Position>(e, {0,0});
reg.addComponent<Velocity>(e, {1,0.5f});
for (auto [id, p, v] : reg.view<Position, Velocity>()) {
if (p && v) { p->x += v->dx; p->y += v->dy; }
}
2. Grouping Hot Components¶
Group components that are always accessed together for better locality.
reg.registerArray<Position, Velocity>(); // future entities sharing both go into same sector
auto e2 = reg.takeEntity();
reg.addComponent<Position>(e2, {5,5});
reg.addComponent<Velocity>(e2, {0.2f, 0});
3. Deferred Erase & Maintenance¶
struct Health { int hp; };
auto e3 = reg.takeEntity();
reg.addComponent<Health>(e3, {10});
// Mark entity for removal when hp <= 0
for (auto [id, h] : reg.view<Health>()) {
if (h && h->hp <= 0) reg.destroyEntity(id); // deferred
}
reg.update(); // processes deferred destroys + optional defrag
4. Ranged Iteration (Sparse Subset)¶
// Build two disjoint id ranges
ecss::Ranges<ecss::EntityId> subset({ {100, 200}, {400, 450} });
for (auto [id, pos] : reg.view<Position>(subset)) {
if (pos) {/* operate only on those ids */}
}
5. Foreign Component Projection¶
Iterate main array (Position) while optionally reading unrelated components (Velocity, Health) stored elsewhere.
for (auto [id, p, v, h] : reg.view<Position, Velocity, Health>()) {
if (v) { p->x += v->dx; }
if (h && h->hp <= 0) { reg.destroyEntity(id); }
}
6. Manual Defragmentation¶
// Force defrag of all arrays you know about (after heavy churn)
reg.defragment();
7. Per-Array Defrag Threshold¶
// Trigger compaction sooner for frequently destroyed transient entities
reg.setDefragmentThreshold<Velocity>(0.10f); // 10% dead triggers
8. Thread-Safe Variant¶
ecss::Registry<true> tsReg; // uses shared/unique locks + pins
auto e = tsReg.takeEntity();
tsReg.addComponent<Position>(e, {0,0});
// Reader thread
auto reader = std::jthread([&]{
for (int i = 0; i < 1000; ++i) {
for (auto [id, p] : tsReg.view<Position>()) {
if (p) (void)p->x; // read only
}
}
});
// Writer thread
auto writer = std::jthread([&]{
for (int i = 0; i < 100; ++i) {
auto w = tsReg.takeEntity();
tsReg.addComponent<Position>(w, {float(i),0});
if (i % 10 == 0) tsReg.update();
}
});
9. Trivial vs Non-Trivial Components¶
Prefer trivial types for fast raw moves during defrag / insertion. Non‑trivial types are fully supported with proper move semantics.
// Trivial: uses fast memmove during defrag/shift
struct TrivialTag { int value; };
// Non-trivial: proper move ctor/dtor called during operations
struct Named { std::string name; };
struct Inventory { std::vector<int> items; };
// Both work correctly
auto e1 = reg.takeEntity();
reg.addComponent<TrivialTag>(e1, {1});
auto e2 = reg.takeEntity();
reg.addComponent<Named>(e2, {"npc_01"});
reg.addComponent<Inventory>(e2, {{1, 2, 3}});
// Safe to defragment - non-trivial types moved correctly
reg.destroyEntity(e1);
reg.update(); // Named/Inventory elements properly relocated, no leaks
Performance notes:
- If a
SectorsArraycontains only trivial types: random insertion / compaction uses batchmemmove. - If any grouped type is non‑trivial: per‑element move constructor / destructor calls.
- Correctness is guaranteed for both cases; trivial is just faster.
10. Thread-Safe Non-Trivial Components¶
Non‑trivial types work correctly with thread‑safe registry:
struct PlayerData {
std::string name;
std::vector<int> inventory;
};
ecss::Registry<true> reg; // thread-safe
// Writer thread
std::jthread writer([&]{
for (int i = 0; i < 100; ++i) {
auto e = reg.takeEntity();
reg.addComponent<PlayerData>(e, {
"player_" + std::to_string(i),
{1, 2, 3, 4, 5}
});
}
});
// Reader thread
std::jthread reader([&]{
for (int i = 0; i < 50; ++i) {
for (auto [id, pd] : reg.view<PlayerData>()) {
if (pd && !pd->name.empty()) {
// Safe: strings properly handled during concurrent ops
}
}
}
});
11. Recycling Entity IDs¶
std::vector<ecss::EntityId> temp;
for (int i = 0; i < 50; ++i) temp.push_back(reg.takeEntity());
for (auto id : temp) reg.destroyEntity(id);
reg.update(); // ids become recyclable
auto reused = reg.takeEntity(); // may reuse a prior id
12. Conditional Component Addition¶
Add a component later without disturbing grouped arrays of other types.
auto eLate = reg.takeEntity();
reg.addComponent<Position>(eLate, {0,0});
// Later decision:
if (/* needs velocity */) reg.addComponent<Velocity>(eLate, {0.5f, 0});
Only the Velocity array changes; existing Position storage untouched.
13. Minimal System Dispatch Pattern¶
void integrate(ecss::Registry<false>& r, float dt) {
for (auto [id, p, v] : r.view<Position, Velocity>()) {
if (p && v) { p->x += v->dx * dt; p->y += v->dy * dt; }
}
}
14. Custom Component Grouping Strategy¶
Group only the hot pairs; leave rarely co-accessed types ungrouped.
reg.registerArray<Position, Velocity>(); // hot pair
// PhysicsMass kept separate (queried sparsely)
reg.registerArray<PhysicsMass>();
This avoids creating archetype explosions while still gaining locality where it matters.
15. Simple Health Cleanup Pass¶
for (auto [id, h] : reg.view<Health>()) {
if (h && h->hp <= 0) reg.destroyEntity(id);
}
reg.update(); // finalize removals
16. Partial Component Access in a View¶
for (auto [id, p, v, h] : reg.view<Position, Velocity, Health>()) {
// You may ignore unused projected pointers safely
if (p) {/* only need position this frame */}
}
17. Manual Opportunistic Defrag After Burst¶
// After large spawn / destroy cycle:
reg.update(); // will internally decide
// Force anyway for a specific hot array:
reg.defragment<Position>(); // if such helper exists; else keep array pointer & call
18. Range-Constrained System (Chunked Work)¶
auto all = reg.entityRanges(); // assume helper returning full range set
// Process in windowed slices (pseudo)
for (auto window : partition(all, 4096)) { // user-defined helper
for (auto [id, pos] : reg.view<Position>(window)) {
if (pos) {/* ... */}
}
}
19. Checking for Component Presence Cheaply¶
if (reg.hasComponent<Velocity>(someEntity)) {
// safe to assume view<Velocity> would yield pointer
}
20. Adding Many Entities (Bulk)¶
reg.reserve<Position>(100'000);
for (int i = 0; i < 100'000; ++i) {
auto e = reg.takeEntity();
reg.addComponent<Position>(e, {float(i), 0});
}
21. Simple Debug Print of Alive Positions¶
for (auto [id, p] : reg.view<Position>()) {
if (p) std::cout << id << ": (" << p->x << "," << p->y << ")\n";
}
These patterns can be combined. Favor:
- Trivial component types for fastest structural edits.
- Group only high‑coherence sets.
- Call update() once per frame (or per simulation tick) to amortize cleanup.
- Study tests + StelForge for deeper, real integration scenarios.