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) {
this[`$$${key}`] = defaults[key];
}
Component.ecs.markChange(this.entity, {[Component.constructor.componentName]: values})
}
toNet(recipient, data) {
return data || Component.constructor.filterDefaults(this);

View File

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

View File

@ -2,11 +2,13 @@ import Component from '@/ecs/component.js';
export default class Behaving extends Component {
instanceFromSchema() {
const {ecs} = this;
return class BehavingInstance extends super.instanceFromSchema() {
$$routineInstances = {};
tick(elapsed) {
const routine = this.$$routineInstances[this.currentRoutine];
if (routine) {
routine.context.entity = ecs.get(this.entity);
routine.tick(elapsed);
}
}
@ -20,12 +22,7 @@ export default class Behaving extends Component {
const promises = [];
for (const key in instance.routines) {
promises.push(
this.ecs.readScript(
instance.routines[key],
{
entity: this.ecs.get(instance.entity),
},
)
this.ecs.readScript(instance.routines[key])
.then((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.$$aabbs = [];
const {bodies} = this;
const {Direction: {direction = 0} = {}} = ecs.get(this.entity);
const {Direction: {direction = 0} = {}} = ecs.get(this.entity) || {};
for (const body of bodies) {
let x0 = Infinity, x1 = -Infinity, y0 = Infinity, y1 = -Infinity;
for (const point of transform(body.points, {rotation: direction})) {

View File

@ -8,7 +8,8 @@ export default class Interactive extends Component {
interact(initiator) {
const script = this.$$interact.clone();
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());
}
get interacting() {
@ -28,7 +29,6 @@ export default class Interactive extends Component {
instance.interactScript,
{
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) {
for (const entityId of entityIds) {
this.$$detached.delete(entityId);
this.$$reindexing.add(entityId);
this.applyDeferredChanges(entityId);
}
}
@ -190,6 +199,7 @@ export default class Ecs {
for (const components of componentsList) {
specificsList.push([this.$$caret, components]);
this.$$detached.add(this.$$caret);
this.deferredChanges[this.$$caret] = [];
this.$$caret += 1;
}
return this.createManySpecific(specificsList);
@ -213,8 +223,6 @@ export default class Ecs {
}
}
entityIds.add(entityId);
this.$$reindexing.add(entityId);
this.rebuild(entityId, () => componentNames);
for (const componentName of componentNames) {
if (!creating[componentName]) {
creating[componentName] = [];
@ -229,15 +237,13 @@ export default class Ecs {
}
await Promise.all(promises);
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)) {
continue;
}
const changes = this.deferredChanges[entityId];
delete this.deferredChanges[entityId];
for (const components of changes) {
this.markChange(entityId, components);
}
this.applyDeferredChanges(entityId);
}
return entityIds;
}
@ -354,7 +360,6 @@ export default class Ecs {
const inserting = {};
const unique = new Set();
for (const [entityId, components] of entities) {
this.rebuild(entityId, (componentNames) => [...new Set(componentNames.concat(Object.keys(components)))]);
const diff = {};
for (const componentName in components) {
if (!inserting[componentName]) {
@ -363,7 +368,6 @@ export default class Ecs {
diff[componentName] = {};
inserting[componentName].push([entityId, components[componentName]]);
}
this.$$reindexing.add(entityId);
unique.add(entityId);
this.markChange(entityId, diff);
}
@ -372,12 +376,13 @@ export default class Ecs {
promises.push(this.Components[componentName].insertMany(inserting[componentName]));
}
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) {
if (this.$$detached.has(entityId)) {
return;
}
if (this.deferredChanges[entityId]) {
this.deferredChanges[entityId].push(components);
return;
@ -486,7 +491,6 @@ export default class Ecs {
const removing = {};
const unique = new Set();
for (const [entityId, components] of entities) {
this.$$reindexing.add(entityId);
unique.add(entityId);
const diff = {};
for (const componentName of components) {
@ -497,11 +501,14 @@ export default class Ecs {
removing[componentName].push(entityId);
}
this.markChange(entityId, diff);
this.rebuild(entityId, (componentNames) => componentNames.filter((type) => !components.includes(type)));
}
for (const componentName in removing) {
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) {
@ -522,6 +529,22 @@ export default class Ecs {
}
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
const destroying = new Set();
for (const [entityId, {promises}] of this.$$destructionDependencies) {
@ -541,22 +564,6 @@ export default class Ecs {
this.$$deindexing.clear();
this.reindex(this.$$reindexing);
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() {

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

View File

@ -23,6 +23,9 @@ export default class EntityFactory {
static componentNames = sorted;
constructor(id) {
this.id = id;
for (const type of sorted) {
this[type] = Components[type].get(id);
}
}
size() {
let size = 0;
@ -34,15 +37,6 @@ export default class EntityFactory {
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', `
${
sorted

View File

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

View File

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

View File

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