Skip to content

Commit

Permalink
refactor deferredRemovals to not rely on entity indexes
Browse files Browse the repository at this point in the history
  • Loading branch information
mreinstein committed Jun 30, 2024
1 parent 82eb047 commit 68251c8
Show file tree
Hide file tree
Showing 6 changed files with 97 additions and 66 deletions.
38 changes: 38 additions & 0 deletions component-set.js
Original file line number Diff line number Diff line change
@@ -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
}
}
}

85 changes: 29 additions & 56 deletions ecs.js
Original file line number Diff line number Diff line change
@@ -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())
Expand Down Expand Up @@ -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 ]
*/

/**
Expand Down Expand Up @@ -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: {
Expand Down Expand Up @@ -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)
}
Expand All @@ -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)
}
}

Expand Down Expand Up @@ -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
Expand All @@ -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)
}
}

Expand Down Expand Up @@ -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
Expand Down
19 changes: 19 additions & 0 deletions test/component-set.js
Original file line number Diff line number Diff line change
@@ -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)
}
2 changes: 1 addition & 1 deletion test/createWorld.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ tap.same(w, {
_removed: new Set()
},
deferredRemovals: {
entities: [ ],
entities: new Set(),
components: [ ]
},
stats: {
Expand Down
5 changes: 3 additions & 2 deletions test/removeComponentFromEntity.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand All @@ -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')
}

Expand Down
14 changes: 7 additions & 7 deletions test/removeEntity.js
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand Down Expand Up @@ -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)

Expand Down Expand Up @@ -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)
Expand All @@ -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)
Expand Down Expand Up @@ -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)
}

0 comments on commit 68251c8

Please sign in to comment.