wolf-ecs
Version:
An entity component system framework for JavaScript and TypeScript
339 lines (334 loc) • 10.5 kB
JavaScript
function custom(init) {
return init ? [init] : [];
}
const types = {
custom, any: Array,
int8: Int8Array, i8: Int8Array, char: Int8Array,
uint8: Uint8Array, u8: Uint8Array, uchar: Uint8Array,
int16: Int16Array, i16: Int16Array, short: Int16Array,
uint16: Uint16Array, u16: Uint16Array, ushort: Uint16Array,
int32: Int32Array, i32: Int32Array, int: Int32Array,
uint32: Uint32Array, u32: Uint32Array, uint: Uint32Array,
float32: Float32Array, f32: Float32Array, float: Float32Array,
float64: Float64Array, f64: Float64Array, double: Float64Array,
int64: BigInt64Array, bigint64: BigInt64Array, i64: BigInt64Array, long: BigInt64Array,
uint64: BigUint64Array, biguint64: BigUint64Array, u64: BigUint64Array, ulong: BigUint64Array,
};
const _componentData = Symbol("componentData");
function createComponentArray(def, max) {
if (typeof def === "function") {
return new def(max);
}
if (def instanceof Array) {
if (def.length) {
return [...new Array(max)].map(def[0]);
}
return new Array(max);
}
const ret = {};
for (let i in def) {
ret[i] = createComponentArray(def[i], max);
}
return ret;
}
function all(...cmps) {
if (!cmps.length) {
throw new Error("no arguments passed");
}
return { op: all, dt: cmps };
}
function not(cmp) {
return { op: not, dt: typeof cmp.op === "function" ? cmp : all(cmp) };
}
function any(...cmps) {
if (!cmps.length) {
throw new Error("no arguments passed");
}
return { op: any, dt: cmps };
}
class Query {
mask;
archetypes = [];
a = this.archetypes;
ecs;
constructor(ecs, q) {
const crQuery = (raw) => {
if (raw.op === not) {
return { op: raw.op, dt: crQuery(raw.dt) };
}
const nums = [];
const ret = [{ op: raw.op, dt: new Uint32Array() }];
for (let i of raw.dt) {
if (_componentData in i) {
if (i[_componentData].ecs === ecs) {
nums.push(i[_componentData].id);
}
else {
throw new Error("component does not belong to this ECS");
}
}
else {
ret.push(crQuery(i));
}
}
ret[0].dt = new Uint32Array(Math.ceil((Math.max(-1, ...nums) + 1) / 32));
for (let i of nums) {
ret[0].dt[Math.floor(i / 32)] |= 1 << i % 32;
}
return { op: raw.op, dt: ret };
};
this.mask = q ? crQuery(q) : { op: all, dt: new Uint32Array() };
this.ecs = ecs;
}
forEach(callbackfn) {
for (let i = 0, l = this.a.length; i < l; i++) {
const ent = this.a[i].e;
for (let j = ent.length; j > 0; j--) {
callbackfn(ent[j - 1], this.ecs);
}
}
this.ecs.destroyPending();
this.ecs.updatePending();
}
_forEach(callbackfn) {
this.forEach(callbackfn);
}
static match(target, mask) {
if ("BYTES_PER_ELEMENT" in mask.dt) {
return Query.partial(target, mask);
}
if (mask.op === not) {
return !Query.match(target, mask.dt);
}
if (mask.op === all) {
for (let q of mask.dt) {
if (!Query.match(target, q)) {
return false;
}
}
return true;
}
for (let q of mask.dt) {
if (Query.match(target, q)) {
return true;
}
}
return false;
}
static partial(target, mask) {
if (mask.op === all) {
for (let i = 0; i < mask.dt.length; i++) {
if ((target[i] & mask.dt[i]) < mask.dt[i]) {
return false;
}
}
return true;
}
for (let i = 0; i < mask.dt.length; i++) {
if ((target[i] & mask.dt[i]) > 0) {
return true;
}
}
return false;
}
}
class SparseSet {
packed = [];
sparse = [];
has(x) {
return this.sparse[x] < this.packed.length && this.packed[this.sparse[x]] === x;
}
add(x) {
if (!this.has(x)) {
this.sparse[x] = this.packed.length;
this.packed.push(x);
}
}
remove(x) {
if (this.has(x)) {
const last = this.packed.pop();
if (x !== last) {
this.sparse[last] = this.sparse[x];
this.packed[this.sparse[x]] = last;
}
}
}
}
class Archetype {
sset = new SparseSet();
entities = this.sset.packed;
e = this.entities;
mask;
change = [];
constructor(mask) {
this.mask = mask;
}
has(x) {
return this.sset.has(x);
}
}
class ECS {
_arch = new Map();
_queries = [];
_ent = [];
_updateTo = [];
_toUpdate = new SparseSet();
_toDestroy = new SparseSet();
_rm = new SparseSet();
_empty = new Archetype(new Uint32Array());
cmpID = 0;
entID = 0;
MAX_ENTITIES;
DEFAULT_DEFER;
constructor(max = 1e4, defer = false) {
this.MAX_ENTITIES = max;
this.DEFAULT_DEFER = defer;
}
bind() {
const proto = ECS.prototype;
const ret = {};
for (let i of Object.getOwnPropertyNames(proto)) {
if (typeof proto[i] === "function" && i !== "bind") {
ret[i] = proto[i].bind(this);
}
}
return ret;
}
defineComponent(def = {}) {
if (this.entID) {
throw new Error("cannot define component after entity creation");
}
return this.registerComponent(createComponentArray(def, this.MAX_ENTITIES));
}
registerComponent(cmp) {
return Object.assign(cmp, { [_componentData]: { ecs: this, id: this.cmpID++ } });
}
createQuery(...raw) {
const query = new Query(this, all(...raw));
this._arch.forEach(i => { if (Query.match(i.mask, query.mask)) {
query.a.push(i);
} });
this._queries.push(query);
return query;
}
_validID(id) {
if (typeof id !== "number") {
return false;
}
return !(this._rm.has(id) || this.entID <= id);
}
_getArch(mask) {
if (!this._arch.has(mask.toString())) {
const arch = new Archetype(mask.slice());
this._arch.set(mask.toString(), arch);
for (let q of this._queries) {
if (Query.match(mask, q.mask)) {
q.a.push(arch);
}
}
}
return this._arch.get(mask.toString());
}
_hasComponent(mask, i) {
return mask[~~(i / 32)] & (1 << i % 32);
}
_archChange(arch, i) {
if (!arch.change[i]) {
arch.mask[~~(i / 32)] ^= 1 << i % 32;
arch.change[i] = this._getArch(arch.mask);
arch.mask[~~(i / 32)] ^= 1 << i % 32;
}
return arch.change[i];
}
_crEnt(id) {
this._ent[id] = this._updateTo[id] = this._empty;
this._empty.sset.add(id);
}
createEntity() {
if (this._rm.packed.length) {
const id = this._rm.packed.pop();
this._crEnt(id);
return id;
}
else {
if (!this.entID) {
this._empty.mask = new Uint32Array(Math.ceil(this.cmpID / 32));
this._arch.set(this._empty.mask.toString(), this._empty);
}
if (this.entID === this.MAX_ENTITIES) {
throw new Error("maximum entity limit reached");
}
this._crEnt(this.entID);
return this.entID++;
}
}
destroyEntity(id, defer = this.DEFAULT_DEFER) {
if (defer) {
this._toDestroy.add(id);
}
else {
this._ent[id].sset.remove(id);
this._toDestroy.remove(id);
this._rm.add(id);
}
}
destroyPending() {
while (this._toDestroy.packed.length > 0) {
this.destroyEntity(this._toDestroy.packed[0]);
}
this._toDestroy.packed.length = 0;
}
addComponent(id, cmp, defer = this.DEFAULT_DEFER) {
if (!this._validID(id)) {
throw new Error("invalid entity id");
}
const i = cmp[_componentData].id;
if (defer) {
this._toUpdate.add(id);
}
else {
if (!this._hasComponent(this._ent[id].mask, i)) {
this._ent[id].sset.remove(id);
this._ent[id] = this._archChange(this._ent[id], i);
this._ent[id].sset.add(id);
}
}
if (!this._hasComponent(this._updateTo[id].mask, i)) {
this._updateTo[id] = this._archChange(this._updateTo[id], i);
}
return this;
}
removeComponent(id, cmp, defer = this.DEFAULT_DEFER) {
if (!this._validID(id)) {
throw new Error("invalid entity id");
}
const i = cmp[_componentData].id;
if (defer) {
this._toUpdate.add(id);
}
else {
if (this._hasComponent(this._ent[id].mask, i)) {
this._ent[id].sset.remove(id);
this._ent[id] = this._archChange(this._ent[id], i);
this._ent[id].sset.add(id);
}
}
if (this._hasComponent(this._updateTo[id].mask, i)) {
this._updateTo[id] = this._archChange(this._updateTo[id], i);
}
return this;
}
updatePending() {
const arr = this._toUpdate.packed;
for (let i = 0; i < arr.length; i++) {
const id = arr[i];
if (this._validID(id)) {
this._ent[id].sset.remove(id);
this._ent[id] = this._updateTo[id];
this._ent[id].sset.add(id);
}
}
this._toUpdate.packed = [];
}
}
export { ECS, all, any, not, types };