flow(ecs): lots
This commit is contained in:
parent
4834b5b294
commit
3373cb7135
|
@ -21,7 +21,21 @@ export default class Ecs {
|
||||||
}
|
}
|
||||||
|
|
||||||
addSystem(System) {
|
addSystem(System) {
|
||||||
const system = new System(this.Components);
|
const ecs = this;
|
||||||
|
class WrappedSystem extends System {
|
||||||
|
|
||||||
|
// eslint-disable-next-line class-methods-use-this
|
||||||
|
createEntity(components) {
|
||||||
|
return ecs.create(components);
|
||||||
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line class-methods-use-this
|
||||||
|
createManyEntities(count, components) {
|
||||||
|
return ecs.createMany(count, components);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
const system = new WrappedSystem(this.Components);
|
||||||
this.$$systems.push(system);
|
this.$$systems.push(system);
|
||||||
return system;
|
return system;
|
||||||
}
|
}
|
||||||
|
@ -164,13 +178,14 @@ export default class Ecs {
|
||||||
}
|
}
|
||||||
|
|
||||||
encode(entities, view) {
|
encode(entities, view) {
|
||||||
|
let cursor = 0;
|
||||||
|
let entitiesWritten = 0;
|
||||||
|
view.setUint32(cursor, entities.length, true);
|
||||||
|
cursor += 4;
|
||||||
if (0 === entities.length) {
|
if (0 === entities.length) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const keys = Object.keys(this.Components);
|
const keys = Object.keys(this.Components);
|
||||||
let cursor = 0;
|
|
||||||
view.setUint32(cursor, entities.length, true);
|
|
||||||
cursor += 4;
|
|
||||||
for (let i = 0; i < entities.length; i++) {
|
for (let i = 0; i < entities.length; i++) {
|
||||||
let entity;
|
let entity;
|
||||||
let onlyDirty = false;
|
let onlyDirty = false;
|
||||||
|
@ -183,6 +198,7 @@ export default class Ecs {
|
||||||
if (onlyDirty && !this.dirty.has(entity)) {
|
if (onlyDirty && !this.dirty.has(entity)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
entitiesWritten += 1;
|
||||||
view.setBigUint64(cursor, BigInt(entity), true);
|
view.setBigUint64(cursor, BigInt(entity), true);
|
||||||
cursor += 8;
|
cursor += 8;
|
||||||
const components = this.$$entities[entity];
|
const components = this.$$entities[entity];
|
||||||
|
@ -208,9 +224,13 @@ export default class Ecs {
|
||||||
}
|
}
|
||||||
view.setUint16(componentsWrittenIndex, componentsWritten, true);
|
view.setUint16(componentsWrittenIndex, componentsWritten, true);
|
||||||
}
|
}
|
||||||
|
view.setUint32(0, entitiesWritten, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
get(entity, Components = Object.keys(this.Components)) {
|
get(entity, Components = Object.keys(this.Components)) {
|
||||||
|
if (!this.$$entities[entity]) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
const result = {};
|
const result = {};
|
||||||
for (let i = 0; i < Components.length; i++) {
|
for (let i = 0; i < Components.length; i++) {
|
||||||
const component = this.Components[Components[i]].get(entity);
|
const component = this.Components[Components[i]].get(entity);
|
||||||
|
|
|
@ -1,113 +1,3 @@
|
||||||
/* eslint-disable */
|
export {default as Component} from './component';
|
||||||
|
export {default as Ecs} from './ecs';
|
||||||
import Component from './component';
|
export {default as System} from './system';
|
||||||
import Ecs from './ecs';
|
|
||||||
import Schema from './schema';
|
|
||||||
import Serializer from './serializer';
|
|
||||||
import System from './system';
|
|
||||||
|
|
||||||
const N = 1000;
|
|
||||||
const warm = 500;
|
|
||||||
|
|
||||||
const marks = [];
|
|
||||||
|
|
||||||
function mark(label, fn) {
|
|
||||||
marks.push(label);
|
|
||||||
performance.mark(`${label}-before`);
|
|
||||||
fn();
|
|
||||||
performance.mark(`${label}-after`);
|
|
||||||
}
|
|
||||||
|
|
||||||
function measure() {
|
|
||||||
for (let i = 0; i < marks.length; i++) {
|
|
||||||
const label = marks[i];
|
|
||||||
performance.measure(label, `${label}-before`, `${label}-after`);
|
|
||||||
}
|
|
||||||
console.log(performance.getEntriesByType('measure').map(({duration, name}) => ({duration, name})));
|
|
||||||
}
|
|
||||||
|
|
||||||
class Position extends Component {
|
|
||||||
|
|
||||||
static schema = {
|
|
||||||
x: {type: 'int32', defaultValue: 32},
|
|
||||||
y: 'int32',
|
|
||||||
z: 'int32',
|
|
||||||
};
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
class Direction extends Component {
|
|
||||||
|
|
||||||
static schema = {
|
|
||||||
direction: 'uint8',
|
|
||||||
};
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
class ZSystem extends System {
|
|
||||||
|
|
||||||
static queries() {
|
|
||||||
return {
|
|
||||||
default: ['Position', 'Direction'],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
tick() {
|
|
||||||
for (let [position, {direction}, entity] of this.select('default')) {
|
|
||||||
position.z = entity * direction * 2;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
const ecs = new Ecs({Direction, Position});
|
|
||||||
ecs.addSystem(ZSystem);
|
|
||||||
|
|
||||||
const createMany = () => {
|
|
||||||
const entities = ecs.createMany(N, {Position: (entity) => ({y: entity})});
|
|
||||||
ecs.insertMany({Direction: entities.map((entity) => [entity, {direction: 1 + entity % 4}])});
|
|
||||||
return entities;
|
|
||||||
};
|
|
||||||
|
|
||||||
for (let i = 0; i < warm; ++i) {
|
|
||||||
ecs.destroyMany(createMany());
|
|
||||||
}
|
|
||||||
mark('create', createMany);
|
|
||||||
|
|
||||||
let buffer, view;
|
|
||||||
|
|
||||||
ecs.tick(0.01);
|
|
||||||
ecs.tick(0.01);
|
|
||||||
if (!view) {
|
|
||||||
buffer = new ArrayBuffer(ecs.sizeOf(Array.from(ecs.dirty.values()), false));
|
|
||||||
view = new DataView(buffer);
|
|
||||||
}
|
|
||||||
console.log('bytes:', buffer.byteLength);
|
|
||||||
const encoding = Array.from(ecs.dirty.values());
|
|
||||||
ecs.tickFinalize();
|
|
||||||
|
|
||||||
console.log('encoding', encoding.length);
|
|
||||||
|
|
||||||
for (let i = 0; i < warm; ++i) {
|
|
||||||
ecs.tick(0.01);
|
|
||||||
ecs.encode(encoding, view);
|
|
||||||
ecs.tickFinalize();
|
|
||||||
}
|
|
||||||
|
|
||||||
mark('tick', () => ecs.tick(0.01));
|
|
||||||
mark('encode', () => ecs.encode(encoding, view));
|
|
||||||
mark('finalize', () => ecs.tickFinalize());
|
|
||||||
|
|
||||||
ecs.destroyAll();
|
|
||||||
|
|
||||||
for (let i = 0; i < warm; ++i) {
|
|
||||||
ecs.decode(view);
|
|
||||||
ecs.destroyAll();
|
|
||||||
}
|
|
||||||
|
|
||||||
mark('decode', () => ecs.decode(view));
|
|
||||||
|
|
||||||
console.log(JSON.stringify(ecs.get(1)));
|
|
||||||
|
|
||||||
console.log(N, 'iterations');
|
|
||||||
measure();
|
|
||||||
|
|
|
@ -31,6 +31,10 @@ export default class System {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static queries() {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
reindex(entities) {
|
reindex(entities) {
|
||||||
for (const i in this.queries) {
|
for (const i in this.queries) {
|
||||||
this.queries[i].reindex(entities);
|
this.queries[i].reindex(entities);
|
||||||
|
|
126
packages/ecs/test/ecs.js
Normal file
126
packages/ecs/test/ecs.js
Normal file
|
@ -0,0 +1,126 @@
|
||||||
|
import {expect} from 'chai';
|
||||||
|
|
||||||
|
import Component from '../src/component';
|
||||||
|
import Ecs from '../src/ecs';
|
||||||
|
import System from '../src/system';
|
||||||
|
|
||||||
|
// eslint-disable-next-line
|
||||||
|
class Empty extends Component {}
|
||||||
|
|
||||||
|
class Position extends Component {
|
||||||
|
|
||||||
|
static schema = {
|
||||||
|
x: {type: 'int32', defaultValue: 32},
|
||||||
|
y: 'int32',
|
||||||
|
z: 'int32',
|
||||||
|
};
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
it('can create entities with components', () => {
|
||||||
|
const ecs = new Ecs({Empty, Position});
|
||||||
|
const entity = ecs.create({Empty: () => {}, Position: () => ({y: 420})});
|
||||||
|
expect(JSON.stringify(ecs.get(entity)))
|
||||||
|
.to.deep.equal(JSON.stringify({Empty: {}, Position: {x: 32, y: 420, z: 0}}));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('can tick systems', () => {
|
||||||
|
class Momentum extends Component {
|
||||||
|
|
||||||
|
static schema = {
|
||||||
|
x: 'int32',
|
||||||
|
y: 'int32',
|
||||||
|
z: 'int32',
|
||||||
|
};
|
||||||
|
|
||||||
|
}
|
||||||
|
const ecs = new Ecs({Momentum, Position});
|
||||||
|
class Physics extends System {
|
||||||
|
|
||||||
|
static queries() {
|
||||||
|
return {
|
||||||
|
default: ['Position', 'Momentum'],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
tick(elapsed) {
|
||||||
|
expect(elapsed)
|
||||||
|
.to.equal(1);
|
||||||
|
// eslint-disable-next-line no-restricted-syntax
|
||||||
|
for (const [position, momentum] of this.select('default')) {
|
||||||
|
position.x += momentum.x * elapsed;
|
||||||
|
position.y += momentum.y * elapsed;
|
||||||
|
position.z += momentum.z * elapsed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
ecs.addSystem(Physics);
|
||||||
|
const entity = ecs.create({Momentum: () => ({}), Position: () => ({y: 420})});
|
||||||
|
const position = JSON.stringify(ecs.get(entity).Position);
|
||||||
|
ecs.tick(1);
|
||||||
|
expect(JSON.stringify(ecs.get(entity).Position))
|
||||||
|
.to.deep.equal(position);
|
||||||
|
ecs.get(1).Momentum.y = 30;
|
||||||
|
ecs.tick(1);
|
||||||
|
expect(JSON.stringify(ecs.get(entity).Position))
|
||||||
|
.to.deep.equal(JSON.stringify({x: 32, y: 450, z: 0}));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('can create entities when ticking systems', () => {
|
||||||
|
const ecs = new Ecs();
|
||||||
|
class Spawn extends System {
|
||||||
|
|
||||||
|
tick() {
|
||||||
|
this.createEntity();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
ecs.addSystem(Spawn);
|
||||||
|
ecs.create();
|
||||||
|
expect(ecs.get(2))
|
||||||
|
.to.be.undefined;
|
||||||
|
ecs.tick(1);
|
||||||
|
expect(ecs.get(2))
|
||||||
|
.to.not.be.undefined;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('can schedule entities to be deleted when ticking systems', () => {
|
||||||
|
const ecs = new Ecs();
|
||||||
|
class Despawn extends System {
|
||||||
|
|
||||||
|
tick() {
|
||||||
|
this.destroyEntity(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
ecs.addSystem(Despawn);
|
||||||
|
ecs.createExact(1);
|
||||||
|
ecs.tick(1);
|
||||||
|
expect(ecs.get(1))
|
||||||
|
.to.not.be.undefined;
|
||||||
|
ecs.tickFinalize();
|
||||||
|
expect(ecs.get(1))
|
||||||
|
.to.be.undefined;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('can encode and decode an ecs', () => {
|
||||||
|
const ecs = new Ecs({Empty, Position});
|
||||||
|
const entity = ecs.create({Empty: () => {}, Position: () => ({y: 420})});
|
||||||
|
const view = new DataView(new ArrayBuffer(1024));
|
||||||
|
ecs.encode([[entity, true]], view);
|
||||||
|
const newEcs = new Ecs({Empty, Position});
|
||||||
|
newEcs.decode(view);
|
||||||
|
expect(JSON.stringify(newEcs.get(entity)))
|
||||||
|
.to.deep.equal(JSON.stringify(ecs.get(entity)));
|
||||||
|
ecs.setClean();
|
||||||
|
ecs.encode([[entity, true]], view);
|
||||||
|
const newEcs2 = new Ecs({Empty, Position});
|
||||||
|
newEcs2.decode(view);
|
||||||
|
expect(newEcs2.get(entity))
|
||||||
|
.to.be.undefined;
|
||||||
|
ecs.encode([entity], view);
|
||||||
|
newEcs2.decode(view);
|
||||||
|
expect(newEcs2.get(entity))
|
||||||
|
.to.not.be.undefined;
|
||||||
|
});
|
61
packages/ecs/test/query.js
Normal file
61
packages/ecs/test/query.js
Normal file
|
@ -0,0 +1,61 @@
|
||||||
|
import {expect} from 'chai';
|
||||||
|
|
||||||
|
import Component from '../src/component';
|
||||||
|
import Query from '../src/query';
|
||||||
|
|
||||||
|
class A extends Component {
|
||||||
|
|
||||||
|
static schema = {
|
||||||
|
a: 'int32',
|
||||||
|
};
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
class B extends Component {
|
||||||
|
|
||||||
|
static schema = {
|
||||||
|
b: 'int32',
|
||||||
|
};
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
const Components = {A: new A(), B: new B()};
|
||||||
|
Components.A.createMany([2, 3]);
|
||||||
|
Components.B.createMany([1, 2]);
|
||||||
|
function testQuery(parameters, expected) {
|
||||||
|
const query = new Query(parameters, Components);
|
||||||
|
query.reindex([1, 2, 3]);
|
||||||
|
expect(query.count)
|
||||||
|
.to.equal(expected.length);
|
||||||
|
// eslint-disable-next-line no-restricted-syntax
|
||||||
|
for (const _ of query.select()) {
|
||||||
|
expect(_.length)
|
||||||
|
.to.equal(parameters.filter((spec) => '!'.charCodeAt(0) !== spec.charCodeAt(0)).length + 1);
|
||||||
|
expect(expected.includes(_.pop()))
|
||||||
|
.to.equal(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
it('can query all', () => {
|
||||||
|
testQuery([], [1, 2, 3]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('can query some', () => {
|
||||||
|
testQuery(['A'], [2, 3]);
|
||||||
|
testQuery(['A', 'B'], [2]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('can query excluding', () => {
|
||||||
|
testQuery(['!A'], [1]);
|
||||||
|
testQuery(['A', '!B'], [3]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('can deindex', () => {
|
||||||
|
const query = new Query(['A'], Components);
|
||||||
|
query.reindex([1, 2, 3]);
|
||||||
|
expect(query.count)
|
||||||
|
.to.equal(2);
|
||||||
|
query.deindex([2]);
|
||||||
|
expect(query.count)
|
||||||
|
.to.equal(1);
|
||||||
|
});
|
Loading…
Reference in New Issue
Block a user