or run

tessl search
Log in

Version

Workspace
tessl
Visibility
Public
Created
Last updated
Describes
npmpkg:npm/ecos@0.2.x

docs

advanced

constraints.mdexecution-order.mdpatterns.md
index.md
tile.json

tessl/npm-ecos

tessl install tessl/npm-ecos@0.2.0

Entity Component System for JavaScript that enables component-based entity creation and management using a factory pattern

patterns.mddocs/advanced/

Design Patterns

This guide presents proven design patterns for building effective entity systems with ECOS. These patterns help you work within the framework's constraints while building maintainable, scalable applications.

Entity Creation Patterns

Factory Registry Pattern

Centralize factory creation and access:

const { factory } = require('ecos');

class FactoryRegistry {
    constructor() {
        this.factories = new Map();
    }

    register(name, config) {
        const factoryInstance = factory.create(config);
        this.factories.set(name, factoryInstance);
        return factoryInstance;
    }

    get(name) {
        return this.factories.get(name);
    }

    create(factoryName, options) {
        const factoryInstance = this.get(factoryName);
        if (!factoryInstance) {
            throw new Error(`Factory '${factoryName}' not found`);
        }
        return factoryInstance.create(options);
    }
}

// Usage
const registry = new FactoryRegistry();

registry.register('player', {
    name: 'player',
    props: ['health', 'mana'],
    default: { health: 100, mana: 50 }
});

registry.register('enemy', {
    name: 'enemy',
    props: ['health', 'damage'],
    default: { health: 50, damage: 10 }
});

// Create entities through registry
const player = registry.create('player', { health: 150 });
const enemy = registry.create('enemy');

Configuration-Based Factory Pattern

Define factories using configuration objects:

const { factory, extenders } = require('ecos');

// Define configurations
const entityConfigs = {
    player: {
        name: 'player',
        presets: ['positioned', 'mortal', 'tracked'],
        default: {
            x: 0,
            y: 0,
            health: 100,
            maxHealth: 100
        }
    },
    enemy: {
        name: 'enemy',
        presets: ['positioned', 'mortal'],
        default: {
            x: 0,
            y: 0,
            health: 50,
            maxHealth: 50
        }
    },
    projectile: {
        name: 'projectile',
        presets: ['positioned'],
        default: {
            x: 0,
            y: 0,
            speed: 5
        }
    }
};

// Create factories from config
function createFactories(configs) {
    const factories = {};
    for (const [key, config] of Object.entries(configs)) {
        factories[key] = factory.create(config);
    }
    return factories;
}

const gameFactories = createFactories(entityConfigs);

// Use factories
const player = gameFactories.player.create({ x: 100, y: 100 });
const enemy = gameFactories.enemy.create({ x: 200, y: 200 });

Builder Pattern for Complex Entities

Create a builder for entities with complex initialization:

const { factory, entities } = require('ecos');

class CharacterBuilder {
    constructor(characterFactory) {
        this.factory = characterFactory;
        this.options = {};
    }

    withPosition(x, y) {
        this.options.x = x;
        this.options.y = y;
        return this;
    }

    withStats(health, mana) {
        this.options.health = health;
        this.options.maxHealth = health;
        this.options.mana = mana;
        this.options.maxMana = mana;
        return this;
    }

    withInventory(items) {
        this.options.initialItems = items;
        return this;
    }

    build() {
        const character = this.factory.create(this.options);

        // Post-creation initialization
        if (this.options.initialItems) {
            character.inventory = [...this.options.initialItems];
        }

        return character;
    }
}

// Usage
const characterFactory = factory.create({
    name: 'character',
    props: ['x', 'y', 'health', 'maxHealth', 'mana', 'maxMana', 'inventory'],
    default: {
        x: 0,
        y: 0,
        health: 100,
        maxHealth: 100,
        mana: 50,
        maxMana: 50,
        inventory: []
    }
});

const warrior = new CharacterBuilder(characterFactory)
    .withPosition(50, 50)
    .withStats(150, 30)
    .withInventory(['sword', 'shield'])
    .build();

Property Initialization Patterns

Deferred Initialization Pattern

