Three.js From Zero · Article s8-02
S8-02 Entity Component Systems
Entity Component Systems
Past 20 different "classes" of things, inheritance hierarchies collapse. ECS: every object is just an ID, "components" are bags of data, "systems" iterate over all entities with matching components. Data-oriented, cache-friendly, scales.
1. The inheritance trap
class Entity { ... }
class Mesh extends Entity { ... }
class Movable extends Mesh { ... }
class Player extends Movable { ... }
class FlyingPlayer extends Player { ... } // flying + regular player?
// Now you need a FlyingNPC. Does it extend FlyingPlayer or Movable?
Diamond inheritance. Deep hierarchies. "Player with fire sword" needs a new class. Dies at 50+ types.
2. ECS in one sentence
Entities are IDs. Components are data (no logic). Systems iterate entities with matching components.
3. Concrete example
// Components: just data
const Position = { x: 0, y: 0, z: 0 };
const Velocity = { x: 0, y: 0, z: 0 };
const Health = { current: 100, max: 100 };
const Renderable = { mesh: THREE.Mesh };
// Entity: just an ID with components attached
entity[42] = { Position, Velocity, Renderable };
// System: logic that operates on entities with matching components
function MovementSystem(dt) {
for (const e of query([Position, Velocity])) {
e.Position.x += e.Velocity.x * dt;
e.Position.y += e.Velocity.y * dt;
e.Position.z += e.Velocity.z * dt;
}
}
4. bitECS (JS library, SoA)
import { createWorld, addEntity, addComponent, defineComponent, defineQuery, Types } from 'bitecs';
const world = createWorld();
const Position = defineComponent({ x: Types.f32, y: Types.f32, z: Types.f32 });
const Velocity = defineComponent({ x: Types.f32, y: Types.f32, z: Types.f32 });
const eid = addEntity(world);
addComponent(world, Position, eid);
addComponent(world, Velocity, eid);
Position.x[eid] = 0;
Velocity.x[eid] = 1;
const movementQuery = defineQuery([Position, Velocity]);
function movementSystem(world, dt) {
for (const eid of movementQuery(world)) {
Position.x[eid] += Velocity.x[eid] * dt;
}
}
Structure-of-Arrays. All Positions contiguous in memory. Iteration is L1-cache fast.
5. Miniplex (tiny, ergonomic)
import { World } from 'miniplex';
const world = new World();
const e = world.add({ position: {x:0}, velocity: {x:1}, mesh: threeMesh });
for (const { position, velocity } of world.archetype('position','velocity')) {
position.x += velocity.x;
}
No decoder ring. 300 lines of source code. Great for < 10k entity projects.
6. Live demo — 5000-entity ECS
5000 entities, 3 components (Position, Velocity, Renderable). Runs at 60fps because systems loop flat arrays.
7. System design
- MovementSystem: Position + Velocity
- GravitySystem: Velocity + GravityAffected
- HealthSystem: Health (auto-heal, death detect)
- CollisionSystem: Position + Collider
- RenderSystem: Position + Rotation + Renderable → writes to Three.js Object3D
Order matters: physics → collision → render. Systems run in a pipeline each frame.
8. Tag components
No data, just a flag:
const IsPlayer = defineComponent({}); // empty
const IsEnemy = defineComponent({});
// Query
const players = defineQuery([IsPlayer, Position]);
9. When ECS overkill
Small scene, < 20 objects: plain classes fine. Prototype / game jam: classes fine. Production game, editor, sim: ECS after ~50+ distinct behaviors.
10. Takeaways
- ECS decouples data (components) from behavior (systems).
- Entities are just IDs. Composable via components.
- bitECS for perf, Miniplex for ergonomics.
- Systems run in a fixed order each frame.
- Scales to tens of thousands of entities.