UNPKG

@thi.ng/ecs

Version:

Entity Component System based around typed arrays & sparse sets

183 lines (182 loc) 4.64 kB
import { intersectionR } from "@thi.ng/associative/intersection"; import { assert } from "@thi.ng/errors/assert"; import { map } from "@thi.ng/transducers/map"; import { transduce } from "@thi.ng/transducers/transduce"; import { EVENT_ADDED, EVENT_CHANGED, EVENT_PRE_DELETE } from "../api.js"; import { UnboundedCache } from "../caches/unbounded.js"; import { ObjectComponent } from "../components/object-component.js"; import { LOGGER } from "../logger.js"; class Group { id; components; owned; ids; n; info; cache; constructor(comps, owned = comps, opts) { this.components = comps; this.ids = /* @__PURE__ */ new Set(); this.n = 0; this.id = opts.id; this.cache = opts.cache || new UnboundedCache(); this.info = comps.reduce((acc, c) => { acc[c.id] = { values: c.vals, size: c.size, stride: c.stride }; return acc; }, {}); owned.forEach((c) => { assert( comps.includes(c), `owned component ${c.id} not in given list` ); assert( !c.owner, () => `component ${c.id} already owned by ${c.owner.id}` ); c.owner = this; }); this.owned = owned; this.addExisting(); this.addRemoveListeners(true); } release() { this.addRemoveListeners(false); this.cache.release(); } has(id) { return this.ids.has(id); } values() { return this.isFullyOwning() ? this.ownedValues() : this.nonOwnedValues(); } getIndex(i) { this.ensureFullyOwning(); return i < this.n ? this.getEntityUnsafe(this.components[0].dense[i]) : void 0; } getEntity(id) { return this.has(id) ? this.getEntityUnsafe(id) : void 0; } getEntityUnsafe(id) { return this.cache.getSet(id, () => { const tuple = { id }; const comps = this.components; for (let j = comps.length; j-- > 0; ) { const c = comps[j]; tuple[c.id] = c.getIndex(c.sparse[id]); } return tuple; }); } run(fn, ...args) { this.ensureFullyOwning(); fn(this.info, this.n, ...args); } forEachRaw(fn, ...args) { this.ensureFullyOwning(); const info = this.info; const ref = this.components[0].dense; for (let i = 0, n = this.n; i < n; i++) { fn(info, ref[i], i, ...args); } } forEach(fn, ...args) { let i = 0; for (let id of this.ids) { fn(this.getEntityUnsafe(id), i++, ...args); } } isFullyOwning() { return this.owned.length === this.components.length; } isValidID(id) { for (let comp of this.components) { if (!comp.has(id)) return false; } return true; } onAddListener(e) { LOGGER.debug(`add ${e.target.id}: ${e.value}`); this.addID(e.value); } onDeleteListener(e) { LOGGER.debug(`delete ${e.target.id}: ${e.value}`); this.removeID(e.value); } onChangeListener(e) { if (e.target instanceof ObjectComponent) { LOGGER.debug(`invalidate ${e.target.id}: ${e.value}`); this.cache.delete(e.value); } } addExisting() { const existing = transduce( map((c) => c.keys()), intersectionR(), this.components ); for (let id of existing) { this.addID(id, false); } } addID(id, validate = true) { if (validate && !this.isValidID(id)) return; this.ids.add(id); this.reorderOwned(id, this.n++); } removeID(id, validate = true) { if (validate && !this.isValidID(id)) return; this.ids.delete(id); this.reorderOwned(id, --this.n); } reorderOwned(id, n) { const owned = this.owned; if (!owned.length) return; const id2 = owned[0].dense[n]; let swapped = false; for (let i = owned.length; i-- > 0; ) { const comp = owned[i]; swapped = comp.swapIndices(comp.sparse[id], n) || swapped; } if (swapped) { this.cache.delete(id); this.cache.delete(id2); } } *ownedValues() { const comps = this.components; const ref = comps[0].dense; for (let i = this.n; i-- > 0; ) { yield this.getEntityUnsafe(ref[i]); } } *nonOwnedValues() { for (let id of this.ids) { yield this.getEntityUnsafe(id); } } ensureFullyOwning() { assert( this.isFullyOwning(), `group ${this.id} isn't fully owning its components` ); } addRemoveListeners(add) { const f = add ? "addListener" : "removeListener"; this.components.forEach((comp) => { comp[f](EVENT_ADDED, this.onAddListener, this); comp[f](EVENT_PRE_DELETE, this.onDeleteListener, this); comp[f](EVENT_CHANGED, this.onChangeListener, this); }); } } export { Group };