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.
struct TrivialTag { int value; }; // trivially movable
struct Heavy { std::string name; }; // non-trivial (move invokes code)
reg.addComponent<TrivialTag>(reg.takeEntity(), {1});
reg.addComponent<Heavy>(reg.takeEntity(), {"npc_01"});
If a SectorsArray
contains only trivial types, random insertion / compaction uses memmove
for best speed.
10. 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
11. 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.
12. 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; }
}
}
13. 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.
14. Simple Health Cleanup Pass¶
for (auto [id, h] : reg.view<Health>()) {
if (h && h->hp <= 0) reg.destroyEntity(id);
}
reg.update(); // finalize removals
15. 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 */}
}
16. 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
17. 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) {/* ... */}
}
}
18. Checking for Component Presence Cheaply¶
if (reg.hasComponent<Velocity>(someEntity)) {
// safe to assume view<Velocity> would yield pointer
}
19. 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});
}
20. 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.