UNPKG

wolf-ecs

Version:

An entity component system framework for JavaScript and TypeScript

351 lines (346 loc) 10.8 kB
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 = []; 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.archetypes.length; i < l; i++) { const ent = this.archetypes[i].entities; for (let j = ent.length; j > 0; j--) { callbackfn(ent[j - 1], this.ecs); } } } _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; mask; change = []; constructor(mask) { this.mask = mask; } has(x) { return this.sset.has(x); } } class ECS { _arch = new Map(); _ent = []; _queries = []; _destroy; _mcmp = { addrm: [], ent: [], cmp: [] }; _rm; _empty; cmpID = 0; entID = 0; MAX_ENTITIES; DEFAULT_DEFER; constructor(max = 1e4, defer = false) { this.MAX_ENTITIES = max; this.DEFAULT_DEFER = defer; this._destroy = new SparseSet(); this._rm = new SparseSet(); this._empty = new Archetype(new Uint32Array()); } 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++ } }); } _initEmpty() { this._empty.mask = new Uint32Array(Math.ceil(this.cmpID / 32)); this._arch.set(this._empty.mask.toString(), this._empty); } createQuery(...raw) { const query = new Query(this, all(...raw)); this._arch.forEach(i => { if (Query.match(i.mask, query.mask)) { query.archetypes.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.archetypes.push(arch); } } } return this._arch.get(mask.toString()); } _hasComponent(mask, i) { return mask[Math.floor(i / 32)] & (1 << i % 32); } _archChange(id, i) { const arch = this._ent[id]; arch.sset.remove(id); if (!arch.change[i]) { if (this._hasComponent(arch.mask, i)) { arch.mask[Math.floor(i / 32)] &= ~(1 << i % 32); arch.change[i] = this._getArch(arch.mask); arch.mask[Math.floor(i / 32)] |= 1 << i % 32; } else { arch.mask[Math.floor(i / 32)] |= 1 << i % 32; arch.change[i] = this._getArch(arch.mask); arch.mask[Math.floor(i / 32)] &= ~(1 << i % 32); } } this._ent[id] = arch.change[i]; this._ent[id].sset.add(id); } _crEnt(id) { this._ent[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._initEmpty(); } 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._destroy.add(id); } else { this._ent[id].sset.remove(id); this._destroy.remove(id); this._rm.add(id); } } destroyPending() { while (this._destroy.packed.length > 0) { this.destroyEntity(this._destroy.packed[0]); } this._destroy.packed.length = 0; } _addcmp(id, cmp) { if (!this._hasComponent(this._ent[id].mask, cmp)) { this._archChange(id, cmp); } } 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._mcmp.addrm.push(true); this._mcmp.ent.push(id); this._mcmp.cmp.push(i); } else { this._addcmp(id, i); } return this; } _rmcmp(id, cmp) { if (this._hasComponent(this._ent[id].mask, cmp)) { this._archChange(id, cmp); } } 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._mcmp.addrm.push(false); this._mcmp.ent.push(id); this._mcmp.cmp.push(i); } else { this._rmcmp(id, i); } return this; } updatePending() { for (let i = this._mcmp.addrm.length - 1; i >= 0; i--) { if (this._validID(this._mcmp.ent[i])) { if (this._mcmp.addrm[i]) { this._addcmp(this._mcmp.ent[i], this._mcmp.cmp[i]); } else { this._rmcmp(this._mcmp.ent[i], this._mcmp.cmp[i]); } } } this._mcmp = { addrm: [], ent: [], cmp: [] }; } } export { ECS, all, any, not, types };