super-ecs
Version:
Entity Component System library
431 lines (424 loc) • 11.2 kB
JavaScript
// 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