@thi.ng/ecs
Version:
Entity Component System based around typed arrays & sparse sets
183 lines (182 loc) • 4.64 kB
JavaScript
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
};