UNPKG

super-ecs

Version:
431 lines (424 loc) 11.2 kB
// src/entity.ts import { Subject } from "rxjs"; var entityId = 0; var Entity = class { /** * unique id of entity */ id; _componentAddedSubject$ = new Subject(); _componentRemovedSubject$ = new Subject(); _componentMap = /* @__PURE__ */ new Map(); constructor() { this.id = entityId++; } /** * Check if this entity has a component by name. * @param componentName */ hasComponent(componentName) { return this._componentMap.has(componentName); } /** * Get a component of this entity by name. * @param componentName */ getComponent(componentName) { const component = this._componentMap.get(componentName); if (typeof component !== "undefined") { return component; } return void 0; } /** * Add a component to this entity. * @param component */ addComponent(component) { this._componentMap.set(component.name, component); this._componentAddedSubject$.next(component); return this; } /** * Remove a component from this entity by name. * @param componentName */ removeComponent(componentName) { const component = this._componentMap.get(componentName); if (typeof component !== "undefined") { this._componentMap.delete(componentName); this._componentRemovedSubject$.next(component); } return this; } /** * Stream triggered when a component is added * Note: make sure to unsubscribe. If needed, use `DisposeBag` util */ get componentAdded$() { return this._componentAddedSubject$.asObservable(); } /** * Stream triggered when a component is removed * Note: make sure to unsubscribe. If needed, use `DisposeBag` util */ get componentRemoved$() { return this._componentRemovedSubject$.asObservable(); } }; // src/system.ts var System = class { _world; /** * Called when system is added to the world * @param world */ addedToWorld(world) { this._world = world; } /** * Called when system is removed from the world * @param world */ removedFromWorld(world) { this._world = void 0; } /** * Update loop * @param tickerData */ update(tickerData) { } /** * Reference to the world */ get world() { if (!this._world) { throw Error("wait till the system is added to the world"); } return this._world; } }; // src/world/world.ts import { Subject as Subject3, takeUntil } from "rxjs"; // src/world/entity-node.ts var EntityNode = class { entity; prev = null; next = null; constructor(entity) { this.entity = entity; } }; // src/world/entity-list.ts var EntityList = class { length = 0; head = null; tail = null; _entityMap = /* @__PURE__ */ new Map(); constructor() { } /** * Add an entity into this list. * @param entity */ add(entity) { const node = new EntityNode(entity); if (this.head === null) { this.head = this.tail = node; } else { node.prev = this.tail; if (this.tail) { this.tail.next = node; } this.tail = node; } this.length += 1; this._entityMap.set(entity.id, node); } /** * Remove an entity from this list. * @param entity */ remove(entity) { const entityId2 = entity.id; const node = this._entityMap.get(entityId2); if (typeof node === "undefined") { return; } if (node.prev === null) { this.head = node.next; } else { node.prev.next = node.next; } if (node.next === null) { this.tail = node.prev; } else { node.next.prev = node.prev; } this.length -= 1; this._entityMap.delete(entityId2); } /** * Check if this list has the entity * @param entity */ has(entity) { return this._entityMap.has(entity.id); } /** * Remove all the entities from this list. */ clear() { this.head = this.tail = null; this.length = 0; this._entityMap.clear(); } /** * Return an array holding all the entities in this list. */ toArray() { let array = [], node; for (node = this.head; node; node = node.next) { array.push(node.entity); } return array; } }; // src/world/family.ts import { Subject as Subject2 } from "rxjs"; var Family = class { _componentNames; _entityAddedSubject$ = new Subject2(); _entityRemovedSubject$ = new Subject2(); _entities = new EntityList(); constructor(componentNames) { this._componentNames = componentNames; } /** * Get the entities of this family. */ getEntities() { return this._entities.toArray(); } /** * Add the entity into the family if match. * @param entity */ addEntityIfMatch(entity) { if (!this._entities.has(entity) && this.matchEntity(entity)) { this._entities.add(entity); this._entityAddedSubject$.next(entity); } } /** * Remove the entity into the family if match. * @param entity */ removeEntity(entity) { if (this._entities.has(entity)) { this._entities.remove(entity); this._entityRemovedSubject$.next(entity); } } onComponentAdded(entity, component) { this.addEntityIfMatch(entity); } onComponentRemoved(entity, component) { if (!this._entities.has(entity)) { return; } const names = this._componentNames; const len = names.length; for (let i = 0; i < len; ++i) { if (names[i] === component.name) { this._entities.remove(entity); this._entityRemovedSubject$.next(entity); } } } /** * Note: make sure to unsubscribe. If needed, use `DisposeBag` util */ get entityAdded$() { return this._entityAddedSubject$.asObservable(); } /** * Note: make sure to unsubscribe. If needed, use `DisposeBag` util */ get entityRemoved$() { return this._entityRemovedSubject$.asObservable(); } matchEntity(entity) { const componentNames = this._componentNames; const len = componentNames.length; for (let i = 0; i < len; ++i) { if (!entity.hasComponent(componentNames[i])) { return false; } } return true; } }; // src/world/world.ts var World = class { _systems = []; _entities = new EntityList(); _families = /* @__PURE__ */ new Map(); _disposeEntityMap = /* @__PURE__ */ new Map(); constructor() { } /** * Add a system to this world. * @param system */ addSystem(system) { this._systems.push(system); system.addedToWorld(this); return this; } /** * Remove a system from this world. * @param system */ removeSystem(system) { const systems = this._systems; const len = systems.length; for (let i = 0; i < len; ++i) { if (systems[i] === system) { systems.splice(i, 1); system.removedFromWorld(this); } } return this; } /** * Remove all systems from this world */ removeAllSystems() { this._systems.forEach((system) => system.removedFromWorld(this)); this._systems.length = 0; return this; } /** * Add an entity to this world. * @param entity */ addEntity(entity) { this._families.forEach((family) => family.addEntityIfMatch(entity)); const dispose$ = new Subject3(); this._disposeEntityMap.set(entity, dispose$); const { componentAdded$, componentRemoved$ } = entity; componentAdded$.pipe(takeUntil(dispose$)).subscribe((component) => this.onComponentAdded(entity, component)); componentRemoved$.pipe(takeUntil(dispose$)).subscribe((component) => this.onComponentRemoved(entity, component)); this._entities.add(entity); return this; } /** * Remove and entity from this world. * @param entity */ removeEntity(entity) { this._families.forEach((family) => family.removeEntity(entity)); this._entities.remove(entity); const dispose$ = this._disposeEntityMap.get(entity); if (typeof dispose$ !== "undefined") { dispose$.next(); this._disposeEntityMap.delete(entity); } return this; } /** * Removes all entities */ removeAllEntities() { this._entities.toArray().forEach((entity) => this.removeEntity(entity)); return this; } /** * Get the entities having all the specified components. * @param componentNames */ getEntities(componentNames) { const familyId = this.generateFamilyId(componentNames); this.ensureFamilyExists(componentNames, familyId); const family = this._families.get(familyId); if (typeof family !== "undefined") { return family.getEntities(); } throw Error(`unable to getEntities, ${componentNames}`); } /** * For each system in the world, call its `update` method. * @param tickerData */ update(tickerData) { const systems = this._systems; const len = systems.length; for (let i = 0; i < len; ++i) { systems[i].update(tickerData); } } /** * Returns the Observable for entities added with the specified components. The * Observable is also emitted when a component is added to an entity causing it * match the specified component names. * Note: make sure to unsubscribe. If needed, use `DisposeBag` util * @param componentNames */ entityAdded$(componentNames) { const familyId = this.generateFamilyId(componentNames); this.ensureFamilyExists(componentNames, familyId); const family = this._families.get(familyId); if (typeof family !== "undefined") { return family.entityAdded$; } throw Error(`unable to perform entityAdded, ${componentNames}`); } /** * Returns the Observable for entities removed with the specified components. * The Observable is also emitted when a component is removed from an entity * causing it to no longer match the specified component names. * Note: make sure to unsubscribe. If needed, use `DisposeBag` util * @param componentNames */ entityRemoved$(componentNames) { const familyId = this.generateFamilyId(componentNames); this.ensureFamilyExists(componentNames, familyId); const family = this._families.get(familyId); if (typeof family !== "undefined") { return family.entityRemoved$; } throw Error(`unable to perform entityRemoved, ${componentNames}`); } generateFamilyId(componentNames) { const keys = componentNames.map((data) => data.toString()); return `$-${keys.join(",")}`; } ensureFamilyExists(componentNames, familyId) { const families = this._families; if (families.has(familyId)) { return; } const family = new Family([...componentNames]); families.set(familyId, family); for (let node = this._entities.head; node; node = node.next) { const family2 = families.get(familyId); if (typeof family2 !== "undefined") { family2.addEntityIfMatch(node.entity); } } } onComponentAdded(entity, component) { this._families.forEach((family) => family.onComponentAdded(entity, component)); } onComponentRemoved(entity, component) { this._families.forEach((family) => family.onComponentRemoved(entity, component)); } }; export { Entity, System, World }; //# sourceMappingURL=super-ecs.js.map