Skip to content

Commit

Permalink
feat(ecs): add ECS main class, update types, Component, Group
Browse files Browse the repository at this point in the history
- 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()
  • Loading branch information
postspectacular committed Oct 30, 2019
1 parent 1d33037 commit 40dc1b6
Show file tree
Hide file tree
Showing 6 changed files with 145 additions and 43 deletions.
4 changes: 4 additions & 0 deletions packages/ecs/src/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<number> | Fn0<ArrayLike<number>>;

export type ComponentTuple = IObjectOf<TypedArray>;
Expand Down
43 changes: 19 additions & 24 deletions packages/ecs/src/component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -27,7 +32,7 @@ export class Component<V extends TypedArray> implements IID<string>, INotify {

readonly size: number;
readonly stride: number;
default: ComponentDefaultValue;
default?: ComponentDefaultValue;

owner?: IID<string>;

Expand All @@ -49,7 +54,7 @@ export class Component<V extends TypedArray> implements IID<string>, 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
? <V>(
typedArray(
Expand Down Expand Up @@ -85,11 +90,11 @@ export class Component<V extends TypedArray> implements IID<string>, 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;
}
Expand All @@ -99,7 +104,7 @@ export class Component<V extends TypedArray> implements IID<string>, 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];
Expand All @@ -109,6 +114,7 @@ export class Component<V extends TypedArray> implements IID<string>, 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;
Expand Down Expand Up @@ -178,19 +184,8 @@ export class Component<V extends TypedArray> implements IID<string>, INotify {

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

const uintType = (num: number) =>
num <= 0x100 ? Type.U8 : num <= 0x10000 ? Type.U16 : Type.U32;

export const defComponent = <T extends Type = Type.F32>(
cap: number,
opts: Partial<ComponentOpts>
) => {
const utype = uintType(cap);
return new Component<TypedArrayTypeMap[T]>(
typedArray(utype, cap),
typedArray(utype, cap),
opts
);
};
notifyChange(key: number) {
this.notify({ id: EVENT_CHANGED, target: this, value: key });
}
}
79 changes: 79 additions & 0 deletions packages/ecs/src/ecs.ts
Original file line number Diff line number Diff line change
@@ -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<string, Component<TypedArray>>;
groups: Map<string, Group>;

constructor(capacity = 1000) {
this.idgen = new IDGen(capacity);
this.components = new Map();
this.groups = new Map();
}

defEntity(
comps?:
| string[]
| Component<TypedArray>[]
| IObjectOf<ReadonlyVec | undefined>
) {
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<T extends Type = Type.F32>(opts: Partial<ComponentOpts>) {
const cap = this.idgen.capacity;
const utype = uintType(cap);
const comp = new Component<TypedArrayTypeMap[T]>(
typedArray(utype, cap),
typedArray(utype, cap),
opts
);
// TODO add exist check
this.components.set(comp.id, comp);
return comp;
}

defGroup(
comps: Component<TypedArray>[],
owned: Component<TypedArray>[] = comps,
opts: Partial<GroupOpts> = {}
) {
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;
58 changes: 40 additions & 18 deletions packages/ecs/src/group.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import { intersection } from "@thi.ng/associative";
import {
ComponentInfo,
ComponentTuple,
EVENT_ADDED,
EVENT_PRE_REMOVE,
GroupOpts,
ICache
} from "./api";
Expand Down Expand Up @@ -47,6 +49,10 @@ export class Group implements IID<string> {

// 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}`
Expand All @@ -57,9 +63,21 @@ export class Group implements IID<string> {
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() {
Expand All @@ -75,11 +93,16 @@ export class Group implements IID<string> {
}
}

run(
fn: (info: IObjectOf<ComponentInfo>, n: number, ...xs: any[]) => void,
...xs: any[]
) {
this.ensureFullyOwned();
fn(this.info, this.n, ...xs);
}

forEachRaw(fn: Fn3<IObjectOf<ComponentInfo>, 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);
Expand All @@ -104,22 +127,14 @@ export class Group implements IID<string> {
}
}

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() {
Expand All @@ -144,7 +159,7 @@ export class Group implements IID<string> {
}
}

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);
Expand All @@ -161,4 +176,11 @@ export class Group implements IID<string> {
}
return true;
}

protected ensureFullyOwned() {
assert(
this.owned.length === this.components.length,
`group ${this.id} isn't fully owning its components`
);
}
}
3 changes: 2 additions & 1 deletion packages/ecs/src/id.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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()!;
Expand Down
1 change: 1 addition & 0 deletions packages/ecs/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
export * from "./api";
export * from "./component";
export * from "./ecs";
export * from "./group";
export * from "./lru";
export * from "./unbounded";

0 comments on commit 40dc1b6

Please sign in to comment.