From 68251c858635a23715ef13b0f8da82c7ce26ec76 Mon Sep 17 00:00:00 2001 From: Mike Date: Sun, 30 Jun 2024 00:46:31 -0700 Subject: [PATCH] refactor deferredRemovals to not rely on entity indexes --- component-set.js | 38 ++++++++++++++ ecs.js | 85 +++++++++++-------------------- test/component-set.js | 19 +++++++ test/createWorld.js | 2 +- test/removeComponentFromEntity.js | 5 +- test/removeEntity.js | 14 ++--- 6 files changed, 97 insertions(+), 66 deletions(-) create mode 100644 component-set.js create mode 100644 test/component-set.js diff --git a/component-set.js b/component-set.js new file mode 100644 index 0000000..c3bbcdb --- /dev/null +++ b/component-set.js @@ -0,0 +1,38 @@ +import removeItems from 'remove-array-items' + + +/** + * Track an entity, component tuple as a unique key in a set. + */ + +export function create () { + return [ ] +} + + +export function includes (set, entity, componentName) { + for (const entry of set) { + if (entry[0] === entity && entry[1] === componentName) { + return true + } + } + + return false +} + + +export function add (set, entity, componentName) { + set.push([ entity, componentName ]) +} + + +export function remove (set, entity, componentName) { + for (let i=set.length-1; i >= 0; i--) { + const entry = set[i] + if (entry[0] === entity && entry[1] === componentName) { + removeItems(set, i, 1) + return + } + } +} + diff --git a/ecs.js b/ecs.js index da2d3d5..aec25a9 100644 --- a/ecs.js +++ b/ecs.js @@ -1,5 +1,5 @@ -import orderedInsert from './ordered-insert.js' -import removeItems from 'remove-array-items' +import * as ComponentSet from './component-set.js' +import removeItems from 'remove-array-items' const now = (typeof performance === 'undefined') ? (() => Date.now()) : (() => performance.now()) @@ -68,9 +68,8 @@ const now = (typeof performance === 'undefined') ? (() => Date.now()) : (() => p /** * @typedef { Object } DeferredRemovalMap - * @prop {number[]} entities indexes into entities array, sorted from highest to lowest - * @prop {string[]} components [ entity index, component name ] pairs sorted from highest to lowest - * Stored as a string but seperated with `__@@ECS@@__` + * @prop {number[]} entities + * @prop {string[]} components [ entity, component name ] */ /** @@ -131,8 +130,8 @@ export function createWorld (worldId=Math.ceil(Math.random() * 999999999) ) { }, deferredRemovals: { - entities: [ ], // indexes into entities array, sorted from highest to lowest - components: [ ] // [ entity index, component name ] pairs (unsorted) + entities: new Set(), + components: ComponentSet.create() // [ entity, componentName ] pairs }, stats: { @@ -260,11 +259,8 @@ export function addComponentToEntity (world, entity, componentName, componentDat if (deferredRemoval) { // add the component to the list of components to remove when the cleanup function is invoked - const idx = world.entities.indexOf(entity) - const removalKey = `${idx}__@@ECS@@__${componentName}` - - if (!world.deferredRemovals.components.includes(removalKey)) - world.deferredRemovals.components.push(removalKey) + if (!ComponentSet.includes(world.deferredRemovals.components, entity, componentName)) + ComponentSet.add(world.deferredRemovals.components, entity, componentName) } else { _removeComponent(world, entity, componentName) } @@ -287,13 +283,12 @@ export function addComponentToEntity (world, entity, componentName, componentDat if (deferredRemoval) { // add the entity to the list of entities to remove when the cleanup function is invoked - if (!world.deferredRemovals.entities.includes(idx)) { - orderedInsert(world.deferredRemovals.entities, idx) + if (!world.deferredRemovals.entities.has(entity)) { + world.deferredRemovals.entities.add(entity) world.stats.entityCount-- } } else { - const shiftUpEntities = true - _removeEntity(world, entity, shiftUpEntities) + _removeEntity(world, entity) } } @@ -574,7 +569,7 @@ function _removeComponent (world, entity, componentName) { * @param {World} world * @param {Entity} entity */ -function _removeEntity (world, entity, shiftUpEntities=false) { +function _removeEntity (world, entity) { for (const componentName in entity) if (entity[componentName]) world.stats.componentCount[componentName] -= 1 @@ -589,35 +584,20 @@ function _removeEntity (world, entity, shiftUpEntities=false) { removeItems(world.entities, entityToRemoveIdx, 1) - if (shiftUpEntities) { - for (let i=world.deferredRemovals.entities.length-1; i >= 0; i--) { - const idx = world.deferredRemovals.entities[i] - if (idx > entityToRemoveIdx) { - world.deferredRemovals.entities[i] -= 1 - } else if (idx === entityToRemoveIdx) { - // if this entity was defer removed, but then insta-removed, we can remove - // this entity from the deferred removal list. e.g., - // - // ECS.removeEntity(w, e, true) // deferred removal - // ECS.removeEntity(w, e, false) // instant removal - // <-- at this point, there shouldnt be anything in the deferred removal list for the cleanup step to run - // ECS.cleanup() - removeItems(world.deferredRemovals.entities, i, 1) - } - } - - for (let i=world.deferredRemovals.components.length-1; i >= 0; i--) { - const str = world.deferredRemovals.components[i] - let [ entityIdx, componentName ] = str.split('__@@ECS@@__') - entityIdx = parseInt(entityIdx, 10) - - if (entityIdx > entityToRemoveIdx) { - world.deferredRemovals.components[i] = `${entityIdx-1}__@@ECS@@__${componentName}` - } - else if (entityIdx === entityToRemoveIdx) { - // remove this component from the deferred list since the entity it belongs to has already been removed - removeItems(world.deferredRemovals.components, i, 1) - } + // if this entity was defer removed, but then insta-removed, we can remove + // this entity from the deferred removal list. e.g., + // + // ECS.removeEntity(w, e, true) // deferred removal + // ECS.removeEntity(w, e, false) // instant removal + // <-- at this point, there shouldnt be anything in the deferred removal list for the cleanup step to run + // ECS.cleanup() + world.deferredRemovals.entities.delete(entity) + + for (let i=world.deferredRemovals.components.length-1; i >= 0; i--) { + const [ e ] = world.deferredRemovals.components[i] + if (e === entity) { + // remove this component from the deferred list since the entity it belongs to has already been removed + removeItems(world.deferredRemovals.components, i, 1) } } @@ -666,25 +646,18 @@ export function cleanup (world) { // process all entity components marked for deferred removal - // - // component removals MUST be processed before entity removals because - // the component removal items include an index into the entity array, and - // this will become invalid when entities are removed and the remaining items shift positions for (let i=0; i < world.deferredRemovals.components.length; i++) { - const [ entityIdx, componentName ] = world.deferredRemovals.components[i].split('__@@ECS@@__') - const entity = world.entities[entityIdx] + const [ entity, componentName ] = world.deferredRemovals.components[i] _removeComponent(world, entity, componentName) } world.deferredRemovals.components.length = 0 // process all entities marked for deferred removal - for (const entityIdx of world.deferredRemovals.entities) { - const entity = world.entities[entityIdx] + for (const entity of world.deferredRemovals.entities) _removeEntity(world, entity) - } - world.deferredRemovals.entities.length = 0 + world.deferredRemovals.entities.clear() if ((typeof window !== 'undefined') && window.__MREINSTEIN_ECS_DEVTOOLS) { // running at 60fps seems to queue up a lot of messages. I'm thinking it might just be more diff --git a/test/component-set.js b/test/component-set.js new file mode 100644 index 0000000..e575900 --- /dev/null +++ b/test/component-set.js @@ -0,0 +1,19 @@ +import * as ComponentSet from '../component-set.js' +import tap from 'tap' + + +{ + const s = ComponentSet.create() + + const a = { } + //const b = { } + + ComponentSet.add(s, a, 'comp-1') + //ComponentSet.add(s, a, 'comp-2') + //ComponentSet.add(s, b, 'comp-1') + + tap.equal(ComponentSet.includes(s, a, 'comp-1'), true) + + ComponentSet.remove(s, a, 'comp-1') + tap.equal(ComponentSet.includes(s, a, 'comp-1'), false) +} diff --git a/test/createWorld.js b/test/createWorld.js index 247c64a..f976cde 100644 --- a/test/createWorld.js +++ b/test/createWorld.js @@ -17,7 +17,7 @@ tap.same(w, { _removed: new Set() }, deferredRemovals: { - entities: [ ], + entities: new Set(), components: [ ] }, stats: { diff --git a/test/removeComponentFromEntity.js b/test/removeComponentFromEntity.js index e11c71a..7282260 100644 --- a/test/removeComponentFromEntity.js +++ b/test/removeComponentFromEntity.js @@ -79,7 +79,8 @@ import tap from 'tap' ECS.removeComponentFromEntity(w, e8, 'dupe') tap.same(ECS.getEntities(w, [ 'dupe' ], 'removed'), [ e8 ], 'multiple removals only appear once in the removed list') - tap.same(w.deferredRemovals.components, [ '0__@@ECS@@__dupe' ], 'multiple removals only appear once in the removed list') + + tap.same(w.deferredRemovals.components, [ [ e8, 'dupe' ] ], 'multiple removals only appear once in the removed list') ECS.cleanup(w) @@ -102,7 +103,7 @@ import tap from 'tap' tap.equal(ECS.getEntities(w, [ 'aabb' ]).length, 0) tap.same(w.entities, [ { transform: {} } ], 'immediately remove component from entity') - tap.same(w.deferredRemovals, { entities: [ ], components: [ ] }, 'does not include the component in the deferred removal list') + tap.same(w.deferredRemovals, { entities: new Set(), components: [ ] }, 'does not include the component in the deferred removal list') tap.same(w.stats.componentCount, { aabb: 0, transform: 1 }, 'immediately adjusts component stats') } diff --git a/test/removeEntity.js b/test/removeEntity.js index 78bfa0f..a0cfb76 100644 --- a/test/removeEntity.js +++ b/test/removeEntity.js @@ -17,7 +17,7 @@ import tap from 'tap' ECS.removeEntity(w, e) - tap.same(w.deferredRemovals.entities, [ 0 ], 'entity is added to deferred entity remove list') + tap.same(w.deferredRemovals.entities.values().next().value, e, 'entity is added to deferred entity remove list') ECS.cleanup(w) // process deferred removals tap.equal(w.entities.length, 0, 'entity gets removed from the world') @@ -87,7 +87,7 @@ import tap from 'tap' ECS.removeEntity(w3, e6) ECS.removeEntity(w3, e6) - tap.same(w3.deferredRemovals.entities, [ 0 ]) + tap.same(w3.deferredRemovals.entities.values().next().value, e6) ECS.cleanup(w3) @@ -157,18 +157,18 @@ import tap from 'tap' ECS.removeEntity(w, e, true) - tap.equal(w.deferredRemovals.entities.length, 1) + tap.equal(w.deferredRemovals.entities.size, 1) ECS.removeEntity(w, e, false) - tap.equal(w.deferredRemovals.entities.length, 0) + tap.equal(w.deferredRemovals.entities.size, 0) tap.equal(w.deferredRemovals.components.length, 0) tap.equal(w.listeners.removed.size, 0) tap.equal(w.listeners._removed.size, 1) ECS.cleanup(w) - tap.equal(w.deferredRemovals.entities.length, 0) + tap.equal(w.deferredRemovals.entities.size, 0) tap.equal(w.deferredRemovals.components.length, 0) tap.equal(w.listeners.removed.size, 1) tap.equal(w.listeners._removed.size, 0) @@ -188,7 +188,7 @@ import tap from 'tap' ECS.removeEntity(w, e, true) // deferred removal - tap.equal(w.deferredRemovals.entities.length, 0) + tap.equal(w.deferredRemovals.entities.size, 0) tap.equal(w.deferredRemovals.components.length, 0) tap.equal(w.listeners.added.size, 1) @@ -216,7 +216,7 @@ import tap from 'tap' ECS.removeEntity(w, e, deferredRemoval) tap.equal(w.deferredRemovals.components.length, 0) - tap.equal(w.deferredRemovals.entities.length, 0) + tap.equal(w.deferredRemovals.entities.size, 0) ECS.cleanup(w) }