ECS Architecture skill for Codex
Data-only components, single-purpose systems. Codex first decides whether ECS is worth it for the game, then picks a library and storage model that fits the language and churn pattern. It models small data-only components and tags, writes one transformation per system over narrow queries, defers spawning and component changes to a defined sync point, and declares system order in one place so behaviour does not depend on registration accidents.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- ecs-architecture Installs to ~/.agents/skills/ecs-architecture/ for every project on this machine.
curl -fsSL codexskills.dev/cli.mjs -o codex-skills.mjsnode codex-skills.mjs install ecs-architecture The CLI also searches, updates and removes skills. Run it with no arguments to pick from a list. Needs Node 20.12+.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- --project ecs-architecture Installs to .agents/skills/ecs-architecture/ in the current repository. Commit it and everyone who runs Codex there gets it.
curl -fsSLO codexskills.dev/downloads/ecs-architecture.zipunzip -o ecs-architecture.zip -d ~/.agents/skills/ The archive holds the skill folder with its generated agents/openai.yaml. Unzip into .agents/skills/ to scope it to a repo.
& ([scriptblock]::Create((irm codexskills.dev/install.ps1))) ecs-architecture PowerShell 5.1 or 7. Installs to %USERPROFILE%\.agents\skills\. Add -Project for the current repo.
104-line POSIX script. No sudo, no telemetry. Read install.sh
Use it in Codex
Start a prompt with $ecs-architecture, or pick it from /skills.
You can also just describe the task: Codex reads the description below and loads the playbook when your request matches.
What Codex reads before loading it
Designs entity component systems for games and simulations - choosing archetype vs sparse-set storage and a library (EnTT, flecs, bevy_ecs, Unity Entities, bitECS, miniplex), modelling data-only components and tags, writing systems over queries, deferring structural changes with command buffers, events, relationships, change detection and system ordering. Use when converting a deep inheritance hierarchy of game objects to ECS, starting an ECS-based engine or game, deciding if ECS is worth it, or fixing slow queries, iterator invalidation and ordering bugs in an existing ECS.
Use it when
- A game has thousands of similar entities or many mix-and-match behaviours
- Starting a custom engine or framework built on ECS
- An existing ECS has ordering bugs, iterator invalidation or slow queries
- Weighing ECS against the engine's native component model
Use something else when
- The game is built in Bevy and you need engine specifics: use bevy-engine
- You need the overall frame and tick structure: use game-loop-architecture
How to check the result
Hot systems benchmark linearly at 10k and 100k entities, headless tests assert component values after N ticks, and stale entity handles are never dereferenced.
The mistake it prevents
Porting each OOP class into one fat component, which keeps all the coupling and adds ECS overhead on top.
Done well means: components are plain data with no behaviour, each system does one transformation over a query, structural changes are deferred to a known sync point, system order is declared rather than accidental, and the design was chosen because the game has many similar entities or many cross-cutting behaviours, not because ECS is fashionable.
Should this be an ECS?
| Situation | Recommendation |
|---|---|
| Thousands of similar entities (bullets, units, particles with logic) | ECS pays off: cache-friendly iteration |
| Many behaviours mixed and matched (burning + frozen + flying) | ECS or composition; avoids class explosion |
| Small game, few dozen bespoke objects, engine already has a node/component model | Use the engine’s model (Godot nodes, Unity GameObjects); add data-oriented arrays only where hot |
| Heavy UI, narrative, turn-based with few actors | ECS adds ceremony for little gain |
If the project uses an engine, extend its native model first. Bevy is ECS by design; Unity Entities is a separate stack with its own workflow (baking, subscenes) and is a large commitment.
Library choice
| Language | Default | Notes |
|---|---|---|
| C++ | EnTT (sparse sets) | Header-only, fast add/remove, flexible groups |
| C / C++ | flecs (archetypes) | Relationships, hierarchies, query language, built-in scheduler, web explorer |
| Rust | bevy_ecs (archetype tables + sparse-set option) | Usable standalone; parallel scheduler |
| C# Unity | Entities (DOTS) | Burst + Jobs; needs baking workflow |
| TypeScript / JS | bitECS (typed arrays, max speed) or miniplex (objects, ergonomic) | Pick miniplex unless profiling shows a need |
Storage trade-off
| Archetype tables | Sparse sets |
|---|---|
| Entities with the same component set stored together; very fast multi-component iteration | One array per component type; fast add/remove, single-component iteration |
| Adding/removing a component moves the entity to another table (costly when frequent) | Multi-component queries need an intersection (EnTT groups fix this for hot sets) |
| Prefer for stable component sets | Prefer when tags toggle often (stunned, selected) |
Modelling components
- Data only:
Position {x, y},Velocity {x, y},Health {current, max}. No methods beyond trivial helpers, no references to systems. - Small and specific beats large and general.
HealthandArmorseparately, notStatswith twenty fields, so systems touch only what they need. - Tags (zero-size components) for categories and states:
Player,Enemy,Dead,Grounded. - Reference other entities by entity ID (handle), never by pointer. Check liveness before use: IDs carry a generation/version so a recycled slot is detected.
- Avoid components that own heap collections per entity in hot paths; use a relationship, child entities, or a resource indexed by entity.
- Global singletons (time, input, RNG, config) are resources, not components on a fake entity (unless the library’s idiom is singleton components, as in flecs or Unity).
Systems
// EnTT
struct Position { float x, y; };
struct Velocity { float x, y; };
struct Lifetime { float seconds; };
void movement(entt::registry& reg, float dt) {
auto view = reg.view<Position, const Velocity>();
for (auto [e, pos, vel] : view.each()) {
pos.x += vel.x * dt;
pos.y += vel.y * dt;
}
}
void expire(entt::registry& reg, float dt) {
std::vector<entt::entity> dead; // defer destruction
for (auto [e, life] : reg.view<Lifetime>().each())
if ((life.seconds -= dt) <= 0.f) dead.push_back(e);
reg.destroy(dead.begin(), dead.end());
}
// miniplex
type Entity = { position: { x: number; y: number }; velocity?: { x: number; y: number }; enemy?: true };
const world = new World<Entity>();
const movers = world.with("position", "velocity");
function movementSystem(dt: number) {
for (const { position, velocity } of movers) {
position.x += velocity.x * dt;
position.y += velocity.y * dt;
}
}
Rules:
- One responsibility per system; name it after the transformation (
apply_gravity,resolve_collisions,tick_cooldowns). - Query the narrowest component set. Mark read-only access (
const Tin EnTT,&Tin Bevy,RefROin Unity) so schedulers can parallelise and so intent is clear. - Systems communicate through components and events, never by calling each other.
Structural changes
Adding/removing components or creating/destroying entities while iterating invalidates iterators in many ECS libraries, or moves entities between tables mid-loop.
| Library | Deferred mechanism |
|---|---|
| EnTT | Changing the entity currently being visited is allowed; touching any other entity is not. Collecting and applying after the loop is the safe default |
| flecs | Systems run in deferred mode by default; changes merge at the end of the system |
| bevy_ecs | Commands, applied at schedule sync points |
| Unity Entities | EntityCommandBuffer from an ECB system singleton |
| miniplex | world.remove/removeComponent are safe in loops over its query iterators (iterate in reverse internally); still batch spawns |
Define the sync point in the frame (usually after gameplay systems, before rendering) and keep spawning/despawning there.
Events and relationships
- Short-lived events (damage dealt, collision started) as an event queue resource cleared each
tick, or as event entities with a
DamageEventcomponent destroyed at end of tick. - Hierarchies (turret on a tank, item in an inventory): use the library’s relationship
support (flecs
ChildOfpairs, BevyChildOf/Children) rather than hand-rolled parent IDs that you must keep consistent on despawn. - Change detection (
Changed<T>in Bevy,on_updatesignals in EnTT, flecs observers) for work that only matters when data changes: rebuilding spatial grids, updating UI.
Ordering
Input -> AI intent -> Movement -> Physics/Collision -> Damage/Gameplay -> Despawn (sync) -> Animation -> Render extraction
Write this pipeline in one place (a schedule list, Bevy system sets, flecs phases). Ambiguous order between two systems writing the same component is a bug even if it works today.
Gotchas
- Converting OOP 1:1 (one
Enemycomponent holding everything) gives ECS overhead with none of the benefit. Split by what systems need. - Storing entity handles in components and not checking validity after the target is destroyed.
- Per-entity
std::functionor closures in components defeat cache locality and serialization. - Very high churn of tag components in an archetype ECS moves entities constantly; use a bool field or sparse-set storage for flags that flip every frame.
- Debugging is harder without an inspector. Add a debug dump (entity ID plus component list) or use the library’s tools (flecs explorer, Unity Entities Hierarchy, bevy-inspector-egui).
Verification
- Benchmark the hot systems at target entity counts (10k, 100k) before and after; iteration time should scale linearly.
- A test spawns entities, runs N ticks, and asserts component values (systems are pure functions of the world, so tests need no rendering).
- Destroy entities referenced by others mid-test; no system dereferences a stale handle.
- Print the resolved system order once at startup and compare it to the documented pipeline.
Hand-off summary
**Library:** EnTT 3.x (sparse sets); group<Position, Velocity> for the movement hot path
**Components:** Position, Velocity, Health, Lifetime, tags Player/Enemy/Dead
**Systems (ordered):** input, ai_intent, movement, collision, damage, expire (sync), render_extract
**Structural changes:** collected per system, applied at expire sync point
**Bench:** movement 100k entities 0.41 ms (was 2.9 ms with GameObject classes) What the zip contains
SKILL.md plus the interface file below. Nothing runs on install.
agents/openai.yaml
Gives the Codex app a display name, a one-line description and a starter prompt for this skill.
interface:
display_name: "ECS Architecture"
short_description: "Data-only components, single-purpose systems"
default_prompt: "$ecs-architecture Convert our Enemy, Bullet and Pickup class hierarchy to an ECS using EnTT" About this skill
Written and maintained by the Codex Skills editors to the same editorial standard as every skill here: a selective description, a real workflow, known pitfalls and a way to verify the work. First published , last updated . MIT licensed. Something wrong or out of date? Tell us.