Initialize computed properties after entity creation:

const { factory } = require('ecos');

factory.registerMethod('initialize', function() {
    // Compute derived values
    this.maxHealth = this.baseHealth * this.vitality;
    this.maxMana = this.baseMana * this.intelligence;

    // Initialize collections
    this.inventory = [];
    this.skills = [];

    // Set computed flags
    this.canFly = this.agility > 15;
    this.canSwim = this.vitality > 10;
});

const characterFactory = factory.create({
    name: 'character',
    props: ['baseHealth', 'baseMana', 'vitality', 'intelligence', 'agility'],
    default: {
        baseHealth: 100,
        baseMana: 50,
        vitality: 10,
        intelligence: 10,
        agility: 10
    },
    methods: ['initialize']
});

// Always call initialize after create
const character = characterFactory.create({ vitality: 15, intelligence: 12 });
character.initialize();

console.log(character.maxHealth); // 1500
console.log(character.maxMana);   // 600

Lazy Initialization Pattern

Initialize properties on first access:

const { factory, extenders } = require('ecos');

factory.registerExtender('lazyInventory', {
    type: extenders.GETSET,
    name: 'inventory',
    get: function() {
        if (!this._inventory) {
            this._inventory = [];
        }
        return this._inventory;
    }
});

factory.registerExtender('lazyMetadata', {
    type: extenders.GETSET,
    name: 'metadata',
    get: function() {
        if (!this._metadata) {
            this._metadata = {};
        }
        return this._metadata;
    }
});

const entityFactory = factory.create({
    name: 'entity',
    extend: ['lazyInventory', 'lazyMetadata']
});

const entity = entityFactory.create();

// No memory used until accessed
console.log(entity.inventory); // [] (created on first access)
entity.inventory.push('item');
console.log(entity.inventory); // ['item']

Template Method Pattern

Define a template for entity initialization:

const { factory } = require('ecos');

factory.registerMethod('postCreate', function() {
    this.onCreate();
    this.initializeCollections();
    this.computeStats();
    this.onReady();
});

factory.registerMethod('onCreate', function() {
    // Override in specific factories
});

factory.registerMethod('initializeCollections', function() {
    this.inventory = [];
    this.buffs = [];
    this.debuffs = [];
});

factory.registerMethod('computeStats', function() {
    this.maxHealth = this.baseHealth * this.vitality;
});

factory.registerMethod('onReady', function() {
    // Override in specific factories
});

// Create base factory
const characterFactory = factory.create({
    name: 'character',
    props: ['baseHealth', 'vitality'],
    default: { baseHealth: 100, vitality: 10 },
    methods: ['postCreate', 'onCreate', 'initializeCollections', 'computeStats', 'onReady']
});

// Usage
const character = characterFactory.create();
character.postCreate();

Entity Relationship Patterns

Parent-Child Pattern

Manage hierarchical relationships:

const { factory, entities } = require('ecos');

factory.registerMethod('addChild', function(childId) {
    if (!this.childIds) {
        this.childIds = [];
    }
    this.childIds.push(childId);
});

factory.registerMethod('removeChild', function(childId) {
    if (!this.childIds) return;

    const index = this.childIds.indexOf(childId);
    if (index > -1) {
        this.childIds.splice(index, 1);
        entities.remove(childId);
    }
});

factory.registerMethod('getChildren', function() {
    if (!this.childIds) return [];
    return this.childIds.map(id => entities.get(id)).filter(Boolean);
});

factory.registerMethod('destroy', function() {
    // Recursively destroy children
    if (this.childIds) {
        this.childIds.forEach(id => {
            const child = entities.get(id);
            if (child && child.destroy) {
                child.destroy();
            }
        });
    }

    // Remove self
    entities.remove(this.id);
});

const nodeFactory = factory.create({
    name: 'node',
    props: ['data', 'childIds'],
    default: { data: null, childIds: [] },
    methods: ['addChild', 'removeChild', 'getChildren', 'destroy']
});

// Usage
const parent = nodeFactory.create({ data: 'root' });
const child1 = nodeFactory.create({ data: 'child1' });
const child2 = nodeFactory.create({ data: 'child2' });

