flow(ecs): lots

This commit is contained in:
cha0s 2022-09-15 12:51:46 -05:00
parent 4834b5b294
commit 3373cb7135
5 changed files with 218 additions and 117 deletions

View File

@ -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);

View File

@ -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();

View File

@ -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
View 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;
});

View 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);
});