UNPKG

ecspresso

Version:

A minimal Entity-Component-System library for typescript and javascript.

371 lines (288 loc) 9.54 kB
# ECSpresso *(pronounced "ex-presso")* __Note: This is a VERY early work in progress. No work on performance has been done while the API is being nailed down. The documention is also being autogenerated while ECSpresso is being iterated on.__ A type-safe, modular, and extensible Entity Component System (ECS) framework for TypeScript. ## Features - 🔒 **Type-Safe**: Full TypeScript support with component, event, and resource type inference - 🧩 **Modular**: Bundle-based architecture for organizing features - 💡 **Developer-Friendly**: Clean, fluent API with method chaining - 🔄 **Event-Driven**: Integrated event system for decoupled communication - 🗄️ **Resource Management**: Global state management with lazy loading - ⚡ **Query System**: Powerful entity filtering with helper type utilities ## Installation ```sh npm install ecspresso ``` ## Quick Start ```typescript import ECSpresso from 'ecspresso'; // Define your component types interface Components { position: { x: number; y: number }; velocity: { x: number; y: number }; health: { value: number }; } // Create a world and add a system const world = new ECSpresso<Components>(); world.addSystem('movement') .addQuery('moving', { with: ['position', 'velocity'] }) .setProcess((queries, deltaTime) => { for (const entity of queries.moving) { entity.components.position.x += entity.components.velocity.x * deltaTime; entity.components.position.y += entity.components.velocity.y * deltaTime; } }) .build(); // Create entities and run const player = world.spawn({ position: { x: 0, y: 0 }, velocity: { x: 10, y: 5 }, health: { value: 100 } }); world.update(1/60); // Run one frame ``` ## Core Concepts ### Entities and Components Entities are containers for components. Use `spawn()` to create entities with initial components: ```typescript // Create entity with components const entity = world.spawn({ position: { x: 10, y: 20 }, health: { value: 100 } }); // Add components later world.entityManager.addComponent(entity.id, 'velocity', { x: 5, y: 0 }); // Get component data const position = world.entityManager.getComponent(entity.id, 'position'); // Remove components or entities world.entityManager.removeComponent(entity.id, 'velocity'); world.entityManager.removeEntity(entity.id); ``` ### Systems and Queries Systems process entities that match specific component patterns: ```typescript world.addSystem('combat') .addQuery('fighters', { with: ['position', 'health'], without: ['dead'] }) .addQuery('projectiles', { with: ['position', 'damage'] }) .setProcess((queries, deltaTime) => { // Process fighters and projectiles for (const fighter of queries.fighters) { for (const projectile of queries.projectiles) { // Combat logic here } } }) .build(); ``` ### Resources Resources provide global state accessible to all systems: ```typescript interface Resources { score: { value: number }; settings: { difficulty: 'easy' | 'hard' }; } const world = new ECSpresso<Components, {}, Resources>(); // Add resources world.addResource('score', { value: 0 }); world.addResource('settings', { difficulty: 'easy' }); // Use in systems world.addSystem('scoring') .setProcess((queries, deltaTime, ecs) => { const score = ecs.getResource('score'); score.value += 10; }) .build(); ``` ## Working with Systems ### Method Chaining ECSpresso supports fluent method chaining for creating multiple systems: ```typescript world.addSystem('physics') .addQuery('moving', { with: ['position', 'velocity'] }) .setProcess((queries, deltaTime) => { // Physics logic }) .and() // Complete this system and continue chaining .addSystem('rendering') .addQuery('visible', { with: ['position', 'sprite'] }) .setProcess((queries) => { // Rendering logic }) .build(); // Only the final system needs .build() ``` ### Query Type Utilities Extract entity types from queries to create reusable helper functions: ```typescript import { createQueryDefinition, QueryResultEntity } from 'ecspresso'; // Create reusable query definitions const movingQuery = createQueryDefinition({ with: ['position', 'velocity'], without: ['frozen'] }); // Extract entity type for helper functions type MovingEntity = QueryResultEntity<Components, typeof movingQuery>; // Create type-safe helper functions function updatePosition(entity: MovingEntity, deltaTime: number) { entity.components.position.x += entity.components.velocity.x * deltaTime; entity.components.position.y += entity.components.velocity.y * deltaTime; } // Use in systems world.addSystem('movement') .addQuery('entities', movingQuery) .setProcess((queries, deltaTime) => { for (const entity of queries.entities) { updatePosition(entity, deltaTime); // Perfect type safety! } }) .build(); ``` ### System Priority Control execution order with priorities (higher numbers execute first): ```typescript world.addSystem('physics') .setPriority(100) // Runs first .setProcess(() => { /* physics */ }) .and() .addSystem('rendering') .setPriority(50) // Runs second .setProcess(() => { /* rendering */ }) .build(); ``` ## Advanced Features ### Bundles Organize related systems and resources into reusable bundles: ```typescript import { Bundle } from 'ecspresso'; const inputBundle = new Bundle<Components, Events, Resources>('input') .addSystem('playerInput') .addQuery('players', { with: ['position', 'velocity', 'player'] }) .setProcess((queries, deltaTime, ecs) => { // Handle input }); const renderBundle = new Bundle<Components, Events, Resources>('render') .addSystem('renderer') .addQuery('sprites', { with: ['position', 'sprite'] }) .setProcess((queries) => { // Render sprites }); // Create world with bundles const game = ECSpresso.create<Components, Events, Resources>() .withBundle(inputBundle) .withBundle(renderBundle) .build(); ``` ### Events Use events for decoupled system communication: ```typescript interface Events { playerDied: { playerId: number }; levelComplete: { score: number }; } const world = new ECSpresso<Components, Events, Resources>(); // Handle events in systems world.addSystem('gameLogic') .setEventHandlers({ playerDied: { handler: (data, ecs) => { console.log(`Player ${data.playerId} died`); // Respawn logic } } }) .build(); // Publish events from anywhere world.eventBus.publish('playerDied', { playerId: 1 }); ``` ### Resource Factories Create resources lazily with factory functions: ```typescript // Sync factory world.addResource('config', () => ({ difficulty: 'normal', soundEnabled: true })); // Async factory world.addResource('assets', async () => { const textures = await loadTextures(); return { textures }; }); // Initialize all resources await world.initializeResources(); ``` ### System Lifecycle Systems can have initialization and cleanup hooks: ```typescript world.addSystem('gameSystem') .setOnInitialize(async (ecs) => { // One-time setup console.log('System starting...'); }) .setOnDetach((ecs) => { // Cleanup when system is removed console.log('System shutting down...'); }) .build(); // Initialize all systems await world.initialize(); ``` ## Type Safety ECSpresso provides comprehensive TypeScript support: ### Component Type Safety ```typescript // ✅ Valid world.entityManager.addComponent(entity.id, 'position', { x: 0, y: 0 }); // ❌ TypeScript error - invalid component world.entityManager.addComponent(entity.id, 'invalid', { data: 'bad' }); // ❌ TypeScript error - wrong component shape world.entityManager.addComponent(entity.id, 'position', { x: 0 }); // missing y ``` ### Query Type Safety ```typescript world.addSystem('example') .addQuery('moving', { with: ['position', 'velocity'] }) .setProcess((queries) => { for (const entity of queries.moving) { // ✅ TypeScript knows these exist entity.components.position.x; entity.components.velocity.y; // ❌ TypeScript error - not guaranteed to exist entity.components.health.value; } }) .build(); ``` ### Bundle Type Compatibility ```typescript // ✅ Compatible bundles merge cleanly const bundle1 = new Bundle<{position: {x: number, y: number}}>('bundle1'); const bundle2 = new Bundle<{velocity: {x: number, y: number}}>('bundle2'); const world = ECSpresso.create() .withBundle(bundle1) .withBundle(bundle2) // ✅ Types merge successfully .build(); // ❌ Conflicting types cause TypeScript errors const conflictingBundle = new Bundle<{position: string}>('conflict'); // world.withBundle(conflictingBundle); // TypeScript error! ``` ## Component Callbacks React to component changes with callbacks: ```typescript // Listen for component additions/removals world.entityManager.onComponentAdded('health', (value, entity) => { console.log(`Health added to entity ${entity.id}:`, value); }); world.entityManager.onComponentRemoved('health', (oldValue, entity) => { console.log(`Health removed from entity ${entity.id}:`, oldValue); }); ``` ## Performance Tips - Use query type utilities to extract business logic into testable helper functions - Bundle related systems for better organization - Use system priorities to ensure correct execution order - Leverage resource factories for expensive initialization - Consider component callbacks for immediate reactions to state changes