@web-engine-dev/ecs
A high-performance, archetype-based Entity Component System (ECS) library for TypeScript/JavaScript. Built for game engines and simulations with support for parallel execution, SharedArrayBuffer, worker threads, and advanced query systems.
Layer 2 · ECS Foundation
Features
- Archetype Storage: Cache-friendly SoA (Structure of Arrays) layout — entities with the same component set share contiguous memory
- Type-Safe Components: Define components with typed schemas backed by TypedArrays
- Advanced Queries: Fluent query builder with
with,without,withOptional, and access modes (read/write) - Deferred Commands: Safe entity mutations during system execution (applied between system runs)
- Double-Buffered Events: Frame-synchronized event system (write this frame, read next frame)
- Observer Pattern: React to component add/remove lifecycle changes
- Parallel Execution: Multi-threaded support via SharedArrayBuffer + Web Workers
- Entity IDs: 32-bit packed format — 20-bit index + 12-bit generation (4M live entities, recycle detection)
- Sparse Components: O(1) add/remove for frequently toggled components (e.g., cooldowns, status effects)
Installation
bash
npm install @web-engine-dev/ecs
# or
pnpm add @web-engine-dev/ecsQuick Start
typescript
import { World, defineComponent, defineTag, queryBuilder } from '@web-engine-dev/ecs';
// Define components
const Position = defineComponent('Position', { x: 'f32', y: 'f32', z: 'f32' });
const Velocity = defineComponent('Velocity', { x: 'f32', y: 'f32', z: 'f32' });
const IsPlayer = defineTag('IsPlayer');
// Create world
const world = new World();
// Spawn entities
const player = world
.spawn()
.with(Position, { x: 0, y: 0, z: 0 })
.with(Velocity, { x: 1, y: 0, z: 0 })
.with(IsPlayer)
.id();
// Query entities
const query = queryBuilder().with(Position, Velocity).read(Velocity).write(Position).build();
// System execution
for (const archetype of query.archetypes) {
const positions = archetype.getColumn(Position);
const velocities = archetype.getColumn(Velocity);
for (let i = 0; i < archetype.count; i++) {
positions[i].x += velocities[i].x * dt;
positions[i].y += velocities[i].y * dt;
}
}Core Concepts
Components
typescript
// Data component with typed schema
const Position = defineComponent('Position', {
x: 'f32',
y: 'f32',
z: 'f32',
});
// Tag (zero-size marker)
const IsPlayer = defineTag('IsPlayer');
// Sparse component (O(1) add/remove)
const Cooldown = defineComponent('Cooldown', { remaining: 'f32' }, { sparse: true });Queries
typescript
const query = queryBuilder()
.with(Position, Velocity) // Required components
.without(Frozen) // Excluded components
.withOptional(Acceleration) // Optional components
.read(Velocity) // Read-only access
.write(Position) // Write access
.build();Commands (Deferred Mutations)
typescript
commands.spawn().with(Position, { x: 0, y: 0, z: 0 });
commands.insert(entity, Health, { value: 100 });
commands.remove(entity, Poisoned);
commands.despawn(entity);Events
typescript
// Write events
const writer = world.eventWriter<DamageEvent>('damage');
writer.send({ target: entity, amount: 10 });
// Read events (double-buffered)
const reader = world.eventReader<DamageEvent>('damage');
for (const event of reader.read()) {
// Process damage
}Resources
typescript
world.insertResource(GameTime, { delta: 0, elapsed: 0 });
const time = world.getResource(GameTime);Schema Types
| Type | Description |
|---|---|
i8 | Signed 8-bit integer |
i16 | Signed 16-bit |
i32 | Signed 32-bit |
u8 | Unsigned 8-bit |
u16 | Unsigned 16-bit |
u32 | Unsigned 32-bit |
f32 | 32-bit float |
f64 | 64-bit float |
bool | Boolean |