@codehz/ecs 0.1.9 → 0.2.1
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/archetype.d.ts +18 -0
- package/changeset.d.ts +6 -0
- package/entity.d.ts +18 -3
- package/index.js +121 -38
- package/package.json +1 -1
- package/query.d.ts +1 -2
- package/world.d.ts +19 -2
package/archetype.d.ts
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import type { EntityId, WildcardRelationId } from "./entity";
|
|
2
2
|
import { type ComponentTuple, type ComponentType } from "./types";
|
|
3
|
+
/**
|
|
4
|
+
* Special value to represent missing component data
|
|
5
|
+
*/
|
|
6
|
+
export declare const MISSING_COMPONENT: unique symbol;
|
|
3
7
|
/**
|
|
4
8
|
* Archetype class for ECS architecture
|
|
5
9
|
* Represents a group of entities that share the same set of components
|
|
@@ -49,6 +53,20 @@ export declare class Archetype {
|
|
|
49
53
|
* @param componentData Map of component type to component data
|
|
50
54
|
*/
|
|
51
55
|
addEntity(entityId: EntityId, componentData: Map<EntityId<any>, any>): void;
|
|
56
|
+
/**
|
|
57
|
+
* Get all component data for a specific entity
|
|
58
|
+
* @param entityId The entity to get data for
|
|
59
|
+
* @returns Map of component type to component data
|
|
60
|
+
*/
|
|
61
|
+
getEntity(entityId: EntityId): Map<EntityId<any>, any> | undefined;
|
|
62
|
+
/**
|
|
63
|
+
* Dump all entities and their component data in this archetype
|
|
64
|
+
* @returns Array of objects with entity and component data
|
|
65
|
+
*/
|
|
66
|
+
dump(): Array<{
|
|
67
|
+
entity: EntityId;
|
|
68
|
+
components: Map<EntityId<any>, any>;
|
|
69
|
+
}>;
|
|
52
70
|
/**
|
|
53
71
|
* Remove an entity from this archetype
|
|
54
72
|
* @param entityId The entity to remove
|
package/changeset.d.ts
CHANGED
|
@@ -29,4 +29,10 @@ export declare class ComponentChangeset {
|
|
|
29
29
|
* Apply the changeset to existing components and return the final state
|
|
30
30
|
*/
|
|
31
31
|
applyTo(existingComponents: Map<EntityId<any>, any>): Map<EntityId<any>, any>;
|
|
32
|
+
/**
|
|
33
|
+
* Get the final component types after applying the changeset
|
|
34
|
+
* @param existingComponentTypes - The current component types on the entity
|
|
35
|
+
* @returns The final component types or undefined if no changes
|
|
36
|
+
*/
|
|
37
|
+
getFinalComponentTypes(existingComponentTypes: EntityId<any>[]): EntityId<any>[] | undefined;
|
|
32
38
|
}
|
package/entity.d.ts
CHANGED
|
@@ -47,7 +47,7 @@ export declare function createEntityId(id: number): EntityId;
|
|
|
47
47
|
/**
|
|
48
48
|
* Type for relation ID based on component and target types
|
|
49
49
|
*/
|
|
50
|
-
type RelationIdType<T, R> = R extends ComponentId<infer U> ? U extends void ? ComponentRelationId<T> : ComponentRelationId<T extends void ? U : T
|
|
50
|
+
type RelationIdType<T, R> = R extends ComponentId<infer U> ? U extends void ? ComponentRelationId<T> : ComponentRelationId<T extends void ? U : T> : R extends EntityId<any> ? EntityRelationId<T> : never;
|
|
51
51
|
/**
|
|
52
52
|
* Create a relation ID by associating a component with another ID (entity or component)
|
|
53
53
|
* @param componentId The component ID (0-1023)
|
|
@@ -167,7 +167,22 @@ export declare class ComponentIdAllocator {
|
|
|
167
167
|
hasAvailableIds(): boolean;
|
|
168
168
|
}
|
|
169
169
|
/**
|
|
170
|
-
* Allocate a new component ID from the global allocator
|
|
170
|
+
* Allocate a new component ID from the global allocator.
|
|
171
|
+
* Optionally register a name for the component.
|
|
172
|
+
* The name is only for serialization/debugging and does not affect base functionality.
|
|
173
|
+
* @param name Optional name for the component
|
|
174
|
+
* @returns The allocated component ID
|
|
171
175
|
*/
|
|
172
|
-
export declare function component<T = void>(): ComponentId<T>;
|
|
176
|
+
export declare function component<T = void>(name?: string): ComponentId<T>;
|
|
177
|
+
/**
|
|
178
|
+
* Get a component ID by its registered name
|
|
179
|
+
* @param name The component name
|
|
180
|
+
* @returns The component ID if found, undefined otherwise
|
|
181
|
+
*/
|
|
182
|
+
export declare function getComponentIdByName(name: string): ComponentId<any> | undefined;
|
|
183
|
+
/** Get a component name by its ID
|
|
184
|
+
* @param id The component ID
|
|
185
|
+
* @returns The component name if found, undefined otherwise
|
|
186
|
+
*/
|
|
187
|
+
export declare function getComponentNameById(id: ComponentId<any>): string | undefined;
|
|
173
188
|
export {};
|
package/index.js
CHANGED
|
@@ -214,8 +214,24 @@ class ComponentIdAllocator {
|
|
|
214
214
|
}
|
|
215
215
|
}
|
|
216
216
|
var globalComponentIdAllocator = new ComponentIdAllocator;
|
|
217
|
-
|
|
218
|
-
|
|
217
|
+
var ComponentNames = new Map;
|
|
218
|
+
var ComponentIdForNames = new Map;
|
|
219
|
+
function component(name) {
|
|
220
|
+
const id = globalComponentIdAllocator.allocate();
|
|
221
|
+
if (name) {
|
|
222
|
+
if (ComponentIdForNames.has(name)) {
|
|
223
|
+
throw new Error(`Component name "${name}" is already registered`);
|
|
224
|
+
}
|
|
225
|
+
ComponentNames.set(id, name);
|
|
226
|
+
ComponentIdForNames.set(name, id);
|
|
227
|
+
}
|
|
228
|
+
return id;
|
|
229
|
+
}
|
|
230
|
+
function getComponentIdByName(name) {
|
|
231
|
+
return ComponentIdForNames.get(name);
|
|
232
|
+
}
|
|
233
|
+
function getComponentNameById(id) {
|
|
234
|
+
return ComponentNames.get(id);
|
|
219
235
|
}
|
|
220
236
|
// src/types.ts
|
|
221
237
|
function isOptionalEntityId(type) {
|
|
@@ -277,6 +293,33 @@ class Archetype {
|
|
|
277
293
|
this.getComponentData(componentType).push(data === undefined ? MISSING_COMPONENT : data);
|
|
278
294
|
}
|
|
279
295
|
}
|
|
296
|
+
getEntity(entityId) {
|
|
297
|
+
const index = this.entityToIndex.get(entityId);
|
|
298
|
+
if (index === undefined) {
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
const entityData = new Map;
|
|
302
|
+
for (const componentType of this.componentTypes) {
|
|
303
|
+
const dataArray = this.getComponentData(componentType);
|
|
304
|
+
const data = dataArray[index];
|
|
305
|
+
entityData.set(componentType, data === MISSING_COMPONENT ? undefined : data);
|
|
306
|
+
}
|
|
307
|
+
return entityData;
|
|
308
|
+
}
|
|
309
|
+
dump() {
|
|
310
|
+
const result = [];
|
|
311
|
+
for (let i = 0;i < this.entities.length; i++) {
|
|
312
|
+
const entity = this.entities[i];
|
|
313
|
+
const components = new Map;
|
|
314
|
+
for (const componentType of this.componentTypes) {
|
|
315
|
+
const dataArray = this.getComponentData(componentType);
|
|
316
|
+
const data = dataArray[i];
|
|
317
|
+
components.set(componentType, data === MISSING_COMPONENT ? undefined : data);
|
|
318
|
+
}
|
|
319
|
+
result.push({ entity, components });
|
|
320
|
+
}
|
|
321
|
+
return result;
|
|
322
|
+
}
|
|
280
323
|
removeEntity(entityId) {
|
|
281
324
|
const index = this.entityToIndex.get(entityId);
|
|
282
325
|
if (index === undefined) {
|
|
@@ -364,7 +407,7 @@ class Archetype {
|
|
|
364
407
|
return result;
|
|
365
408
|
}
|
|
366
409
|
forEachWithComponents(componentTypes, callback) {
|
|
367
|
-
const cacheKey = componentTypes.map((id) => id.
|
|
410
|
+
const cacheKey = componentTypes.map((id) => isOptionalEntityId(id) ? `opt(${id.optional})` : `${id}`).join(",");
|
|
368
411
|
const componentDataSources = getOrComputeCache(this.componentDataSourcesCache, cacheKey, () => {
|
|
369
412
|
return componentTypes.map((compType) => {
|
|
370
413
|
let optional = false;
|
|
@@ -480,6 +523,25 @@ class ComponentChangeset {
|
|
|
480
523
|
}
|
|
481
524
|
return existingComponents;
|
|
482
525
|
}
|
|
526
|
+
getFinalComponentTypes(existingComponentTypes) {
|
|
527
|
+
const finalComponentTypes = new Set(existingComponentTypes);
|
|
528
|
+
let changed = false;
|
|
529
|
+
for (const componentType of this.removes) {
|
|
530
|
+
if (!finalComponentTypes.has(componentType)) {
|
|
531
|
+
continue;
|
|
532
|
+
}
|
|
533
|
+
changed = true;
|
|
534
|
+
finalComponentTypes.delete(componentType);
|
|
535
|
+
}
|
|
536
|
+
for (const componentType of this.adds.keys()) {
|
|
537
|
+
if (finalComponentTypes.has(componentType)) {
|
|
538
|
+
continue;
|
|
539
|
+
}
|
|
540
|
+
changed = true;
|
|
541
|
+
finalComponentTypes.add(componentType);
|
|
542
|
+
}
|
|
543
|
+
return changed ? Array.from(finalComponentTypes) : undefined;
|
|
544
|
+
}
|
|
483
545
|
}
|
|
484
546
|
|
|
485
547
|
// src/command-buffer.ts
|
|
@@ -564,17 +626,15 @@ function matchesFilter(archetype, filter) {
|
|
|
564
626
|
|
|
565
627
|
// src/query.ts
|
|
566
628
|
class Query {
|
|
567
|
-
key;
|
|
568
629
|
world;
|
|
569
630
|
componentTypes;
|
|
570
631
|
filter;
|
|
571
632
|
cachedArchetypes = [];
|
|
572
633
|
isDisposed = false;
|
|
573
|
-
constructor(world, componentTypes, filter = {}
|
|
634
|
+
constructor(world, componentTypes, filter = {}) {
|
|
574
635
|
this.world = world;
|
|
575
636
|
this.componentTypes = [...componentTypes].sort((a, b) => a - b);
|
|
576
637
|
this.filter = filter;
|
|
577
|
-
this.key = key ?? `${this.componentTypes.join(",")}|`;
|
|
578
638
|
this.updateCache();
|
|
579
639
|
world._registerQuery(this);
|
|
580
640
|
}
|
|
@@ -744,8 +804,27 @@ class World {
|
|
|
744
804
|
const componentMap = new Map;
|
|
745
805
|
const componentTypes = [];
|
|
746
806
|
for (const componentEntry of componentsArray) {
|
|
747
|
-
|
|
748
|
-
|
|
807
|
+
const componentTypeRaw = componentEntry.type;
|
|
808
|
+
let componentType;
|
|
809
|
+
if (typeof componentTypeRaw === "number") {
|
|
810
|
+
componentType = componentTypeRaw;
|
|
811
|
+
} else if (typeof componentTypeRaw === "string") {
|
|
812
|
+
const compId = getComponentIdByName(componentTypeRaw);
|
|
813
|
+
if (compId === undefined) {
|
|
814
|
+
throw new Error(`Unknown component name in snapshot: ${componentTypeRaw}`);
|
|
815
|
+
}
|
|
816
|
+
componentType = compId;
|
|
817
|
+
} else if (typeof componentTypeRaw === "object" && componentTypeRaw !== null && typeof componentTypeRaw.component === "string" && typeof componentTypeRaw.target === "number") {
|
|
818
|
+
const compId = getComponentIdByName(componentTypeRaw.component);
|
|
819
|
+
if (compId === undefined) {
|
|
820
|
+
throw new Error(`Unknown component name in snapshot: ${componentTypeRaw.component}`);
|
|
821
|
+
}
|
|
822
|
+
componentType = relation(compId, componentTypeRaw.target);
|
|
823
|
+
} else {
|
|
824
|
+
throw new Error(`Invalid component type in snapshot: ${JSON.stringify(componentTypeRaw)}`);
|
|
825
|
+
}
|
|
826
|
+
componentMap.set(componentType, componentEntry.value);
|
|
827
|
+
componentTypes.push(componentType);
|
|
749
828
|
}
|
|
750
829
|
const archetype = this.ensureArchetype(componentTypes);
|
|
751
830
|
archetype.addEntity(entityId, componentMap);
|
|
@@ -895,7 +974,7 @@ class World {
|
|
|
895
974
|
cached.refCount++;
|
|
896
975
|
return cached.query;
|
|
897
976
|
}
|
|
898
|
-
const query = new Query(this, sortedTypes, filter
|
|
977
|
+
const query = new Query(this, sortedTypes, filter);
|
|
899
978
|
this.queryCache.set(key, { query, refCount: 1 });
|
|
900
979
|
return query;
|
|
901
980
|
}
|
|
@@ -909,27 +988,17 @@ class World {
|
|
|
909
988
|
}
|
|
910
989
|
}
|
|
911
990
|
releaseQuery(query) {
|
|
912
|
-
const key = query.key;
|
|
913
991
|
for (const [k, v] of this.queryCache.entries()) {
|
|
914
992
|
if (v.query === query) {
|
|
915
993
|
v.refCount--;
|
|
916
994
|
if (v.refCount <= 0) {
|
|
917
995
|
this.queryCache.delete(k);
|
|
996
|
+
this._unregisterQuery(query);
|
|
918
997
|
v.query._disposeInternal();
|
|
919
998
|
}
|
|
920
999
|
return;
|
|
921
1000
|
}
|
|
922
1001
|
}
|
|
923
|
-
const entry = this.queryCache.get(key);
|
|
924
|
-
if (!entry) {
|
|
925
|
-
this._unregisterQuery(query);
|
|
926
|
-
return;
|
|
927
|
-
}
|
|
928
|
-
entry.refCount--;
|
|
929
|
-
if (entry.refCount <= 0) {
|
|
930
|
-
this.queryCache.delete(key);
|
|
931
|
-
entry.query._disposeInternal();
|
|
932
|
-
}
|
|
933
1002
|
}
|
|
934
1003
|
getMatchingArchetypes(componentTypes) {
|
|
935
1004
|
if (componentTypes.length === 0) {
|
|
@@ -1010,11 +1079,6 @@ class World {
|
|
|
1010
1079
|
if (!currentArchetype) {
|
|
1011
1080
|
return changeset;
|
|
1012
1081
|
}
|
|
1013
|
-
const currentComponents = new Map;
|
|
1014
|
-
for (const componentType of currentArchetype.componentTypes) {
|
|
1015
|
-
const componentData = currentArchetype.get(entityId, componentType);
|
|
1016
|
-
currentComponents.set(componentType, componentData);
|
|
1017
|
-
}
|
|
1018
1082
|
for (const command of commands) {
|
|
1019
1083
|
switch (command.type) {
|
|
1020
1084
|
case "set":
|
|
@@ -1051,13 +1115,11 @@ class World {
|
|
|
1051
1115
|
break;
|
|
1052
1116
|
}
|
|
1053
1117
|
}
|
|
1054
|
-
const
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
currentArchetype.removeEntity(entityId);
|
|
1060
|
-
newArchetype.addEntity(entityId, finalComponents);
|
|
1118
|
+
const finalComponentTypes = changeset.getFinalComponentTypes(currentArchetype.componentTypes);
|
|
1119
|
+
if (finalComponentTypes) {
|
|
1120
|
+
const newArchetype = this.ensureArchetype(finalComponentTypes);
|
|
1121
|
+
const currentComponents = currentArchetype.removeEntity(entityId);
|
|
1122
|
+
newArchetype.addEntity(entityId, changeset.applyTo(currentComponents));
|
|
1061
1123
|
this.entityToArchetype.set(entityId, newArchetype);
|
|
1062
1124
|
} else {
|
|
1063
1125
|
for (const [componentType, component2] of changeset.adds) {
|
|
@@ -1196,13 +1258,31 @@ class World {
|
|
|
1196
1258
|
}
|
|
1197
1259
|
serialize() {
|
|
1198
1260
|
const entities = [];
|
|
1199
|
-
for (const
|
|
1200
|
-
const
|
|
1201
|
-
for (const
|
|
1202
|
-
|
|
1203
|
-
|
|
1261
|
+
for (const archetype of this.archetypes) {
|
|
1262
|
+
const dumpedEntities = archetype.dump();
|
|
1263
|
+
for (const { entity, components } of dumpedEntities) {
|
|
1264
|
+
entities.push({
|
|
1265
|
+
id: entity,
|
|
1266
|
+
components: Array.from(components.entries()).map(([rawType, value]) => {
|
|
1267
|
+
const detailedType = getDetailedIdType(rawType);
|
|
1268
|
+
let type = rawType;
|
|
1269
|
+
let componentName;
|
|
1270
|
+
switch (detailedType.type) {
|
|
1271
|
+
case "component":
|
|
1272
|
+
type = getComponentNameById(rawType) || rawType;
|
|
1273
|
+
break;
|
|
1274
|
+
case "entity-relation":
|
|
1275
|
+
case "component-relation":
|
|
1276
|
+
componentName = getComponentNameById(detailedType.componentId);
|
|
1277
|
+
if (componentName) {
|
|
1278
|
+
type = { component: componentName, target: detailedType.targetId };
|
|
1279
|
+
}
|
|
1280
|
+
break;
|
|
1281
|
+
}
|
|
1282
|
+
return { type, value: value === MISSING_COMPONENT ? undefined : value };
|
|
1283
|
+
})
|
|
1284
|
+
});
|
|
1204
1285
|
}
|
|
1205
|
-
entities.push({ id: entityId, components: compEntries });
|
|
1206
1286
|
}
|
|
1207
1287
|
return {
|
|
1208
1288
|
version: 1,
|
|
@@ -1222,6 +1302,8 @@ export {
|
|
|
1222
1302
|
inspectEntityId,
|
|
1223
1303
|
getIdType,
|
|
1224
1304
|
getDetailedIdType,
|
|
1305
|
+
getComponentNameById,
|
|
1306
|
+
getComponentIdByName,
|
|
1225
1307
|
decodeRelationId,
|
|
1226
1308
|
createEntityId,
|
|
1227
1309
|
createComponentId,
|
|
@@ -1231,6 +1313,7 @@ export {
|
|
|
1231
1313
|
SystemScheduler,
|
|
1232
1314
|
RELATION_SHIFT,
|
|
1233
1315
|
Query,
|
|
1316
|
+
MISSING_COMPONENT,
|
|
1234
1317
|
INVALID_COMPONENT_ID,
|
|
1235
1318
|
EntityIdManager,
|
|
1236
1319
|
ENTITY_ID_START,
|
package/package.json
CHANGED
package/query.d.ts
CHANGED
|
@@ -7,13 +7,12 @@ import type { World } from "./world";
|
|
|
7
7
|
* Query class for efficient entity queries with cached archetypes
|
|
8
8
|
*/
|
|
9
9
|
export declare class Query {
|
|
10
|
-
readonly key: string;
|
|
11
10
|
private world;
|
|
12
11
|
private componentTypes;
|
|
13
12
|
private filter;
|
|
14
13
|
private cachedArchetypes;
|
|
15
14
|
private isDisposed;
|
|
16
|
-
constructor(world: World<any[]>, componentTypes: EntityId<any>[], filter?: QueryFilter
|
|
15
|
+
constructor(world: World<any[]>, componentTypes: EntityId<any>[], filter?: QueryFilter);
|
|
17
16
|
/**
|
|
18
17
|
* Get all entities matching the query
|
|
19
18
|
*/
|
package/world.d.ts
CHANGED
|
@@ -40,7 +40,7 @@ export declare class World<UpdateParams extends any[] = []> {
|
|
|
40
40
|
* If an optional snapshot object is provided (previously produced by `world.serialize()`),
|
|
41
41
|
* the world will be restored from that snapshot. The snapshot may contain non-JSON values.
|
|
42
42
|
*/
|
|
43
|
-
constructor(snapshot?:
|
|
43
|
+
constructor(snapshot?: SerializedWorld);
|
|
44
44
|
/**
|
|
45
45
|
* Generate a signature string for component types array
|
|
46
46
|
* @returns A string signature for the component types
|
|
@@ -192,5 +192,22 @@ export declare class World<UpdateParams extends any[] = []> {
|
|
|
192
192
|
* This returns an in-memory structure and does not perform JSON stringification.
|
|
193
193
|
* Component values are stored as-is (they may be non-JSON-serializable).
|
|
194
194
|
*/
|
|
195
|
-
serialize():
|
|
195
|
+
serialize(): SerializedWorld;
|
|
196
196
|
}
|
|
197
|
+
export type SerializedWorld = {
|
|
198
|
+
version: number;
|
|
199
|
+
entityManager: any;
|
|
200
|
+
exclusiveComponents: EntityId[];
|
|
201
|
+
entities: SerializedEntity[];
|
|
202
|
+
};
|
|
203
|
+
export type SerializedEntity = {
|
|
204
|
+
id: number;
|
|
205
|
+
components: SerializedComponent[];
|
|
206
|
+
};
|
|
207
|
+
export type SerializedComponent = {
|
|
208
|
+
type: number | string | {
|
|
209
|
+
component: string;
|
|
210
|
+
target: number;
|
|
211
|
+
};
|
|
212
|
+
value: any;
|
|
213
|
+
};
|