From 40dc1b6abcfd0f11e04c7f7f22359bc928a9ff7d Mon Sep 17 00:00:00 2001 From: Karsten Schmidt Date: Wed, 30 Oct 2019 01:52:26 +0000 Subject: [PATCH] feat(ecs): add ECS main class, update types, Component, Group - move defComponent to ECS - add event consts - make component default vals optional - add Component.notifyChange() - fix component value caching (on delete) - add Group.run() - rename Group.deleteID() => removeID() --- packages/ecs/src/api.ts | 4 ++ packages/ecs/src/component.ts | 43 +++++++++---------- packages/ecs/src/ecs.ts | 79 +++++++++++++++++++++++++++++++++++ packages/ecs/src/group.ts | 58 +++++++++++++++++-------- packages/ecs/src/id.ts | 3 +- packages/ecs/src/index.ts | 1 + 6 files changed, 145 insertions(+), 43 deletions(-) create mode 100644 packages/ecs/src/ecs.ts diff --git a/packages/ecs/src/api.ts b/packages/ecs/src/api.ts index 7b263ee8c1..5b20006296 100644 --- a/packages/ecs/src/api.ts +++ b/packages/ecs/src/api.ts @@ -6,6 +6,10 @@ import { TypedArray } from "@thi.ng/api"; +export const EVENT_ADDED = "added"; +export const EVENT_PRE_REMOVE = "pre-remove"; +export const EVENT_CHANGED = "changed"; + export type ComponentDefaultValue = ArrayLike | Fn0>; export type ComponentTuple = IObjectOf; diff --git a/packages/ecs/src/component.ts b/packages/ecs/src/component.ts index babf7f0f48..1229141918 100644 --- a/packages/ecs/src/component.ts +++ b/packages/ecs/src/component.ts @@ -7,12 +7,17 @@ import { Type, TypedArray, typedArray, - TypedArrayTypeMap, UIntArray } from "@thi.ng/api"; import { isFunction } from "@thi.ng/checks"; -import { zeroes } from "@thi.ng/vectors"; -import { ComponentDefaultValue, ComponentOpts, ICache } from "./api"; +import { + ComponentDefaultValue, + ComponentOpts, + EVENT_ADDED, + EVENT_CHANGED, + EVENT_PRE_REMOVE, + ICache +} from "./api"; let NEXT_ID = 0; @@ -27,7 +32,7 @@ export class Component implements IID, INotify { readonly size: number; readonly stride: number; - default: ComponentDefaultValue; + default?: ComponentDefaultValue; owner?: IID; @@ -49,7 +54,7 @@ export class Component implements IID, INotify { this.id = opts.id || `comp-${NEXT_ID++}`; this.size = opts.size!; this.stride = opts.stride || this.size; - this.default = opts.default || zeroes(this.size); + this.default = opts.default; // || zeroes(this.size); this.vals = opts.buf ? ( typedArray( @@ -85,11 +90,11 @@ export class Component implements IID, INotify { if (key < max && n < max && !(i < n && dense[i] === key)) { dense[n] = key; sparse[key] = n; - const addr = n * this.stride; const def = this.default; - this.vals.set(val || (isFunction(def) ? def() : def), addr); + const initVal = val || (isFunction(def) ? def() : def); + initVal && this.vals.set(initVal, n * this.stride); this.n++; - this.notify({ id: "add", target: this, value: key }); + this.notify({ id: EVENT_ADDED, target: this, value: key }); } return this; } @@ -99,7 +104,7 @@ export class Component implements IID, INotify { let i = sparse[key]; if (i < n && dense[i] === key) { // notify listeners prior to removal to allow restructure / swaps - this.notify({ id: "delete", target: this, value: key }); + this.notify({ id: EVENT_PRE_REMOVE, target: this, value: key }); // get possibly updated slot i = sparse[key]; const j = dense[--n]; @@ -109,6 +114,7 @@ export class Component implements IID, INotify { const s = this.stride; n *= s; this.vals.copyWithin(i * s, n, n + this.size); + this.cache && this.cache.delete(i); return true; } return false; @@ -178,19 +184,8 @@ export class Component implements IID, INotify { // @ts-ignore: arguments notify(event: Event) {} -} -const uintType = (num: number) => - num <= 0x100 ? Type.U8 : num <= 0x10000 ? Type.U16 : Type.U32; - -export const defComponent = ( - cap: number, - opts: Partial -) => { - const utype = uintType(cap); - return new Component( - typedArray(utype, cap), - typedArray(utype, cap), - opts - ); -}; + notifyChange(key: number) { + this.notify({ id: EVENT_CHANGED, target: this, value: key }); + } +} diff --git a/packages/ecs/src/ecs.ts b/packages/ecs/src/ecs.ts new file mode 100644 index 0000000000..e0ec6b95d8 --- /dev/null +++ b/packages/ecs/src/ecs.ts @@ -0,0 +1,79 @@ +import { + assert, + IObjectOf, + Type, + TypedArray, + typedArray, + TypedArrayTypeMap +} from "@thi.ng/api"; +import { isArray, isString } from "@thi.ng/checks"; +import { ReadonlyVec } from "@thi.ng/vectors"; +import { ComponentOpts, GroupOpts } from "./api"; +import { Component } from "./component"; +import { Group } from "./group"; +import { IDGen } from "./id"; + +export class ECS { + idgen: IDGen; + components: Map>; + groups: Map; + + constructor(capacity = 1000) { + this.idgen = new IDGen(capacity); + this.components = new Map(); + this.groups = new Map(); + } + + defEntity( + comps?: + | string[] + | Component[] + | IObjectOf + ) { + const id = this.idgen.next(); + if (comps) { + if (isArray(comps)) { + if (!comps.length) return id; + for (let cid of comps) { + const comp = isString(cid) ? this.components.get(cid) : cid; + assert(!!comp, `unknown component ID: ${cid}`); + comp!.add(id); + } + } else { + for (let cid in comps) { + const comp = this.components.get(cid); + assert(!!comp, `unknown component ID: ${cid}`); + comp!.add(id, comps[cid]); + } + } + } + return id; + } + + defComponent(opts: Partial) { + const cap = this.idgen.capacity; + const utype = uintType(cap); + const comp = new Component( + typedArray(utype, cap), + typedArray(utype, cap), + opts + ); + // TODO add exist check + this.components.set(comp.id, comp); + return comp; + } + + defGroup( + comps: Component[], + owned: Component[] = comps, + opts: Partial = {} + ) { + const g = new Group(comps, owned, opts); + // TODO add exist check + this.groups.set(g.id, g); + return g; + } +} + +const uintType = (num: number) => + num <= 0x100 ? Type.U8 : num <= 0x10000 ? Type.U16 : Type.U32; diff --git a/packages/ecs/src/group.ts b/packages/ecs/src/group.ts index a174d9ce33..a48be26934 100644 --- a/packages/ecs/src/group.ts +++ b/packages/ecs/src/group.ts @@ -10,6 +10,8 @@ import { intersection } from "@thi.ng/associative"; import { ComponentInfo, ComponentTuple, + EVENT_ADDED, + EVENT_PRE_REMOVE, GroupOpts, ICache } from "./api"; @@ -47,6 +49,10 @@ export class Group implements IID { // update ownerships 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}` @@ -57,9 +63,21 @@ export class Group implements IID { this.addExisting(); comps.forEach((comp) => { - comp.addListener("add", this.onAddListener, this); - comp.addListener("delete", this.onDeleteListener, this); + comp.addListener(EVENT_ADDED, this.onAddListener, this); + comp.addListener(EVENT_PRE_REMOVE, this.onRemoveListener, this); + }); + } + + release() { + this.components.forEach((comp) => { + comp.removeListener(EVENT_ADDED, this.onAddListener, this); + comp.removeListener(EVENT_PRE_REMOVE, this.onRemoveListener, this); }); + this.cache.release(); + } + + has(key: number) { + return this.ids.has(key); } *values() { @@ -75,11 +93,16 @@ export class Group implements IID { } } + run( + fn: (info: IObjectOf, n: number, ...xs: any[]) => void, + ...xs: any[] + ) { + this.ensureFullyOwned(); + fn(this.info, this.n, ...xs); + } + forEachRaw(fn: Fn3, number, number, void>) { - assert( - this.owned.length === this.components.length, - `group ${this.id} isn't fully owning its components` - ); + this.ensureFullyOwned(); const ref = this.components[0].dense; for (let i = 0, n = this.n; i < n; i++) { fn(this.info, ref[i], i); @@ -104,22 +127,14 @@ export class Group implements IID { } } - release() { - this.components.forEach((comp) => { - comp.removeListener("add", this.onAddListener, this); - comp.removeListener("delete", this.onDeleteListener, this); - }); - this.cache.release(); - } - - onAddListener(e: Event) { + protected onAddListener(e: Event) { // console.log(`add ${e.target.id}: ${e.value}`); this.addID(e.value); } - onDeleteListener(e: Event) { + protected onRemoveListener(e: Event) { // console.log(`delete ${e.target.id}: ${e.value}`); - this.deleteID(e.value); + this.removeID(e.value); } protected addExisting() { @@ -144,7 +159,7 @@ export class Group implements IID { } } - protected deleteID(id: number, validate = true) { + protected removeID(id: number, validate = true) { if (validate && !this.validID(id)) return; this.ids.delete(id); this.cache.delete(id); @@ -161,4 +176,11 @@ export class Group implements IID { } return true; } + + protected ensureFullyOwned() { + assert( + this.owned.length === this.components.length, + `group ${this.id} isn't fully owning its components` + ); + } } diff --git a/packages/ecs/src/id.ts b/packages/ecs/src/id.ts index bdd6c07643..1448dc3aed 100644 --- a/packages/ecs/src/id.ts +++ b/packages/ecs/src/id.ts @@ -5,12 +5,13 @@ export class IDGen { nextID: number; capacity: number; - constructor(cap: number, next = 0) { + constructor(cap = Infinity, next = 0) { this.ids = []; this.capacity = cap; this.nextID = next; } + // FIXME return undefined if fail, update ECS.defEntity() next() { if (this.ids.length) { return this.ids.pop()!; diff --git a/packages/ecs/src/index.ts b/packages/ecs/src/index.ts index dcc7971676..63da2301b8 100644 --- a/packages/ecs/src/index.ts +++ b/packages/ecs/src/index.ts @@ -1,5 +1,6 @@ export * from "./api"; export * from "./component"; +export * from "./ecs"; export * from "./group"; export * from "./lru"; export * from "./unbounded";