diff --git a/src/ecs.js b/src/ecs.js new file mode 100644 index 0000000..03be401 --- /dev/null +++ b/src/ecs.js @@ -0,0 +1,124 @@ +import { TreapSet } from "./misc/treap.js"; + +export class EntityRegistry { + constructor() { + this.free_ids = new TreapSet(); + this.signatures = []; + this.components = []; + this.component_map = new Map(); + } + + requireComponentList(cname) { + if (!this.component_map.has(cname)) { + this.component_map.set(cname, this.components.length); + this.components.push([]); + } + return this.components[this.component_map.get(cname)]; + } + + componentListLowerBound(cl, entity) { + if (cl.length === 0) { + return 0; + } else if (cl[cl.length - 1].__owner_entity < entity) { + return cl.length; + } + + let l = 0, r = cl.length - 1, p = 0; + while (l <= r) { + let mid = (l + r) >>> 1; + if (cl[mid].__owner_entity < entity) { + l = mid + 1; + } else { + p = mid; + r = mid - 1; + } + } + return p; + } + + assign(entity, cv) { + const cname = cv.constructor.name; + const cl = this.requireComponentList(cname); + + cv.__owner_entity = entity; + const p = this.componentListLowerBound(cl, entity); + + if (cl[p]?.__owner_entity === entity) { + cl[p] = cv; + return this; + } + + cl.splice(p, 0, cv); + this.signatures[entity][this.component_map.get(cname)] = p; + return this; + } + + create() { + let id = this.free_ids.takeInstance(); + if (id == null) { + id = this.signatures.length; + } else { + this.free_ids.erase(id); + } + + this.signatures[id] = []; + return id; + } + + destory(entity) { + for (let i in this.signatures[entity]) { + const p = this.signatures[entity][i]; + this.components[i].splice(p, 1); + } + + this.signatures[entity] = undefined; + this.free_ids.insertRaw(entity); + return this; + } + + getRaw(entity, cid) { + return this.components[cid][this.signatures[entity][cid]]; + } + + get(entity, ctype) { + if (!this.component_map.has(ctype.name)) { + return null; + } + + const cid = this.component_map.get(ctype.name); + if (this.signatures[entity][cid] == null) { + return null; + } + return this.getRaw(entity, cid); + } + + forEach(rc, func) { + if (rc.length === 0) { + for (let i = 0; i < this.signatures.length; ++i) { + if (this.signatures[i]) { + func.call(this, i); + } + } + return this; + } + + const rcid = rc.map(ctype => this.component_map.get(ctype.name)); + + let p, v = Infinity; + for (let cid of rcid) { + const cl = this.components[cid]; + if (cl.length < v) { + v = cl.length; + p = cid; + } + } + + for (let c of this.components[p]) { + const e = c.__owner_entity; + if (rcid.every(cid => this.signatures[e][cid] != null)) { + func.apply(this, [e].concat(rcid.map(cid => this.getRaw(e, cid)))); + } + } + return this; + } +}; \ No newline at end of file diff --git a/tests/ecs.test.js b/tests/ecs.test.js new file mode 100644 index 0000000..0df5cbe --- /dev/null +++ b/tests/ecs.test.js @@ -0,0 +1,59 @@ +import assert from 'node:assert'; +import { EntityRegistry } from '../src/ecs.js'; + +class Position { + constructor(x, y) { + this.x = x; + this.y = y; + } +} + +describe('ECS', function () { + it('create', function () { + let registry = new EntityRegistry(); + let id = registry.create(); + }); + + it('destory', function () { + let registry = new EntityRegistry(); + let id0 = registry.create(); + let id1 = registry.destory(id0).create(); + assert.equal(id0, id1); + }); + + it('component', function () { + let registry = new EntityRegistry(); + let id3; + for (let i = 0; i < 5; i += 1) { + let e = registry.create(); + registry.assign(e, new Position(i, i)); + if (i == 3) { + id3 = e; + } + } + assert.equal(registry.get(id3, Position).x, 3); + }); + + it('forEach', function () { + let registry = new EntityRegistry(); + let id3; + registry.create(); + registry.create(); + for (let i = 0; i < 5; i += 1) { + let e = registry.create(); + registry.assign(e, new Position(i, i)); + if (i == 3) { + id3 = e; + } + } + let count = 0; + registry.forEach([], () => { + count += 1; + }); + assert.equal(count, 7); + registry.forEach([Position], (id, pos) => { + pos.x += 1; + }); + assert.equal(registry.get(id3, Position).x, 4); + }); +}); \ No newline at end of file