diff --git a/README.md b/README.md index fc3b75e..7b6c002 100644 --- a/README.md +++ b/README.md @@ -197,6 +197,32 @@ ECS.getEntities(world, [ 'test_component' ]).length // because we are not defer ``` +### get entity by unique id + +ECS will generate a unique integer id for every entity created in a world: + +```javascript +const world = ECS.createWorld() +const e = ECS.createEntity(world) +const e2 = ECS.createEntity(world) + +const id1 = ECS.getEntityId(world, e) +const id2 = ECS.getEntityId(world, e2) + +console.log(id1) // 1 +console.log(id2) // 2 +``` + +You can then lookup this entity directly: +```javascript +const e = ECS.getEntityById(world, id1) // e is the entity +``` + +This can be useful in cases where you need to reference an entity from another context. for example, +if you're running a networked simulation and need to refer to a specific entity across peers. + + + ### devtools chrome extension If you'd like to see a real time view of the data in your ECS powered program, there is a dev tools extension! diff --git a/ecs.js b/ecs.js index 08bc7eb..62a3c42 100644 --- a/ecs.js +++ b/ecs.js @@ -108,6 +108,10 @@ export function createWorld (worldId=Math.ceil(Math.random() * 999999999) ) { * @type {World} */ const world = { + + entityIds: new Map(), // maps both entityId -> entity, and entity -> entityId + nextId: 1, // id of the next entity to be created + entities: [ ], filters: { }, systems: [ ], @@ -177,11 +181,26 @@ export function createEntity (world) { world.entities.push(entity) world.stats.entityCount++ + world.entityIds.set(world.nextId, entity) + world.entityIds.set(entity, world.nextId) + + world.nextId++ + world.listeners._added.add(entity) return entity } +export function getEntityId (world, entity) { + return world.entityIds.get(entity) +} + + +export function getEntityById (world, entityId) { + return world.entityIds.get(entityId) +} + + /** * Adds a component to the entity * @param {World} world world where listener will be invoked @@ -558,6 +577,12 @@ function _removeEntity (world, entity, shiftUpEntities=false) { const entityToRemoveIdx = world.entities.indexOf(entity) + const entityId = world.entityIds.get(entity) + if (entityId !== undefined) { + world.entityIds.delete(entity) + world.entityIds.delete(entityId) + } + removeItems(world.entities, entityToRemoveIdx, 1) if (shiftUpEntities) { @@ -679,6 +704,10 @@ export function cleanup (world) { export default { createWorld, createEntity, + + getEntityId, + getEntityById, + addComponentToEntity, removeComponentFromEntity, getEntities, diff --git a/test/createWorld.js b/test/createWorld.js index 67edf5a..247c64a 100644 --- a/test/createWorld.js +++ b/test/createWorld.js @@ -5,6 +5,8 @@ import tap from 'tap' const w = ECS.createWorld() tap.same(w, { + entityIds: new Map(), + nextId: 1, entities: [ ], filters: { }, systems: [ ], diff --git a/test/id.js b/test/id.js new file mode 100644 index 0000000..c3ef7be --- /dev/null +++ b/test/id.js @@ -0,0 +1,87 @@ +import ECS from '../ecs.js' +import tap from 'tap' + + +{ + const w = ECS.createWorld() + + const e1 = ECS.createEntity(w) + const e2 = ECS.createEntity(w) + const e3 = ECS.createEntity(w) + + tap.equal(ECS.getEntityId(w, e1), 1, 'generates an integer id') + tap.equal(ECS.getEntityId(w, e2), 2, 'generates an integer id') + tap.equal(ECS.getEntityId(w, e3), 3, 'generates an integer id') + + tap.equal(e1, ECS.getEntityById(w, 1), 'retrieves entity by id') + tap.equal(e2, ECS.getEntityById(w, 2), 'retrieves entity by id') + tap.equal(e3, ECS.getEntityById(w, 3), 'retrieves entity by id') +} + + +// immediate entity removal works as expected +{ + const w = ECS.createWorld() + + const e1 = ECS.createEntity(w) + + tap.equal(ECS.getEntityId(w, e1), 1, 'generates an integer id') + tap.equal(e1, ECS.getEntityById(w, 1), 'retrieves entity by id') + + // removing an entity removes it's id + + const deferredRemoval = false + ECS.removeEntity(w, e1, deferredRemoval) + + tap.equal(ECS.getEntityId(w, e1), undefined, 'entity id is removed') + tap.equal(undefined, ECS.getEntityById(w, 1), 'entity is removed') +} + + +// deferred entity removal works as expected +{ + const w = ECS.createWorld() + + const e1 = ECS.createEntity(w) + + tap.equal(ECS.getEntityId(w, e1), 1, 'generates an integer id') + tap.equal(e1, ECS.getEntityById(w, 1), 'retrieves entity by id') + + // removing an entity removes it's id + + const deferredRemoval = true + ECS.removeEntity(w, e1, deferredRemoval) + + // entity is still present because we haven't cleaned up yet + tap.equal(ECS.getEntityId(w, e1), 1, 'entity id is removed') + tap.equal(e1, ECS.getEntityById(w, 1), 'retrieves entity by id') + + ECS.cleanup(w) + + // now they should be gone + tap.equal(ECS.getEntityId(w, e1), undefined, 'entity id is removed') + tap.equal(undefined, ECS.getEntityById(w, 1), 'entity is removed') +} + + +{ + const w = ECS.createWorld() + + const e1 = ECS.createEntity(w) + const e2 = ECS.createEntity(w) + const e3 = ECS.createEntity(w) + + const deferredRemoval = true + + ECS.removeEntity(w, e1, deferredRemoval) + ECS.removeEntity(w, e2, deferredRemoval) + ECS.removeEntity(w, e3, deferredRemoval) + + ECS.cleanup(w) + + const e4 = ECS.createEntity(w) + tap.equal(ECS.getEntityId(w, e4), 4, 'entity id still increments despite entity removal') +} + + +