From 2fe9203be9315f888c92ef422f277fb9a83ee595 Mon Sep 17 00:00:00 2001 From: hypercross Date: Mon, 1 Jun 2026 14:34:51 +0800 Subject: [PATCH] perf: track component and relationship counts per entity Introduce component and relationship counters to optimize entity destruction and component checks. This allows for short-circuiting the destruction process for bare entities and provides an O(1) check for `hasAnyComponent`. --- src/world.ts | 31 +++++++++++++++++++++++++++---- 1 file changed, 27 insertions(+), 4 deletions(-) diff --git a/src/world.ts b/src/world.ts index 459fa06..0178cc7 100644 --- a/src/world.ts +++ b/src/world.ts @@ -21,6 +21,8 @@ export class World { // ── Entity pools ────────────────────────────────── private _generations: number[] = []; private _free: number[] = []; + private _componentCounts: number[] = []; + private _relCounts: number[] = []; // ── Component storage ───────────────────────────── private _components = new Map>(); @@ -52,6 +54,8 @@ export class World { if (this._free.length > 0) { const idx = this._free.pop()!; const gen = this._generations[idx]; + this._componentCounts[idx] = 0; + this._relCounts[idx] = 0; const e = makeEntity(idx, gen); this._emit({ type: "spawned", entity: e }); return e; @@ -59,6 +63,8 @@ export class World { const idx = this._generations.length; this._generations.push(1); + this._componentCounts.push(0); + this._relCounts.push(0); const e = makeEntity(idx, 1); this._emit({ type: "spawned", entity: e }); return e; @@ -69,6 +75,14 @@ export class World { const idx = entityIndex(entity); if (!this._isAlive(idx, entity)) return; + // Short-circuit: truly bare entities have nothing to clean up + if (this._componentCounts[idx] === 0 && this._relCounts[idx] === 0) { + this._generations[idx]++; + this._free.push(idx); + this._emit({ type: "destroyed", entity }); + return; + } + // Clean relationships before components for (const [key] of this._relForward) { // Entity as source @@ -99,6 +113,8 @@ export class World { dirty.delete(idx); } + this._componentCounts[idx] = 0; + this._relCounts[idx] = 0; this._generations[idx]++; this._free.push(idx); @@ -114,10 +130,7 @@ export class World { hasAnyComponent(entity: Entity): boolean { const idx = entityIndex(entity); if (!this._isAlive(idx, entity)) return false; - for (const store of this._components.values()) { - if (store.has(idx)) return true; - } - return false; + return this._componentCounts[idx] > 0; } // ── Component operations ────────────────────────── @@ -131,9 +144,12 @@ export class World { this._assertAlive(idx, entity); const store = this._getOrCreateStore(def); + const existed = store.has(idx); const value = { ...def.defaults, ...init }; store.set(idx, value); + if (!existed) this._componentCounts[idx]++; + this._emit({ type: "componentAdded", entity, component: def }); return value; } @@ -149,6 +165,7 @@ export class World { this._dirty.get(def._key)?.delete(idx); if (removed) { + this._componentCounts[idx]--; this._emit({ type: "componentRemoved", entity, component: def }); } } @@ -265,6 +282,9 @@ export class World { } rev.add(si); + this._relCounts[si]++; + this._relCounts[ti]++; + this._emit({ type: "relationshipAdded", source, @@ -570,6 +590,9 @@ export class World { } } + this._relCounts[si]--; + this._relCounts[ti]--; + this._emit({ type: "relationshipRemoved", source,