feat: bundle

This commit is contained in:
cha0s 2022-09-17 04:15:16 -05:00
parent 22568bd999
commit bbf11c0b63
6 changed files with 232 additions and 34 deletions

View File

@ -0,0 +1,58 @@
/* eslint-disable guard-for-in, max-classes-per-file, no-restricted-syntax */
export default class Bundle {
Components = {};
constructor(Components) {
this.Components = this.constructor.Components.reduce(
(r, Component, i) => {
if (!Components[i]) {
throw new TypeError(`Bundle(): no such component '${Component}'`);
}
return {...r, [Component]: Components[i]};
},
{},
);
}
configure(configuration) {
return this.constructor.configure(configuration, this.Components);
}
static configure(configuration, Components) {
const result = {};
const entries = Object.entries(Components);
for (let i = 0; i < entries.length; i++) {
const [component] = entries[i];
result[component] = configuration[component];
}
return result;
}
static maybeNormalize(BundleLike) {
if (Array.isArray(BundleLike)) {
return class AdhocBundle extends Bundle {
static Components = BundleLike;
};
}
if (BundleLike.prototype instanceof Bundle) {
return BundleLike;
}
return undefined;
}
static select(entity, Components) {
const bundle = {};
for (const i in Components) {
bundle[i] = Components[i].getUnsafe(entity);
}
return bundle;
}
select(entity) {
return this.constructor.select(entity, this.Components);
}
}

View File

