Skip to content

Commit

Permalink
feat(ecs): add/update types, new components, update Group, ECS, add t…
Browse files Browse the repository at this point in the history
…ests

- 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
  • Loading branch information
postspectacular committed Oct 31, 2019
1 parent 14ba8f0 commit fdae8a7
Show file tree
Hide file tree
Showing 10 changed files with 517 additions and 195 deletions.
1 change: 1 addition & 0 deletions packages/ecs/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
62 changes: 48 additions & 14 deletions packages/ecs/src/api.ts
Original file line number Diff line number Diff line change
@@ -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<number> | Fn0<ArrayLike<number>>;
export type ComponentID<S> = keyof S & string;

export type ComponentTuple<K extends string> = Record<K, TypedArray> &
export type ComponentDefaultValue<T> = T | Fn0<T>;

export type GroupTuple<SPEC, K extends ComponentID<SPEC>> = Pick<SPEC, K> &
IID<number>;

export interface ComponentOpts<ID extends string, T extends Type = Type.F32> {
export type GroupInfo<SPEC, K extends ComponentID<SPEC>> = {
[P in K]: ComponentInfo<SPEC, P>;
};

export interface ComponentInfo<SPEC, K extends ComponentID<SPEC>> {
values: SPEC[K] extends TypedArray ? SPEC[K] : SPEC[K][];
size: number;
stride: number;
}

export interface IComponent<K extends string, T, V> extends IID<K>, INotify {
dense: UIntArray;
sparse: UIntArray;
vals: ArrayLike<any>;
size: number;
stride: number;

owner?: IID<string>;

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<number>;
values(): IterableIterator<T>;

swapIndices(a: number, b: number): boolean;
}

export interface MemMappedComponentOpts<ID extends string> {
id: ID;
type?: T;
type: Type;
buf?: ArrayBuffer;
byteOffset?: number;
size?: number;
stride?: number;
default?: ComponentDefaultValue;
cache?: ICache<TypedArrayTypeMap[T]>;
default?: ComponentDefaultValue<ArrayLike<number>>;
cache?: ICache<TypedArray>;
}

export interface GroupOpts {
id: string;
cache: ICache<ComponentTuple<string>>;
export interface ObjectComponentOpts<ID extends string, T> {
id: ID;
default?: ComponentDefaultValue<T>;
}

export interface ComponentInfo {
buffer: TypedArray;
size: number;
stride: number;
export interface GroupOpts {
id: string;
cache: ICache<any>;
}

export interface ICache<T> extends IRelease {
Expand Down
207 changes: 207 additions & 0 deletions packages/ecs/src/component-mm.ts
Original file line number Diff line number Diff line change
@@ -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<K extends string>
implements IComponent<K, TypedArray, ArrayLike<number>>, INotify {
readonly id: K;

sparse: UIntArray;
dense: UIntArray;
vals: TypedArray;
n: number;

readonly size: number;
readonly stride: number;
default?: ComponentDefaultValue<ArrayLike<number>>;

owner?: IID<string>;

cache?: ICache<TypedArray>;

constructor(
sparse: UIntArray,
dense: UIntArray,
opts: MemMappedComponentOpts<K>
) {
this.sparse = sparse;
this.dense = dense;
opts = {
type: <any>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<number>) {
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<number>) {
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<number>) {
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<Event, void>, scope?: any): boolean {
return false;
}

// @ts-ignore: arguments
removeListener(id: string, fn: Fn<Event, void>, scope?: any): boolean {
return false;
}

// @ts-ignore: arguments
notify(event: Event) {}

notifyChange(id: number) {
this.notify({ id: EVENT_CHANGED, target: this, value: id });
}
}
Loading

0 comments on commit fdae8a7

Please sign in to comment.