flow: ecs
This commit is contained in:
parent
788e94db41
commit
90fcb90831
|
@ -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 = {
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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);
|
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue
Block a user