@ -23,6 +23,9 @@ ComponentRouter.normalize = (ComponentLike) => {
if (ComponentLike.prototype instanceof ComponentRouter) { if (ComponentLike.prototype instanceof ComponentRouter) {
return ComponentLike; return ComponentLike;
} }
if ('object' !== typeof ComponentLike) {
throw new TypeError(`Component.normalize(): couldn't normalize '${ComponentLike}'`);
}
return class AdhocComponent extends ComponentRouter { return class AdhocComponent extends ComponentRouter {
static schema = ComponentLike; static schema = ComponentLike;

View File

@ -1,10 +1,13 @@
/* eslint-disable guard-for-in, max-classes-per-file, no-continue, no-restricted-syntax */ /* eslint-disable guard-for-in, max-classes-per-file, no-continue, no-restricted-syntax */
import Bundle from './bundle';
import Component from './component'; import Component from './component';
export default class Ecs { export default class Ecs {
$$caret = 1; $$caret = 1;
Bundles = {};
Components = {}; Components = {};
dirty = new Set(); dirty = new Set();
@ -15,9 +18,21 @@ export default class Ecs {
$$systems = []; $$systems = [];
constructor(Components) { constructor(ComponentLikesAndOrBundleLikes) {
for (const i in Components) { const Bundles = [];
this.Components[i] = this.trackDirtyEntities(Component.normalize(Components[i])); for (const i in ComponentLikesAndOrBundleLikes) {
const MaybeBundle = Bundle.maybeNormalize(ComponentLikesAndOrBundleLikes[i]);
if (MaybeBundle) {
Bundles.push([i, MaybeBundle]);
continue;
}
this.Components[i] = this.trackDirtyEntities(
Component.normalize(ComponentLikesAndOrBundleLikes[i]),
);
}
for (let i = 0; i < Bundles.length; i++) {
const [j, Bundle] = Bundles[i];
this.Bundles[j] = new Bundle(Bundle.Components.map((c) => this.Components[c]));
} }
} }
@ -55,7 +70,18 @@ export default class Ecs {
const creating = {}; const creating = {};
for (let i = 0; i < componentsList.length; i++) { for (let i = 0; i < componentsList.length; i++) {
const components = componentsList[i]; const components = componentsList[i];
const componentKeys = Object.keys(components); const componentKeys = Object.keys(components)
.reduce(
(r, componentOrBundle) => {
if (this.Components[componentOrBundle]) {
return r.concat(componentOrBundle);
}
const Bundle = this.Bundles[componentOrBundle];
Object.assign(components, Bundle.configure(components[componentOrBundle]));
return r.concat(Object.keys(Bundle.Components));
},
[],
);
let entity; let entity;
if (this.$$pool.length > 0) { if (this.$$pool.length > 0) {
entity = this.$$pool.pop(); entity = this.$$pool.pop();
@ -70,7 +96,7 @@ export default class Ecs {
if (!creating[component]) { if (!creating[component]) {
creating[component] = []; creating[component] = [];
} }
creating[component].push([entity, components[component]]); creating[component].push(components[component] ? [entity, components[component]] : entity);
} }
} }
for (const i in creating) { for (const i in creating) {

View File

@ -1,18 +1,21 @@
/* eslint-disable no-restricted-syntax */
import BaseComponent from './component/base';
export default class Query { export default class Query {
$$compiled = {with: [], without: []}; $$compiled = {with: [], without: []};
$$index = []; $$index = [];
constructor(parameters, Components) { constructor(parameters, ComponentsAndOrBundles) {
for (let i = 0; i < parameters.length; ++i) { for (let i = 0; i < parameters.length; ++i) {
const parameter = parameters[i]; const parameter = parameters[i];
switch (parameter.charCodeAt(0)) { switch (parameter.charCodeAt(0)) {
case '!'.charCodeAt(0): case '!'.charCodeAt(0):
this.$$compiled.without.push(Components[parameter.slice(1)]); this.$$compiled.without.push(ComponentsAndOrBundles[parameter.slice(1)]);
break; break;
default: default:
this.$$compiled.with.push(Components[parameter]); this.$$compiled.with.push(ComponentsAndOrBundles[parameter]);
break; break;
} }
} }
@ -46,19 +49,47 @@ export default class Query {
const entity = entities[i]; const entity = entities[i];
const index = this.$$index.indexOf(entity); const index = this.$$index.indexOf(entity);
let should = true; let should = true;
for (let j = 0; j < this.$$compiled.with.length; ++j) { // eslint-disable-next-line no-restricted-syntax, no-labels
const C = this.$$compiled.with[j]; withCheck: for (let j = 0; j < this.$$compiled.with.length; ++j) {
if ('undefined' === typeof C.getUnsafe(entity)) { const ComponentOrBundle = this.$$compiled.with[j];
should = false; if (ComponentOrBundle instanceof BaseComponent) {
break; if ('undefined' === typeof ComponentOrBundle.getUnsafe(entity)) {
should = false;
break;
}
}
else {
for (const k in ComponentOrBundle.Components) {
if ('undefined' === typeof ComponentOrBundle.Components[k].getUnsafe(entity)) {
should = false;
// eslint-disable-next-line no-labels
break withCheck;
}
}
} }
} }
if (should) { if (should) {
// eslint-disable-next-line no-restricted-syntax, no-labels
for (let j = 0; j < this.$$compiled.without.length; ++j) { for (let j = 0; j < this.$$compiled.without.length; ++j) {
const C = this.$$compiled.without[j]; const ComponentOrBundle = this.$$compiled.without[j];
if ('undefined' !== typeof C.getUnsafe(entity)) { if (ComponentOrBundle instanceof BaseComponent) {
should = false; if ('undefined' !== typeof ComponentOrBundle.getUnsafe(entity)) {
break; should = false;
break;
}
}
else {
let shouldLocal = false;
for (const k in ComponentOrBundle.Components) {
if ('undefined' === typeof ComponentOrBundle.Components[k].getUnsafe(entity)) {
shouldLocal = true;
break;
}
}
if (!shouldLocal) {
should = false;
break;
}
} }
} }
} }
@ -84,7 +115,12 @@ export default class Query {
const entity = entities.pop(); const entity = entities.pop();
const value = []; const value = [];
for (let i = 0; i < this.$$compiled.with.length; ++i) { for (let i = 0; i < this.$$compiled.with.length; ++i) {
value.push(this.$$compiled.with[i].getUnsafe(entity)); if (this.$$compiled.with[i] instanceof BaseComponent) {
value.push(this.$$compiled.with[i].getUnsafe(entity));
}
else {
value.push(this.$$compiled.with[i].select(entity));
}
} }
value.push(entity); value.push(entity);
return {done: false, value}; return {done: false, value};

View File

@ -1,6 +1,7 @@
/* eslint-disable react/prefer-stateless-function */ /* eslint-disable react/prefer-stateless-function */
import {expect} from 'chai'; import {expect} from 'chai';
import Bundle from '../src/bundle';
import Ecs from '../src/ecs'; import Ecs from '../src/ecs';
import System from '../src/system'; import System from '../src/system';
@ -115,3 +116,34 @@ it('can encode and decode an ecs', () => {
expect(newEcs2.get(entity)) expect(newEcs2.get(entity))
.to.not.be.undefined; .to.not.be.undefined;
}); });
it('can add bundles', () => {
const Height = {height: 'uint32'};
const Width = {width: 'uint32'};
const Area = ['Height', 'Width'];
class ConfiguredArea extends Bundle {
static configure({height, width}) {
return {Height: {height}, Width: {width}};
}
static Components = ['Height', 'Width'];
}
const ecs = new Ecs({
Area,
ConfiguredArea,
Height,
Width,
});
const entity = ecs.get(ecs.create({Area: {Width: {width: 420}}}));
expect(entity.Height.height)
.to.equal(0);
expect(entity.Width.width)
.to.equal(420);
const configuredEntity = ecs.get(ecs.create({ConfiguredArea: {width: 420}}));
expect(configuredEntity.Height.height)
.to.equal(0);
expect(configuredEntity.Width.width)
.to.equal(420);
});

View File

@ -1,29 +1,37 @@
import {expect} from 'chai'; import {expect} from 'chai';
import Bundle from '../src/bundle';
import Component from '../src/component'; import Component from '../src/component';
import Query from '../src/query'; import Query from '../src/query';
class A extends Component { const A = Component.normalize({a: {type: 'int32', defaultValue: 420}});
const B = Component.normalize({b: {type: 'int32', defaultValue: 69}});
const C = Component.normalize({c: 'int32'});
const D = Bundle.maybeNormalize(['B', 'C']);
class E extends Bundle {
static schema = { static Components = ['A', 'B'];
a: 'int32',
}; static select(entity, Components) {
const {A: {a}, B: {b}} = super.select(entity, Components);
return {a, b};
}
} }
class B extends Component { const ComponentsAndOrBundles = {
A: new A(),
B: new B(),
C: new C(),
};
ComponentsAndOrBundles.A.createMany([2n, 3n]);
ComponentsAndOrBundles.B.createMany([1n, 2n]);
ComponentsAndOrBundles.C.createMany([2n, 4n]);
ComponentsAndOrBundles.D = new D([ComponentsAndOrBundles.B, ComponentsAndOrBundles.C]);
ComponentsAndOrBundles.E = new E([ComponentsAndOrBundles.A, ComponentsAndOrBundles.B]);
static schema = {
b: 'int32',
};
}
const Components = {A: new A(), B: new B()};
Components.A.createMany([2n, 3n]);
Components.B.createMany([1n, 2n]);
function testQuery(parameters, expected) { function testQuery(parameters, expected) {
const query = new Query(parameters, Components); const query = new Query(parameters, ComponentsAndOrBundles);
query.reindex([1n, 2n, 3n]); query.reindex([1n, 2n, 3n]);
expect(query.count) expect(query.count)
.to.equal(expected.length); .to.equal(expected.length);
@ -50,8 +58,16 @@ it('can query excluding', () => {
testQuery(['A', '!B'], [3n]); testQuery(['A', '!B'], [3n]);
}); });
it('can query bundles', () => {
testQuery(['D'], [2n]);
});
it('can query excluding bundles', () => {
testQuery(['!D'], [1n, 3n]);
});
it('can deindex', () => { it('can deindex', () => {
const query = new Query(['A'], Components); const query = new Query(['A'], ComponentsAndOrBundles);
query.reindex([1n, 2n, 3n]); query.reindex([1n, 2n, 3n]);
expect(query.count) expect(query.count)
.to.equal(2); .to.equal(2);
@ -59,3 +75,30 @@ it('can deindex', () => {
expect(query.count) expect(query.count)
.to.equal(1); .to.equal(1);
}); });
it('can select', () => {
const query = new Query(['A'], ComponentsAndOrBundles);
query.reindex([1n, 2n, 3n]);
const it = query.select();
const result = it.next();
expect(result.value[0].a)
.to.equal(420);
});
it('can select bundles', () => {
const query = new Query(['D'], ComponentsAndOrBundles);
query.reindex([1n, 2n, 3n]);
const it = query.select();
const result = it.next();
expect(result.value[0].B.b)
.to.equal(69);
});
it('can select configured bundles', () => {
const query = new Query(['E'], ComponentsAndOrBundles);
query.reindex([1n, 2n, 3n]);
const it = query.select();
const result = it.next();
expect(result.value[0])
.to.deep.equal({a: 420, b: 69});
});