flow+perf: indexing, ticking, flat components, etc.

This commit is contained in:
cha0s 2024-08-05 01:48:01 -05:00
parent b5698cd392
commit 51bdda5eb9
11 changed files with 80 additions and 93 deletions

View File

@ -142,6 +142,7 @@ export default class Component {
for (const key in defaults) { for (const key in defaults) {
this[`$$${key}`] = defaults[key]; this[`$$${key}`] = defaults[key];
} }
Component.ecs.markChange(this.entity, {[Component.constructor.componentName]: values})
} }
toNet(recipient, data) { toNet(recipient, data) {
return data || Component.constructor.filterDefaults(this); return data || Component.constructor.filterDefaults(this);

View File

@ -19,6 +19,7 @@ export default class Alive extends Component {
this.$$dead = true; this.$$dead = true;
const {Ticking} = ecs.get(this.entity); const {Ticking} = ecs.get(this.entity);
if (Ticking) { if (Ticking) {
this.$$death.context.entity = ecs.get(this.entity);
const ticker = this.$$death.ticker(); const ticker = this.$$death.ticker();
ecs.addDestructionDependency(this.entity.id, ticker); ecs.addDestructionDependency(this.entity.id, ticker);
Ticking.add(ticker); Ticking.add(ticker);
@ -35,7 +36,6 @@ export default class Alive extends Component {
instance.deathScript, instance.deathScript,
{ {
ecs: this.ecs, ecs: this.ecs,
entity: this.ecs.get(instance.entity),
}, },
); );
if (0 === instance.maxHealth) { if (0 === instance.maxHealth) {

View File

@ -2,11 +2,13 @@ import Component from '@/ecs/component.js';
export default class Behaving extends Component { export default class Behaving extends Component {
instanceFromSchema() { instanceFromSchema() {
const {ecs} = this;
return class BehavingInstance extends super.instanceFromSchema() { return class BehavingInstance extends super.instanceFromSchema() {
$$routineInstances = {}; $$routineInstances = {};
tick(elapsed) { tick(elapsed) {
const routine = this.$$routineInstances[this.currentRoutine]; const routine = this.$$routineInstances[this.currentRoutine];
if (routine) { if (routine) {
routine.context.entity = ecs.get(this.entity);
routine.tick(elapsed); routine.tick(elapsed);
} }
} }
@ -20,12 +22,7 @@ export default class Behaving extends Component {
const promises = []; const promises = [];
for (const key in instance.routines) { for (const key in instance.routines) {
promises.push( promises.push(
this.ecs.readScript( this.ecs.readScript(instance.routines[key])
instance.routines[key],
{
entity: this.ecs.get(instance.entity),
},
)
.then((script) => { .then((script) => {
instance.$$routineInstances[key] = script; instance.$$routineInstances[key] = script;
}), }),

View File

@ -229,7 +229,7 @@ export default class Collider extends Component {
this.$$aabb = {x0: Infinity, x1: -Infinity, y0: Infinity, y1: -Infinity}; this.$$aabb = {x0: Infinity, x1: -Infinity, y0: Infinity, y1: -Infinity};
this.$$aabbs = []; this.$$aabbs = [];
const {bodies} = this; const {bodies} = this;
const {Direction: {direction = 0} = {}} = ecs.get(this.entity); const {Direction: {direction = 0} = {}} = ecs.get(this.entity) || {};
for (const body of bodies) { for (const body of bodies) {
let x0 = Infinity, x1 = -Infinity, y0 = Infinity, y1 = -Infinity; let x0 = Infinity, x1 = -Infinity, y0 = Infinity, y1 = -Infinity;
for (const point of transform(body.points, {rotation: direction})) { for (const point of transform(body.points, {rotation: direction})) {

View File

@ -8,7 +8,8 @@ export default class Interactive extends Component {
interact(initiator) { interact(initiator) {
const script = this.$$interact.clone(); const script = this.$$interact.clone();
script.context.initiator = initiator; script.context.initiator = initiator;
const {Ticking} = ecs.get(this.entity); script.context.subject = ecs.get(this.entity);
const {Ticking} = script.context.subject;
Ticking.add(script.ticker()); Ticking.add(script.ticker());
} }
get interacting() { get interacting() {
@ -28,7 +29,6 @@ export default class Interactive extends Component {
instance.interactScript, instance.interactScript,
{ {
ecs: this.ecs, ecs: this.ecs,
subject: this.ecs.get(instance.entity),
}, },
); );
} }

View File

@ -130,10 +130,19 @@ export default class Ecs {
} }
} }
applyDeferredChanges(entityId) {
const changes = this.deferredChanges[entityId];
delete this.deferredChanges[entityId];
for (const components of changes) {
this.markChange(entityId, components);
}
}
attach(entityIds) { attach(entityIds) {
for (const entityId of entityIds) { for (const entityId of entityIds) {
this.$$detached.delete(entityId); this.$$detached.delete(entityId);
this.$$reindexing.add(entityId); this.$$reindexing.add(entityId);
this.applyDeferredChanges(entityId);
} }
} }
@ -190,6 +199,7 @@ export default class Ecs {
for (const components of componentsList) { for (const components of componentsList) {
specificsList.push([this.$$caret, components]); specificsList.push([this.$$caret, components]);
this.$$detached.add(this.$$caret); this.$$detached.add(this.$$caret);
this.deferredChanges[this.$$caret] = [];
this.$$caret += 1; this.$$caret += 1;
} }
return this.createManySpecific(specificsList); return this.createManySpecific(specificsList);
@ -213,8 +223,6 @@ export default class Ecs {
} }
} }
entityIds.add(entityId); entityIds.add(entityId);
this.$$reindexing.add(entityId);
this.rebuild(entityId, () => componentNames);
for (const componentName of componentNames) { for (const componentName of componentNames) {
if (!creating[componentName]) { if (!creating[componentName]) {
creating[componentName] = []; creating[componentName] = [];
@ -229,15 +237,13 @@ export default class Ecs {
} }
await Promise.all(promises); await Promise.all(promises);
for (let i = 0; i < specificsList.length; i++) { for (let i = 0; i < specificsList.length; i++) {
const [entityId] = specificsList[i]; const [entityId, components] = specificsList[i];
this.$$reindexing.add(entityId);
this.rebuild(entityId, () => Object.keys(components));
if (this.$$detached.has(entityId)) { if (this.$$detached.has(entityId)) {
continue; continue;
} }
const changes = this.deferredChanges[entityId]; this.applyDeferredChanges(entityId);
delete this.deferredChanges[entityId];
for (const components of changes) {
this.markChange(entityId, components);
}
} }
return entityIds; return entityIds;
} }
@ -354,7 +360,6 @@ export default class Ecs {
const inserting = {}; const inserting = {};
const unique = new Set(); const unique = new Set();
for (const [entityId, components] of entities) { for (const [entityId, components] of entities) {
this.rebuild(entityId, (componentNames) => [...new Set(componentNames.concat(Object.keys(components)))]);
const diff = {}; const diff = {};
for (const componentName in components) { for (const componentName in components) {
if (!inserting[componentName]) { if (!inserting[componentName]) {
@ -363,7 +368,6 @@ export default class Ecs {
diff[componentName] = {}; diff[componentName] = {};
inserting[componentName].push([entityId, components[componentName]]); inserting[componentName].push([entityId, components[componentName]]);
} }
this.$$reindexing.add(entityId);
unique.add(entityId); unique.add(entityId);
this.markChange(entityId, diff); this.markChange(entityId, diff);
} }
@ -372,12 +376,13 @@ export default class Ecs {
promises.push(this.Components[componentName].insertMany(inserting[componentName])); promises.push(this.Components[componentName].insertMany(inserting[componentName]));
} }
await Promise.all(promises); await Promise.all(promises);
for (const [entityId, components] of entities) {
this.$$reindexing.add(entityId);
this.rebuild(entityId, (componentNames) => [...new Set(componentNames.concat(Object.keys(components)))]);
}
} }
markChange(entityId, components) { markChange(entityId, components) {
if (this.$$detached.has(entityId)) {
return;
}
if (this.deferredChanges[entityId]) { if (this.deferredChanges[entityId]) {
this.deferredChanges[entityId].push(components); this.deferredChanges[entityId].push(components);
return; return;
@ -486,7 +491,6 @@ export default class Ecs {
const removing = {}; const removing = {};
const unique = new Set(); const unique = new Set();
for (const [entityId, components] of entities) { for (const [entityId, components] of entities) {
this.$$reindexing.add(entityId);
unique.add(entityId); unique.add(entityId);
const diff = {}; const diff = {};
for (const componentName of components) { for (const componentName of components) {
@ -497,11 +501,14 @@ export default class Ecs {
removing[componentName].push(entityId); removing[componentName].push(entityId);
} }
this.markChange(entityId, diff); this.markChange(entityId, diff);
this.rebuild(entityId, (componentNames) => componentNames.filter((type) => !components.includes(type)));
} }
for (const componentName in removing) { for (const componentName in removing) {
this.Components[componentName].destroyMany(removing[componentName]); this.Components[componentName].destroyMany(removing[componentName]);
} }
for (const [entityId, components] of entities) {
this.$$reindexing.add(entityId);
this.rebuild(entityId, (componentNames) => componentNames.filter((type) => !components.includes(type)));
}
} }
static serialize(ecs, view) { static serialize(ecs, view) {
@ -522,6 +529,22 @@ export default class Ecs {
} }
tick(elapsed) { tick(elapsed) {
// tick systems
for (const systemName in this.Systems) {
const System = this.Systems[systemName];
if (!System.active) {
continue;
}
if (!System.frequency) {
System.tick(elapsed);
continue;
}
System.elapsed += elapsed;
while (System.elapsed >= System.frequency) {
System.tick(System.frequency);
System.elapsed -= System.frequency;
}
}
// destroy entities // destroy entities
const destroying = new Set(); const destroying = new Set();
for (const [entityId, {promises}] of this.$$destructionDependencies) { for (const [entityId, {promises}] of this.$$destructionDependencies) {
@ -541,22 +564,6 @@ export default class Ecs {
this.$$deindexing.clear(); this.$$deindexing.clear();
this.reindex(this.$$reindexing); this.reindex(this.$$reindexing);
this.$$reindexing.clear(); this.$$reindexing.clear();
// tick systems
for (const systemName in this.Systems) {
const System = this.Systems[systemName];
if (!System.active) {
continue;
}
if (!System.frequency) {
System.tick(elapsed);
continue;
}
System.elapsed += elapsed;
while (System.elapsed >= System.frequency) {
System.tick(System.frequency);
System.elapsed -= System.frequency;
}
}
} }
toJSON() { toJSON() {

View File

@ -210,19 +210,23 @@ test('skips indexing detached entities', async () => {
}, },
}, },
}); });
const {$$index: index} = ecs.system('Indexer').queries.default; const {$$map: map} = ecs.system('Indexer').queries.default;
ecs.system('Indexer').active = true; ecs.system('Indexer').active = true;
const attached = await ecs.create({Empty: {}}); const attached = await ecs.create({Empty: {}});
expect(Array.from(index)) ecs.tick(0);
expect(Array.from(map.keys()))
.to.deep.equal([attached]); .to.deep.equal([attached]);
ecs.destroyMany(new Set([attached])); ecs.destroyMany(new Set([attached]));
expect(Array.from(index)) ecs.tick(0);
expect(Array.from(map.keys()))
.to.deep.equal([]); .to.deep.equal([]);
const detached = await ecs.createDetached({Empty: {}}); const detached = await ecs.createDetached({Empty: {}});
expect(Array.from(index)) ecs.tick(0);
expect(Array.from(map.keys()))
.to.deep.equal([]); .to.deep.equal([]);
ecs.destroyMany(new Set([detached])); ecs.destroyMany(new Set([detached]));
expect(Array.from(index)) ecs.tick(0);
expect(Array.from(map.keys()))
.to.deep.equal([]); .to.deep.equal([]);
}); });
@ -336,11 +340,11 @@ test('applies creation patches', async () => {
.to.equal(64); .to.equal(64);
}); });
test('applies update patches', () => { test('applies update patches', async () => {
const ecs = new Ecs({Components: {Position}}); const ecs = new Ecs({Components: {Position}});
ecs.createSpecific(16, {Position: {x: 64}}); await ecs.createSpecific(16, {Position: {x: 64}});
ecs.apply({16: {Position: {x: 128}}}); await ecs.apply({16: {Position: {x: 128}}});
expect(Array.from(ecs.entities).length) expect(Object.keys(ecs.$$entities).length)
.to.equal(1); .to.equal(1);
expect(ecs.get(16).Position.x) expect(ecs.get(16).Position.x)
.to.equal(128); .to.equal(128);
@ -364,9 +368,9 @@ test('applies component deletion patches', async () => {
.to.deep.equal(['Position']); .to.deep.equal(['Position']);
}); });
test('calculates entity size', () => { test('calculates entity size', async () => {
const ecs = new Ecs({Components: {Empty, Position}}); const ecs = new Ecs({Components: {Empty, Position}});
ecs.createSpecific(1, {Empty: {}, Position: {}}); await ecs.createSpecific(1, {Empty: {}, Position: {}});
// ID + # of components + Empty + Position + x + y + z // ID + # of components + Empty + Position + x + y + z
// 4 + 2 + 2 + 4 + 2 + 4 + 4 + 4 + 4 = 30 // 4 + 2 + 2 + 4 + 2 + 4 + 4 + 4 + 4 = 30
expect(ecs.get(1).size()) expect(ecs.get(1).size())
@ -375,8 +379,8 @@ test('calculates entity size', () => {
test('serializes and deserializes', async () => { test('serializes and deserializes', async () => {
const ecs = new Ecs({Components: {Empty, Name, Position}}); const ecs = new Ecs({Components: {Empty, Name, Position}});
ecs.createSpecific(1, {Empty: {}, Position: {x: 64}}); await ecs.createSpecific(1, {Empty: {}, Position: {x: 64}});
ecs.createSpecific(16, {Name: {name: 'foobar'}, Position: {x: 128}}); await ecs.createSpecific(16, {Name: {name: 'foobar'}, Position: {x: 128}});
expect(ecs.toJSON()) expect(ecs.toJSON())
.to.deep.equal({ .to.deep.equal({
entities: { entities: {
@ -408,8 +412,8 @@ test('serializes and deserializes', async () => {
test('deserializes from compatible ECS', async () => { test('deserializes from compatible ECS', async () => {
const ecs = new Ecs({Components: {Empty, Name, Position}}); const ecs = new Ecs({Components: {Empty, Name, Position}});
ecs.createSpecific(1, {Empty: {}, Position: {x: 64}}); await ecs.createSpecific(1, {Empty: {}, Position: {x: 64}});
ecs.createSpecific(16, {Name: {name: 'foobar'}, Position: {x: 128}}); await ecs.createSpecific(16, {Name: {name: 'foobar'}, Position: {x: 128}});
const view = Ecs.serialize(ecs); const view = Ecs.serialize(ecs);
const deserialized = await Ecs.deserialize( const deserialized = await Ecs.deserialize(
new Ecs({Components: {Empty, Name}}), new Ecs({Components: {Empty, Name}}),

View File

@ -23,6 +23,9 @@ export default class EntityFactory {
static componentNames = sorted; static componentNames = sorted;
constructor(id) { constructor(id) {
this.id = id; this.id = id;
for (const type of sorted) {
this[type] = Components[type].get(id);
}
} }
size() { size() {
let size = 0; let size = 0;
@ -34,15 +37,6 @@ export default class EntityFactory {
return size + 4 + 2; return size + 4 + 2;
} }
} }
const properties = {};
for (const type of sorted) {
properties[type] = {};
const get = Components[type].get.bind(Components[type]);
properties[type].get = function() {
return get(this.id);
};
}
Object.defineProperties(Entity.prototype, properties);
Entity.prototype.updateAttachments = new Function('update', ` Entity.prototype.updateAttachments = new Function('update', `
${ ${
sorted sorted

View File

@ -54,8 +54,10 @@ export default class MaintainColliderHash extends System {
within(query) { within(query) {
const within = new Set(); const within = new Set();
for (const id of this.hash.within(query)) { if (this.hash) {
within.add(this.ecs.get(id)); for (const id of this.hash.within(query)) {
within.add(this.ecs.get(id));
}
} }
return within; return within;
} }

View File

@ -32,10 +32,8 @@ test('emits particles over time', async () => {
current.onValue(resolve); current.onValue(resolve);
})) }))
.to.deep.include({id: 1}); .to.deep.include({id: 1});
expect(ecs.get(1)) expect(Array.from(ecs.$$detached))
.to.not.be.undefined; .to.deep.equal([2]);
expect(ecs.get(2))
.to.be.undefined;
emitter.tick(0.06); emitter.tick(0.06);
expect(await new Promise((resolve) => { expect(await new Promise((resolve) => {
current.onValue(resolve); current.onValue(resolve);
@ -47,6 +45,6 @@ test('emits particles over time', async () => {
current.onValue(resolve); current.onValue(resolve);
})) }))
.to.deep.include({id: 2}); .to.deep.include({id: 2});
expect(ecs.get(2)) expect(Array.from(ecs.$$detached))
.to.not.be.undefined; .to.deep.equal([]);
}); });

View File

@ -28,8 +28,6 @@ addEventListener('message', (particle) => {
.onEnd(() => {}); .onEnd(() => {});
}); });
const memory = new Set();
let last = performance.now(); let last = performance.now();
function tick(now) { function tick(now) {
const elapsed = (now - last) / 1000; const elapsed = (now - last) / 1000;
@ -40,25 +38,11 @@ function tick(now) {
} }
ecs.tick(elapsed); ecs.tick(elapsed);
emitter.tick(elapsed); emitter.tick(elapsed);
const update = {}; if ('1' in ecs.diff) {
for (const id in ecs.diff) { delete ecs.diff['1'];
if (false === ecs.diff[id]) {
memory.delete(id);
update[id] = false;
}
else if (!memory.has(id)) {
update[id] = ecs.$$entities[id].toJSON();
}
else if (ecs.diff[id]) {
update[id] = ecs.diff[id];
}
memory.add(id);
} }
if ('1' in update) { if (Object.keys(ecs.diff).length > 0) {
delete update['1']; postMessage(ecs.diff);
}
if (Object.keys(update).length > 0) {
postMessage(update);
} }
ecs.setClean(); ecs.setClean();
} }