-A high-performance header-only Archetype ECS written in modern C++20.
ent is a high-performance, header-only archetype ECS for C++20 focused on game engines and real-time simulation workloads.
-Designed for game engine development and real-time simulation systems.
- Single-header include (
include/ent/ent.h) - SoA chunk storage for cache-friendly iteration
- Runtime component registry + compile-time typed APIs
- Query, cached query, command buffer, observers, singletons, systems, serialization, and debugging tools
-## Features
-- Archetype + Chunk-based SoA storage -- O(1) entity lookup system -- Component registry (runtime type-safe) -- Fast query system -- Entity migration (add/remove components) -- Cache-friendly memory layout -- Header-only (no dependencies)
- Quick Start
- Core Concepts
- Setup and Configuration
- API Walkthrough
- Query and Iteration Patterns
- Structural Changes and CommandBuffer
- Observers (OnAdd / OnRemove)
- Singleton Components
- Serialization
- System Scheduler
- EntityHandle and WorldBuilder
- Debugging, Inspection, and Profiling
- Performance Notes and Best Practices
- Complete Example
-## Example Usage +## Quick Start
struct Position { float x, y, z; };
struct Velocity { float x, y, z; };
int main() {
ent::World world;
ent::RegisterComponent<Position>();
ent::RegisterComponent<Velocity>();
auto e = world.CreateEntity();
world.AddComponent<Position>(e, {0, 0, 0});
world.AddComponent<Velocity>(e, {1, 0, 0});
world.Query<Position, Velocity>().ForEach<Position, Velocity>([](Position& p, const Velocity& v) {
p.x += v.x;
p.y += v.y;
p.z += v.z;
});
}An Entity is a numeric ID (uint32_t) that points to component data in archetype chunks.
A component is any C++ type you register via RegisterComponent<T>(). Registered components get a runtime ComponentID (bit index in signature mask).
An archetype represents one exact component set/signature. Entities migrate between archetypes when adding/removing components.
Each archetype stores entities in fixed-size chunks (ENT_CHUNK_SIZE, default 16KB). Data is laid out as:
- entity ID array
- aligned component arrays (one contiguous array per component)
This gives high cache locality for tight loops.
World::Query<Ts...>() selects all archetypes containing Ts..., then iterates all matching chunk rows.
- Main header:
include/ent/ent.h
- Requires C++20.
- No external dependencies.
Define before including ent.h:
ENT_CHUNK_SIZE(default16384)ENT_MAX_COMPONENTS(default64)ENT_INITIAL_ENTITY_CAPACITY(default4096)ENT_PARALLEL_MIN_CHUNKS(default2)
Define before include:
#define ENT_PROFILE_BEGIN(name) MyProfilerBegin(name)
#define ENT_PROFILE_END(name) MyProfilerEnd(name)
#include "ent/ent.h"RegisterComponent<T>() — register at startup.
GetComponentID<T>() — get runtime ID.
Important: register components before gameplay/world mutation.
Entity CreateEntity()void DestroyEntity(Entity e)bool IsAlive(Entity e)
T& AddComponent<T>(Entity e, T value = {})void RemoveComponent<T>(Entity e)bool HasComponent<T>(Entity e)T& GetComponent<T>(Entity e)/ const overload
Entity CloneEntity(Entity source)— deep copy all source components.
WorldStats GetStats()void PrintStats()void PrintMemoryStats()void InspectEntity(Entity e)size_t ArchetypeCount()uint32_t LiveEntityCount()
QueryResult supports several iteration styles.
world.Query<Position, Velocity>()
.ForEach<Position, Velocity>([](Position& p, const Velocity& v) {
p.x += v.x;
});world.Query<Position>()
.ForEachWithEntity<Position>([](ent::Entity e, Position& p) {
(void)e;
p.x += 1.0f;
});const ent::Signature excluded = ent::SignatureBit(ent::GetComponentID<Disabled>());
world.QueryExcluding<Position, Velocity>(excluded)
.ForEach<Position, Velocity>([](Position& p, Velocity& v){ p.x += v.x; });ForEachChunk(...)ParallelForEachChunk(...)ParallelForEach<Ts...>(...)
Use these for heavy workloads and better thread utilization.
CachedQuery<Ts...> caches matching archetypes and auto-refreshes when world structure changes (tracked by world version).
ent::CachedQuery<Position, Velocity> q(world);
q.ForEach([](Position& p, Velocity& v) { p.x += v.x; });Use CommandBuffer when iterating and you need deferred create/add/remove/destroy without invalidating in-flight iteration.
Typical pattern:
- Iterate and queue structural ops in
CommandBuffer. - Call
world.FlushCommands(cmd)after iteration.
High-level operations include:
CreateEntity()(phantom ID until flush)DestroyEntity(e)AddComponent<T>(e, value)RemoveComponent<T>(e)
+Register lifecycle hooks by component type:
world.OnAdd<Health>([](ent::World& w, ent::Entity e) {
auto& h = w.GetComponent<Health>(e);
if (h.current <= 0) h.current = h.max;
});
world.OnRemove<Health>([](ent::World&, ent::Entity e) {
// called before component storage is removed/destroyed
(void)e;
});Semantics:
OnAdd<T>: fires after successful add.OnRemove<T>: fires before removal/destroy (while still present).
World-level shared objects keyed by component ID:
SetSingleton<T>(value)GetSingleton<T>()HasSingleton<T>()RemoveSingleton<T>()
Great for global configs, service pointers, timers, frame context, etc.
Binary save/load APIs:
bool Save(const std::string& path) constbool Load(const std::string& path)
Format includes:
- file magic/version
- component registry snapshot (name+size)
- entity pool state
- archetypes, entities, and component bytes
Notes:
- Register compatible component types before loading.
- Components matched by
(name, size)when loading. - Unmatched stored components are skipped.
SystemScheduler executes systems in priority order.
System concept:
struct MySystem {
void Update(ent::World& world, float dt);
};APIs:
Register(system, name, priority)RegisterFn(name, fn, priority)SetEnabled(name, bool)Update(world, dt)PrintOrder()
Built-in sample systems in header:
MovementSystemHealthSystemLifetimeSystemPendingKillSystem
RAII helper wrapping entity ownership.
auto h = ent::EntityHandle::Create(world)
.Add<Position>({1,2,3})
.Add<Velocity>({0,1,0});
ent::Entity id = h.ID();
h.Release(); // prevent auto-destroy on handle dtorIf not released, owned entity is automatically destroyed when handle is destroyed.
Fluent startup helper:
ent::MovementSystem move;
ent::HealthSystem health;
auto built = ent::WorldBuilder{}
.RegisterComponents<ent::Position, ent::Velocity, ent::Health>()
.AddSystem(move, "Movement", 100)
.AddSystem(health, "Health", 300)
.Build();Returns unique pointers to world and scheduler.
InspectEntity(e)prints archetype, location, and components.PrintStats()andPrintMemoryStats()dump world + archetype memory stats.WorldStatsprovides machine-readable counters.- profiling scopes wrap important operations via
ENT_PROFILE_BEGIN/END.
- Register all components early; avoid runtime registration mid-frame.
- Prefer tight component sets for hot loops to minimize bytes/entity.
- Batch structural changes via
CommandBuffer. - Reuse CachedQuery inside systems.
- Use chunk/parallel iteration only for meaningful workloads.
- Minimize false sharing by per-thread chunk partitioning patterns.
- Tune
ENT_CHUNK_SIZEandENT_PARALLEL_MIN_CHUNKSfor your target.
See: examples/basic_example.cpp
Build example (adjust include path as needed):
g++ -std=c++20 -O2 -Iinclude examples/basic_example.cpp -o basic_example
./basic_example+## License
+
+MIT. See LICENSE.