parent.addChild(child1.id);
parent.addChild(child2.id);

console.log(parent.getChildren().length); // 2

// Clean up
parent.destroy(); // Removes parent and all children

Reference Manager Pattern

Manage entity references safely:

const { entities } = require('ecos');

class ReferenceManager {
    constructor() {
        this.references = new Map(); // entity ID -> array of referencing entity IDs
    }

    addReference(fromId, toId) {
        if (!this.references.has(toId)) {
            this.references.set(toId, []);
        }
        this.references.get(toId).push(fromId);
    }

    removeReference(fromId, toId) {
        const refs = this.references.get(toId);
        if (!refs) return;

        const index = refs.indexOf(fromId);
        if (index > -1) {
            refs.splice(index, 1);
        }

        if (refs.length === 0) {
            this.references.delete(toId);
        }
    }

    getReferences(entityId) {
        return this.references.get(entityId) || [];
    }

    canRemove(entityId) {
        const refs = this.getReferences(entityId);
        return refs.length === 0;
    }

    safeRemove(entityId) {
        if (!this.canRemove(entityId)) {
            throw new Error(`Cannot remove entity ${entityId}: still referenced`);
        }
        entities.remove(entityId);
        this.references.delete(entityId);
    }
}

// Usage
const refManager = new ReferenceManager();

const item = itemFactory.create({ name: 'Sword' });
const player = playerFactory.create({ name: 'Hero' });

// Player references item
player.equippedId = item.id;
refManager.addReference(player.id, item.id);

// Try to remove item
try {
    refManager.safeRemove(item.id); // Error: still referenced
} catch (error) {
    console.log(error.message);
}

// Remove reference first
player.equippedId = null;
refManager.removeReference(player.id, item.id);

// Now can remove
refManager.safeRemove(item.id); // Success

Component Pattern

Compose entities from reusable components:

const { factory, entities } = require('ecos');

factory.registerMethod('addComponent', function(componentId) {
    if (!this.componentIds) {
        this.componentIds = [];
    }
    this.componentIds.push(componentId);
});

factory.registerMethod('getComponent', function(type) {
    if (!this.componentIds) return null;

    for (const id of this.componentIds) {
        const component = entities.get(id);
        if (component && component.type === type) {
            return component;
        }
    }
    return null;
});

factory.registerMethod('hasComponent', function(type) {
    return this.getComponent(type) !== null;
});

// Component factories
const positionFactory = factory.create({
    name: 'position',
    props: ['x', 'y'],
    default: { x: 0, y: 0 }
});

const healthFactory = factory.create({
    name: 'health',
    props: ['current', 'max'],
    default: { current: 100, max: 100 }
});

const entityFactory = factory.create({
    name: 'entity',
    props: ['componentIds'],
    default: { componentIds: [] },
    methods: ['addComponent', 'getComponent', 'hasComponent']
});

// Usage
const entity = entityFactory.create();

const position = positionFactory.create({ x: 10, y: 20 });
const health = healthFactory.create({ current: 80, max: 100 });

entity.addComponent(position.id);
entity.addComponent(health.id);

if (entity.hasComponent('position')) {
    const pos = entity.getComponent('position');
    console.log(pos.x, pos.y); // 10, 20
}

State Management Patterns

State Machine Pattern

Implement entity state machines:

const { factory } = require('ecos');

factory.registerMethod('setState', function(newState) {
    const oldState = this.state;

    // Exit old state
    if (this[`onExit${oldState}`]) {
        this[`onExit${oldState}`]();
    }

    // Change state
    this.state = newState;

    // Enter new state
    if (this[`onEnter${newState}`]) {
        this[`onEnter${newState}`]();
    }
});

factory.registerMethod('onEnterIdle', function() {
    console.log('Entering idle state');
    this.speed = 0;
});

factory.registerMethod('onExitIdle', function() {
    console.log('Exiting idle state');
});

factory.registerMethod('onEnterMoving', function() {
    console.log('Entering moving state');
    this.speed = this.baseSpeed;
});

factory.registerMethod('onEnterAttacking', function() {
    console.log('Entering attacking state');
    this.speed = 0;
});

