From fdae8a794093e42f71165f7552231d9af744dfcd Mon Sep 17 00:00:00 2001 From: Karsten Schmidt Date: Thu, 31 Oct 2019 22:06:31 +0000 Subject: [PATCH] feat(ecs): add/update types, new components, update Group, ECS, add tests - add new type aliases & interfaces (mainly mapped types) - rename: Component => MemMappedComponent - add new Component class for JS value types - update ECS.defComponent() to choose correct comp type - update Group generics - update Group cache invalidation --- packages/ecs/package.json | 1 + packages/ecs/src/api.ts | 62 ++++++--- packages/ecs/src/component-mm.ts | 207 +++++++++++++++++++++++++++++++ packages/ecs/src/component.ts | 122 +++++++----------- packages/ecs/src/ecs.ts | 99 ++++++--------- packages/ecs/src/group.ts | 92 ++++++++------ packages/ecs/src/index.ts | 1 + packages/ecs/test/component.ts | 79 ++++++++++++ packages/ecs/test/group.ts | 43 +++++++ packages/ecs/test/index.ts | 6 - 10 files changed, 517 insertions(+), 195 deletions(-) create mode 100644 packages/ecs/src/component-mm.ts create mode 100644 packages/ecs/test/component.ts create mode 100644 packages/ecs/test/group.ts delete mode 100644 packages/ecs/test/index.ts diff --git a/packages/ecs/package.json b/packages/ecs/package.json index e40064e9ad..19bbb4b90e 100644 --- a/packages/ecs/package.json +++ b/packages/ecs/package.json @@ -25,6 +25,7 @@ "pub": "yarn build:release && yarn publish --access public" }, "devDependencies": { + "@thi.ng/equiv": "^1.0.9", "@types/mocha": "^5.2.6", "@types/node": "^12.6.3", "mocha": "^6.1.4", diff --git a/packages/ecs/src/api.ts b/packages/ecs/src/api.ts index 270aea68e3..beb649d91a 100644 --- a/packages/ecs/src/api.ts +++ b/packages/ecs/src/api.ts @@ -1,41 +1,75 @@ import { + ArrayLikeIterable, Fn0, IID, + INotify, IRelease, Type, TypedArray, - TypedArrayTypeMap + UIntArray } 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 ComponentID = keyof S & string; -export type ComponentTuple = Record & +export type ComponentDefaultValue = T | Fn0; + +export type GroupTuple> = Pick & IID; -export interface ComponentOpts { +export type GroupInfo> = { + [P in K]: ComponentInfo; +}; + +export interface ComponentInfo> { + values: SPEC[K] extends TypedArray ? SPEC[K] : SPEC[K][]; + size: number; + stride: number; +} + +export interface IComponent extends IID, INotify { + dense: UIntArray; + sparse: UIntArray; + vals: ArrayLike; + size: number; + stride: number; + + owner?: IID; + + has(id: number): boolean; + add(id: number, val?: V): boolean; + delete(id: number): boolean; + get(id: number): T | undefined; + getIndex(i: number): T | undefined; + + keys(): ArrayLikeIterable; + values(): IterableIterator; + + swapIndices(a: number, b: number): boolean; +} + +export interface MemMappedComponentOpts { id: ID; - type?: T; + type: Type; buf?: ArrayBuffer; byteOffset?: number; size?: number; stride?: number; - default?: ComponentDefaultValue; - cache?: ICache; + default?: ComponentDefaultValue>; + cache?: ICache; } -export interface GroupOpts { - id: string; - cache: ICache>; +export interface ObjectComponentOpts { + id: ID; + default?: ComponentDefaultValue; } -export interface ComponentInfo { - buffer: TypedArray; - size: number; - stride: number; +export interface GroupOpts { + id: string; + cache: ICache; } export interface ICache extends IRelease { diff --git a/packages/ecs/src/component-mm.ts b/packages/ecs/src/component-mm.ts new file mode 100644 index 0000000000..438b2618e8 --- /dev/null +++ b/packages/ecs/src/component-mm.ts @@ -0,0 +1,207 @@ +import { + Event, + Fn, + IID, + INotify, + INotifyMixin, + Type, + typedArray, + TypedArray, + UIntArray +} from "@thi.ng/api"; +import { isFunction } from "@thi.ng/checks"; +import { + ComponentDefaultValue, + EVENT_ADDED, + EVENT_CHANGED, + EVENT_PRE_REMOVE, + ICache, + IComponent, + MemMappedComponentOpts +} from "./api"; + +@INotifyMixin +export class MemMappedComponent + implements IComponent>, INotify { + readonly id: K; + + sparse: UIntArray; + dense: UIntArray; + vals: TypedArray; + n: number; + + readonly size: number; + readonly stride: number; + default?: ComponentDefaultValue>; + + owner?: IID; + + cache?: ICache; + + constructor( + sparse: UIntArray, + dense: UIntArray, + opts: MemMappedComponentOpts + ) { + this.sparse = sparse; + this.dense = dense; + opts = { + type: Type.F32, + size: 1, + byteOffset: 0, + ...opts + }; + this.id = opts.id; + this.size = opts.size!; + this.stride = opts.stride || this.size; + this.default = opts.default; // || zeroes(this.size); + this.vals = opts.buf + ? typedArray( + opts.type!, + opts.buf, + opts.byteOffset!, + dense.length * this.stride + ) + : typedArray(opts.type!, dense.length * this.stride); + this.cache = opts.cache; + this.n = 0; + } + + keys() { + return this.dense.slice(0, this.n); + } + + *values() { + for (let i = this.n; --i >= 0; ) { + yield this.getIndex(i)!; + } + } + + packedValues() { + return this.vals.subarray(0, this.n * this.stride); + } + + // TODO add version support via IDGen + add(id: number, val?: ArrayLike) { + const { dense, sparse, n } = this; + const max = dense.length; + const i = sparse[id]; + if (id < max && n < max && !(i < n && dense[i] === id)) { + dense[n] = id; + sparse[id] = n; + const def = this.default; + const initVal = val || (isFunction(def) ? def() : def); + initVal && this.vals.set(initVal, n * this.stride); + this.n++; + this.notify({ id: EVENT_ADDED, target: this, value: id }); + return true; + } + return false; + } + + delete(id: number) { + let { dense, sparse, n } = this; + let i = sparse[id]; + if (i < n && dense[i] === id) { + // notify listeners prior to removal to allow restructure / swaps + this.notify({ id: EVENT_PRE_REMOVE, target: this, value: id }); + // get possibly updated slot + i = sparse[id]; + const j = dense[--n]; + dense[i] = j; + sparse[j] = i; + this.n = n; + 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; + } + + has(id: number): boolean { + const i = this.sparse[id]; + return i < this.n && this.dense[i] === id; + } + + get(id: number) { + let i = this.sparse[id]; + return i < this.n && this.dense[i] === id + ? this.cache + ? this.cache.getSet(i, () => { + i *= this.stride; + return this.vals.subarray(i, i + this.size); + }) + : ((i *= this.stride), this.vals.subarray(i, i + this.size)) + : undefined; + } + + getIndex(i: number) { + return i < this.n + ? this.cache + ? this.cache.getSet(i, () => { + i *= this.stride; + return this.vals.subarray(i, i + this.size); + }) + : ((i *= this.stride), this.vals.subarray(i, i + this.size)) + : undefined; + } + + set(id: number, val: ArrayLike) { + let i = this.sparse[id]; + if (i < this.n && this.dense[i] === id) { + this.vals.set(val, i * this.stride); + this.notifyChange(id); + return true; + } + return false; + } + + setIndex(i: number, val: ArrayLike) { + return this.set(this.dense[i], val); + } + + /** + * Swaps slots of `src` & `dest` indices. The given args are NOT + * entity IDs, but indices in the `dense` array. The corresponding + * sparse & value slots are swapped too. Returns true if swap + * happened (false, if `src` and `dest` are equal) + * + * @param src + * @param dest + */ + swapIndices(src: number, dest: number) { + if (src === dest) return false; + const { dense, sparse, vals, size, stride } = this; + const ss = dense[src]; + const sd = dense[dest]; + dense[src] = sd; + dense[dest] = ss; + sparse[ss] = dest; + sparse[sd] = src; + src *= stride; + dest *= stride; + const tmp = vals.slice(src, src + size); + vals.copyWithin(src, dest, dest + size); + vals.set(tmp, dest); + return true; + } + + // @ts-ignore: arguments + addListener(id: string, fn: Fn, scope?: any): boolean { + return false; + } + + // @ts-ignore: arguments + removeListener(id: string, fn: Fn, scope?: any): boolean { + return false; + } + + // @ts-ignore: arguments + notify(event: Event) {} + + notifyChange(id: number) { + this.notify({ id: EVENT_CHANGED, target: this, value: id }); + } +} diff --git a/packages/ecs/src/component.ts b/packages/ecs/src/component.ts index 6d3e579fc6..cc0fd30ef5 100644 --- a/packages/ecs/src/component.ts +++ b/packages/ecs/src/component.ts @@ -4,68 +4,51 @@ import { IID, INotify, INotifyMixin, - Type, - typedArray, - TypedArrayTypeMap, UIntArray } from "@thi.ng/api"; import { isFunction } from "@thi.ng/checks"; import { ComponentDefaultValue, - ComponentOpts, EVENT_ADDED, EVENT_CHANGED, EVENT_PRE_REMOVE, - ICache + IComponent, + ObjectComponentOpts } from "./api"; @INotifyMixin -export class Component - implements IID, INotify { - readonly id: ID; +export class Component + implements IComponent, IID, INotify { + readonly id: K; sparse: UIntArray; dense: UIntArray; - vals: TypedArrayTypeMap[T]; + vals: T[]; n: number; - readonly size: number; - readonly stride: number; - default?: ComponentDefaultValue; - + default?: ComponentDefaultValue; owner?: IID; - cache?: ICache; - constructor( sparse: UIntArray, dense: UIntArray, - opts: ComponentOpts + opts: ObjectComponentOpts ) { this.sparse = sparse; this.dense = dense; - opts = { - type: Type.F32, - size: 1, - byteOffset: 0, - ...opts - }; this.id = opts.id; - this.size = opts.size!; - this.stride = opts.stride || this.size; - this.default = opts.default; // || zeroes(this.size); - this.vals = opts.buf - ? typedArray( - opts.type!, - opts.buf, - opts.byteOffset!, - dense.length * this.stride - ) - : typedArray(opts.type!, dense.length * this.stride); - this.cache = opts.cache; + this.default = opts.default; + this.vals = new Array(this.dense.length); this.n = 0; } + get size() { + return 1; + } + get stride() { + return 1; + } + keys() { return this.dense.slice(0, this.n); } @@ -77,13 +60,11 @@ export class Component } packedValues() { - return ( - this.vals.subarray(0, this.n * this.stride) - ); + return this.vals.slice(0, this.n); } // TODO add version support via IDGen - add(id: number, val?: ArrayLike) { + add(id: number, val?: T) { const { dense, sparse, n } = this; const max = dense.length; const i = sparse[id]; @@ -92,15 +73,16 @@ export class Component sparse[id] = n; const def = this.default; const initVal = val || (isFunction(def) ? def() : def); - initVal && this.vals.set(initVal, n * this.stride); + initVal && (this.vals[n] = initVal); this.n++; this.notify({ id: EVENT_ADDED, target: this, value: id }); + return true; } - return this; + return false; } delete(id: number) { - let { dense, sparse, n } = this; + let { dense, sparse, vals, n } = this; let i = sparse[id]; if (i < n && dense[i] === id) { // notify listeners prior to removal to allow restructure / swaps @@ -111,10 +93,8 @@ export class Component dense[i] = j; sparse[j] = i; this.n = n; - const s = this.stride; - n *= s; - this.vals.copyWithin(i * s, n, n + this.size); - this.cache && this.cache.delete(i); + vals[i] = vals[n]; + delete vals[n]; return true; } return false; @@ -127,31 +107,25 @@ export class Component get(id: number) { let i = this.sparse[id]; - return i < this.n && this.dense[i] === id - ? this.cache - ? this.cache.getSet(i, () => { - i *= this.stride; - return ( - this.vals.subarray(i, i + this.size) - ); - }) - : ((i *= this.stride), - this.vals.subarray(i, i + this.size)) - : undefined; + return i < this.n && this.dense[i] === id ? this.vals[i] : undefined; } getIndex(i: number) { - return i < this.n - ? this.cache - ? this.cache.getSet(i, () => { - i *= this.stride; - return ( - this.vals.subarray(i, i + this.size) - ); - }) - : ((i *= this.stride), - this.vals.subarray(i, i + this.size)) - : undefined; + return i < this.n ? this.vals[i] : undefined; + } + + set(id: number, val: T) { + let i = this.sparse[id]; + if (i < this.n && this.dense[i] === id) { + this.vals[i] = val; + this.notifyChange(id); + return true; + } + return false; + } + + setIndex(i: number, val: T) { + return this.set(this.dense[i], val); } /** @@ -165,18 +139,16 @@ export class Component */ swapIndices(src: number, dest: number) { if (src === dest) return false; - const { dense, sparse, vals, size, stride } = this; + const { dense, sparse, vals } = this; const ss = dense[src]; const sd = dense[dest]; dense[src] = sd; dense[dest] = ss; sparse[ss] = dest; sparse[sd] = src; - src *= stride; - dest *= stride; - const tmp = vals.slice(src, src + size); - vals.copyWithin(src, dest, dest + size); - vals.set(tmp, dest); + const tmp = vals[src]; + vals[src] = vals[dest]; + vals[dest] = tmp; return true; } @@ -193,7 +165,7 @@ export class Component // @ts-ignore: arguments notify(event: Event) {} - notifyChange(key: number) { - this.notify({ id: EVENT_CHANGED, target: this, value: key }); + notifyChange(id: number) { + this.notify({ id: EVENT_CHANGED, target: this, value: id }); } } diff --git a/packages/ecs/src/ecs.ts b/packages/ecs/src/ecs.ts index 9e819dc37b..4ee1aeca45 100644 --- a/packages/ecs/src/ecs.ts +++ b/packages/ecs/src/ecs.ts @@ -1,27 +1,30 @@ import { assert, Type, typedArray } from "@thi.ng/api"; import { isArray, isString } from "@thi.ng/checks"; -import { ReadonlyVec } from "@thi.ng/vectors"; -import { ComponentOpts, GroupOpts } from "./api"; +import { + ComponentID, + GroupOpts, + IComponent, + MemMappedComponentOpts, + ObjectComponentOpts +} from "./api"; import { Component } from "./component"; +import { MemMappedComponent } from "./component-mm"; import { Group } from "./group"; import { IDGen } from "./id"; -export class ECS { +export class ECS { idgen: IDGen; - components: Map>; - groups: Map>; + components: Map, IComponent, any, any>>; + groups: Map>; constructor(capacity = 1000) { - this.idgen = new IDGen(capacity); + this.idgen = new IDGen(capacity, 0); this.components = new Map(); this.groups = new Map(); } - defEntity( - comps?: - | string[] - | Component[] - | Record + defEntity>( + comps?: string[] | IComponent[] | Partial> ) { const id = this.idgen.next(); assert( @@ -32,7 +35,9 @@ export class ECS { if (isArray(comps)) { if (!comps.length) return id!; for (let cid of comps) { - const comp = isString(cid) ? this.components.get(cid) : cid; + const comp = isString(cid) + ? this.components.get(>cid) + : cid; assert(!!comp, `unknown component ID: ${cid}`); comp!.add(id!); } @@ -40,69 +45,39 @@ export class ECS { for (let cid in comps) { const comp = this.components.get(cid); assert(!!comp, `unknown component ID: ${cid}`); - comp!.add(id!, comps[cid]); + comp!.add(id!, comps[cid]); } } } return id!; } - defComponent( - opts: ComponentOpts - ) { + defComponent>( + opts: MemMappedComponentOpts + ): MemMappedComponent; + defComponent>( + opts: ObjectComponentOpts + ): Component; + defComponent>(opts: any) { const cap = this.idgen.capacity; const utype = uintType(cap); - const comp = new Component( - typedArray(utype, cap), - typedArray(utype, cap), - opts - ); + const sparse = typedArray(utype, cap); + const dense = typedArray(utype, cap); + const comp: IComponent = + opts.type !== undefined + ? new MemMappedComponent(dense, sparse, opts) + : new Component(sparse, dense, opts); // TODO add exist check - this.components.set(comp.id, comp); + this.components.set(opts.id, comp); return comp; } - defGroup( - comps: [Component], - owned?: Component[], - opts?: Partial - ): Group; - defGroup( - comps: [Component, Component], - owned?: Component[], - opts?: Partial - ): Group; - defGroup( - comps: [Component, Component, Component], - owned?: Component[], - opts?: Partial - ): Group; - defGroup< - A extends string, - B extends string, - C extends string, - D extends string - >( - comps: [ - Component, - Component, - Component, - Component - ], - owned?: Component[], - opts?: Partial - ): Group; - defGroup( - comps: Component[], - owned?: Component[], - opts?: Partial - ): Group; - defGroup( - comps: Component[], - owned: Component[] = comps, + defGroup>( + comps: IComponent[], + owned: IComponent[] = comps, opts: Partial = {} - ): Group { - const g = new Group(comps, owned, opts); + ) { + const g = new Group(comps, owned, opts); // TODO add exist check this.groups.set(g.id, g); return g; diff --git a/packages/ecs/src/group.ts b/packages/ecs/src/group.ts index a9eba201bc..4e0147a070 100644 --- a/packages/ecs/src/group.ts +++ b/packages/ecs/src/group.ts @@ -3,37 +3,39 @@ import { Event, FnO2, FnO3, - IID, - Type + IID } from "@thi.ng/api"; import { intersection } from "@thi.ng/associative"; import { - ComponentInfo, - ComponentTuple, + ComponentID, EVENT_ADDED, + EVENT_CHANGED, EVENT_PRE_REMOVE, + GroupInfo, GroupOpts, - ICache + GroupTuple, + ICache, + IComponent } from "./api"; import { Component } from "./component"; import { UnboundedCache } from "./unbounded"; let NEXT_ID = 0; -export class Group implements IID { +export class Group> implements IID { readonly id: string; - components: Component[]; - owned: Component[]; + components: IComponent[]; + owned: IComponent[]; ids: Set; n: number; - info: Record; - cache: ICache>; + info: GroupInfo; + cache: ICache>; constructor( - comps: Component[], - owned: Component[] = comps, + comps: IComponent[], + owned: IComponent[] = comps, opts: Partial = {} ) { this.components = comps; @@ -43,8 +45,12 @@ export class Group implements IID { this.cache = opts.cache || new UnboundedCache(); this.info = comps.reduce( - (acc: Record, c) => { - acc[c.id] = { buffer: c.vals, size: c.size, stride: c.stride }; + (acc: GroupInfo, c) => { + acc[c.id] = { + values: c.vals, + size: c.size, + stride: c.stride + }; return acc; }, {} @@ -68,6 +74,7 @@ export class Group implements IID { comps.forEach((comp) => { comp.addListener(EVENT_ADDED, this.onAddListener, this); comp.addListener(EVENT_PRE_REMOVE, this.onRemoveListener, this); + comp.addListener(EVENT_CHANGED, this.onChangeListener, this); }); } @@ -75,6 +82,7 @@ export class Group implements IID { this.components.forEach((comp) => { comp.removeListener(EVENT_ADDED, this.onAddListener, this); comp.removeListener(EVENT_PRE_REMOVE, this.onRemoveListener, this); + comp.removeListener(EVENT_CHANGED, this.onChangeListener, this); }); this.cache.release(); } @@ -102,7 +110,7 @@ export class Group implements IID { getEntityUnsafe(id: number) { return this.cache.getSet(id, () => { - const tuple = >{ id: id }; + const tuple = >{ id: id }; const comps = this.components; for (let j = comps.length; --j >= 0; ) { const c = comps[j]; @@ -112,13 +120,13 @@ export class Group implements IID { }); } - run(fn: FnO2, number, void>, ...xs: any[]) { + run(fn: FnO2, number, void>, ...xs: any[]) { this.ensureFullyOwning(); fn(this.info, this.n, ...xs); } forEachRaw( - fn: FnO3, number, number, void>, + fn: FnO3, number, number, void>, ...xs: any[] ) { this.ensureFullyOwning(); @@ -129,7 +137,7 @@ export class Group implements IID { } } - forEach(fn: FnO2, number, void>, ...xs: any[]) { + forEach(fn: FnO2, number, void>, ...xs: any[]) { let i = 0; for (let id of this.ids) { fn(this.getEntityUnsafe(id), i++, ...xs); @@ -140,6 +148,13 @@ export class Group implements IID { return this.owned.length === this.components.length; } + isValidID(id: number) { + for (let comp of this.components) { + if (!comp.has(id)) return false; + } + return true; + } + protected onAddListener(e: Event) { // console.log(`add ${e.target.id}: ${e.value}`); this.addID(e.value); @@ -150,6 +165,13 @@ export class Group implements IID { this.removeID(e.value); } + protected onChangeListener(e: Event) { + if (e.target instanceof Component) { + // console.log(`invalidate ${e.target.id}: ${e.value}`); + this.cache.delete(e.value); + } + } + protected addExisting() { const existing = this.components .slice(1) @@ -163,35 +185,29 @@ export class Group implements IID { } protected addID(id: number, validate = true) { - if (validate && !this.validID(id)) return; + if (validate && !this.isValidID(id)) return; this.ids.add(id); - const n = this.n++; - for (let comp of this.owned) { - // console.log(`moving id: ${id} in ${comp.id}...`); - comp.swapIndices(comp.sparse[id], n) && this.invalidateCache(id); - } + this.reorderOwned(id, this.n++); } protected removeID(id: number, validate = true) { - if (validate && !this.validID(id)) return; + if (validate && !this.isValidID(id)) return; this.ids.delete(id); - this.cache.delete(id); - const n = --this.n; + this.reorderOwned(id, --this.n); + } + + protected reorderOwned(id: number, n: number) { + if (!this.owned.length) return; + const id2 = this.owned[0].dense[n]; + let swapped = false; for (let comp of this.owned) { // console.log(`moving id: ${id} in ${comp.id}...`); - comp.swapIndices(comp.sparse[id], n) && this.invalidateCache(id); + swapped = comp.swapIndices(comp.sparse[id], n) || swapped; } - } - - protected validID(id: number) { - for (let comp of this.components) { - if (!comp.has(id)) return false; + if (swapped) { + this.cache.delete(id); + this.cache.delete(id2); } - return true; - } - - protected invalidateCache(id: number) { - this.cache && this.cache.delete(id); } protected *ownedValues() { diff --git a/packages/ecs/src/index.ts b/packages/ecs/src/index.ts index 63da2301b8..be45eaf389 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 "./component-mm"; export * from "./ecs"; export * from "./group"; export * from "./lru"; diff --git a/packages/ecs/test/component.ts b/packages/ecs/test/component.ts new file mode 100644 index 0000000000..040bd3baa9 --- /dev/null +++ b/packages/ecs/test/component.ts @@ -0,0 +1,79 @@ +import { Type } from "@thi.ng/api"; +import { equiv } from "@thi.ng/equiv"; +import * as assert from "assert"; +import { ECS, MemMappedComponent } from "../src"; + +describe("component", () => { + let ecs: ECS; + + beforeEach(() => (ecs = new ECS(16))); + + it("defComponent (minimal)", () => { + const a = ecs.defComponent({ id: "a", type: Type.F32 }); + assert(a instanceof MemMappedComponent); + assert(a.dense instanceof Uint8Array); + assert(a.sparse instanceof Uint8Array); + assert(a.vals instanceof Float32Array); + assert.equal(a.dense.length, ecs.idgen.capacity); + assert.equal(a.sparse.length, ecs.idgen.capacity); + assert.equal(a.vals.length, ecs.idgen.capacity); + assert.equal(a.size, 1); + assert.equal(a.stride, 1); + }); + + it("defComponent (w/ type)", () => { + const a = ecs.defComponent({ id: "a", type: Type.U8 }); + assert(a.vals instanceof Uint8Array); + assert.equal(a.dense.length, ecs.idgen.capacity); + assert.equal(a.sparse.length, ecs.idgen.capacity); + assert.equal(a.vals.length, ecs.idgen.capacity); + assert.equal(a.size, 1); + assert.equal(a.stride, 1); + }); + + it("defComponent (w/ size)", () => { + const a = ecs.defComponent({ id: "a", type: Type.F32, size: 2 }); + assert(a.vals instanceof Float32Array); + assert.equal(a.vals.length, ecs.idgen.capacity * 2); + assert.equal(a.size, 2); + assert.equal(a.stride, 2); + const b = ecs.defComponent({ + id: "b", + type: Type.F32, + size: 3, + stride: 4 + }); + assert.equal(b.vals.length, ecs.idgen.capacity * 4); + assert.equal(b.size, 3); + assert.equal(b.stride, 4); + }); + + it("add (w/ default val)", () => { + const a = ecs.defComponent({ + id: "a", + type: Type.F32, + size: 2, + default: [1, 2] + }); + assert(a.add(8)); + assert(a.add(9, [10, 20])); + assert(!a.add(16)); + assert.deepEqual([...a.get(8)!], [1, 2]); + assert.deepEqual([...a.get(9)!], [10, 20]); + assert(!a.add(8, [-1, -2])); + assert.deepEqual([...a.get(8)!], [1, 2]); + }); + + it("values / packeValues", () => { + const a = ecs.defComponent({ + id: "a", + type: Type.F32, + size: 2, + default: [1, 2] + }); + assert(a.add(8)); + assert(a.add(9, [10, 20])); + assert.deepEqual([...a.packedValues()], [1, 2, 10, 20]); + assert(equiv([...a.values()], [[10, 20], [1, 2]])); + }); +}); diff --git a/packages/ecs/test/group.ts b/packages/ecs/test/group.ts new file mode 100644 index 0000000000..36517992db --- /dev/null +++ b/packages/ecs/test/group.ts @@ -0,0 +1,43 @@ +import { equiv } from "@thi.ng/equiv"; +import * as assert from "assert"; +import { ECS, Group } from "../src"; + +const collect = (g: Group) => { + let res: any[] = []; + g.forEach((x) => res.push(x)); + return res; +}; + +describe("component", () => { + let ecs: ECS; + + beforeEach(() => (ecs = new ECS(16))); + + it("group shrink", () => { + const a = ecs.defComponent({ id: "a", default: () => "a" }); + const b = ecs.defComponent({ id: "b", type: 7, size: 2 }); + const g = ecs.defGroup([a, b]); + ecs.defEntity(["a", "b"]); + ecs.defEntity({ a: "aa", b: [1, 2] }); + ecs.defEntity({ a: "aaa", b: [3, 4] }); + assert.ok( + equiv(collect(g), [ + { a: "a", b: [0, 0], id: 0 }, + { a: "aa", b: [1, 2], id: 1 }, + { a: "aaa", b: [3, 4], id: 2 } + ]) + ); + + a.delete(0); + assert.ok( + equiv(collect(g), [ + { a: "aa", b: [1, 2], id: 1 }, + { a: "aaa", b: [3, 4], id: 2 } + ]) + ); + a.delete(2); + assert.ok(equiv(collect(g), [{ a: "aa", b: [1, 2], id: 1 }])); + a.set(1, "hi"); + assert.ok(equiv(collect(g), [{ a: "hi", b: [1, 2], id: 1 }])); + }); +}); diff --git a/packages/ecs/test/index.ts b/packages/ecs/test/index.ts deleted file mode 100644 index 3be28916f3..0000000000 --- a/packages/ecs/test/index.ts +++ /dev/null @@ -1,6 +0,0 @@ -// import * as assert from "assert"; -// import * as e from "../src/index"; - -describe("ecs", () => { - it("tests pending"); -});