Entity Component System for uē-tôo
npm install @ue-too/ecsHigh-performance Entity Component System (ECS) architecture for TypeScript.


> Experimental: This package is an experimental implementation based on Austin Morlan's ECS tutorial. Please DO NOT use this in production.
@ue-too/ecs provides a lightweight Entity Component System implementation for TypeScript. ECS is an architectural pattern commonly used in game development that promotes composition over inheritance and enables high-performance iteration over game objects.
- Efficient Storage: Component arrays using sparse-set data structure for O(1) lookups
- Fast Iteration: Dense packing enables cache-friendly iteration over components
- Type Safety: Full TypeScript generics for component type safety
- Signature Matching: Automatic system updates when entity component composition changes
- Entity Pooling: Entity ID recycling for memory efficiency
- Minimal Overhead: Lightweight architecture with predictable performance
Using Bun:
``bash`
bun add @ue-too/ecs
Using npm:
`bash`
npm install @ue-too/ecs
Here's a simple example demonstrating the core ECS workflow:
`typescript
import { Coordinator } from '@ue-too/ecs';
// 1. Define component types
type Position = { x: number; y: number };
type Velocity = { x: number; y: number };
type Health = { current: number; max: number };
// 2. Create coordinator
const ecs = new Coordinator();
// 3. Register components
ecs.registerComponent
ecs.registerComponent
ecs.registerComponent
// 4. Create entities and add components
const player = ecs.createEntity();
ecs.addComponentToEntity('Position', player, { x: 0, y: 0 });
ecs.addComponentToEntity('Velocity', player, { x: 5, y: 0 });
ecs.addComponentToEntity('Health', player, { current: 100, max: 100 });
const enemy = ecs.createEntity();
ecs.addComponentToEntity('Position', enemy, { x: 50, y: 50 });
ecs.addComponentToEntity('Health', enemy, { current: 50, max: 50 });
// 5. Query and update components
const playerPos = ecs.getComponentFromEntity
const playerVel = ecs.getComponentFromEntity
if (playerPos && playerVel) {
playerPos.x += playerVel.x;
playerPos.y += playerVel.y;
}
// 6. Clean up
ecs.destroyEntity(enemy);
`
The Entity Component System pattern separates data from logic:
- Entities: Unique identifiers (numbers) representing game objects
- Components: Plain data containers (no logic)
- Systems: Functions that operate on entities with specific component combinations
Traditional object-oriented hierarchies can become complex and rigid. ECS promotes:
- Composition over inheritance: Build entities by combining components
- Data locality: Components are stored in dense arrays for better cache performance
- Flexibility: Easy to add/remove behaviors by adding/removing components
- Parallelization: Systems can operate independently on entity subsets
The main ECS coordinator that manages all subsystems.
`typescript`
const ecs = new Coordinator();
Entity Management:
- createEntity(): Entity - Creates a new entity, returns entity IDdestroyEntity(entity: Entity): void
- - Destroys entity and removes all components
Component Management:
- registerComponent - Registers a component typeaddComponentToEntity
- - Adds component to entityremoveComponentFromEntity
- - Removes component from entitygetComponentFromEntity
- - Retrieves component datagetComponentType(name: string): ComponentType | null
- - Gets component type ID
System Management:
- registerSystem(name: string, system: System): void - Registers a systemsetSystemSignature(name: string, signature: ComponentSignature): void
- - Sets which components a system requires
Systems maintain a set of entities that match their component signature:
`typescript`
interface System {
entities: Set
}
Bit flags indicating which components an entity has:
`typescript`
type ComponentSignature = number; // Bit field
type ComponentType = number; // Component type ID (0-31)
type Entity = number; // Entity ID
Update positions based on velocities:
`typescript
import { Coordinator, System } from '@ue-too/ecs';
const ecs = new Coordinator();
// Register components
ecs.registerComponent
ecs.registerComponent
// Create movement system
const movementSystem: System = {
entities: new Set()
};
ecs.registerSystem('Movement', movementSystem);
// Set signature: entities with Position AND Velocity
const posType = ecs.getComponentType('Position')!;
const velType = ecs.getComponentType('Velocity')!;
const signature = (1 << posType) | (1 << velType);
ecs.setSystemSignature('Movement', signature);
// Update loop
function update(deltaTime: number) {
movementSystem.entities.forEach(entity => {
const pos = ecs.getComponentFromEntity
const vel = ecs.getComponentFromEntity
pos.x += vel.x * deltaTime;
pos.y += vel.y * deltaTime;
});
}
// Game loop
setInterval(() => update(0.016), 16); // ~60 FPS
`
Process health and damage components:
`typescript
type Health = { current: number; max: number };
type Damage = { amount: number; source: Entity };
ecs.registerComponent
ecs.registerComponent
const damageSystem: System = { entities: new Set() };
ecs.registerSystem('Damage', damageSystem);
const healthType = ecs.getComponentType('Health')!;
const damageType = ecs.getComponentType('Damage')!;
const damageSignature = (1 << healthType) | (1 << damageType);
ecs.setSystemSignature('Damage', damageSignature);
function processDamage() {
damageSystem.entities.forEach(entity => {
const health = ecs.getComponentFromEntity
const damage = ecs.getComponentFromEntity
health.current -= damage.amount;
if (health.current <= 0) {
console.log(Entity ${entity} destroyed);`
ecs.destroyEntity(entity);
} else {
// Remove damage component after processing
ecs.removeComponentFromEntity
}
});
}
Render entities with position and sprite components:
`typescript
type Sprite = { imageSrc: string; width: number; height: number };
ecs.registerComponent
const renderSystem: System = { entities: new Set() };
ecs.registerSystem('Render', renderSystem);
const spriteType = ecs.getComponentType('Sprite')!;
const renderSignature = (1 << posType) | (1 << spriteType);
ecs.setSystemSignature('Render', renderSignature);
function render(ctx: CanvasRenderingContext2D) {
ctx.clearRect(0, 0, canvas.width, canvas.height);
renderSystem.entities.forEach(entity => {
const pos = ecs.getComponentFromEntity
const sprite = ecs.getComponentFromEntity
// Draw sprite at position
const img = new Image();
img.src = sprite.imageSrc;
ctx.drawImage(img, pos.x, pos.y, sprite.width, sprite.height);
});
}
`
Build complex component requirements:
`typescript
// Entities that have Position, Velocity, AND Sprite
const movingRenderables =
(1 << ecs.getComponentType('Position')!) |
(1 << ecs.getComponentType('Velocity')!) |
(1 << ecs.getComponentType('Sprite')!);
// Helper function for cleaner syntax
function buildSignature(ecs: Coordinator, ...componentNames: string[]): number {
return componentNames.reduce((signature, name) => {
const type = ecs.getComponentType(name);
return type !== null ? signature | (1 << type) : signature;
}, 0);
}
// Usage
const signature = buildSignature(ecs, 'Position', 'Velocity', 'Health');
ecs.setSystemSignature('MySystem', signature);
`
The package provides configuration constants:
`typescript`
export const MAX_ENTITIES = 10000; // Maximum simultaneous entities
export const MAX_COMPONENTS = 32; // Maximum component types (bit limit)
To customize, you can create your own EntityManager:
`typescript
import { EntityManager } from '@ue-too/ecs';
const entityManager = new EntityManager(5000); // Custom max entities
`
For complete API documentation with detailed type information, see the TypeDoc-generated documentation.
This package is written in TypeScript with complete type definitions:
`typescript
// Component types are fully typed
type Position = { x: number; y: number };
ecs.registerComponent
// Type-safe component retrieval
const pos = ecs.getComponentFromEntity
if (pos) {
pos.x += 10; // TypeScript knows pos has x and y properties
}
// Generic component arrays
import { ComponentArray } from '@ue-too/ecs';
const positions = new ComponentArray
``
This ECS implementation follows these principles:
- Simplicity: Minimal API surface for easy learning
- Performance: Sparse-set data structure for O(1) operations
- Type Safety: Leverage TypeScript's type system
- Flexibility: Components are plain data objects
- Explicit: No magic, predictable behavior
- Entity Creation: O(1) - pops from available entity pool
- Component Lookup: O(1) - sparse-set provides constant-time access
- Component Iteration: O(n) - dense array iteration for cache efficiency
- Signature Matching: O(m) where m is number of systems (typically small)
Performance Tips:
- Keep component data small and focused
- Process components in batches (system-by-system) rather than entity-by-entity
- Reuse entities when possible instead of create/destroy cycles
- Limit number of component types (max 32 due to bit signature)
- Max 32 component types: Component signatures use 32-bit integers
- No component queries: Must register systems with signatures upfront
- No hierarchical entities: Flat entity structure only
- No built-in serialization: Component data must be manually serialized
- @ue-too/being: State machine library for entity AI and behavior
- @ue-too/math: Vector and transformation utilities for component data
- @ue-too/board: Canvas rendering system that can integrate with ECS
- Austin Morlan's ECS Tutorial - Original tutorial this implementation is based on
- ECS FAQ - Comprehensive ECS concepts and patterns
- Data-Oriented Design - Principles behind ECS architecture
MIT