diff --git a/packages/ecs/src/ecs.js b/packages/ecs/src/ecs.js index 5bd0217..537ad3f 100644 --- a/packages/ecs/src/ecs.js +++ b/packages/ecs/src/ecs.js @@ -21,7 +21,21 @@ export default class Ecs { } 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); return system; } @@ -164,13 +178,14 @@ export default class Ecs { } encode(entities, view) { + let cursor = 0; + let entitiesWritten = 0; + view.setUint32(cursor, entities.length, true); + cursor += 4; 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; @@ -183,6 +198,7 @@ export default class Ecs { if (onlyDirty && !this.dirty.has(entity)) { continue; } + entitiesWritten += 1; view.setBigUint64(cursor, BigInt(entity), true); cursor += 8; const components = this.$$entities[entity]; @@ -208,9 +224,13 @@ export default class Ecs { } view.setUint16(componentsWrittenIndex, componentsWritten, true); } + view.setUint32(0, entitiesWritten, true); } get(entity, Components = Object.keys(this.Components)) { + if (!this.$$entities[entity]) { + return undefined; + } const result = {}; for (let i = 0; i < Components.length; i++) { const component = this.Components[Components[i]].get(entity); diff --git a/packages/ecs/src/index.js b/packages/ecs/src/index.js index 33af058..1ebb1ec 100644 --- a/packages/ecs/src/index.js +++ b/packages/ecs/src/index.js @@ -1,113 +1,3 @@ -/* eslint-disable */ - -import Component from './component'; -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(); +export {default as Component} from './component'; +export {default as Ecs} from './ecs'; +export {default as System} from './system'; diff --git a/packages/ecs/src/system.js b/packages/ecs/src/system.js index 580caa4..4c97ffb 100644 --- a/packages/ecs/src/system.js +++ b/packages/ecs/src/system.js @@ -31,6 +31,10 @@ export default class System { } } + static queries() { + return {}; + } + reindex(entities) { for (const i in this.queries) { this.queries[i].reindex(entities); diff --git a/packages/ecs/test/ecs.js b/packages/ecs/test/ecs.js new file mode 100644 index 0000000..8d8fee7 --- /dev/null +++ b/packages/ecs/test/ecs.js @@ -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; +}); diff --git a/packages/ecs/test/query.js b/packages/ecs/test/query.js new file mode 100644 index 0000000..eac90d2 --- /dev/null +++ b/packages/ecs/test/query.js @@ -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); +});