feat: bundle
This commit is contained in:
parent
22568bd999
commit
bbf11c0b63
58
packages/ecs/src/bundle.js
Normal file
58
packages/ecs/src/bundle.js
Normal 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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;
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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};
|
||||||
|
|
|
@ -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);
|
||||||
|
});
|
||||||
|
|
|
@ -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});
|
||||||
|
});
|
||||||
|
|
Loading…
Reference in New Issue
Block a user