ecs/EntityComponentSystem.js

import EventEmitter from "../events/EventEmitter.js";

/**
 * An entity component system holds entites and systems  
 * Each entity can have zero or more components which hold the data for a certain "feature"
 * A system can then query for all entities with a certain set of components and act on the entities by modifying the data inside the entities component instances
 * 
 * When an entity for example has a component 'position' that means it has a component instance with the name 'position' that holds a vector, each entity that has the component 'position' has an own component instance
 */
export default class EntityComponentSystem extends EventEmitter {
    constructor() {
        super();

        this.entities = new Map();
        this.systems = [];

        //cache that holds [component name -> ids of all entities with that component]
        this.component_map = new Map();

        //cache that holds [component names -> ids of all entities with these component names]
        this.lookup_cache = new Map();

        //event emitter filter
        this.__event_filter = (event, handler, data) => {
            if (!data.entity || !handler.system) return;
            return handler.system.entities.has(data.entity.id);
        };
    }

    /**
     * Add an entity to the system
     * @param {Entity} entity 
     */
    add(entity) {
        entity.ecs = this;
        this.entities.set(entity.id, entity);

        //if the entity already has components, update the lookup maps
        this.addedComponentToEntity(entity, [...entity.components.values()]);

        this.updateSystems();
    }

    /**
     * Remove an entity from the system
     * @param {*} id the ID of the entity that should be deleted 
     */
    delete(id) {
        let entity = this.entities.get(id);
        if (!entity) return;

        //remove the entity id from the component lookup maps
        entity.components.forEach(c => {
            this.removedComponentFromEntity(entity, c);
        });

        //remove the entity from the system and the system from the entity
        delete entity.ecs;
        this.entities.delete(id);
    }

    /**
     * Get all entities with the given component(s)
     * @param {String|String[]} component_names 
     * @returns {Map<String, Entity>} all entities that have ALL requested component_names
     */
    get(component_names) {
        if (!Array.isArray(component_names)) component_names = [component_names];

        let ids;

        let cacheKey = component_names.join(";");

        if (this.lookup_cache.has(cacheKey)) {
            ids = this.lookup_cache.get(cacheKey);
        } else {
            let sets = [];

            component_names.forEach(name => {
                if (!this.component_map.has(name)) return;
                sets.push(this.component_map.get(name));
            });

            if (sets.length < 1) {
                ids = [];
            } else {
                let min_size = sets[0].length;
                let min_set_index = 0;

                for (let i = 1; i < sets.length; i++) {
                    const size = sets[i].size;
                    if (size < min_size) {
                        min_size = size;
                        min_set_index = i;
                    }
                }

                const result = new Set(sets[min_set_index]);
                for (let i = 1; i < sets.length && i != min_set_index; i++) {
                    for (const v of result) {
                        if (!sets[i].has(v)) {
                            result.delete(v);
                        }
                    }
                }

                ids = [...result];
            }

            this.lookup_cache.set(cacheKey, ids);
        }

        let entities = new Map();
        ids.forEach(id => {
            entities.set(id, this.entities.get(id));
        });

        return entities;
    }

    addedComponentToEntity(entity, components) {
        if (!Array.isArray(components)) {
            components = [components];
        }

        components.forEach(component => {
            if (!this.component_map.has(component.name)) {
                this.component_map.set(component.name, new Set());
            }

            this.component_map.get(component.name).add(entity.id);
        })

        this.updateSystems();
    }

    removedComponentFromEntity(entity, component) {
        this.component_map.get(component.name).delete(entity.id);
        this.updateSystems();
    }

    update() {
        this.systems.forEach(s => {
            s.update(this.get(s.component_names))
        });
    }

    updateSystems() {
        this.lookup_cache.clear();

        this.systems.forEach(s => {
            s.entities = this.get(s.component_names);
        });
    }

    /**
     * 
     * @param {System} system
     */
    addSystem(system) {
        this.systems.push(system);

        this.systems.sort((a, b) => {
            return b.priority - a.priority
        });

        system.entities = this.get(system.component_names);
        system.ecs = this;
        system.init();
    }

    on(event, system, handler) {
        handler.system = system;
        super.on(event, handler)
    }
}