From da6d15e9108ec6beca3b2c5cce84011349f9b0fc Mon Sep 17 00:00:00 2001 From: cha0s Date: Thu, 15 Sep 2022 09:00:49 -0500 Subject: [PATCH] refactor: test and clean --- packages/ecs/src/component.js | 372 +----------------------- packages/ecs/src/component/arbitrary.js | 121 ++++++++ packages/ecs/src/component/base.js | 79 +++++ packages/ecs/src/component/flat.js | 172 +++++++++++ packages/ecs/src/ecs.js | 11 +- packages/ecs/src/serializer.js | 2 +- packages/ecs/test/schema.js | 15 + packages/ecs/test/serializer.js | 32 ++ 8 files changed, 431 insertions(+), 373 deletions(-) create mode 100644 packages/ecs/src/component/arbitrary.js create mode 100644 packages/ecs/src/component/base.js create mode 100644 packages/ecs/src/component/flat.js create mode 100644 packages/ecs/test/schema.js create mode 100644 packages/ecs/test/serializer.js diff --git a/packages/ecs/src/component.js b/packages/ecs/src/component.js index c5a0b93..17f4e8a 100644 --- a/packages/ecs/src/component.js +++ b/packages/ecs/src/component.js @@ -1,375 +1,17 @@ /* eslint-disable guard-for-in, max-classes-per-file, no-restricted-syntax */ +import ArbitraryComponent from './component/arbitrary'; +import FlatComponent from './component/flat'; import Schema from './schema'; -import Serializer from './serializer'; - -class BaseComponent { - - $$dirty = true; - - map = []; - - pool = []; - - schema; - - serializer; - - constructor(schema) { - this.schema = schema; - this.serializer = new Serializer(schema); - } - - allocate() { - const [index] = this.allocateMany(1); - return index; - } - - allocateMany(count) { - const results = []; - // eslint-disable-next-line no-param-reassign - while (count-- > 0 && this.pool.length > 0) { - results.push(this.pool.pop()); - } - return results; - } - - create(entity, values) { - const [result] = this.createMany([entity, values]); - return result; - } - - destroy(entity) { - this.destroyMany([entity]); - } - - destroyMany(entities) { - this.freeMany(entities.map((entity) => this.map[entity]).filter((index) => !!index)); - for (let i = 0; i < entities.length; i++) { - this.map[entities[i]] = undefined; - } - } - - get dirty() { - return this.$$dirty; - } - - setDirty(dirty) { - this.$$dirty = dirty; - } - - free(index) { - this.freeMany([index]); - } - - freeMany(indices) { - for (let i = 0; i < indices.length; ++i) { - this.pool.push(indices[i]); - } - } - - set(entity, values) { - const instance = this.getUnsafe(entity); - for (const i in values) { - instance[i] = values[i]; - } - } - -} - -class FlatComponent extends BaseComponent { - - chunkSize = 64; - - caret = 0; - - data = new ArrayBuffer(0); - - Window; - - window; - - allocateMany(count) { - const results = super.allocateMany(count); - // eslint-disable-next-line no-param-reassign - count -= results.length; - if (count > 0) { - const required = (this.caret + count) * this.constructor.width; - if (required > this.data.byteLength) { - const chunkWidth = this.chunkSize * this.constructor.width; - const remainder = required % chunkWidth; - const extra = 0 === remainder ? 0 : chunkWidth - remainder; - const size = required + extra; - const data = new ArrayBuffer(size); - (new Uint8Array(data)).set(this.data); - this.data = data; - } - for (let i = 0; i < count; ++i) { - results.push(this.caret++); - } - } - return results; - } - - clean() { - if (!this.dirty) { - return; - } - if (!this.Window) { - this.Window = this.makeWindowClass(); - } - const window = new this.Window(this.data, this); - for (let i = 0; i < this.caret; ++i) { - window.dirty = false; - window.cursor += this.constructor.width; - } - this.setDirty(0, 0); - } - - createMany(entries) { - if (entries.length > 0) { - const allocated = this.allocateMany(entries.length); - if (!this.Window) { - this.Window = this.makeWindowClass(); - } - const window = new this.Window(this.data, this); - const {defaultValues} = this.schema; - for (let i = 0; i < entries.length; ++i) { - let entity; - let values = {}; - if (Array.isArray(entries[i])) { - [entity, values] = entries[i]; - } - else { - entity = entries[i]; - } - this.map[entity] = allocated[i]; - window.cursor = allocated[i] * this.constructor.width; - window.entity = entity; - for (const [i] of this.schema) { - if (i in values) { - window[i] = values[i]; - } - else if (i in defaultValues) { - window[i] = defaultValues[i]; - } - } - } - } - } - - get(entity) { - if ('undefined' === typeof this.map[entity]) { - return undefined; - } - if (!this.Window) { - this.Window = this.makeWindowClass(); - } - const window = new this.Window(this.data, this); - window.cursor = this.map[entity] * this.constructor.width; - return window; - } - - getUnsafe(entity) { - if ('undefined' === typeof this.map[entity]) { - return undefined; - } - if (!this.Window) { - this.Window = this.makeWindowClass(); - } - if (!this.window) { - this.window = new this.Window(this.data, this); - } - this.window.cursor = this.map[entity] * this.constructor.width; - return this.window; - } - - makeWindowClass() { - const Component = this; - class Window { - - cursor = 0; - - parent; - - view; - - constructor(data, parent) { - if (data) { - this.view = new DataView(data); - } - if (parent) { - this.parent = parent; - } - } - - toJSON() { - const json = {}; - for (const [i] of Component.schema) { - json[i] = this[i]; - } - return json; - } - - } - let offset = 0; - const properties = {}; - const {width} = this.constructor; - const get = (type) => ( - `return this.view.get${Schema.viewMethodFromType(type)}(this.cursor + ${offset}, true);` - ); - const set = (type) => [ - `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(''); - /* eslint-disable no-new-func */ - properties.dirty = { - get: new Function('', `return !!this.view.getUint8(this.cursor + ${width - 1}, true);`), - set: new Function('v', `this.view.setUint8(this.cursor + ${width - 1}, v ? 1 : 0, true);`), - }; - properties.entity = { - 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; - properties[i] = {}; - properties[i].get = new Function('', get(type)); - properties[i].set = new Function('v', set(type)); - offset += Schema.sizeOfType(type); - } - /* eslint-enable no-new-func */ - Object.defineProperties(Window.prototype, properties); - return Window; - } - -} - -class ArbitraryComponent extends BaseComponent { - - data = []; - - Instance; - - allocateMany(count) { - if (!this.Instance) { - this.Instance = this.instanceFromSchema(); - } - const results = super.allocateMany(count); - // eslint-disable-next-line no-param-reassign - count -= results.length; - // eslint-disable-next-line no-param-reassign - while (count--) { - results.push(this.data.push(new this.Instance()) - 1); - } - return results; - } - - clean() { - if (!this.dirty) { - return; - } - for (const i in this.map) { - this.data[this.map[i]].dirty = false; - } - this.setDirty(false, 0); - } - - createMany(entries) { - if (entries.length > 0) { - const allocated = this.allocateMany(entries.length); - for (let i = 0; i < entries.length; ++i) { - let entity; - let values = {}; - if (Array.isArray(entries[i])) { - [entity, values] = entries[i]; - } - else { - entity = entries[i]; - } - this.map[entity] = allocated[i]; - this.data[allocated[i]].entity = entity; - for (const j in values) { - if (this.schema.has(j)) { - this.data[allocated[i]][j] = values[j]; - } - } - } - } - } - - get(entity) { - return this.data[this.map[entity]]; - } - - getUnsafe(entity) { - return this.get(entity); - } - - instanceFromSchema() { - const Component = this; - const Instance = class { - - $$dirty = 1; - - $$entity = 0; - - constructor() { - for (const [i, {defaultValue}] of Component.schema) { - this[i] = defaultValue; - } - } - - toJSON() { - const json = {}; - for (const [i] of Component.schema) { - json[i] = this[i]; - } - return json; - } - - }; - const properties = {}; - properties.dirty = { - get: function get() { - return !!this.$$dirty; - }, - set: function set(v) { - this.$$dirty = v; - }, - }; - properties.entity = { - get: function get() { - return this.$$entity; - }, - set: function set(v) { - this.$$entity = v; - }, - }; - for (const [i] of Component.schema) { - properties[i] = { - get: function get() { - return this[`$$${i}`]; - }, - set: function set(v) { - this[`$$${i}`] = v; - this.$$dirty = 1; - Component.setDirty(1, this.entity); - }, - }; - } - Object.defineProperties(Instance.prototype, properties); - return Instance; - } - -} export default function ComponentRouter() { const schema = new Schema(this.constructor.schema); let RealClass; if (schema.width > 0) { - RealClass = class extends FlatComponent {}; - RealClass.width = schema.width + 9; // 1 for dirty, 8 for entity + RealClass = class extends FlatComponent { + + static width = schema.width + 9; // 1 for dirty, 8 for entity + + }; } else { RealClass = class extends ArbitraryComponent {}; diff --git a/packages/ecs/src/component/arbitrary.js b/packages/ecs/src/component/arbitrary.js new file mode 100644 index 0000000..08907f7 --- /dev/null +++ b/packages/ecs/src/component/arbitrary.js @@ -0,0 +1,121 @@ +/* eslint-disable guard-for-in, max-classes-per-file, no-restricted-syntax */ +import BaseComponent from './base'; + +export default class ArbitraryComponent extends BaseComponent { + + data = []; + + Instance; + + allocateMany(count) { + if (!this.Instance) { + this.Instance = this.instanceFromSchema(); + } + const results = super.allocateMany(count); + // eslint-disable-next-line no-param-reassign + count -= results.length; + // eslint-disable-next-line no-param-reassign + while (count--) { + results.push(this.data.push(new this.Instance()) - 1); + } + return results; + } + + createMany(entries) { + if (entries.length > 0) { + const allocated = this.allocateMany(entries.length); + for (let i = 0; i < entries.length; ++i) { + let entity; + let values = {}; + if (Array.isArray(entries[i])) { + [entity, values] = entries[i]; + } + else { + entity = entries[i]; + } + this.map[entity] = allocated[i]; + this.data[allocated[i]].entity = entity; + for (const j in values) { + if (this.schema.has(j)) { + this.data[allocated[i]][j] = values[j]; + } + } + } + } + } + + get(entity) { + return this.data[this.map[entity]]; + } + + getUnsafe(entity) { + return this.get(entity); + } + + instanceFromSchema() { + const Component = this; + const Instance = class { + + $$dirty = 1; + + $$entity = 0; + + constructor() { + for (const [i, {defaultValue}] of Component.schema) { + this[i] = defaultValue; + } + } + + toJSON() { + const json = {}; + for (const [i] of Component.schema) { + json[i] = this[i]; + } + return json; + } + + }; + const properties = {}; + properties.dirty = { + get: function get() { + return !!this.$$dirty; + }, + set: function set(v) { + this.$$dirty = v; + }, + }; + properties.entity = { + get: function get() { + return this.$$entity; + }, + set: function set(v) { + this.$$entity = v; + }, + }; + for (const [i] of Component.schema) { + properties[i] = { + get: function get() { + return this[`$$${i}`]; + }, + set: function set(v) { + this[`$$${i}`] = v; + this.$$dirty = 1; + Component.setDirty(this.entity); + }, + }; + } + Object.defineProperties(Instance.prototype, properties); + return Instance; + } + + setClean() { + super.setClean(); + if (!this.dirty) { + return; + } + for (let i = 0; i < this.data.length; i++) { + this.data[i].dirty = false; + } + } + +} diff --git a/packages/ecs/src/component/base.js b/packages/ecs/src/component/base.js new file mode 100644 index 0000000..adc6f83 --- /dev/null +++ b/packages/ecs/src/component/base.js @@ -0,0 +1,79 @@ +/* eslint-disable no-restricted-syntax, guard-for-in */ +import Serializer from '../serializer'; + +export default class BaseComponent { + + $$dirty = true; + + map = []; + + pool = []; + + schema; + + serializer; + + constructor(schema) { + this.schema = schema; + this.serializer = new Serializer(schema); + } + + allocate() { + const [index] = this.allocateMany(1); + return index; + } + + allocateMany(count) { + const results = []; + // eslint-disable-next-line no-param-reassign + while (count-- > 0 && this.pool.length > 0) { + results.push(this.pool.pop()); + } + return results; + } + + create(entity, values) { + this.createMany([entity, values]); + } + + destroy(entity) { + this.destroyMany([entity]); + } + + destroyMany(entities) { + this.freeMany(entities.map((entity) => this.map[entity]).filter((index) => !!index)); + for (let i = 0; i < entities.length; i++) { + this.map[entities[i]] = undefined; + } + } + + get dirty() { + return this.$$dirty; + } + + free(index) { + this.freeMany([index]); + } + + freeMany(indices) { + for (let i = 0; i < indices.length; ++i) { + this.pool.push(indices[i]); + } + } + + set(entity, values) { + const instance = this.getUnsafe(entity); + for (const i in values) { + instance[i] = values[i]; + } + } + + setClean() { + this.$$dirty = false; + } + + setDirty() { + this.$$dirty = true; + } + +} diff --git a/packages/ecs/src/component/flat.js b/packages/ecs/src/component/flat.js new file mode 100644 index 0000000..e4da73d --- /dev/null +++ b/packages/ecs/src/component/flat.js @@ -0,0 +1,172 @@ +/* eslint-disable guard-for-in, max-classes-per-file, no-restricted-syntax */ +import BaseComponent from './base'; +import Schema from '../schema'; + +export default class FlatComponent extends BaseComponent { + + chunkSize = 64; + + caret = 0; + + data = new ArrayBuffer(0); + + Window; + + window; + + allocateMany(count) { + const results = super.allocateMany(count); + // eslint-disable-next-line no-param-reassign + count -= results.length; + if (count > 0) { + const required = (this.caret + count) * this.constructor.width; + if (required > this.data.byteLength) { + const chunkWidth = this.chunkSize * this.constructor.width; + const remainder = required % chunkWidth; + const extra = 0 === remainder ? 0 : chunkWidth - remainder; + const size = required + extra; + const data = new ArrayBuffer(size); + (new Uint8Array(data)).set(this.data); + this.data = data; + } + for (let i = 0; i < count; ++i) { + results.push(this.caret++); + } + } + return results; + } + + createMany(entries) { + if (entries.length > 0) { + const allocated = this.allocateMany(entries.length); + if (!this.Window) { + this.Window = this.makeWindowClass(); + } + const window = new this.Window(this.data, this); + const {defaultValues} = this.schema; + for (let i = 0; i < entries.length; ++i) { + let entity; + let values = {}; + if (Array.isArray(entries[i])) { + [entity, values] = entries[i]; + } + else { + entity = entries[i]; + } + this.map[entity] = allocated[i]; + window.cursor = allocated[i] * this.constructor.width; + window.entity = entity; + for (const [i] of this.schema) { + if (i in values) { + window[i] = values[i]; + } + else if ('undefined' !== typeof defaultValues[i]) { + window[i] = defaultValues[i]; + } + } + } + } + } + + get(entity) { + if ('undefined' === typeof this.map[entity]) { + return undefined; + } + if (!this.Window) { + this.Window = this.makeWindowClass(); + } + const window = new this.Window(this.data, this); + window.cursor = this.map[entity] * this.constructor.width; + return window; + } + + getUnsafe(entity) { + if ('undefined' === typeof this.map[entity]) { + return undefined; + } + if (!this.Window) { + this.Window = this.makeWindowClass(); + } + if (!this.window) { + this.window = new this.Window(this.data, this); + } + this.window.cursor = this.map[entity] * this.constructor.width; + return this.window; + } + + makeWindowClass() { + const Component = this; + class Window { + + cursor = 0; + + parent; + + view; + + constructor(data, parent) { + if (data) { + this.view = new DataView(data); + } + if (parent) { + this.parent = parent; + } + } + + toJSON() { + const json = {}; + for (const [i] of Component.schema) { + json[i] = this[i]; + } + return json; + } + + } + let offset = 0; + const properties = {}; + const {width} = this.constructor; + const get = (type) => ( + `return this.view.get${Schema.viewMethodFromType(type)}(this.cursor + ${offset}, true);` + ); + const set = (type) => [ + `this.parent.setDirty(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(''); + /* eslint-disable no-new-func */ + properties.dirty = { + get: new Function('', `return !!this.view.getUint8(this.cursor + ${width - 1}, true);`), + set: new Function('v', `this.view.setUint8(this.cursor + ${width - 1}, v ? 1 : 0, true);`), + }; + properties.entity = { + 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; + properties[i] = {}; + properties[i].get = new Function('', get(type)); + properties[i].set = new Function('v', set(type)); + offset += Schema.sizeOfType(type); + } + /* eslint-enable no-new-func */ + Object.defineProperties(Window.prototype, properties); + return Window; + } + + setClean() { + super.setClean(); + if (!this.dirty) { + return; + } + if (!this.Window) { + this.Window = this.makeWindowClass(); + } + const window = new this.Window(this.data, this); + for (let i = 0; i < this.caret; ++i) { + window.dirty = false; + window.cursor += this.constructor.width; + } + } + +} diff --git a/packages/ecs/src/ecs.js b/packages/ecs/src/ecs.js index 39c79b0..4eb392c 100644 --- a/packages/ecs/src/ecs.js +++ b/packages/ecs/src/ecs.js @@ -19,12 +19,9 @@ export default class Ecs { constructor(Components) { for (const i in Components) { const comp = new Components[i](); - const {setDirty} = comp; - comp.setDirty = (dirty, entity) => { - setDirty.call(comp, dirty); - if (entity) { - this.dirty.add(entity); - } + comp.setDirty = (entity) => { + comp.$$dirty = true; + this.dirty.add(entity); }; this.Components[i] = comp; } @@ -299,7 +296,7 @@ export default class Ecs { tickClean() { for (const i in this.Components) { - this.Components[i].clean(); + this.Components[i].setClean(); } this.dirty.clear(); } diff --git a/packages/ecs/src/serializer.js b/packages/ecs/src/serializer.js index 0ffa878..b34e0ce 100644 --- a/packages/ecs/src/serializer.js +++ b/packages/ecs/src/serializer.js @@ -4,7 +4,7 @@ import Schema from './schema'; export default class Serializer { constructor(schema) { - this.schema = schema; + this.schema = schema instanceof Schema ? schema : new Schema(schema); } decode(view, destination, offset = 0) { diff --git a/packages/ecs/test/schema.js b/packages/ecs/test/schema.js new file mode 100644 index 0000000..6f0aad7 --- /dev/null +++ b/packages/ecs/test/schema.js @@ -0,0 +1,15 @@ +import {expect} from 'chai'; + +import Schema from '../src/schema'; + +it('can validate a schema', () => { + expect(() => new Schema({test: 'unknown'})) + .to.throw(); +}); + +it('can calculate the size of an instance', () => { + expect((new Schema({foo: 'uint8', bar: 'uint32'})).sizeOf({foo: 69, bar: 420})) + .to.equal(5); + expect((new Schema({foo: 'string'})).sizeOf({foo: 'hi'})) + .to.equal(4 + 2); +}); diff --git a/packages/ecs/test/serializer.js b/packages/ecs/test/serializer.js new file mode 100644 index 0000000..37925d0 --- /dev/null +++ b/packages/ecs/test/serializer.js @@ -0,0 +1,32 @@ +import {expect} from 'chai'; + +import Serializer from '../src/serializer'; + +it('can encode and decode', () => { + const entries = [ + ['uint8', 255], + ['int8', -128], + ['int8', 127], + ['uint16', 65535], + ['int16', -32768], + ['int16', 32767], + ['uint32', 4294967295], + ['int32', -2147483648], + ['int32', 2147483647], + ['uint64', 18446744073709551615n], + ['int64', -9223372036854775808n], + ['int64', 9223372036854775807n], + ['float32', 0.5], + ['float64', 1.234], + ['string', 'hello world'], + ]; + const schema = entries.reduce((r, [type]) => ({...r, [Object.keys(r).length]: type}), {}); + const data = entries.reduce((r, [, value]) => ({...r, [Object.keys(r).length]: value}), {}); + const serializer = new Serializer(schema); + const view = new DataView(new ArrayBuffer(serializer.schema.sizeOf(data))); + serializer.encode(data, view); + const result = {}; + serializer.decode(view, result); + expect(data) + .to.deep.equal(result); +});