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.

  • Game Development
  • $ecs-architecture
  • 167 lines
  • 2 files
  • ~1,950 tokens
  • MIT
  • Updated

Install

curl -fsSL codexskills.dev/install.sh \  | sh -s -- ecs-architecture

Installs to ~/.agents/skills/ecs-architecture/ for every project on this machine.

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

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.

~/.agents/skills/ecs-architecture/SKILL.md

--- name: ecs-architecture description: Designs entity component systems for games and simulations - choosing archetype vs sparse-set storage and a library (EnTT, flecs, bevy_ecs, Unity… license: MIT ---

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?

SituationRecommendation
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 modelUse the engine’s model (Godot nodes, Unity GameObjects); add data-oriented arrays only where hot
Heavy UI, narrative, turn-based with few actorsECS 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

LanguageDefaultNotes
C++EnTT (sparse sets)Header-only, fast add/remove, flexible groups
C / C++flecs (archetypes)Relationships, hierarchies, query language, built-in scheduler, web explorer
Rustbevy_ecs (archetype tables + sparse-set option)Usable standalone; parallel scheduler
C# UnityEntities (DOTS)Burst + Jobs; needs baking workflow
TypeScript / JSbitECS (typed arrays, max speed) or miniplex (objects, ergonomic)Pick miniplex unless profiling shows a need

Storage trade-off

Archetype tablesSparse sets
Entities with the same component set stored together; very fast multi-component iterationOne 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 setsPrefer 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. Health and Armor separately, not Stats with 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 T in EnTT, &T in Bevy, RefRO in 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.

LibraryDeferred mechanism
EnTTChanging the entity currently being visited is allowed; touching any other entity is not. Collecting and applying after the loop is the safe default
flecsSystems run in deferred mode by default; changes merge at the end of the system
bevy_ecsCommands, applied at schedule sync points
Unity EntitiesEntityCommandBuffer from an ECB system singleton
miniplexworld.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 DamageEvent component destroyed at end of tick.
  • Hierarchies (turret on a tank, item in an inventory): use the library’s relationship support (flecs ChildOf pairs, Bevy ChildOf/Children) rather than hand-rolled parent IDs that you must keep consistent on despawn.
  • Change detection (Changed<T> in Bevy, on_update signals 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 Enemy component 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::function or 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.yamlgenerated from this page

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.

esc
Just launched

Codex Skills Pro is live

79 premium skills for Codex, each with scripts, references and an evidence report that proves the result.

Bought one by one$882.21
All 79 skills$49
94% off

That's $0.62 a skill, paid once.

Get all 79 for $49Or pick single skills from $4.99 →

Secure checkout by Creem