perf: detached entities

This commit is contained in:
cha0s 2024-08-03 15:25:30 -05:00
parent 097bf9505f
commit 172b457a8c
4 changed files with 122 additions and 22 deletions

View File

@ -73,7 +73,7 @@ export default class Component {
} }
destroy(entityId) { destroy(entityId) {
this.destroyMany([entityId]); this.destroyMany(new Set([entityId]));
} }
destroyMany(entityIds) { destroyMany(entityIds) {

View File

@ -25,6 +25,8 @@ export default class Ecs {
$$destructionDependencies = new Map(); $$destructionDependencies = new Map();
$$detached = new Set();
diff = {}; diff = {};
Systems = {}; Systems = {};
@ -58,7 +60,7 @@ export default class Ecs {
async apply(patch) { async apply(patch) {
const creating = []; const creating = [];
const destroying = []; const destroying = new Set();
const inserting = []; const inserting = [];
const removing = []; const removing = [];
const updating = []; const updating = [];
@ -66,7 +68,7 @@ export default class Ecs {
const entityId = parseInt(entityIdString); const entityId = parseInt(entityIdString);
const components = patch[entityId]; const components = patch[entityId];
if (false === components) { if (false === components) {
destroying.push(entityId); destroying.add(entityId);
continue; continue;
} }
const componentsToRemove = []; const componentsToRemove = [];
@ -105,7 +107,7 @@ export default class Ecs {
creating.push([entityId, componentsToUpdate]); creating.push([entityId, componentsToUpdate]);
} }
} }
if (destroying.length > 0) { if (destroying.size > 0) {
this.destroyMany(destroying); this.destroyMany(destroying);
} }
const promises = []; const promises = [];
@ -124,6 +126,13 @@ export default class Ecs {
} }
} }
attach(entityIds) {
for (const entityId of entityIds) {
this.$$detached.delete(entityId);
}
this.reindex(entityIds);
}
changed(criteria) { changed(criteria) {
const it = Object.entries(this.diff).values(); const it = Object.entries(this.diff).values();
return { return {
@ -158,10 +167,26 @@ export default class Ecs {
return entityId; return entityId;
} }
async createDetached(components = {}) {
const [entityId] = await this.createManyDetached([components]);
return entityId;
}
async createMany(componentsList) { async createMany(componentsList) {
const specificsList = []; const specificsList = [];
for (const components of componentsList) { for (const components of componentsList) {
specificsList.push([this.$$caret++, components]); specificsList.push([this.$$caret, components]);
this.$$caret += 1;
}
return this.createManySpecific(specificsList);
}
async createManyDetached(componentsList) {
const specificsList = [];
for (const components of componentsList) {
specificsList.push([this.$$caret, components]);
this.$$detached.add(this.$$caret);
this.$$caret += 1;
} }
return this.createManySpecific(specificsList); return this.createManySpecific(specificsList);
} }
@ -170,18 +195,20 @@ export default class Ecs {
if (0 === specificsList.length) { if (0 === specificsList.length) {
return; return;
} }
const entityIds = []; const entityIds = new Set();
const creating = {}; const creating = {};
for (let i = 0; i < specificsList.length; i++) { for (let i = 0; i < specificsList.length; i++) {
const [entityId, components] = specificsList[i]; const [entityId, components] = specificsList[i];
if (!this.$$detached.has(entityId)) {
this.deferredChanges[entityId] = []; this.deferredChanges[entityId] = [];
}
const componentNames = []; const componentNames = [];
for (const componentName in components) { for (const componentName in components) {
if (this.Components[componentName]) { if (this.Components[componentName]) {
componentNames.push(componentName); componentNames.push(componentName);
} }
} }
entityIds.push(entityId); entityIds.add(entityId);
this.rebuild(entityId, () => componentNames); this.rebuild(entityId, () => componentNames);
for (const componentName of componentNames) { for (const componentName of componentNames) {
if (!creating[componentName]) { if (!creating[componentName]) {
@ -198,6 +225,9 @@ 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] = specificsList[i];
if (this.$$detached.has(entityId)) {
continue;
}
const changes = this.deferredChanges[entityId]; const changes = this.deferredChanges[entityId];
delete this.deferredChanges[entityId]; delete this.deferredChanges[entityId];
for (const components of changes) { for (const components of changes) {
@ -209,16 +239,26 @@ export default class Ecs {
} }
async createSpecific(entityId, components) { async createSpecific(entityId, components) {
return this.createManySpecific([[entityId, components]]); const [created] = await this.createManySpecific([[entityId, components]]);
return created;
} }
deindex(entityIds) { deindex(entityIds) {
// Stage 4 Draft / July 6, 2024
// const attached = entityIds.difference(this.$$detached);
const attached = new Set(entityIds);
for (const detached of this.$$detached) {
attached.delete(detached);
}
if (0 === attached.size) {
return;
}
for (const systemName in this.Systems) { for (const systemName in this.Systems) {
const System = this.Systems[systemName]; const System = this.Systems[systemName];
if (!System.active) { if (!System.active) {
continue; continue;
} }
System.deindex(entityIds); System.deindex(attached);
} }
} }
@ -259,10 +299,6 @@ export default class Ecs {
return dependencies.resolvers.promise; return dependencies.resolvers.promise;
} }
destroyAll() {
this.destroyMany(this.entities);
}
destroyMany(entityIds) { destroyMany(entityIds) {
const destroying = {}; const destroying = {};
this.deindex(entityIds); this.deindex(entityIds);
@ -286,6 +322,13 @@ export default class Ecs {
} }
} }
detach(entityIds) {
this.deindex(entityIds);
for (const entityId of entityIds) {
this.$$detached.add(entityId);
}
}
get entities() { get entities() {
const ids = []; const ids = [];
for (const entity of Object.values(this.$$entities)) { for (const entity of Object.values(this.$$entities)) {
@ -327,6 +370,9 @@ export default class Ecs {
} }
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;
@ -415,12 +461,21 @@ export default class Ecs {
} }
reindex(entityIds) { reindex(entityIds) {
// Stage 4 Draft / July 6, 2024
// const attached = entityIds.difference(this.$$detached);
const attached = new Set(entityIds);
for (const detached of this.$$detached) {
attached.delete(detached);
}
if (0 === attached.size) {
return;
}
for (const systemName in this.Systems) { for (const systemName in this.Systems) {
const System = this.Systems[systemName]; const System = this.Systems[systemName];
if (!System.active) { if (!System.active) {
continue; continue;
} }
System.reindex(entityIds); System.reindex(attached);
} }
} }

View File

@ -110,11 +110,11 @@ test('destroys entities', async () => {
.to.deep.equal(JSON.stringify({Empty: {}, Position: {y: 128}})); .to.deep.equal(JSON.stringify({Empty: {}, Position: {y: 128}}));
expect(ecs.get(entity)) expect(ecs.get(entity))
.to.not.be.undefined; .to.not.be.undefined;
ecs.destroyAll(); ecs.destroyMany(new Set([entity]));
expect(ecs.get(entity)) expect(ecs.get(entity))
.to.be.undefined; .to.be.undefined;
expect(() => { expect(() => {
ecs.destroyMany([entity]); ecs.destroyMany(new Set([entity]));
}) })
.to.throw(); .to.throw();
}); });
@ -122,10 +122,10 @@ test('destroys entities', async () => {
test('inserts components into entities', async () => { test('inserts components into entities', async () => {
const ecs = new Ecs({Components: {Empty, Position}}); const ecs = new Ecs({Components: {Empty, Position}});
const entity = await ecs.create({Empty: {}}); const entity = await ecs.create({Empty: {}});
ecs.insert(entity, {Position: {y: 128}}); await ecs.insert(entity, {Position: {y: 128}});
expect(JSON.stringify(ecs.get(entity))) expect(JSON.stringify(ecs.get(entity)))
.to.deep.equal(JSON.stringify({Empty: {}, Position: {y: 128}})); .to.deep.equal(JSON.stringify({Empty: {}, Position: {y: 128}}));
ecs.insert(entity, {Position: {y: 64}}); await ecs.insert(entity, {Position: {y: 64}});
expect(JSON.stringify(ecs.get(entity))) expect(JSON.stringify(ecs.get(entity)))
.to.deep.equal(JSON.stringify({Empty: {}, Position: {y: 64}})); .to.deep.equal(JSON.stringify({Empty: {}, Position: {y: 64}}));
}); });
@ -197,6 +197,35 @@ test('schedules entities to be deleted when ticking systems', async () => {
.to.be.undefined; .to.be.undefined;
}); });
test('skips indexing detached entities', async () => {
const ecs = new Ecs({
Components: {Empty},
Systems: {
Indexer: class extends System {
static queries() {
return {
default: ['Empty'],
};
}
},
},
});
const {$$index: index} = ecs.system('Indexer').queries.default;
ecs.system('Indexer').active = true;
const attached = await ecs.create({Empty: {}});
expect(Array.from(index))
.to.deep.equal([attached]);
ecs.destroyMany(new Set([attached]));
expect(Array.from(index))
.to.deep.equal([]);
const detached = await ecs.createDetached({Empty: {}});
expect(Array.from(index))
.to.deep.equal([]);
ecs.destroyMany(new Set([detached]));
expect(Array.from(index))
.to.deep.equal([]);
});
test('generates diffs for entity creation', async () => { test('generates diffs for entity creation', async () => {
const ecs = new Ecs(); const ecs = new Ecs();
let entity; let entity;
@ -210,7 +239,7 @@ test('generates diffs for adding and removing components', async () => {
let entity; let entity;
entity = await ecs.create(); entity = await ecs.create();
ecs.setClean(); ecs.setClean();
ecs.insert(entity, {Position: {x: 64}}); await ecs.insert(entity, {Position: {x: 64}});
expect(ecs.diff) expect(ecs.diff)
.to.deep.equal({[entity]: {Position: {x: 64}}}); .to.deep.equal({[entity]: {Position: {x: 64}}});
ecs.setClean(); ecs.setClean();
@ -246,6 +275,23 @@ test('generates diffs for entity mutations', async () => {
.to.deep.equal({}); .to.deep.equal({});
}); });
test('generates no diffs for detached entities', async () => {
const ecs = new Ecs({Components: {Position}});
let entity;
entity = await ecs.createDetached();
expect(ecs.diff)
.to.deep.equal({});
await ecs.insert(entity, {Position: {x: 64}});
expect(ecs.diff)
.to.deep.equal({});
ecs.get(entity).Position.x = 128;
expect(ecs.diff)
.to.deep.equal({});
ecs.remove(entity, ['Position']);
expect(ecs.diff)
.to.deep.equal({});
});
test('generates coalesced diffs for components', async () => { test('generates coalesced diffs for components', async () => {
const ecs = new Ecs({Components: {Position}}); const ecs = new Ecs({Components: {Position}});
let entity; let entity;
@ -253,7 +299,7 @@ test('generates coalesced diffs for components', async () => {
ecs.remove(entity, ['Position']); ecs.remove(entity, ['Position']);
expect(ecs.diff) expect(ecs.diff)
.to.deep.equal({[entity]: {Position: false}}); .to.deep.equal({[entity]: {Position: false}});
ecs.insert(entity, {Position: {}}); await ecs.insert(entity, {Position: {}});
expect(ecs.diff) expect(ecs.diff)
.to.deep.equal({[entity]: {Position: {}}}); .to.deep.equal({[entity]: {Position: {}}});
}); });

View File

@ -20,7 +20,6 @@ export default class System {
for (const i in queries) { for (const i in queries) {
this.queries[i] = new Query(queries[i], ecs); this.queries[i] = new Query(queries[i], ecs);
} }
this.reindex(ecs.entities);
} }
deindex(entityIds) { deindex(entityIds) {