const characterFactory = factory.create({
    name: 'character',
    props: ['state', 'speed', 'baseSpeed'],
    default: {
        state: 'Idle',
        speed: 0,
        baseSpeed: 5
    },
    methods: [
        'setState',
        'onEnterIdle',
        'onExitIdle',
        'onEnterMoving',
        'onEnterAttacking'
    ]
});

// Usage
const character = characterFactory.create();
character.setState('Moving');  // Logs: Exiting idle state, Entering moving state
character.setState('Attacking'); // Entering attacking state

Observer Pattern

Notify observers of entity changes:

const { factory } = require('ecos');

factory.registerMethod('addObserver', function(observer) {
    if (!this.observers) {
        this.observers = [];
    }
    this.observers.push(observer);
});

factory.registerMethod('removeObserver', function(observer) {
    if (!this.observers) return;

    const index = this.observers.indexOf(observer);
    if (index > -1) {
        this.observers.splice(index, 1);
    }
});

factory.registerMethod('notify', function(event, data) {
    if (!this.observers) return;

    this.observers.forEach(observer => {
        if (observer.onNotify) {
            observer.onNotify(this, event, data);
        }
    });
});

factory.registerMethod('takeDamage', function(amount) {
    const oldHealth = this.health;
    this.health = Math.max(0, this.health - amount);

    this.notify('healthChanged', {
        oldHealth,
        newHealth: this.health,
        damage: amount
    });

    if (this.health === 0) {
        this.notify('died', {});
    }
});

const characterFactory = factory.create({
    name: 'character',
    props: ['health', 'observers'],
    default: { health: 100, observers: [] },
    methods: ['addObserver', 'removeObserver', 'notify', 'takeDamage']
});

// Create observer
const healthBar = {
    onNotify(entity, event, data) {
        if (event === 'healthChanged') {
            console.log(`Health: ${data.newHealth}/${entity.health}`);
        } else if (event === 'died') {
            console.log('Character died!');
        }
    }
};

// Usage
const character = characterFactory.create();
character.addObserver(healthBar);

character.takeDamage(30); // Logs: Health: 70/100
character.takeDamage(80); // Logs: Health: 0/100, Character died!

Command Pattern

Encapsulate entity actions as commands:

const { entities } = require('ecos');

class Command {
    execute() {
        throw new Error('execute() must be implemented');
    }

    undo() {
        throw new Error('undo() must be implemented');
    }
}

class MoveCommand extends Command {
    constructor(entityId, dx, dy) {
        super();
        this.entityId = entityId;
        this.dx = dx;
        this.dy = dy;
    }

    execute() {
        const entity = entities.get(this.entityId);
        entity.x += this.dx;
        entity.y += this.dy;
    }

    undo() {
        const entity = entities.get(this.entityId);
        entity.x -= this.dx;
        entity.y -= this.dy;
    }
}

class SetHealthCommand extends Command {
    constructor(entityId, newHealth) {
        super();
        this.entityId = entityId;
        this.newHealth = newHealth;
        this.oldHealth = null;
    }

    execute() {
        const entity = entities.get(this.entityId);
        this.oldHealth = entity.health;
        entity.health = this.newHealth;
    }

    undo() {
        const entity = entities.get(this.entityId);
        entity.health = this.oldHealth;
    }
}

class CommandManager {
    constructor() {
        this.history = [];
        this.currentIndex = -1;
    }

    execute(command) {
        command.execute();

        // Remove any commands after current index
        this.history = this.history.slice(0, this.currentIndex + 1);

        // Add new command
        this.history.push(command);
        this.currentIndex++;
    }

    undo() {
        if (this.currentIndex < 0) return;

        const command = this.history[this.currentIndex];
        command.undo();
        this.currentIndex--;
    }

    redo() {
        if (this.currentIndex >= this.history.length - 1) return;

        this.currentIndex++;
        const command = this.history[this.currentIndex];
        command.execute();
    }
}

// Usage
const manager = new CommandManager();
const entity = entityFactory.create({ x: 0, y: 0, health: 100 });

