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)
}
}