flow: ecs

This commit is contained in:
cha0s 2022-09-14 10:44:36 -05:00
parent 788e94db41
commit 90fcb90831
4 changed files with 252 additions and 51 deletions

View File

@ -1,5 +1,6 @@
/* eslint-disable guard-for-in, max-classes-per-file, no-restricted-syntax */ /* eslint-disable guard-for-in, max-classes-per-file, no-restricted-syntax */
import Schema from './schema'; import Schema from './schema';
import Serializer from './serializer';
class BaseComponent { class BaseComponent {
@ -15,6 +16,7 @@ class BaseComponent {
constructor(schema) { constructor(schema) {
this.schema = schema; this.schema = schema;
this.serializer = new Serializer(schema);
} }
allocate() { allocate() {
@ -65,6 +67,13 @@ class BaseComponent {
} }
} }
set(entity, values) {
const instance = this.getUnsafe(entity);
for (const i in values) {
instance[i] = values[i];
}
}
} }
class FlatComponent extends BaseComponent { class FlatComponent extends BaseComponent {
@ -113,7 +122,7 @@ class FlatComponent extends BaseComponent {
window.dirty = false; window.dirty = false;
window.cursor += this.constructor.width; window.cursor += this.constructor.width;
} }
this.dirty = false; this.setDirty(0, 0);
} }
createMany(entries) { createMany(entries) {
@ -175,6 +184,7 @@ class FlatComponent extends BaseComponent {
} }
makeWindowClass() { makeWindowClass() {
const Component = this;
class Window { class Window {
cursor = 0; cursor = 0;
@ -192,6 +202,14 @@ class FlatComponent extends BaseComponent {
} }
} }
toJSON() {
const json = {};
for (const [i] of Component.schema) {
json[i] = this[i];
}
return json;
}
} }
let offset = 0; let offset = 0;
const properties = {}; const properties = {};
@ -200,7 +218,7 @@ class FlatComponent extends BaseComponent {
`return this.view.get${Schema.viewMethodFromType(type)}(this.cursor + ${offset}, true);` `return this.view.get${Schema.viewMethodFromType(type)}(this.cursor + ${offset}, true);`
); );
const set = (type) => [ const set = (type) => [
`this.parent.setDirty(1, this.view.getBigUint64(this.cursor + ${width - 9}, true));`, `this.parent.setDirty(1, Number(this.view.getBigUint64(this.cursor + ${width - 9}, true)));`,
`this.view.set${Schema.viewMethodFromType(type)}(this.cursor + ${offset}, v, true);`, `this.view.set${Schema.viewMethodFromType(type)}(this.cursor + ${offset}, v, true);`,
`this.view.setUint8(this.cursor + ${width - 1}, 1, true);`, `this.view.setUint8(this.cursor + ${width - 1}, 1, true);`,
].join(''); ].join('');
@ -210,8 +228,8 @@ class FlatComponent extends BaseComponent {
set: new Function('v', `this.view.setUint8(this.cursor + ${width - 1}, v ? 1 : 0, true);`), set: new Function('v', `this.view.setUint8(this.cursor + ${width - 1}, v ? 1 : 0, true);`),
}; };
properties.entity = { properties.entity = {
get: new Function('', `return this.view.getBigUint64(this.cursor + ${width - 9}, true);`), get: new Function('', `return Number(this.view.getBigUint64(this.cursor + ${width - 9}, true));`),
set: new Function('v', `this.view.setBigUint64(this.cursor + ${width - 9}, v ? 1 : 0, true);`), set: new Function('v', `this.view.setBigUint64(this.cursor + ${width - 9}, BigInt(v), true);`),
}; };
for (const [i, spec] of this.schema) { for (const [i, spec] of this.schema) {
const {type} = spec; const {type} = spec;
@ -302,6 +320,14 @@ class ArbitraryComponent extends BaseComponent {
} }
} }
toJSON() {
const json = {};
for (const [i] of Component.schema) {
json[i] = this[i];
}
return json;
}
}; };
const properties = {}; const properties = {};
properties.dirty = { properties.dirty = {

View File

@ -1,4 +1,4 @@
/* eslint-disable guard-for-in, max-classes-per-file, no-restricted-syntax */ /* eslint-disable guard-for-in, max-classes-per-file, no-continue, no-restricted-syntax */
export default class Ecs { export default class Ecs {
@ -41,6 +41,10 @@ export default class Ecs {
return entity; return entity;
} }
createExact(entity, components) {
this.createManyExact([entity, components]);
}
createMany(count, components) { createMany(count, components) {
const componentKeys = Object.keys(components); const componentKeys = Object.keys(components);
const entities = []; const entities = [];
@ -65,17 +69,102 @@ export default class Ecs {
return entities; return entities;
} }
createManyExact(entities) {
const creating = {};
for (let i = 0; i < entities.length; i++) {
let components = {};
let entity;
if (Array.isArray(entities[i])) {
[entity, components] = entities[i];
}
else {
entity = entities[i];
}
if (this.$$entities[entity]) {
throw new Error(`can't create existing entity ${entity}`);
}
const index = this.$$pool.indexOf(entity);
if (-1 !== index) {
this.$$pool.splice(index, 1);
}
this.$$entities[entity] = Object.keys(components);
for (const j in components) {
if (!creating[j]) {
creating[j] = [];
}
creating[j].push([entity, components[j]]);
}
}
for (const i in creating) {
this.Components[i].createMany(creating[i]);
}
}
decode(view) {
let cursor = 0;
const count = view.getUint32(cursor, true);
if (0 === count) {
return;
}
const keys = Object.keys(this.Components);
cursor += 4;
const create = [];
const update = [];
for (let i = 0; i < count; ++i) {
const entity = Number(view.getBigUint64(cursor, true));
cursor += 8;
const components = {};
const componentCount = view.getUint16(cursor, true);
cursor += 2;
for (let j = 0; j < componentCount; ++j) {
const id = view.getUint16(cursor, true);
const component = keys[id];
if (!component) {
throw new Error(`can't decode component ${id}`);
}
components[component] = {};
cursor += 2;
const instance = this.Components[component];
instance.serializer.decode(view, components[component], cursor);
cursor += instance.schema.sizeOf(components[component]);
}
if (this.$$entities[entity]) {
update.push([entity, components]);
}
else {
create.push([entity, components]);
}
}
this.createManyExact(create);
for (let i = 0; i < update.length; i++) {
const [entity, components] = update[i];
for (const j in components) {
this.Components[j].set(entity, components[j]);
}
}
}
destroyAll() {
this.destroyMany(Object.keys(this.$$entities).map((entity) => parseInt(entity, 10)));
}
destroyMany(entities) { destroyMany(entities) {
const map = {}; const map = {};
for (let i = 0; i < entities.length; i++) { for (let i = 0; i < entities.length; i++) {
for (let j = 0; j < this.$$entities[entities[i]].length; j++) { const entity = entities[i];
const key = this.$$entities[entities[i]][j]; if (!this.$$entities[entity]) {
throw new Error(`can't destroy non-existent entity ${entity}`);
}
for (let j = 0; j < this.$$entities[entity].length; j++) {
const key = this.$$entities[entity][j];
if (!map[key]) { if (!map[key]) {
map[key] = []; map[key] = [];
} }
map[key].push(entities[i]); map[key].push(entity);
} }
this.$$pool.push(entities[i]); this.$$pool.push(entity);
delete this.$$entities[entity];
this.dirty.delete(entity);
} }
for (const i in map) { for (const i in map) {
this.Components[i].destroyMany(map[i]); this.Components[i].destroyMany(map[i]);
@ -85,6 +174,54 @@ export default class Ecs {
} }
} }
encode(entities, view) {
if (0 === entities.length) {
return;
}
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++) {
let entity;
let onlyDirty = false;
if (Array.isArray(entities[i])) {
[entity, onlyDirty] = entities[i];
}
else {
entity = entities[i];
}
if (onlyDirty && !this.dirty.has(entity)) {
// eslint-disable-next-line no-continue
continue;
}
view.setBigUint64(cursor, BigInt(entity), true);
cursor += 8;
const components = this.$$entities[entity];
view.setUint16(cursor, components.length, true);
const componentLengthIndex = cursor;
cursor += 2;
if (0 === components.length) {
continue;
}
let componentsWritten = 0;
for (let i = 0; i < components.length; i++) {
const component = components[i];
const instance = this.Components[component];
if (onlyDirty && !instance.dirty) {
continue;
}
componentsWritten += 1;
view.setUint16(cursor, keys.indexOf(component), true);
cursor += 2;
const source = instance.getUnsafe(entity);
instance.serializer.encode(source, view, cursor);
cursor += instance.schema.sizeOf(source);
}
view.setUint16(componentLengthIndex, componentsWritten, true);
}
}
get(entity, Components = Object.keys(this.Components)) { get(entity, Components = Object.keys(this.Components)) {
const result = {}; const result = {};
for (let i = 0; i < Components.length; i++) { for (let i = 0; i < Components.length; i++) {
@ -122,6 +259,38 @@ export default class Ecs {
} }
} }
sizeOf(entities, onlyDirty) {
let size = 0;
if (0 === entities.length) {
return size;
}
size += 4;
for (let i = 0; i < entities.length; i++) {
const entity = entities[i];
if (!this.$$entities[entity]) {
throw new Error(`can't encode non-existent entity ${entity}`);
}
if (onlyDirty && !this.dirty.has(entity)) {
continue;
}
size += 8;
const components = this.$$entities[entity];
if (0 === components.length) {
continue;
}
size += 2;
for (let i = 0; i < components.length; i++) {
const component = components[i];
const instance = this.Components[component];
if (onlyDirty && !instance.dirty) {
continue;
}
size += 2 + instance.schema.sizeOf(instance.getUnsafe(entity));
}
}
return size;
}
tick(elapsed) { tick(elapsed) {
for (let i = 0; i < this.$$systems.length; i++) { for (let i = 0; i < this.$$systems.length; i++) {
this.$$systems[i].tick(elapsed); this.$$systems[i].tick(elapsed);

View File

@ -6,9 +6,26 @@ import Schema from './schema';
import Serializer from './serializer'; import Serializer from './serializer';
import System from './system'; import System from './system';
const N = 100; const N = 1000;
const warm = 500; 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 { class Position extends Component {
static schema = { static schema = {
@ -55,53 +72,42 @@ const createMany = () => {
for (let i = 0; i < warm; ++i) { for (let i = 0; i < warm; ++i) {
ecs.destroyMany(createMany()); ecs.destroyMany(createMany());
} }
performance.mark('create0'); mark('create', createMany);
createMany();
performance.mark('create1'); 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) { for (let i = 0; i < warm; ++i) {
ecs.tick(0.01); ecs.tick(0.01);
ecs.encode(encoding, view);
ecs.tickFinalize(); ecs.tickFinalize();
} }
performance.mark('tick0');
ecs.tick(0.01);
console.log(ecs.dirty.size);
ecs.tickFinalize();
console.log(ecs.dirty.size);
performance.mark('tick1');
console.log(ecs.$$systems[0].queries.default.count);
performance.measure(`create ${N}`, 'create0', 'create1'); mark('tick', () => ecs.tick(0.01));
performance.measure(`tick ${N}`, 'tick0', 'tick1'); mark('encode', () => ecs.encode(encoding, view));
mark('finalize', () => ecs.tickFinalize());
const {Position: p} = ecs.get(1); ecs.destroyAll();
console.log({
x: p.x,
y: p.y,
z: p.z,
dirty: p.dirty,
entity: p.entity,
});
console.log(performance.getEntriesByType('measure').map(({duration, name}) => ({duration, name}))); for (let i = 0; i < warm; ++i) {
ecs.decode(view);
ecs.destroyAll();
}
// const schema = new Schema({ mark('decode', () => ecs.decode(view));
// foo: 'uint8',
// bar: 'string',
// });
// const serializer = new Serializer(schema); console.log(JSON.stringify(ecs.get(1)));
// // const source = {foo: 8, bar: 'hello'};
// // const size = schema.sizeOf(source);
// const buffer = new ArrayBuffer(10 + 8); console.log(N, 'iterations');
// const view = new DataView(buffer); measure();
// serializer.encode({foo: 8, bar: 'hello'}, view);
// serializer.encode({foo: 8, bar: 'sup'}, view, 10);
// console.log(buffer);
// const thing = {};
// serializer.decode(view, thing);
// console.log(thing);
// serializer.decode(view, thing, 10);
// console.log(thing);

View File

@ -11,7 +11,7 @@ export default class Schema {
constructor(spec) { constructor(spec) {
this.spec = this.constructor.normalize(spec); this.spec = this.constructor.normalize(spec);
for (const i in this.spec) { for (const i in this.spec) {
const {type} = spec[i]; const {type} = this.spec[i];
const size = this.constructor.sizeOfType(type); const size = this.constructor.sizeOfType(type);
if (0 === size) { if (0 === size) {
this.width = 0; this.width = 0;
@ -20,7 +20,7 @@ export default class Schema {
this.width += size; this.width += size;
} }
for (const i in this.spec) { for (const i in this.spec) {
this.defaultValues[i] = spec[i].defaultValue; this.defaultValues[i] = this.spec[i].defaultValue;
} }
} }