manager.execute(new MoveCommand(entity.id, 10, 5));
console.log(entity.x, entity.y); // 10, 5

manager.execute(new SetHealthCommand(entity.id, 50));
console.log(entity.health); // 50

manager.undo();
console.log(entity.health); // 100

manager.undo();
console.log(entity.x, entity.y); // 0, 0

manager.redo();
console.log(entity.x, entity.y); // 10, 5

Query and Collection Patterns

Entity Index Pattern

Create indices for efficient entity queries:

const { entities } = require('ecos');

class EntityIndex {
    constructor() {
        this.byType = new Map();
        this.byProperty = new Map();
    }

    add(entity) {
        // Index by type
        if (!this.byType.has(entity.type)) {
            this.byType.set(entity.type, new Set());
        }
        this.byType.get(entity.type).add(entity.id);
    }

    remove(entityId) {
        const entity = entities.get(entityId);
        if (!entity) return;

        // Remove from type index
        const typeSet = this.byType.get(entity.type);
        if (typeSet) {
            typeSet.delete(entityId);
        }
    }

    getByType(type) {
        const ids = this.byType.get(type) || new Set();
        return Array.from(ids).map(id => entities.get(id)).filter(Boolean);
    }

    indexProperty(entityId, propertyName, value) {
        const key = `${propertyName}:${value}`;

        if (!this.byProperty.has(key)) {
            this.byProperty.set(key, new Set());
        }

        this.byProperty.get(key).add(entityId);
    }

    getByProperty(propertyName, value) {
        const key = `${propertyName}:${value}`;
        const ids = this.byProperty.get(key) || new Set();
        return Array.from(ids).map(id => entities.get(id)).filter(Boolean);
    }
}

// Usage
const index = new EntityIndex();

const player = playerFactory.create({ team: 'red' });
const enemy1 = enemyFactory.create({ team: 'blue' });
const enemy2 = enemyFactory.create({ team: 'blue' });

index.add(player);
index.add(enemy1);
index.add(enemy2);

index.indexProperty(player.id, 'team', 'red');
index.indexProperty(enemy1.id, 'team', 'blue');
index.indexProperty(enemy2.id, 'team', 'blue');

// Query
const players = index.getByType('player');
const enemies = index.getByType('enemy');
const blueTeam = index.getByProperty('team', 'blue');

console.log(players.length);  // 1
console.log(enemies.length);  // 2
console.log(blueTeam.length); // 2

Pool Pattern

Reuse entities to reduce garbage collection:

const { entities } = require('ecos');

class EntityPool {
    constructor(factory, initialSize = 10) {
        this.factory = factory;
        this.available = [];
        this.active = new Set();

        // Pre-create entities
        for (let i = 0; i < initialSize; i++) {
            const entity = factory.create();
            this.available.push(entity.id);
        }
    }

    acquire(options = {}) {
        let entityId;

        if (this.available.length > 0) {
            // Reuse existing entity
            entityId = this.available.pop();
        } else {
            // Create new entity
            const entity = this.factory.create();
            entityId = entity.id;
        }

        // Reset and configure entity
        const entity = entities.get(entityId);
        Object.assign(entity, options);

        this.active.add(entityId);
        return entity;
    }

    release(entityId) {
        if (!this.active.has(entityId)) return;

        this.active.delete(entityId);
        this.available.push(entityId);

        // Reset entity state
        const entity = entities.get(entityId);
        // Reset properties to defaults...
    }

    clear() {
        this.active.forEach(id => this.release(id));
    }
}

// Usage
const projectileFactory = factory.create({
    name: 'projectile',
    props: ['x', 'y', 'vx', 'vy', 'active'],
    default: { x: 0, y: 0, vx: 0, vy: 0, active: false }
});

const pool = new EntityPool(projectileFactory, 50);

// Acquire projectile
const projectile = pool.acquire({ x: 100, y: 100, vx: 5, vy: 0, active: true });

// Use projectile...

// Release back to pool
pool.release(projectile.id);

See Also

  • Execution Order - Understanding entity creation flow
  • Constraints - Working within ECOS limitations
  • Working with Extenders - Extender usage patterns
  • Using Presets - Preset composition patterns