@codehz/ecs 0.7.2 → 0.7.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/examples/advanced-scheduling.ts +96 -0
- package/examples/collision-detection.ts +229 -0
- package/examples/inventory-system-relations.ts +108 -0
- package/examples/parent-child-hierarchy.ts +206 -0
- package/examples/serialization.ts +337 -0
- package/examples/simple.ts +96 -0
- package/examples/spatial-grid.ts +276 -0
- package/examples/state-machine.ts +273 -0
- package/examples/tag-filtering.ts +266 -0
- package/package.json +60 -12
- package/src/__tests__/commands/buffer-limits.test.ts +72 -0
- package/src/__tests__/commands/buffer.test.ts +195 -0
- package/src/__tests__/component/singleton.test.ts +148 -0
- package/src/__tests__/core/archetype.test.ts +247 -0
- package/src/__tests__/core/bitset.test.ts +171 -0
- package/src/__tests__/core/changeset.test.ts +254 -0
- package/src/__tests__/core/multi-map.test.ts +74 -0
- package/src/__tests__/entity/component-registry.test.ts +66 -0
- package/src/__tests__/entity/entity.test.ts +520 -0
- package/src/__tests__/entity/id-manager.test.ts +157 -0
- package/src/__tests__/entity/id-system.test.ts +260 -0
- package/src/__tests__/perf/comprehensive.perf.test.ts +300 -0
- package/src/__tests__/perf/sync-hotpath.perf.test.ts +79 -0
- package/src/__tests__/query/basic.test.ts +341 -0
- package/src/__tests__/query/caching.test.ts +112 -0
- package/src/__tests__/query/filter.test.ts +111 -0
- package/src/__tests__/query/optional.test.ts +231 -0
- package/src/__tests__/query/perf.test.ts +99 -0
- package/src/__tests__/relations/dont-fragment/basic.test.ts +496 -0
- package/src/__tests__/relations/dont-fragment/query-notification.test.ts +125 -0
- package/src/__tests__/relations/wildcard.test.ts +179 -0
- package/src/__tests__/serialization/bounds.test.ts +237 -0
- package/src/__tests__/testing/assertions.test.ts +224 -0
- package/src/__tests__/testing/entity-builder.test.ts +84 -0
- package/src/__tests__/testing/snapshot.test.ts +150 -0
- package/src/__tests__/testing/world-fixture.test.ts +73 -0
- package/src/__tests__/world/component-hooks.test.ts +185 -0
- package/src/__tests__/world/component-management.test.ts +447 -0
- package/src/__tests__/world/entity-management.test.ts +86 -0
- package/src/__tests__/world/get-optional.test.ts +96 -0
- package/src/__tests__/world/multi-component-hooks.test.ts +502 -0
- package/src/__tests__/world/perf.test.ts +93 -0
- package/src/__tests__/world/query.test.ts +223 -0
- package/src/__tests__/world/serialize.test.ts +83 -0
- package/src/__tests__/world/wildcard-relation-hooks.test.ts +332 -0
- package/src/archetype/archetype.ts +472 -0
- package/src/archetype/helpers.ts +186 -0
- package/src/archetype/store.ts +33 -0
- package/src/commands/buffer.ts +110 -0
- package/src/commands/changeset.ts +104 -0
- package/src/component/entity-store.ts +223 -0
- package/src/component/registry.ts +657 -0
- package/src/component/type-utils.ts +9 -0
- package/src/entity/index.ts +63 -0
- package/src/entity/manager.ts +115 -0
- package/src/entity/relation.ts +319 -0
- package/src/entity/types.ts +135 -0
- package/src/index.ts +41 -0
- package/src/query/filter.ts +75 -0
- package/src/query/query.ts +313 -0
- package/src/query/registry.ts +101 -0
- package/src/storage/serialization.ts +130 -0
- package/src/testing/index.ts +634 -0
- package/src/types/index.ts +99 -0
- package/src/utils/bit-set.ts +133 -0
- package/src/utils/multi-map.ts +96 -0
- package/src/utils/utils.ts +19 -0
- package/src/world/builder.ts +100 -0
- package/src/world/commands.ts +378 -0
- package/src/world/hooks.ts +358 -0
- package/src/world/references.ts +38 -0
- package/src/world/serialization.ts +122 -0
- package/src/world/world.ts +1201 -0
- /package/{builder.d.mts → dist/builder.d.mts} +0 -0
- /package/{index.d.mts → dist/index.d.mts} +0 -0
- /package/{index.mjs → dist/index.mjs} +0 -0
- /package/{testing.d.mts → dist/testing.d.mts} +0 -0
- /package/{testing.mjs → dist/testing.mjs} +0 -0
- /package/{testing.mjs.map → dist/testing.mjs.map} +0 -0
- /package/{world.mjs → dist/world.mjs} +0 -0
- /package/{world.mjs.map → dist/world.mjs.map} +0 -0
|
@@ -0,0 +1,1201 @@
|
|
|
1
|
+
import { Archetype } from "../archetype/archetype";
|
|
2
|
+
import { DontFragmentStoreImpl } from "../archetype/store";
|
|
3
|
+
import { CommandBuffer, type Command } from "../commands/buffer";
|
|
4
|
+
import { ComponentChangeset } from "../commands/changeset";
|
|
5
|
+
import { ComponentEntityStore } from "../component/entity-store";
|
|
6
|
+
import { normalizeComponentTypes } from "../component/type-utils";
|
|
7
|
+
import type { ComponentId, EntityId, WildcardRelationId } from "../entity";
|
|
8
|
+
import {
|
|
9
|
+
ENTITY_ID_START,
|
|
10
|
+
EntityIdManager,
|
|
11
|
+
RELATION_SHIFT,
|
|
12
|
+
getComponentIdFromRelationId,
|
|
13
|
+
getDetailedIdType,
|
|
14
|
+
getTargetIdFromRelationId,
|
|
15
|
+
isCascadeDeleteRelation,
|
|
16
|
+
isDontFragmentRelation,
|
|
17
|
+
isDontFragmentWildcard,
|
|
18
|
+
isEntityRelation,
|
|
19
|
+
isExclusiveComponent,
|
|
20
|
+
isWildcardRelationId,
|
|
21
|
+
} from "../entity";
|
|
22
|
+
import { matchesFilter, serializeQueryFilter, type QueryFilter } from "../query/filter";
|
|
23
|
+
import type { Query } from "../query/query";
|
|
24
|
+
import { QueryRegistry } from "../query/registry";
|
|
25
|
+
import type { SerializedWorld } from "../storage/serialization";
|
|
26
|
+
import type { ComponentTuple, ComponentType, LifecycleCallback, LifecycleHook, LifecycleHookEntry } from "../types";
|
|
27
|
+
import { isOptionalEntityId } from "../types";
|
|
28
|
+
import { getOrCompute } from "../utils/utils";
|
|
29
|
+
import { EntityBuilder } from "./builder";
|
|
30
|
+
import {
|
|
31
|
+
applyChangeset,
|
|
32
|
+
filterRegularComponentTypes,
|
|
33
|
+
maybeRemoveWildcardMarker,
|
|
34
|
+
processCommands,
|
|
35
|
+
removeMatchingRelations,
|
|
36
|
+
type CommandProcessorContext,
|
|
37
|
+
} from "./commands";
|
|
38
|
+
import {
|
|
39
|
+
collectMultiHookComponents,
|
|
40
|
+
triggerLifecycleHooks,
|
|
41
|
+
triggerRemoveHooksForEntityDeletion,
|
|
42
|
+
type HooksContext,
|
|
43
|
+
} from "./hooks";
|
|
44
|
+
import {
|
|
45
|
+
getEntityReferences,
|
|
46
|
+
trackEntityReference,
|
|
47
|
+
untrackEntityReference,
|
|
48
|
+
type EntityReferencesMap,
|
|
49
|
+
} from "./references";
|
|
50
|
+
import { deserializeWorld, serializeWorld } from "./serialization";
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* World class for ECS architecture
|
|
54
|
+
* Manages entities and components
|
|
55
|
+
*/
|
|
56
|
+
export class World {
|
|
57
|
+
// Core data structures for entity and archetype management
|
|
58
|
+
private entityIdManager = new EntityIdManager();
|
|
59
|
+
private archetypes: Archetype[] = [];
|
|
60
|
+
private archetypeBySignature = new Map<string, Archetype>();
|
|
61
|
+
private entityToArchetype = new Map<EntityId, Archetype>();
|
|
62
|
+
private archetypesByComponent = new Map<EntityId<any>, Set<Archetype>>();
|
|
63
|
+
private entityReferences: EntityReferencesMap = new Map();
|
|
64
|
+
/** Reverse index: entity ID → set of archetypes whose componentTypes include that entity ID */
|
|
65
|
+
private entityToReferencingArchetypes = new Map<EntityId, Set<Archetype>>();
|
|
66
|
+
/** DontFragment relation storage, shared with all Archetype instances */
|
|
67
|
+
private readonly dontFragmentStore = new DontFragmentStoreImpl();
|
|
68
|
+
/** Component entity (singleton) storage */
|
|
69
|
+
private readonly componentEntities = new ComponentEntityStore();
|
|
70
|
+
|
|
71
|
+
// Query registry – manages caching, ref counts, and archetype notifications
|
|
72
|
+
private readonly queryRegistry = new QueryRegistry();
|
|
73
|
+
|
|
74
|
+
// Lifecycle hooks (declared before cached contexts that reference them)
|
|
75
|
+
private hooks: Set<LifecycleHookEntry> = new Set();
|
|
76
|
+
|
|
77
|
+
// Command execution
|
|
78
|
+
private commandBuffer = new CommandBuffer((entityId, commands) => this.executeEntityCommands(entityId, commands));
|
|
79
|
+
|
|
80
|
+
// Reusable instances to reduce per-frame allocations
|
|
81
|
+
private readonly _changeset = new ComponentChangeset();
|
|
82
|
+
private readonly _removeChangeset = new ComponentChangeset();
|
|
83
|
+
/** Cached command processor context to avoid per-entity object allocation */
|
|
84
|
+
private readonly _commandCtx: CommandProcessorContext = {
|
|
85
|
+
dontFragmentStore: this.dontFragmentStore,
|
|
86
|
+
ensureArchetype: (ct) => this.ensureArchetype(ct),
|
|
87
|
+
};
|
|
88
|
+
/** Cached hooks context to avoid per-entity object allocation */
|
|
89
|
+
private readonly _hooksCtx: HooksContext = {
|
|
90
|
+
multiHooks: this.hooks,
|
|
91
|
+
has: (eid, ct) => this.has(eid, ct),
|
|
92
|
+
get: (eid, ct) => this.get(eid, ct),
|
|
93
|
+
getOptional: (eid, ct) => this.getOptional(eid, ct),
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
constructor(snapshot?: SerializedWorld) {
|
|
97
|
+
if (snapshot && typeof snapshot === "object") {
|
|
98
|
+
deserializeWorld(
|
|
99
|
+
{
|
|
100
|
+
entityIdManager: this.entityIdManager,
|
|
101
|
+
componentEntities: this.componentEntities,
|
|
102
|
+
entityReferences: this.entityReferences,
|
|
103
|
+
ensureArchetype: (ct) => this.ensureArchetype(ct),
|
|
104
|
+
setEntityToArchetype: (eid, arch) => this.entityToArchetype.set(eid, arch),
|
|
105
|
+
},
|
|
106
|
+
snapshot,
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
private createArchetypeSignature(componentTypes: EntityId<any>[]): string {
|
|
112
|
+
return componentTypes.join(",");
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Creates a new entity.
|
|
117
|
+
* The entity is created with an empty component set and can be configured using `set()`.
|
|
118
|
+
*
|
|
119
|
+
* @template T - The initial component type (defaults to void if not specified)
|
|
120
|
+
* @returns A unique identifier for the new entity
|
|
121
|
+
*
|
|
122
|
+
* @example
|
|
123
|
+
* const entity = world.new<MyComponent>();
|
|
124
|
+
* world.set(entity, MyComponent, { value: 42 });
|
|
125
|
+
* world.sync();
|
|
126
|
+
*/
|
|
127
|
+
new<T = void>(): EntityId<T> {
|
|
128
|
+
const entityId = this.entityIdManager.allocate();
|
|
129
|
+
let emptyArchetype = this.ensureArchetype([]);
|
|
130
|
+
emptyArchetype.addEntity(entityId, new Map());
|
|
131
|
+
this.entityToArchetype.set(entityId, emptyArchetype);
|
|
132
|
+
return entityId as EntityId<T>;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Semantic alias for `new()` to avoid confusion with the `new` keyword.
|
|
137
|
+
* Creates a new entity with an empty component set.
|
|
138
|
+
*
|
|
139
|
+
* @example
|
|
140
|
+
* const entity = world.create<MyComponent>();
|
|
141
|
+
*/
|
|
142
|
+
create<T = void>(): EntityId<T> {
|
|
143
|
+
return this.new<T>();
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/** Fast path: destroy an entity that is not referenced by any other entity, skipping BFS */
|
|
147
|
+
private destroySingleEntity(entityId: EntityId): void {
|
|
148
|
+
const archetype = this.entityToArchetype.get(entityId);
|
|
149
|
+
if (!archetype) return;
|
|
150
|
+
|
|
151
|
+
// Handle entity references (this entity references other entities)
|
|
152
|
+
for (const [sourceEntityId, componentType] of getEntityReferences(this.entityReferences, entityId)) {
|
|
153
|
+
if (this.entityToArchetype.has(sourceEntityId)) {
|
|
154
|
+
this.removeComponentImmediate(sourceEntityId, componentType, entityId);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
this.entityReferences.delete(entityId);
|
|
159
|
+
const removedComponents = archetype.removeEntity(entityId)!;
|
|
160
|
+
this.entityToArchetype.delete(entityId);
|
|
161
|
+
|
|
162
|
+
triggerRemoveHooksForEntityDeletion(entityId, removedComponents, archetype);
|
|
163
|
+
|
|
164
|
+
this.cleanupArchetypesReferencingEntity(entityId);
|
|
165
|
+
this.entityIdManager.deallocate(entityId);
|
|
166
|
+
this.componentEntities.cleanupReferencesTo(entityId);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
private destroyEntityImmediate(entityId: EntityId): void {
|
|
170
|
+
// Fast path: no other entity references this one, delete directly
|
|
171
|
+
if (!this.entityReferences.has(entityId)) {
|
|
172
|
+
this.destroySingleEntity(entityId);
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const queue: EntityId[] = [entityId];
|
|
177
|
+
const visited = new Set<EntityId>();
|
|
178
|
+
let queueIndex = 0;
|
|
179
|
+
|
|
180
|
+
while (queueIndex < queue.length) {
|
|
181
|
+
const cur = queue[queueIndex++]!;
|
|
182
|
+
if (visited.has(cur)) continue;
|
|
183
|
+
visited.add(cur);
|
|
184
|
+
|
|
185
|
+
const archetype = this.entityToArchetype.get(cur);
|
|
186
|
+
if (!archetype) continue;
|
|
187
|
+
|
|
188
|
+
// Process entity references before removal
|
|
189
|
+
for (const [sourceEntityId, componentType] of getEntityReferences(this.entityReferences, cur)) {
|
|
190
|
+
if (!this.entityToArchetype.has(sourceEntityId)) continue;
|
|
191
|
+
|
|
192
|
+
if (isCascadeDeleteRelation(componentType)) {
|
|
193
|
+
if (!visited.has(sourceEntityId)) {
|
|
194
|
+
queue.push(sourceEntityId);
|
|
195
|
+
}
|
|
196
|
+
} else {
|
|
197
|
+
this.removeComponentImmediate(sourceEntityId, componentType, cur);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Remove entity from archetype - this also cleans up dontFragment relations
|
|
202
|
+
// and returns all removed component data
|
|
203
|
+
this.entityReferences.delete(cur);
|
|
204
|
+
const removedComponents = archetype.removeEntity(cur)!;
|
|
205
|
+
this.entityToArchetype.delete(cur);
|
|
206
|
+
|
|
207
|
+
// Trigger lifecycle hooks for removed components (fast path for entity deletion)
|
|
208
|
+
triggerRemoveHooksForEntityDeletion(cur, removedComponents, archetype);
|
|
209
|
+
|
|
210
|
+
this.cleanupArchetypesReferencingEntity(cur);
|
|
211
|
+
this.entityIdManager.deallocate(cur);
|
|
212
|
+
this.componentEntities.cleanupReferencesTo(cur);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Checks if an **entity** (not a component) exists in the world.
|
|
218
|
+
*
|
|
219
|
+
* This is specifically for checking entity liveness — whether the given entity ID
|
|
220
|
+
* is currently alive in the world. For checking if a component is present on an
|
|
221
|
+
* entity, use {@link has} instead.
|
|
222
|
+
*
|
|
223
|
+
* @param entityId - The entity identifier to check
|
|
224
|
+
* @returns `true` if the entity exists, `false` otherwise
|
|
225
|
+
*
|
|
226
|
+
* @example
|
|
227
|
+
* // Check if an entity is alive
|
|
228
|
+
* if (world.exists(entityId)) {
|
|
229
|
+
* console.log("Entity exists");
|
|
230
|
+
* }
|
|
231
|
+
*
|
|
232
|
+
* // To check for a component, use has() instead:
|
|
233
|
+
* if (world.has(entity, Position)) { ... }
|
|
234
|
+
*/
|
|
235
|
+
exists(entityId: EntityId): boolean {
|
|
236
|
+
if (this.componentEntities.exists(entityId)) return true;
|
|
237
|
+
return this.entityToArchetype.has(entityId);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
private assertEntityExists(entityId: EntityId, label: "Entity" | "Component entity"): void {
|
|
241
|
+
if (!this.exists(entityId)) {
|
|
242
|
+
throw new Error(`${label} ${entityId} does not exist`);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
private assertComponentTypeValid(componentType: EntityId): void {
|
|
247
|
+
const detailedType = getDetailedIdType(componentType);
|
|
248
|
+
if (detailedType.type === "invalid") {
|
|
249
|
+
throw new Error(`Invalid component type: ${componentType}`);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
private assertSetComponentTypeValid(componentType: EntityId): void {
|
|
254
|
+
const detailedType = getDetailedIdType(componentType);
|
|
255
|
+
if (detailedType.type === "invalid") {
|
|
256
|
+
throw new Error(`Invalid component type: ${componentType}`);
|
|
257
|
+
}
|
|
258
|
+
if (detailedType.type === "wildcard-relation") {
|
|
259
|
+
throw new Error(`Cannot directly add wildcard relation components: ${componentType}`);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
private resolveSetOperation(
|
|
264
|
+
entityId: EntityId | ComponentId,
|
|
265
|
+
componentTypeOrComponent?: EntityId | any,
|
|
266
|
+
maybeComponent?: any,
|
|
267
|
+
): { entityId: EntityId; componentType: EntityId; component: any } {
|
|
268
|
+
// Handle singleton component overload: set(componentId, data)
|
|
269
|
+
if (maybeComponent === undefined && componentTypeOrComponent !== undefined) {
|
|
270
|
+
const detailedType = getDetailedIdType(entityId);
|
|
271
|
+
if (detailedType.type === "component" || detailedType.type === "component-relation") {
|
|
272
|
+
const componentId = entityId as ComponentId;
|
|
273
|
+
this.assertEntityExists(componentId, "Component entity");
|
|
274
|
+
this.assertSetComponentTypeValid(componentId);
|
|
275
|
+
return { entityId: componentId, componentType: componentId, component: componentTypeOrComponent };
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
const targetEntityId = entityId as EntityId;
|
|
280
|
+
const componentType = componentTypeOrComponent as EntityId;
|
|
281
|
+
this.assertEntityExists(targetEntityId, "Entity");
|
|
282
|
+
this.assertSetComponentTypeValid(componentType);
|
|
283
|
+
|
|
284
|
+
return { entityId: targetEntityId, componentType, component: maybeComponent };
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
private resolveRemoveOperation<T>(
|
|
288
|
+
entityId: EntityId | ComponentId,
|
|
289
|
+
componentType?: EntityId<T>,
|
|
290
|
+
): { entityId: EntityId; componentType: EntityId } {
|
|
291
|
+
// Handle singleton component overload: remove(componentId)
|
|
292
|
+
if (componentType === undefined) {
|
|
293
|
+
const componentId = entityId as ComponentId<T>;
|
|
294
|
+
this.assertEntityExists(componentId, "Component entity");
|
|
295
|
+
return { entityId: componentId, componentType: componentId };
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
const targetEntityId = entityId as EntityId;
|
|
299
|
+
this.assertEntityExists(targetEntityId, "Entity");
|
|
300
|
+
this.assertComponentTypeValid(componentType);
|
|
301
|
+
|
|
302
|
+
return { entityId: targetEntityId, componentType };
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* Adds or updates a component on an entity (or marks void component as present).
|
|
307
|
+
* The change is buffered and takes effect after calling `world.sync()`.
|
|
308
|
+
* If the entity does not exist, throws an error.
|
|
309
|
+
*
|
|
310
|
+
* @overload set(entityId: EntityId, componentType: EntityId<void>): void
|
|
311
|
+
* Marks a void component as present on the entity
|
|
312
|
+
*
|
|
313
|
+
* @overload set<T>(entityId: EntityId, componentType: EntityId<T>, component: NoInfer<T>): void
|
|
314
|
+
* Adds or updates a component with data on the entity
|
|
315
|
+
*
|
|
316
|
+
* @overload set<T>(componentId: ComponentId<T>, component: NoInfer<T>): void
|
|
317
|
+
* Adds or updates a singleton component (shorthand for set(componentId, componentId, component))
|
|
318
|
+
*
|
|
319
|
+
* @throws {Error} If the entity does not exist
|
|
320
|
+
* @throws {Error} If the component type is invalid or is a wildcard relation
|
|
321
|
+
*
|
|
322
|
+
* @example
|
|
323
|
+
* world.set(entity, Position, { x: 10, y: 20 });
|
|
324
|
+
* world.set(entity, Marker); // void component
|
|
325
|
+
* world.set(GlobalConfig, { debug: true }); // singleton component
|
|
326
|
+
* world.sync(); // Apply changes
|
|
327
|
+
*/
|
|
328
|
+
set(entityId: EntityId, componentType: EntityId<void>): void;
|
|
329
|
+
set<T>(entityId: EntityId, componentType: EntityId<T>, component: NoInfer<T>): void;
|
|
330
|
+
set<T>(componentId: ComponentId<T>, component: NoInfer<T>): void;
|
|
331
|
+
set(entityId: EntityId | ComponentId, componentTypeOrComponent?: EntityId | any, maybeComponent?: any): void {
|
|
332
|
+
const {
|
|
333
|
+
entityId: targetEntityId,
|
|
334
|
+
componentType,
|
|
335
|
+
component,
|
|
336
|
+
} = this.resolveSetOperation(entityId, componentTypeOrComponent, maybeComponent);
|
|
337
|
+
this.commandBuffer.set(targetEntityId, componentType, component);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* Removes a component from an entity.
|
|
342
|
+
* The change is buffered and takes effect after calling `world.sync()`.
|
|
343
|
+
* If the entity does not exist, throws an error.
|
|
344
|
+
*
|
|
345
|
+
* @overload remove<T>(entityId: EntityId, componentType: EntityId<T>): void
|
|
346
|
+
* Removes a component from an entity.
|
|
347
|
+
*
|
|
348
|
+
* @overload remove<T>(componentId: ComponentId<T>): void
|
|
349
|
+
* Removes a singleton component (shorthand for remove(componentId, componentId)).
|
|
350
|
+
*
|
|
351
|
+
* @template T - The component data type
|
|
352
|
+
* @param entityId - The entity identifier
|
|
353
|
+
* @param componentType - The component type to remove
|
|
354
|
+
*
|
|
355
|
+
* @throws {Error} If the entity does not exist
|
|
356
|
+
* @throws {Error} If the component type is invalid
|
|
357
|
+
*
|
|
358
|
+
* @example
|
|
359
|
+
* world.remove(entity, Position);
|
|
360
|
+
* world.remove(GlobalConfig); // Remove singleton component
|
|
361
|
+
* world.sync(); // Apply changes
|
|
362
|
+
*/
|
|
363
|
+
remove<T>(componentId: ComponentId<T>): void;
|
|
364
|
+
remove<T>(entityId: EntityId, componentType: EntityId<T>): void;
|
|
365
|
+
remove<T>(entityId: EntityId | ComponentId, componentType?: EntityId<T>): void {
|
|
366
|
+
const { entityId: targetEntityId, componentType: targetComponentType } = this.resolveRemoveOperation(
|
|
367
|
+
entityId,
|
|
368
|
+
componentType,
|
|
369
|
+
);
|
|
370
|
+
this.commandBuffer.remove(targetEntityId, targetComponentType);
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
/**
|
|
374
|
+
* Deletes an entity and all its components from the world.
|
|
375
|
+
* The change is buffered and takes effect after calling `world.sync()`.
|
|
376
|
+
* Related entities may trigger cascade delete hooks if configured.
|
|
377
|
+
*
|
|
378
|
+
* @param entityId - The entity identifier to delete
|
|
379
|
+
*
|
|
380
|
+
* @example
|
|
381
|
+
* world.delete(entity);
|
|
382
|
+
* world.sync(); // Apply changes
|
|
383
|
+
*/
|
|
384
|
+
delete(entityId: EntityId): void {
|
|
385
|
+
this.commandBuffer.delete(entityId);
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
/**
|
|
389
|
+
* Checks if a specific **component** is present on an entity.
|
|
390
|
+
*
|
|
391
|
+
* This is for component membership checks — does the given entity have this
|
|
392
|
+
* component type? For checking whether an entity itself is alive, use
|
|
393
|
+
* {@link exists} instead.
|
|
394
|
+
*
|
|
395
|
+
* Immediately reflects the current state without waiting for `sync()`.
|
|
396
|
+
*
|
|
397
|
+
* @overload has<T>(entityId: EntityId, componentType: EntityId<T>): boolean
|
|
398
|
+
* Checks if a specific component type is present on the entity.
|
|
399
|
+
*
|
|
400
|
+
* @overload has<T>(componentId: ComponentId<T>): boolean
|
|
401
|
+
* Shorthand for checking a **singleton component** — a component that is its own
|
|
402
|
+
* entity (component-as-entity pattern). Equivalent to `has(componentId, componentId)`.
|
|
403
|
+
*
|
|
404
|
+
* @template T - The component data type
|
|
405
|
+
* @param entityId - The entity identifier, or a singleton component ID
|
|
406
|
+
* @param componentType - The component type to check
|
|
407
|
+
* @returns `true` if the entity has the component, `false` otherwise
|
|
408
|
+
*
|
|
409
|
+
* @example
|
|
410
|
+
* // Check if an entity has a component
|
|
411
|
+
* if (world.has(entity, Position)) {
|
|
412
|
+
* const pos = world.get(entity, Position);
|
|
413
|
+
* }
|
|
414
|
+
*
|
|
415
|
+
* // Check a singleton component (component-as-entity)
|
|
416
|
+
* if (world.has(GlobalConfig)) {
|
|
417
|
+
* const config = world.get(GlobalConfig);
|
|
418
|
+
* }
|
|
419
|
+
*
|
|
420
|
+
* // Use exists() for entity liveness checks
|
|
421
|
+
* if (world.exists(entity)) { ... }
|
|
422
|
+
*/
|
|
423
|
+
has<T>(componentId: ComponentId<T>): boolean;
|
|
424
|
+
has<T>(entityId: EntityId, componentType: EntityId<T>): boolean;
|
|
425
|
+
has<T>(entityId: EntityId | ComponentId, componentType?: EntityId<T>): boolean {
|
|
426
|
+
// Handle singleton component overload: has(componentId)
|
|
427
|
+
if (componentType === undefined) {
|
|
428
|
+
const componentId = entityId as ComponentId<T>;
|
|
429
|
+
return this.componentEntities.hasSingleton(componentId);
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
if (this.componentEntities.exists(entityId)) {
|
|
433
|
+
if (isWildcardRelationId(componentType)) {
|
|
434
|
+
const componentId = getComponentIdFromRelationId(componentType);
|
|
435
|
+
if (componentId === undefined) return false;
|
|
436
|
+
return this.componentEntities.hasWildcard(entityId, componentId);
|
|
437
|
+
}
|
|
438
|
+
return this.componentEntities.has(entityId, componentType);
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
const archetype = this.entityToArchetype.get(entityId);
|
|
442
|
+
if (!archetype) return false;
|
|
443
|
+
|
|
444
|
+
if (archetype.componentTypeSet.has(componentType)) return true;
|
|
445
|
+
|
|
446
|
+
if (isDontFragmentRelation(componentType)) {
|
|
447
|
+
return this.dontFragmentStore.get(entityId)?.has(componentType) ?? false;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
return false;
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
/**
|
|
454
|
+
* Retrieves a component from an entity.
|
|
455
|
+
* For wildcard relations, returns all relations of that type.
|
|
456
|
+
* Throws an error if the component does not exist; use `has()` to check first or use `getOptional()`.
|
|
457
|
+
*
|
|
458
|
+
* @overload get<T>(entityId: EntityId<T>): T
|
|
459
|
+
* When called with only an entity ID, retrieves the entity's primary component.
|
|
460
|
+
*
|
|
461
|
+
* @overload get<T>(entityId: EntityId, componentType: WildcardRelationId<T>): [EntityId<unknown>, T][]
|
|
462
|
+
* For wildcard relations, returns an array of [target entity, component value] pairs.
|
|
463
|
+
*
|
|
464
|
+
* @overload get<T>(entityId: EntityId, componentType: EntityId<T>): T
|
|
465
|
+
* Retrieves a specific component from the entity.
|
|
466
|
+
*
|
|
467
|
+
* @throws {Error} If the entity does not exist
|
|
468
|
+
* @throws {Error} If the component does not exist on the entity
|
|
469
|
+
*
|
|
470
|
+
* @example
|
|
471
|
+
* const position = world.get(entity, Position); // Throws if no Position
|
|
472
|
+
* const relations = world.get(entity, relation(Parent, "*")); // Wildcard relation
|
|
473
|
+
*/
|
|
474
|
+
get<T>(entityId: EntityId<T>): T;
|
|
475
|
+
get<T>(entityId: EntityId, componentType: WildcardRelationId<T>): [EntityId<unknown>, T][];
|
|
476
|
+
get<T>(entityId: EntityId, componentType: EntityId<T>): T;
|
|
477
|
+
get<T>(
|
|
478
|
+
entityId: EntityId,
|
|
479
|
+
componentType: EntityId<T> | WildcardRelationId<T> = entityId as EntityId<T>,
|
|
480
|
+
): T | [EntityId<unknown>, any][] {
|
|
481
|
+
if (this.componentEntities.exists(entityId)) {
|
|
482
|
+
if (isWildcardRelationId(componentType as EntityId<any>)) {
|
|
483
|
+
return this.componentEntities.getWildcard(entityId, componentType as WildcardRelationId<T>);
|
|
484
|
+
}
|
|
485
|
+
return this.componentEntities.get(entityId, componentType as EntityId<T>);
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
const archetype = this.entityToArchetype.get(entityId);
|
|
489
|
+
if (!archetype) {
|
|
490
|
+
throw new Error(`Entity ${entityId} does not exist`);
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
if (componentType >= 0 || componentType % RELATION_SHIFT !== 0) {
|
|
494
|
+
const inArchetype = archetype.componentTypeSet.has(componentType);
|
|
495
|
+
const hasDontFragment = isDontFragmentRelation(componentType);
|
|
496
|
+
const hasComponent = inArchetype || (hasDontFragment && this.dontFragmentStore.get(entityId)?.has(componentType));
|
|
497
|
+
|
|
498
|
+
if (!hasComponent) {
|
|
499
|
+
throw new Error(
|
|
500
|
+
`Entity ${entityId} does not have component ${componentType}. Use has() to check component existence before calling get().`,
|
|
501
|
+
);
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
return archetype.get(entityId, componentType) as T | [EntityId<unknown>, any][];
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
/**
|
|
509
|
+
* Safely retrieves a component from an entity without throwing an error.
|
|
510
|
+
* Returns `undefined` if the component does not exist.
|
|
511
|
+
* For wildcard relations, returns `undefined` if there are no relations.
|
|
512
|
+
*
|
|
513
|
+
* @template T - The component data type
|
|
514
|
+
* @overload getOptional<T>(entityId: EntityId<T>): { value: T } | undefined
|
|
515
|
+
* Retrieves the entity's primary component safely.
|
|
516
|
+
*
|
|
517
|
+
* @overload getOptional<T>(entityId: EntityId, componentType: WildcardRelationId<T>): { value: [EntityId<unknown>, T][] } | undefined
|
|
518
|
+
* Retrieves all matching relation values safely.
|
|
519
|
+
*
|
|
520
|
+
* @overload getOptional<T>(entityId: EntityId, componentType: EntityId<T>): { value: T } | undefined
|
|
521
|
+
* Retrieves a specific component safely.
|
|
522
|
+
*
|
|
523
|
+
* @throws {Error} If the entity does not exist
|
|
524
|
+
*
|
|
525
|
+
* @example
|
|
526
|
+
* const position = world.getOptional(entity, Position);
|
|
527
|
+
* if (position) {
|
|
528
|
+
* console.log(position.value.x);
|
|
529
|
+
* }
|
|
530
|
+
*/
|
|
531
|
+
getOptional<T>(entityId: EntityId<T>): { value: T } | undefined;
|
|
532
|
+
getOptional<T>(
|
|
533
|
+
entityId: EntityId,
|
|
534
|
+
componentType: WildcardRelationId<T>,
|
|
535
|
+
): { value: [EntityId<unknown>, T][] } | undefined;
|
|
536
|
+
getOptional<T>(entityId: EntityId, componentType: EntityId<T>): { value: T } | undefined;
|
|
537
|
+
getOptional<T>(
|
|
538
|
+
entityId: EntityId,
|
|
539
|
+
componentType: EntityId<T> | WildcardRelationId<T> = entityId as EntityId<T>,
|
|
540
|
+
): { value: T } | { value: [EntityId<unknown>, T][] } | undefined {
|
|
541
|
+
if (this.componentEntities.exists(entityId)) {
|
|
542
|
+
if (isWildcardRelationId(componentType)) {
|
|
543
|
+
const relations = this.componentEntities.getWildcard(entityId, componentType);
|
|
544
|
+
if (relations.length === 0) return undefined;
|
|
545
|
+
return { value: relations };
|
|
546
|
+
}
|
|
547
|
+
return this.componentEntities.getOptional(entityId, componentType);
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
const archetype = this.entityToArchetype.get(entityId);
|
|
551
|
+
if (!archetype) {
|
|
552
|
+
throw new Error(`Entity ${entityId} does not exist`);
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
if (isWildcardRelationId(componentType)) {
|
|
556
|
+
// For wildcard relations, get the data and wrap in optional if non-empty
|
|
557
|
+
const wildcardData = archetype.get(entityId, componentType) as [EntityId<unknown>, T][];
|
|
558
|
+
if (Array.isArray(wildcardData) && wildcardData.length > 0) {
|
|
559
|
+
return { value: wildcardData };
|
|
560
|
+
}
|
|
561
|
+
return undefined;
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
return archetype.getOptional(entityId, componentType);
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
/**
|
|
568
|
+
* Registers a lifecycle hook that responds to component changes.
|
|
569
|
+
* The hook callback is invoked when components matching the specified types are added, updated, or removed.
|
|
570
|
+
* @overload hook<const T extends readonly ComponentType<any>[]>(
|
|
571
|
+
* componentTypes: T,
|
|
572
|
+
* hook: LifecycleHook<T> | LifecycleCallback<T>,
|
|
573
|
+
* filter?: QueryFilter,
|
|
574
|
+
* ): () => void
|
|
575
|
+
* Registers a hook for multiple component types.
|
|
576
|
+
* The hook is triggered when entities enter/exit the matching set.
|
|
577
|
+
*
|
|
578
|
+
* @param componentTypes - Component types that define the matching entity set
|
|
579
|
+
* @param hook - Either a hook object with on_init/on_set/on_remove handlers, or a callback function
|
|
580
|
+
* @param filter - Optional query-style filter applied to the hook match set
|
|
581
|
+
* @returns A function that unsubscribes the hook when called
|
|
582
|
+
*
|
|
583
|
+
* @throws {Error} If no required components are specified in array overload
|
|
584
|
+
*
|
|
585
|
+
* @example
|
|
586
|
+
* const unsubscribe = world.hook([Position, Velocity], {
|
|
587
|
+
* on_init: (entityId, position, velocity) => console.log("Initialized"),
|
|
588
|
+
* on_set: (entityId, position, velocity) => console.log("Updated"),
|
|
589
|
+
* on_remove: (entityId, position, velocity) => console.log("Removed"),
|
|
590
|
+
* });
|
|
591
|
+
* unsubscribe(); // Remove hook
|
|
592
|
+
*
|
|
593
|
+
* // Callback style
|
|
594
|
+
* const unsubscribe = world.hook([Position], (event, entityId, position) => {
|
|
595
|
+
* if (event === "init") console.log("Initialized");
|
|
596
|
+
* });
|
|
597
|
+
*
|
|
598
|
+
* // With filter
|
|
599
|
+
* const unsubscribe2 = world.hook(
|
|
600
|
+
* [Position, Velocity],
|
|
601
|
+
* {
|
|
602
|
+
* on_set: (entityId, position, velocity) => console.log(entityId, position, velocity),
|
|
603
|
+
* },
|
|
604
|
+
* { negativeComponentTypes: [Disabled] },
|
|
605
|
+
* );
|
|
606
|
+
*/
|
|
607
|
+
hook<const T extends readonly ComponentType<any>[]>(
|
|
608
|
+
componentTypes: T,
|
|
609
|
+
hook: LifecycleHook<T> | LifecycleCallback<T>,
|
|
610
|
+
filter?: QueryFilter,
|
|
611
|
+
): () => void;
|
|
612
|
+
hook(
|
|
613
|
+
componentTypes: readonly ComponentType<any>[],
|
|
614
|
+
hook: LifecycleHook<any> | LifecycleCallback<any>,
|
|
615
|
+
filter?: QueryFilter,
|
|
616
|
+
): () => void {
|
|
617
|
+
const isCallback = typeof hook === "function";
|
|
618
|
+
const callback = isCallback ? (hook as LifecycleCallback<any>) : undefined;
|
|
619
|
+
|
|
620
|
+
const requiredComponents: EntityId<any>[] = [];
|
|
621
|
+
const optionalComponents: EntityId<any>[] = [];
|
|
622
|
+
for (const ct of componentTypes) {
|
|
623
|
+
if (!isOptionalEntityId(ct)) {
|
|
624
|
+
requiredComponents.push(ct as EntityId<any>);
|
|
625
|
+
} else {
|
|
626
|
+
optionalComponents.push(ct.optional);
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
if (requiredComponents.length === 0) {
|
|
631
|
+
throw new Error("Hook must have at least one required component");
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
const entry: LifecycleHookEntry = {
|
|
635
|
+
componentTypes,
|
|
636
|
+
requiredComponents,
|
|
637
|
+
optionalComponents,
|
|
638
|
+
filter: filter || {},
|
|
639
|
+
hook: isCallback ? ({} as LifecycleHook<any>) : (hook as LifecycleHook<any>),
|
|
640
|
+
callback,
|
|
641
|
+
matchedArchetypes: new Set(),
|
|
642
|
+
};
|
|
643
|
+
this.hooks.add(entry);
|
|
644
|
+
|
|
645
|
+
// Single pass: collect matching archetypes
|
|
646
|
+
const matchedArchetypes: Archetype[] = [];
|
|
647
|
+
for (const archetype of this.archetypes) {
|
|
648
|
+
if (this.archetypeMatchesHook(archetype, entry)) {
|
|
649
|
+
archetype.matchingMultiHooks.add(entry);
|
|
650
|
+
entry.matchedArchetypes!.add(archetype);
|
|
651
|
+
matchedArchetypes.push(archetype);
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
// Callback style: invoke callback("init", ...); hook style: invoke hook.on_init(...)
|
|
656
|
+
const shouldFireInit = isCallback || (hook as LifecycleHook<any>).on_init !== undefined;
|
|
657
|
+
if (shouldFireInit) {
|
|
658
|
+
for (const archetype of matchedArchetypes) {
|
|
659
|
+
for (const entityId of archetype.getEntities()) {
|
|
660
|
+
const components = collectMultiHookComponents(this.createHooksContext(), entityId, componentTypes);
|
|
661
|
+
if (isCallback) {
|
|
662
|
+
(callback as LifecycleCallback<any>)("init", entityId, ...components);
|
|
663
|
+
} else {
|
|
664
|
+
(hook as LifecycleHook<any>).on_init!(entityId, ...components);
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
return () => {
|
|
671
|
+
this.hooks.delete(entry);
|
|
672
|
+
if (entry.matchedArchetypes) {
|
|
673
|
+
for (const archetype of entry.matchedArchetypes) {
|
|
674
|
+
archetype.matchingMultiHooks.delete(entry);
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
};
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
/**
|
|
681
|
+
* Synchronizes all buffered commands (set/remove/delete) to the world.
|
|
682
|
+
* This method must be called after making changes via `set()`, `remove()`, or `delete()` for them to take effect.
|
|
683
|
+
* Typically called once per frame at the end of your game loop.
|
|
684
|
+
*
|
|
685
|
+
* @example
|
|
686
|
+
* world.set(entity, Position, { x: 10, y: 20 });
|
|
687
|
+
* world.remove(entity, OldComponent);
|
|
688
|
+
* world.sync(); // Apply all buffered changes
|
|
689
|
+
*/
|
|
690
|
+
sync(): void {
|
|
691
|
+
this.commandBuffer.execute();
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
/**
|
|
695
|
+
* Creates a cached query for efficiently iterating entities with specific components.
|
|
696
|
+
* The query is cached internally and reused across calls with the same component types and filter.
|
|
697
|
+
*
|
|
698
|
+
* **Important:** Store the query reference and reuse it across frames for optimal performance.
|
|
699
|
+
* Creating a new query each frame defeats the caching mechanism.
|
|
700
|
+
*
|
|
701
|
+
* @param componentTypes - Array of component types to match
|
|
702
|
+
* @param filter - Optional filter for additional constraints (e.g., without specific components)
|
|
703
|
+
* @returns A Query instance that can be used to iterate matching entities
|
|
704
|
+
*
|
|
705
|
+
* @example
|
|
706
|
+
* // Create once, reuse many times
|
|
707
|
+
* const movementQuery = world.createQuery([Position, Velocity]);
|
|
708
|
+
*
|
|
709
|
+
* // In game loop
|
|
710
|
+
* movementQuery.forEach((entity) => {
|
|
711
|
+
* const pos = world.get(entity, Position);
|
|
712
|
+
* const vel = world.get(entity, Velocity);
|
|
713
|
+
* pos.x += vel.x;
|
|
714
|
+
* pos.y += vel.y;
|
|
715
|
+
* });
|
|
716
|
+
*
|
|
717
|
+
* // With filter
|
|
718
|
+
* const activeQuery = world.createQuery([Position], {
|
|
719
|
+
* without: [Disabled]
|
|
720
|
+
* });
|
|
721
|
+
*/
|
|
722
|
+
createQuery(componentTypes: EntityId<any>[], filter: QueryFilter = {}): Query {
|
|
723
|
+
const sortedTypes = normalizeComponentTypes(componentTypes);
|
|
724
|
+
const filterKey = serializeQueryFilter(filter);
|
|
725
|
+
const key = `${this.createArchetypeSignature(sortedTypes)}${filterKey ? `|${filterKey}` : ""}`;
|
|
726
|
+
return this.queryRegistry.getOrCreate(this, sortedTypes, key, filter);
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
/**
|
|
730
|
+
* Creates a new entity builder for fluent entity configuration.
|
|
731
|
+
* Useful for building entities with multiple components in a single expression.
|
|
732
|
+
*
|
|
733
|
+
* @returns An EntityBuilder instance
|
|
734
|
+
*
|
|
735
|
+
* @example
|
|
736
|
+
* const entity = world.spawn()
|
|
737
|
+
* .with(Position, { x: 0, y: 0 })
|
|
738
|
+
* .with(Velocity, { x: 1, y: 1 })
|
|
739
|
+
* .build();
|
|
740
|
+
* world.sync(); // Apply changes
|
|
741
|
+
*/
|
|
742
|
+
spawn(): EntityBuilder {
|
|
743
|
+
return new EntityBuilder(this);
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
/**
|
|
747
|
+
* Spawns multiple entities with a configuration callback.
|
|
748
|
+
* More efficient than calling `spawn()` multiple times when creating many entities.
|
|
749
|
+
*
|
|
750
|
+
* @param count - Number of entities to spawn
|
|
751
|
+
* @param configure - Callback that receives an EntityBuilder and index; must return the configured builder
|
|
752
|
+
* @returns Array of created entity IDs
|
|
753
|
+
*
|
|
754
|
+
* @example
|
|
755
|
+
* const entities = world.spawnMany(100, (builder, index) => {
|
|
756
|
+
* return builder
|
|
757
|
+
* .with(Position, { x: index * 10, y: 0 })
|
|
758
|
+
* .with(Velocity, { x: 0, y: 1 });
|
|
759
|
+
* });
|
|
760
|
+
* world.sync();
|
|
761
|
+
*/
|
|
762
|
+
spawnMany(count: number, configure: (builder: EntityBuilder, index: number) => EntityBuilder): EntityId[] {
|
|
763
|
+
const entities: EntityId[] = [];
|
|
764
|
+
for (let i = 0; i < count; i++) {
|
|
765
|
+
const builder = new EntityBuilder(this);
|
|
766
|
+
entities.push(configure(builder, i).build());
|
|
767
|
+
}
|
|
768
|
+
return entities;
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
/**
|
|
772
|
+
* Releases a cached query and frees its resources if no longer needed.
|
|
773
|
+
* Call this when you're done using a query to allow the world to clean up its cache entry.
|
|
774
|
+
*
|
|
775
|
+
* @param query - The query to release
|
|
776
|
+
*
|
|
777
|
+
* @example
|
|
778
|
+
* const query = world.createQuery([Position]);
|
|
779
|
+
* // ... use query ...
|
|
780
|
+
* world.releaseQuery(query); // Optional cleanup
|
|
781
|
+
*/
|
|
782
|
+
releaseQuery(query: Query): void {
|
|
783
|
+
this.queryRegistry.release(query);
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
/**
|
|
787
|
+
* Returns all archetypes that contain entities with the specified components.
|
|
788
|
+
* Used internally for query optimization but can be useful for debugging.
|
|
789
|
+
*
|
|
790
|
+
* @param componentTypes - Array of component types to match
|
|
791
|
+
* @returns Array of Archetype objects containing matching components
|
|
792
|
+
* @internal
|
|
793
|
+
*/
|
|
794
|
+
getMatchingArchetypes(componentTypes: EntityId<any>[]): Archetype[] {
|
|
795
|
+
if (componentTypes.length === 0) {
|
|
796
|
+
return [...this.archetypes];
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
const regularComponents: EntityId<any>[] = [];
|
|
800
|
+
const wildcardRelations: { componentId: ComponentId<any>; relationId: EntityId<any> }[] = [];
|
|
801
|
+
|
|
802
|
+
for (const componentType of componentTypes) {
|
|
803
|
+
if (isWildcardRelationId(componentType)) {
|
|
804
|
+
const componentId = getComponentIdFromRelationId(componentType);
|
|
805
|
+
if (componentId !== undefined) {
|
|
806
|
+
wildcardRelations.push({ componentId, relationId: componentType });
|
|
807
|
+
}
|
|
808
|
+
} else {
|
|
809
|
+
regularComponents.push(componentType);
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
let matchingArchetypes = this.getArchetypesWithComponents(regularComponents);
|
|
814
|
+
|
|
815
|
+
for (const { componentId, relationId } of wildcardRelations) {
|
|
816
|
+
const markerSet = this.archetypesByComponent.get(relationId);
|
|
817
|
+
const archetypesWithMarker = markerSet ? Array.from(markerSet) : [];
|
|
818
|
+
matchingArchetypes =
|
|
819
|
+
matchingArchetypes.length === 0
|
|
820
|
+
? archetypesWithMarker
|
|
821
|
+
: matchingArchetypes.filter((a) => markerSet?.has(a) || a.hasRelationWithComponentId(componentId));
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
return matchingArchetypes;
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
private getArchetypesWithComponents(componentTypes: EntityId<any>[]): Archetype[] {
|
|
828
|
+
if (componentTypes.length === 0) return [...this.archetypes];
|
|
829
|
+
if (componentTypes.length === 1) {
|
|
830
|
+
const set = this.archetypesByComponent.get(componentTypes[0]!);
|
|
831
|
+
return set ? Array.from(set) : [];
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
// Sort by Set size, intersect starting from the smallest
|
|
835
|
+
const sets = componentTypes
|
|
836
|
+
.map((type) => this.archetypesByComponent.get(type))
|
|
837
|
+
.filter((s): s is Set<Archetype> => s !== undefined && s.size > 0)
|
|
838
|
+
.sort((a, b) => a.size - b.size);
|
|
839
|
+
|
|
840
|
+
if (sets.length === 0) return [];
|
|
841
|
+
if (sets.length < componentTypes.length) return []; // One component has no matching archetypes
|
|
842
|
+
|
|
843
|
+
const smallest = sets[0]!;
|
|
844
|
+
|
|
845
|
+
// 2-component fast path
|
|
846
|
+
if (sets.length === 2) {
|
|
847
|
+
const other = sets[1]!;
|
|
848
|
+
return Array.from(smallest).filter((a) => other.has(a));
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
// Multi-component intersection
|
|
852
|
+
let result = new Set(smallest);
|
|
853
|
+
for (let i = 1; i < sets.length; i++) {
|
|
854
|
+
for (const item of result) {
|
|
855
|
+
if (!sets[i]!.has(item)) result.delete(item);
|
|
856
|
+
}
|
|
857
|
+
if (result.size === 0) return [];
|
|
858
|
+
}
|
|
859
|
+
return Array.from(result);
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
/**
|
|
863
|
+
* Queries entities with specific components.
|
|
864
|
+
* For simpler use cases, prefer using `createQuery()` with `forEach()` which is cached and more efficient.
|
|
865
|
+
*
|
|
866
|
+
* @overload query(componentTypes: EntityId<any>[]): EntityId[]
|
|
867
|
+
* Returns an array of entity IDs that have all specified components.
|
|
868
|
+
*
|
|
869
|
+
* @overload query<const T extends readonly EntityId<any>[]>(
|
|
870
|
+
* componentTypes: T,
|
|
871
|
+
* includeComponents: true,
|
|
872
|
+
* ): Array<{ entity: EntityId; components: ComponentTuple<T> }>
|
|
873
|
+
* Returns entities along with their component data.
|
|
874
|
+
*
|
|
875
|
+
* @param componentTypes - Array of component types to query
|
|
876
|
+
* @param includeComponents - If true, includes component data in results
|
|
877
|
+
* @returns Array of entity IDs or objects with entities and components
|
|
878
|
+
*
|
|
879
|
+
* @example
|
|
880
|
+
* // Just entity IDs
|
|
881
|
+
* const entities = world.query([Position, Velocity]);
|
|
882
|
+
*
|
|
883
|
+
* // With components
|
|
884
|
+
* const results = world.query([Position, Velocity], true);
|
|
885
|
+
* results.forEach(({ entity, components: [pos, vel] }) => {
|
|
886
|
+
* pos.x += vel.x;
|
|
887
|
+
* });
|
|
888
|
+
*/
|
|
889
|
+
query(componentTypes: EntityId<any>[]): EntityId[];
|
|
890
|
+
query<const T extends readonly EntityId<any>[]>(
|
|
891
|
+
componentTypes: T,
|
|
892
|
+
includeComponents: true,
|
|
893
|
+
): Array<{ entity: EntityId; components: ComponentTuple<T> }>;
|
|
894
|
+
query(
|
|
895
|
+
componentTypes: EntityId<any>[],
|
|
896
|
+
includeComponents?: boolean,
|
|
897
|
+
): EntityId[] | Array<{ entity: EntityId; components: any }> {
|
|
898
|
+
const matchingArchetypes = this.getMatchingArchetypes(componentTypes);
|
|
899
|
+
|
|
900
|
+
if (includeComponents) {
|
|
901
|
+
const result: Array<{ entity: EntityId; components: any }> = [];
|
|
902
|
+
for (const archetype of matchingArchetypes) {
|
|
903
|
+
archetype.appendEntitiesWithComponents(componentTypes as EntityId<any>[], result);
|
|
904
|
+
}
|
|
905
|
+
return result;
|
|
906
|
+
} else {
|
|
907
|
+
const result: EntityId[] = [];
|
|
908
|
+
for (const archetype of matchingArchetypes) {
|
|
909
|
+
for (const entity of archetype.getEntities()) {
|
|
910
|
+
result.push(entity);
|
|
911
|
+
}
|
|
912
|
+
}
|
|
913
|
+
return result;
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
private executeEntityCommands(entityId: EntityId, commands: Command[]): void {
|
|
918
|
+
this._changeset.clear();
|
|
919
|
+
|
|
920
|
+
// 1. Route: component entities use flat-map storage
|
|
921
|
+
if (this.componentEntities.exists(entityId)) {
|
|
922
|
+
this.componentEntities.executeCommands(entityId, commands);
|
|
923
|
+
return;
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
// 2. Route: destroy uses fast path
|
|
927
|
+
if (commands.some((cmd) => cmd.type === "destroy")) {
|
|
928
|
+
this.destroyEntityImmediate(entityId);
|
|
929
|
+
return;
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
// 3. Apply structural changes
|
|
933
|
+
this.applyEntityCommands(entityId, commands);
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
private applyEntityCommands(entityId: EntityId, commands: Command[]): void {
|
|
937
|
+
const currentArchetype = this.entityToArchetype.get(entityId);
|
|
938
|
+
if (!currentArchetype) return;
|
|
939
|
+
|
|
940
|
+
const changeset = this._changeset;
|
|
941
|
+
processCommands(entityId, currentArchetype, commands, changeset, (eid, arch, compId) => {
|
|
942
|
+
if (isExclusiveComponent(compId)) {
|
|
943
|
+
removeMatchingRelations(eid, arch, compId, changeset);
|
|
944
|
+
}
|
|
945
|
+
});
|
|
946
|
+
|
|
947
|
+
const hasStructuralChange = changeset.removes.size > 0 || changeset.adds.size > 0;
|
|
948
|
+
|
|
949
|
+
if (this.hooks.size === 0) {
|
|
950
|
+
// Fast path: no hooks, skip removedComponents map allocation and hook triggering
|
|
951
|
+
applyChangeset(this._commandCtx, entityId, currentArchetype, changeset, this.entityToArchetype, null);
|
|
952
|
+
if (hasStructuralChange) {
|
|
953
|
+
this.updateEntityReferences(entityId, changeset);
|
|
954
|
+
}
|
|
955
|
+
return;
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
const removedComponents = new Map<EntityId<any>, any>();
|
|
959
|
+
const newArchetype = applyChangeset(
|
|
960
|
+
this._commandCtx,
|
|
961
|
+
entityId,
|
|
962
|
+
currentArchetype,
|
|
963
|
+
changeset,
|
|
964
|
+
this.entityToArchetype,
|
|
965
|
+
removedComponents,
|
|
966
|
+
);
|
|
967
|
+
|
|
968
|
+
if (hasStructuralChange) {
|
|
969
|
+
this.updateEntityReferences(entityId, changeset);
|
|
970
|
+
}
|
|
971
|
+
triggerLifecycleHooks(
|
|
972
|
+
this.createHooksContext(),
|
|
973
|
+
entityId,
|
|
974
|
+
changeset.adds,
|
|
975
|
+
removedComponents,
|
|
976
|
+
currentArchetype,
|
|
977
|
+
newArchetype,
|
|
978
|
+
);
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
private createHooksContext(): HooksContext {
|
|
982
|
+
return this._hooksCtx;
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
private removeComponentImmediate(entityId: EntityId, componentType: EntityId<any>, targetEntityId: EntityId): void {
|
|
986
|
+
const sourceArchetype = this.entityToArchetype.get(entityId);
|
|
987
|
+
if (!sourceArchetype) return;
|
|
988
|
+
|
|
989
|
+
const changeset = this._removeChangeset;
|
|
990
|
+
changeset.clear();
|
|
991
|
+
changeset.delete(componentType);
|
|
992
|
+
maybeRemoveWildcardMarker(
|
|
993
|
+
entityId,
|
|
994
|
+
sourceArchetype,
|
|
995
|
+
componentType,
|
|
996
|
+
getComponentIdFromRelationId(componentType),
|
|
997
|
+
changeset,
|
|
998
|
+
);
|
|
999
|
+
|
|
1000
|
+
const removedComponent = sourceArchetype.get(entityId, componentType);
|
|
1001
|
+
const newArchetype = applyChangeset(
|
|
1002
|
+
this._commandCtx,
|
|
1003
|
+
entityId,
|
|
1004
|
+
sourceArchetype,
|
|
1005
|
+
changeset,
|
|
1006
|
+
this.entityToArchetype,
|
|
1007
|
+
null,
|
|
1008
|
+
);
|
|
1009
|
+
untrackEntityReference(this.entityReferences, entityId, componentType, targetEntityId);
|
|
1010
|
+
triggerLifecycleHooks(
|
|
1011
|
+
this.createHooksContext(),
|
|
1012
|
+
entityId,
|
|
1013
|
+
new Map(),
|
|
1014
|
+
new Map([[componentType, removedComponent]]),
|
|
1015
|
+
sourceArchetype,
|
|
1016
|
+
newArchetype,
|
|
1017
|
+
);
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
private updateEntityReferences(entityId: EntityId, changeset: ComponentChangeset): void {
|
|
1021
|
+
for (const componentType of changeset.removes) {
|
|
1022
|
+
if (isEntityRelation(componentType)) {
|
|
1023
|
+
const targetId = getTargetIdFromRelationId(componentType)!;
|
|
1024
|
+
untrackEntityReference(this.entityReferences, entityId, componentType, targetId);
|
|
1025
|
+
} else if (componentType >= ENTITY_ID_START) {
|
|
1026
|
+
untrackEntityReference(this.entityReferences, entityId, componentType, componentType);
|
|
1027
|
+
}
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
for (const [componentType] of changeset.adds) {
|
|
1031
|
+
if (isEntityRelation(componentType)) {
|
|
1032
|
+
const targetId = getTargetIdFromRelationId(componentType)!;
|
|
1033
|
+
trackEntityReference(this.entityReferences, entityId, componentType, targetId);
|
|
1034
|
+
} else if (componentType >= ENTITY_ID_START) {
|
|
1035
|
+
trackEntityReference(this.entityReferences, entityId, componentType, componentType);
|
|
1036
|
+
}
|
|
1037
|
+
}
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1040
|
+
private ensureArchetype(componentTypes: Iterable<EntityId<any>>): Archetype {
|
|
1041
|
+
const regularTypes = filterRegularComponentTypes(componentTypes);
|
|
1042
|
+
const sortedTypes = normalizeComponentTypes(regularTypes);
|
|
1043
|
+
const hashKey = this.createArchetypeSignature(sortedTypes);
|
|
1044
|
+
|
|
1045
|
+
return getOrCompute(this.archetypeBySignature, hashKey, () => this.createNewArchetype(sortedTypes));
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
/** Add componentType to the reverse index if it contains an entity ID */
|
|
1049
|
+
private addToReferencingIndex(componentType: EntityId<any>, archetype: Archetype): void {
|
|
1050
|
+
const detailedType = getDetailedIdType(componentType);
|
|
1051
|
+
let entityId: EntityId | undefined;
|
|
1052
|
+
|
|
1053
|
+
if (detailedType.type === "entity") {
|
|
1054
|
+
entityId = componentType as EntityId;
|
|
1055
|
+
} else if (detailedType.type === "entity-relation") {
|
|
1056
|
+
entityId = detailedType.targetId;
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1059
|
+
if (entityId !== undefined) {
|
|
1060
|
+
let refs = this.entityToReferencingArchetypes.get(entityId);
|
|
1061
|
+
if (!refs) {
|
|
1062
|
+
refs = new Set();
|
|
1063
|
+
this.entityToReferencingArchetypes.set(entityId, refs);
|
|
1064
|
+
}
|
|
1065
|
+
refs.add(archetype);
|
|
1066
|
+
}
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1069
|
+
/** Remove componentType from the reverse index */
|
|
1070
|
+
private removeFromReferencingIndex(componentType: EntityId<any>, archetype: Archetype): void {
|
|
1071
|
+
const detailedType = getDetailedIdType(componentType);
|
|
1072
|
+
let entityId: EntityId | undefined;
|
|
1073
|
+
|
|
1074
|
+
if (detailedType.type === "entity") {
|
|
1075
|
+
entityId = componentType as EntityId;
|
|
1076
|
+
} else if (detailedType.type === "entity-relation") {
|
|
1077
|
+
entityId = detailedType.targetId;
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1080
|
+
if (entityId !== undefined) {
|
|
1081
|
+
const refs = this.entityToReferencingArchetypes.get(entityId);
|
|
1082
|
+
if (refs) {
|
|
1083
|
+
refs.delete(archetype);
|
|
1084
|
+
if (refs.size === 0) {
|
|
1085
|
+
this.entityToReferencingArchetypes.delete(entityId);
|
|
1086
|
+
}
|
|
1087
|
+
}
|
|
1088
|
+
}
|
|
1089
|
+
}
|
|
1090
|
+
|
|
1091
|
+
private createNewArchetype(componentTypes: EntityId<any>[]): Archetype {
|
|
1092
|
+
const newArchetype = new Archetype(componentTypes, this.dontFragmentStore);
|
|
1093
|
+
this.archetypes.push(newArchetype);
|
|
1094
|
+
|
|
1095
|
+
for (const componentType of componentTypes) {
|
|
1096
|
+
let archetypes = this.archetypesByComponent.get(componentType);
|
|
1097
|
+
if (!archetypes) {
|
|
1098
|
+
archetypes = new Set();
|
|
1099
|
+
this.archetypesByComponent.set(componentType, archetypes);
|
|
1100
|
+
}
|
|
1101
|
+
archetypes.add(newArchetype);
|
|
1102
|
+
|
|
1103
|
+
// Update reverse index
|
|
1104
|
+
this.addToReferencingIndex(componentType, newArchetype);
|
|
1105
|
+
}
|
|
1106
|
+
|
|
1107
|
+
this.queryRegistry.onNewArchetype(newArchetype);
|
|
1108
|
+
this.updateArchetypeHookMatches(newArchetype);
|
|
1109
|
+
|
|
1110
|
+
return newArchetype;
|
|
1111
|
+
}
|
|
1112
|
+
|
|
1113
|
+
private updateArchetypeHookMatches(archetype: Archetype): void {
|
|
1114
|
+
for (const entry of this.hooks) {
|
|
1115
|
+
if (this.archetypeMatchesHook(archetype, entry)) {
|
|
1116
|
+
archetype.matchingMultiHooks.add(entry);
|
|
1117
|
+
if (entry.matchedArchetypes) {
|
|
1118
|
+
entry.matchedArchetypes.add(archetype);
|
|
1119
|
+
}
|
|
1120
|
+
}
|
|
1121
|
+
}
|
|
1122
|
+
}
|
|
1123
|
+
|
|
1124
|
+
private archetypeMatchesHook(archetype: Archetype, entry: LifecycleHookEntry): boolean {
|
|
1125
|
+
return (
|
|
1126
|
+
entry.requiredComponents.every((c: EntityId<any>) => {
|
|
1127
|
+
if (isWildcardRelationId(c)) {
|
|
1128
|
+
if (isDontFragmentWildcard(c)) return true;
|
|
1129
|
+
const componentId = getComponentIdFromRelationId(c);
|
|
1130
|
+
return componentId !== undefined && archetype.hasRelationWithComponentId(componentId);
|
|
1131
|
+
}
|
|
1132
|
+
return archetype.componentTypeSet.has(c) || isDontFragmentRelation(c);
|
|
1133
|
+
}) && matchesFilter(archetype, entry.filter)
|
|
1134
|
+
);
|
|
1135
|
+
}
|
|
1136
|
+
|
|
1137
|
+
private cleanupArchetypesReferencingEntity(entityId: EntityId): void {
|
|
1138
|
+
const refs = this.entityToReferencingArchetypes.get(entityId);
|
|
1139
|
+
if (!refs) return;
|
|
1140
|
+
|
|
1141
|
+
for (const archetype of refs) {
|
|
1142
|
+
if (archetype.getEntities().length === 0) {
|
|
1143
|
+
this.removeArchetype(archetype);
|
|
1144
|
+
}
|
|
1145
|
+
}
|
|
1146
|
+
// removeArchetype already cleans up the reverse index entries
|
|
1147
|
+
this.entityToReferencingArchetypes.delete(entityId);
|
|
1148
|
+
}
|
|
1149
|
+
|
|
1150
|
+
private removeArchetype(archetype: Archetype): void {
|
|
1151
|
+
const index = this.archetypes.indexOf(archetype);
|
|
1152
|
+
if (index !== -1) {
|
|
1153
|
+
// swap-and-pop: O(1) removal
|
|
1154
|
+
const last = this.archetypes[this.archetypes.length - 1]!;
|
|
1155
|
+
this.archetypes[index] = last;
|
|
1156
|
+
this.archetypes.pop();
|
|
1157
|
+
}
|
|
1158
|
+
|
|
1159
|
+
this.archetypeBySignature.delete(this.createArchetypeSignature(archetype.componentTypes));
|
|
1160
|
+
|
|
1161
|
+
for (const componentType of archetype.componentTypes) {
|
|
1162
|
+
const archetypes = this.archetypesByComponent.get(componentType);
|
|
1163
|
+
if (archetypes) {
|
|
1164
|
+
archetypes.delete(archetype);
|
|
1165
|
+
if (archetypes.size === 0) {
|
|
1166
|
+
this.archetypesByComponent.delete(componentType);
|
|
1167
|
+
}
|
|
1168
|
+
}
|
|
1169
|
+
|
|
1170
|
+
// Clean up reverse index
|
|
1171
|
+
this.removeFromReferencingIndex(componentType, archetype);
|
|
1172
|
+
}
|
|
1173
|
+
|
|
1174
|
+
this.queryRegistry.onArchetypeRemoved(archetype);
|
|
1175
|
+
}
|
|
1176
|
+
|
|
1177
|
+
/**
|
|
1178
|
+
* Serializes the entire world state to a plain JavaScript object.
|
|
1179
|
+
* This creates a "memory snapshot" that can be stored or transmitted.
|
|
1180
|
+
* The snapshot can be restored using `new World(snapshot)`.
|
|
1181
|
+
*
|
|
1182
|
+
* **Note:** This is NOT automatically persistent storage. To persist data,
|
|
1183
|
+
* you must serialize the returned object to JSON or another format yourself.
|
|
1184
|
+
*
|
|
1185
|
+
* @returns A serializable object representing the world state
|
|
1186
|
+
*
|
|
1187
|
+
* @example
|
|
1188
|
+
* // Create snapshot
|
|
1189
|
+
* const snapshot = world.serialize();
|
|
1190
|
+
*
|
|
1191
|
+
* // Save to storage (example)
|
|
1192
|
+
* localStorage.setItem('save', JSON.stringify(snapshot));
|
|
1193
|
+
*
|
|
1194
|
+
* // Later, restore from snapshot
|
|
1195
|
+
* const savedData = JSON.parse(localStorage.getItem('save'));
|
|
1196
|
+
* const newWorld = new World(savedData);
|
|
1197
|
+
*/
|
|
1198
|
+
serialize(): SerializedWorld {
|
|
1199
|
+
return serializeWorld(this.archetypes, this.componentEntities, this.entityIdManager);
|
|
1200
|
+
}
|
|
1201
|
+
}
|