From 70d0b9ec8278d594f4b612547e54ca47eb05312c Mon Sep 17 00:00:00 2001 From: waynemwashuma <94756970+waynemwashuma@users.noreply.github.com> Date: Tue, 30 Sep 2025 11:41:45 +0300 Subject: [PATCH 1/7] Add `Entity.generation` --- src/ecs/entities/entity.js | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/ecs/entities/entity.js b/src/ecs/entities/entity.js index 2511a7f7..8ef08824 100644 --- a/src/ecs/entities/entity.js +++ b/src/ecs/entities/entity.js @@ -6,11 +6,19 @@ export class Entity { */ index + /** + * @readonly + * @type {number} + */ + generation + /** * @param {number} index + * @param {number} generation */ - constructor(index){ + constructor(index, generation){ this.index = index + this.generation = generation } /** From c8ab338d3a04384f70fb65ca62a671d719e04d2a Mon Sep 17 00:00:00 2001 From: waynemwashuma <94756970+waynemwashuma@users.noreply.github.com> Date: Tue, 30 Sep 2025 11:43:40 +0300 Subject: [PATCH 2/7] Update methods of `Entity` to consider entity generation --- src/ecs/entities/entity.js | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/src/ecs/entities/entity.js b/src/ecs/entities/entity.js index 8ef08824..3001934f 100644 --- a/src/ecs/entities/entity.js +++ b/src/ecs/entities/entity.js @@ -1,3 +1,5 @@ +import { packInto64Int, unpackFrom64Int } from '../../algorithms/packnumber.js' + export class Entity { /** @@ -25,14 +27,28 @@ export class Entity { * @param {Entity} other */ equals(other){ - return this.index === other.index + return ( + this.index === other.index && + this.generation === other.generation + ) } /** * @returns {EntityId} */ id(){ - return this.index + const { index, generation } = this + + return packInto64Int(index, generation) + } + + /** + * @param {EntityId} id + */ + static from(id){ + const [index, generation] = unpackFrom64Int(id) + + return new Entity(index, generation) } } From aaa2e55c19c6a20be1a1ed7ea0e5ed79d30c7150 Mon Sep 17 00:00:00 2001 From: waynemwashuma <94756970+waynemwashuma@users.noreply.github.com> Date: Tue, 30 Sep 2025 11:45:16 +0300 Subject: [PATCH 3/7] Update `World` --- src/ecs/entities/entities.js | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/src/ecs/entities/entities.js b/src/ecs/entities/entities.js index d7909ff1..9bdb924f 100644 --- a/src/ecs/entities/entities.js +++ b/src/ecs/entities/entities.js @@ -3,11 +3,4 @@ import { DenseList } from '../../datastructures/index.js' import { EntityLocation } from './location.js' /** @augments {DenseList} */ -export class Entities extends DenseList { - reserve() { - return this.push(new EntityLocation( - /** @type {ArchetypeId}*/(-1), - /** @type {TableRow}*/(-1) - )) - } -} \ No newline at end of file +export class Entities extends DenseList {} \ No newline at end of file From 417a76f65c5f6858e1e991bb78f55f4a1ebeaf4f Mon Sep 17 00:00:00 2001 From: waynemwashuma <94756970+waynemwashuma@users.noreply.github.com> Date: Tue, 30 Sep 2025 12:19:45 +0300 Subject: [PATCH 4/7] Add `EntityLocation.generation` --- src/ecs/entities/entitycell.js | 8 +++++++- src/ecs/entities/location.js | 5 +++++ src/ecs/registry.js | 27 +++++++++++++++++++-------- 3 files changed, 31 insertions(+), 9 deletions(-) diff --git a/src/ecs/entities/entitycell.js b/src/ecs/entities/entitycell.js index c7d99fbc..a076294c 100644 --- a/src/ecs/entities/entitycell.js +++ b/src/ecs/entities/entitycell.js @@ -38,8 +38,14 @@ export class EntityCell { */ constructor(world, entity) { const entities = world.getEntities() + const location = entities.get(entity.index) + + if(location && location.generation === entity.generation){ + this.location = location + } else { + this.location = new EntityLocation() + } - this.location = entities.get(entity.index) || new EntityLocation() this.tables = world.getTables() this.archetypes = world.getArchetypes() this.entity = entity diff --git a/src/ecs/entities/location.js b/src/ecs/entities/location.js index 273ffeb8..67131118 100644 --- a/src/ecs/entities/location.js +++ b/src/ecs/entities/location.js @@ -12,6 +12,11 @@ export class EntityLocation { */ index + /** + * @type {number} + */ + generation = 0 + /** * @type {ArchetypeId} */ diff --git a/src/ecs/registry.js b/src/ecs/registry.js index 81fe13d4..57a82f5c 100644 --- a/src/ecs/registry.js +++ b/src/ecs/registry.js @@ -134,13 +134,20 @@ export class World { */ spawn(components) { const entityIndex = this.entities.reserve() + let location = this.entities.get(entityIndex) - // SAFETY: the entity was reserved in this function so we know its there. - const location = /** @type {EntityLocation}*/ (this.entities.get(entityIndex)) + if(!location){ + const newLocation = new EntityLocation() + + this.entities.set(entityIndex, newLocation) + location = newLocation + } + + location.generation += 1 // SAFETY:Object constructors can be casted from `Function` to `Constructor` - const newIds = (components.map((c) => typeid( /** @type {Constructor} */ (c.constructor)))) - const entity = new Entity(entityIndex) + const newIds = (components.map((c) => typeid( /** @type {Constructor} */(c.constructor)))) + const entity = new Entity(entityIndex, location.generation) newIds.push(typeid(Entity)) components.push(entity) @@ -166,7 +173,7 @@ export class World { insert(entity, components) { const location = this.entities.get(entity.index) - if (!location) { + if (!location || location.generation !== entity.generation) { return } @@ -212,7 +219,7 @@ export class World { remove(entity, components) { const location = this.entities.get(entity.index) - if (!location) { + if (!location || location.generation !== entity.generation) { return } @@ -265,7 +272,9 @@ export class World { despawn(entity) { const location = this.entities.get(entity.index) - if (!location) return + if (!location || location.generation !== entity.generation){ + return + } const { archetypeId, tableId, index } = location const archetype = this.archetypes.get(archetypeId) @@ -303,7 +312,9 @@ export class World { get(entity, type) { const location = this.entities.get(entity.index) - if (!location) return null + if (!location || location.generation !== entity.generation) { + return null + } const { tableId, index } = location const table = this.tables.get(tableId) From 4a5588dc153c325ca686e6d9644dc11e193b9834 Mon Sep 17 00:00:00 2001 From: waynemwashuma <94756970+waynemwashuma@users.noreply.github.com> Date: Tue, 30 Sep 2025 12:20:20 +0300 Subject: [PATCH 5/7] Update `Query` --- src/ecs/query/query.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/ecs/query/query.js b/src/ecs/query/query.js index ea4894fc..a493d7db 100644 --- a/src/ecs/query/query.js +++ b/src/ecs/query/query.js @@ -107,10 +107,10 @@ export class Query { */ get(entity) { const { world, descriptors, tableIds } = this - const entities = world.getEntities() - const location = entities.get(entity.index) + const cell = world.getEntity(entity) + const {location} = cell - if (!location) return null + if (!cell.exists()) return null const { tableId, index } = location const table = world.getTables().get(tableId) From f597646c48d4258b2d4d43d3d2934d989633c016 Mon Sep 17 00:00:00 2001 From: waynemwashuma <94756970+waynemwashuma@users.noreply.github.com> Date: Tue, 30 Sep 2025 12:21:35 +0300 Subject: [PATCH 6/7] Add and update unit tests --- src/ecs/tests/entities/entitycell.test.js | 4 +-- src/ecs/tests/query.test.js | 4 +-- src/ecs/tests/world.test.js | 35 +++++++++++++++++++++++ 3 files changed, 39 insertions(+), 4 deletions(-) diff --git a/src/ecs/tests/entities/entitycell.test.js b/src/ecs/tests/entities/entitycell.test.js index 2fdd7db1..65ec7c4f 100644 --- a/src/ecs/tests/entities/entitycell.test.js +++ b/src/ecs/tests/entities/entitycell.test.js @@ -28,8 +28,8 @@ describe("Testing `EntityCell`", () => { const cell1 = world.getEntity(entity1) const cell2 = world.getEntity(entity2) - deepStrictEqual(cell1.id(), new Entity(0)) - deepStrictEqual(cell2.id(), new Entity(1)) + deepStrictEqual(cell1.id(), new Entity(0, 1)) + deepStrictEqual(cell2.id(), new Entity(1, 1)) }) test('`EntityCell` tests entity existence correctly.', () => { diff --git a/src/ecs/tests/query.test.js b/src/ecs/tests/query.test.js index bc58a801..b915a0dd 100644 --- a/src/ecs/tests/query.test.js +++ b/src/ecs/tests/query.test.js @@ -75,7 +75,7 @@ describe("Testing `Query`", () => { test('query for single component, specific entity', () => { const world = createWorld() const query = new Query(world, [A]) - const entity = new Entity(0) + const entity = new Entity(0, 1) const components = query.get(entity) assert(components) @@ -89,7 +89,7 @@ describe("Testing `Query`", () => { test('query for multiple components, specific entity', () => { const world = createWorld() const query = new Query(world, [A, B, C]) - const entity = new Entity(0) + const entity = new Entity(0, 1) const components = query.get(entity) assert(components) diff --git a/src/ecs/tests/world.test.js b/src/ecs/tests/world.test.js index be0622cc..772cadd4 100644 --- a/src/ecs/tests/world.test.js +++ b/src/ecs/tests/world.test.js @@ -28,6 +28,26 @@ describe("Testing `World`", () => { deepStrictEqual(components, [typeid(A), typeid(B), typeid(C), typeid(Entity)]) }) + test('Entity generation starts at one.', () => { + const world = new World() + const entity = world.spawn([new A(), new B(), new C()]) + + deepStrictEqual(entity,new Entity(0,1)) + }) + + test('Entity generation is incremented.', () => { + const world = new World() + const entity1 = world.spawn([new A(), new B(), new C()]) + world.despawn(entity1) + const entity2 = world.spawn([new A(), new B(), new C()]) + world.despawn(entity2) + const entity3 = world.spawn([new A(), new B(), new C()]) + + deepStrictEqual(entity1,new Entity(0,1)) + deepStrictEqual(entity2,new Entity(0,2)) + deepStrictEqual(entity3,new Entity(0,3)) + }) + test('Entity is despawned correctly from a world.', () => { const world = new World() const entity = world.spawn([new A(), new B(), new C()]) @@ -38,6 +58,21 @@ describe("Testing `World`", () => { deepStrictEqual(components, []) }) + test('Invalidated entity cannot be accessed on same index', () => { + const world = new World() + const entity1 = world.spawn([new A()]) + world.despawn(entity1) + const entity2 = world.spawn([new A()]) + + const cell1 = world.getEntity(entity1) + const cell2 = world.getEntity(entity2) + + deepStrictEqual(entity1,new Entity(0,1)) + deepStrictEqual(entity2,new Entity(0,2)) + deepStrictEqual(cell1.exists(), false) + deepStrictEqual(cell2.exists(), true) + }) + test('Components are inserted into an entity.', () => { const world = new World() const entity = world.spawn([]) From 7aed7a1ffbbda32a1105257bbfe86c9f4d13acaa Mon Sep 17 00:00:00 2001 From: waynemwashuma <94756970+waynemwashuma@users.noreply.github.com> Date: Tue, 7 Oct 2025 12:34:09 +0300 Subject: [PATCH 7/7] Lint files --- src/ecs/query/query.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ecs/query/query.js b/src/ecs/query/query.js index a493d7db..36e34334 100644 --- a/src/ecs/query/query.js +++ b/src/ecs/query/query.js @@ -108,7 +108,7 @@ export class Query { get(entity) { const { world, descriptors, tableIds } = this const cell = world.getEntity(entity) - const {location} = cell + const { location } = cell if (!cell.exists()) return null