diff --git a/packages/ecs/src/component.js b/packages/ecs/src/component.js index 0013927..c5a0b93 100644 --- a/packages/ecs/src/component.js +++ b/packages/ecs/src/component.js @@ -1,5 +1,6 @@ /* eslint-disable guard-for-in, max-classes-per-file, no-restricted-syntax */ import Schema from './schema'; +import Serializer from './serializer'; class BaseComponent { @@ -15,6 +16,7 @@ class BaseComponent { constructor(schema) { this.schema = schema; + this.serializer = new Serializer(schema); } 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 { @@ -113,7 +122,7 @@ class FlatComponent extends BaseComponent { window.dirty = false; window.cursor += this.constructor.width; } - this.dirty = false; + this.setDirty(0, 0); } createMany(entries) { @@ -175,6 +184,7 @@ class FlatComponent extends BaseComponent { } makeWindowClass() { + const Component = this; class Window { 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; const properties = {}; @@ -200,7 +218,7 @@ class FlatComponent extends BaseComponent { `return this.view.get${Schema.viewMethodFromType(type)}(this.cursor + ${offset}, true);` ); 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.setUint8(this.cursor + ${width - 1}, 1, true);`, ].join(''); @@ -210,8 +228,8 @@ class FlatComponent extends BaseComponent { set: new Function('v', `this.view.setUint8(this.cursor + ${width - 1}, v ? 1 : 0, true);`), }; properties.entity = { - get: new Function('', `return this.view.getBigUint64(this.cursor + ${width - 9}, true);`), - set: new Function('v', `this.view.setBigUint64(this.cursor + ${width - 9}, v ? 1 : 0, 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}, BigInt(v), true);`), }; for (const [i, spec] of this.schema) { 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 = {}; properties.dirty = { diff --git a/packages/ecs/src/ecs.js b/packages/ecs/src/ecs.js index 13116da..39c79b0 100644 --- a/packages/ecs/src/ecs.js +++ b/packages/ecs/src/ecs.js @@ -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 { @@ -41,6 +41,10 @@ export default class Ecs { return entity; } + createExact(entity, components) { + this.createManyExact([entity, components]); + } + createMany(count, components) { const componentKeys = Object.keys(components); const entities = []; @@ -65,17 +69,102 @@ export default class Ecs { 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) { const map = {}; for (let i = 0; i < entities.length; i++) { - for (let j = 0; j < this.$$entities[entities[i]].length; j++) { - const key = this.$$entities[entities[i]][j]; + const entity = entities[i]; + 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]) { 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) { 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)) { const result = {}; 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) { for (let i = 0; i < this.$$systems.length; i++) { this.$$systems[i].tick(elapsed); diff --git a/packages/ecs/src/index.js b/packages/ecs/src/index.js index 0bed1f3..33af058 100644 --- a/packages/ecs/src/index.js +++ b/packages/ecs/src/index.js @@ -6,9 +6,26 @@ import Schema from './schema'; import Serializer from './serializer'; import System from './system'; -const N = 100; +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 = { @@ -55,53 +72,42 @@ const createMany = () => { for (let i = 0; i < warm; ++i) { ecs.destroyMany(createMany()); } -performance.mark('create0'); -createMany(); -performance.mark('create1'); +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(); } -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'); -performance.measure(`tick ${N}`, 'tick0', 'tick1'); +mark('tick', () => ecs.tick(0.01)); +mark('encode', () => ecs.encode(encoding, view)); +mark('finalize', () => ecs.tickFinalize()); -const {Position: p} = ecs.get(1); -console.log({ - x: p.x, - y: p.y, - z: p.z, - dirty: p.dirty, - entity: p.entity, -}); +ecs.destroyAll(); -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({ -// foo: 'uint8', -// bar: 'string', -// }); +mark('decode', () => ecs.decode(view)); -// const serializer = new Serializer(schema); -// // const source = {foo: 8, bar: 'hello'}; -// // const size = schema.sizeOf(source); +console.log(JSON.stringify(ecs.get(1))); -// const buffer = new ArrayBuffer(10 + 8); -// const view = new DataView(buffer); -// 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); +console.log(N, 'iterations'); +measure(); diff --git a/packages/ecs/src/schema.js b/packages/ecs/src/schema.js index f68f3e2..c8acd1c 100644 --- a/packages/ecs/src/schema.js +++ b/packages/ecs/src/schema.js @@ -11,7 +11,7 @@ export default class Schema { constructor(spec) { this.spec = this.constructor.normalize(spec); for (const i in this.spec) { - const {type} = spec[i]; + const {type} = this.spec[i]; const size = this.constructor.sizeOfType(type); if (0 === size) { this.width = 0; @@ -20,7 +20,7 @@ export default class Schema { this.width += size; } for (const i in this.spec) { - this.defaultValues[i] = spec[i].defaultValue; + this.defaultValues[i] = this.spec[i].defaultValue; } }