@codehz/ecs 0.0.4 → 0.0.6

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/README.md CHANGED
@@ -10,6 +10,7 @@
10
10
  - 📦 轻量级:零依赖,易于集成
11
11
  - ⚡ 内存高效:连续内存布局,优化的迭代性能
12
12
  - 🎣 生命周期钩子:支持组件和通配符关系的事件监听
13
+ - 🔄 系统调度:支持系统依赖关系和拓扑排序执行
13
14
 
14
15
  ## 安装
15
16
 
@@ -119,6 +120,39 @@ world.addComponent(entity, positionRelation, { x: 10, y: 20 });
119
120
  world.flushCommands(); // 通配符钩子会被触发
120
121
  ```
121
122
 
123
+ ### Exclusive Relations
124
+
125
+ ECS 支持 Exclusive Relations,确保实体对于指定的组件类型最多只能有一个关系。当添加新的关系时,会自动移除之前的所有同类型关系:
126
+
127
+ ```typescript
128
+ import { World, component, relation } from "@codehz/ecs";
129
+
130
+ // 定义组件ID
131
+ const ChildOf = component(); // 空组件,用于关系
132
+
133
+ // 创建世界
134
+ const world = new World();
135
+
136
+ // 设置 ChildOf 为独占关系
137
+ world.setExclusive(ChildOf);
138
+
139
+ // 创建实体
140
+ const child = world.createEntity();
141
+ const parent1 = world.createEntity();
142
+ const parent2 = world.createEntity();
143
+
144
+ // 添加第一个关系
145
+ world.addComponent(child, relation(ChildOf, parent1));
146
+ world.flushCommands();
147
+ console.log(world.hasComponent(child, relation(ChildOf, parent1))); // true
148
+
149
+ // 添加第二个关系 - 会自动移除第一个
150
+ world.addComponent(child, relation(ChildOf, parent2));
151
+ world.flushCommands();
152
+ console.log(world.hasComponent(child, relation(ChildOf, parent1))); // false
153
+ console.log(world.hasComponent(child, relation(ChildOf, parent2))); // true
154
+ ```
155
+
122
156
  ### 运行示例
123
157
 
124
158
  ```bash
@@ -138,6 +172,7 @@ bun run examples/simple/demo.ts
138
172
  - `createEntity()`: 创建新实体
139
173
  - `addComponent(entity, componentId, data)`: 向实体添加组件
140
174
  - `removeComponent(entity, componentId)`: 从实体移除组件
175
+ - `setExclusive(componentId)`: 将组件标记为独占关系
141
176
  - `createQuery(componentIds)`: 创建查询
142
177
  - `registerSystem(system)`: 注册系统
143
178
  - `registerLifecycleHook(componentId, hook)`: 注册组件或通配符关系生命周期钩子
@@ -167,6 +202,17 @@ class MySystem implements System {
167
202
  }
168
203
  ```
169
204
 
205
+ 系统支持依赖关系排序,确保正确的执行顺序:
206
+
207
+ ```typescript
208
+ // 注册系统时指定依赖
209
+ world.registerSystem(inputSystem);
210
+ world.registerSystem(movementSystem, [inputSystem]); // movementSystem 依赖 inputSystem
211
+ world.registerSystem(renderSystem, [movementSystem]); // renderSystem 依赖 movementSystem
212
+ ```
213
+
214
+ 系统将按照拓扑排序执行,依赖系统始终在被依赖系统之前运行。
215
+
170
216
  ## 性能特点
171
217
 
172
218
  - **Archetype 系统**:实体按组件组合分组,实现连续内存访问
@@ -199,6 +245,7 @@ src/
199
245
  ├── query.ts # 查询系统
200
246
  ├── query-filter.ts # 查询过滤器
201
247
  ├── system.ts # 系统接口
248
+ ├── system-scheduler.ts # 系统调度器
202
249
  ├── command-buffer.ts # 命令缓冲区
203
250
  ├── types.ts # 类型定义
204
251
  ├── utils.ts # 工具函数
package/archetype.d.ts CHANGED
@@ -66,13 +66,13 @@ export declare class Archetype {
66
66
  * @param entityId The entity
67
67
  * @param componentType The wildcard relation type
68
68
  */
69
- getComponent<T>(entityId: EntityId, componentType: WildcardRelationId<T>): [EntityId<unknown>, any][] | undefined;
69
+ getComponent<T>(entityId: EntityId, componentType: WildcardRelationId<T>): [EntityId<unknown>, any][];
70
70
  /**
71
71
  * Get component data for a specific entity and component type
72
72
  * @param entityId The entity
73
73
  * @param componentType The component type
74
74
  */
75
- getComponent<T>(entityId: EntityId, componentType: EntityId<T>): T | undefined;
75
+ getComponent<T>(entityId: EntityId, componentType: EntityId<T>): T;
76
76
  /**
77
77
  * Set component data for a specific entity and component type
78
78
  * @param entityId The entity
package/changeset.d.ts ADDED
@@ -0,0 +1,36 @@
1
+ import type { EntityId } from "./entity";
2
+ /**
3
+ * @internal Represents a set of component changes to be applied to an entity
4
+ */
5
+ export declare class ComponentChangeset {
6
+ readonly adds: Map<EntityId<any>, any>;
7
+ readonly removes: Set<EntityId<any>>;
8
+ /**
9
+ * Add a component to the changeset
10
+ */
11
+ addComponent<T>(componentType: EntityId<T>, component: T): void;
12
+ /**
13
+ * Remove a component from the changeset
14
+ */
15
+ removeComponent<T>(componentType: EntityId<T>): void;
16
+ /**
17
+ * Check if the changeset has any changes
18
+ */
19
+ hasChanges(): boolean;
20
+ /**
21
+ * Clear all changes
22
+ */
23
+ clear(): void;
24
+ /**
25
+ * Merge another changeset into this one
26
+ */
27
+ merge(other: ComponentChangeset): void;
28
+ /**
29
+ * Apply the changeset to existing components and return the final state
30
+ */
31
+ applyTo(existingComponents: Map<EntityId<any>, any>): Map<EntityId<any>, any>;
32
+ /**
33
+ * Get the final component types after applying changes
34
+ */
35
+ getFinalComponentTypes(existingComponents: Map<EntityId<any>, any>): EntityId<any>[];
36
+ }
@@ -21,7 +21,8 @@ export declare class CommandBuffer {
21
21
  /**
22
22
  * Add a component to an entity (deferred)
23
23
  */
24
- addComponent<T>(entityId: EntityId, componentType: EntityId<T>, component: T): void;
24
+ addComponent(entityId: EntityId, componentType: EntityId<void>): void;
25
+ addComponent<T>(entityId: EntityId, componentType: EntityId<T>, component: NoInfer<T>): void;
25
26
  /**
26
27
  * Remove a component from an entity (deferred)
27
28
  */
package/entity.d.ts CHANGED
@@ -153,5 +153,5 @@ export declare class ComponentIdAllocator {
153
153
  /**
154
154
  * Allocate a new component ID from the global allocator
155
155
  */
156
- export declare function component<T>(): ComponentId<T>;
156
+ export declare function component<T = void>(): ComponentId<T>;
157
157
  export {};
package/index.d.ts CHANGED
@@ -3,4 +3,5 @@ export * from "./world";
3
3
  export * from "./archetype";
4
4
  export * from "./query";
5
5
  export * from "./system";
6
+ export * from "./system-scheduler";
6
7
  export * from "./types";
package/index.js CHANGED
@@ -226,6 +226,8 @@ function getOrCreateWithSideEffect(cache, key, create) {
226
226
  }
227
227
 
228
228
  // src/archetype.ts
229
+ var MISSING_COMPONENT = Symbol("missing component");
230
+
229
231
  class Archetype {
230
232
  componentTypes;
231
233
  entities = [];
@@ -257,10 +259,7 @@ class Archetype {
257
259
  this.entityToIndex.set(entityId, index);
258
260
  for (const componentType of this.componentTypes) {
259
261
  const data = componentData.get(componentType);
260
- if (data === undefined) {
261
- throw new Error(`Missing component data for type ${componentType}`);
262
- }
263
- this.componentData.get(componentType).push(data);
262
+ this.componentData.get(componentType).push(data === undefined ? MISSING_COMPONENT : data);
264
263
  }
265
264
  }
266
265
  removeEntity(entityId) {
@@ -287,11 +286,7 @@ class Archetype {
287
286
  getComponent(entityId, componentType) {
288
287
  const index = this.entityToIndex.get(entityId);
289
288
  if (index === undefined) {
290
- if (getIdType(componentType) === "wildcard-relation") {
291
- return [];
292
- } else {
293
- return;
294
- }
289
+ throw new Error(`Entity ${entityId} is not in this archetype`);
295
290
  }
296
291
  if (isWildcardRelationId(componentType)) {
297
292
  const decoded = decodeRelationId(componentType);
@@ -302,24 +297,26 @@ class Archetype {
302
297
  if ((relDetailed.type === "entity-relation" || relDetailed.type === "component-relation") && relDetailed.componentId === componentId) {
303
298
  const dataArray = this.componentData.get(relType);
304
299
  if (dataArray && dataArray[index] !== undefined) {
305
- relations.push([relDetailed.targetId, dataArray[index]]);
300
+ const data = dataArray[index];
301
+ relations.push([relDetailed.targetId, data === MISSING_COMPONENT ? undefined : data]);
306
302
  }
307
303
  }
308
304
  }
309
305
  return relations;
310
306
  } else {
311
- return this.componentData.get(componentType)?.[index];
307
+ const data = this.componentData.get(componentType)?.[index];
308
+ return data === MISSING_COMPONENT ? undefined : data;
312
309
  }
313
310
  }
314
311
  setComponent(entityId, componentType, data) {
312
+ if (!this.componentData.has(componentType)) {
313
+ throw new Error(`Component type ${componentType} is not in this archetype`);
314
+ }
315
315
  const index = this.entityToIndex.get(entityId);
316
316
  if (index === undefined) {
317
317
  throw new Error(`Entity ${entityId} is not in this archetype`);
318
318
  }
319
319
  const dataArray = this.componentData.get(componentType);
320
- if (!dataArray) {
321
- throw new Error(`Component type ${componentType} is not in this archetype`);
322
- }
323
320
  dataArray[index] = data;
324
321
  }
325
322
  getEntities() {
@@ -364,14 +361,16 @@ class Archetype {
364
361
  for (const relType of matchingRelations) {
365
362
  const dataArray = this.componentData.get(relType);
366
363
  if (dataArray && dataArray[entityIndex] !== undefined) {
364
+ const data = dataArray[entityIndex];
367
365
  const decodedRel = decodeRelationId(relType);
368
- relations.push([decodedRel.targetId, dataArray[entityIndex]]);
366
+ relations.push([decodedRel.targetId, data === MISSING_COMPONENT ? undefined : data]);
369
367
  }
370
368
  }
371
369
  return relations;
372
370
  } else {
373
371
  const dataArray = dataSource;
374
- return dataArray ? dataArray[entityIndex] : undefined;
372
+ const data = dataArray ? dataArray[entityIndex] : undefined;
373
+ return data === MISSING_COMPONENT ? undefined : data;
375
374
  }
376
375
  });
377
376
  callback(entity, ...components);
@@ -381,13 +380,59 @@ class Archetype {
381
380
  for (let i = 0;i < this.entities.length; i++) {
382
381
  const components = new Map;
383
382
  for (const componentType of this.componentTypes) {
384
- components.set(componentType, this.componentData.get(componentType)[i]);
383
+ const data = this.componentData.get(componentType)[i];
384
+ components.set(componentType, data === MISSING_COMPONENT ? undefined : data);
385
385
  }
386
386
  callback(this.entities[i], components);
387
387
  }
388
388
  }
389
389
  }
390
390
 
391
+ // src/changeset.ts
392
+ class ComponentChangeset {
393
+ adds = new Map;
394
+ removes = new Set;
395
+ addComponent(componentType, component2) {
396
+ this.adds.set(componentType, component2);
397
+ this.removes.delete(componentType);
398
+ }
399
+ removeComponent(componentType) {
400
+ this.removes.add(componentType);
401
+ this.adds.delete(componentType);
402
+ }
403
+ hasChanges() {
404
+ return this.adds.size > 0 || this.removes.size > 0;
405
+ }
406
+ clear() {
407
+ this.adds.clear();
408
+ this.removes.clear();
409
+ }
410
+ merge(other) {
411
+ for (const [componentType, component2] of other.adds) {
412
+ this.adds.set(componentType, component2);
413
+ this.removes.delete(componentType);
414
+ }
415
+ for (const componentType of other.removes) {
416
+ this.removes.add(componentType);
417
+ this.adds.delete(componentType);
418
+ }
419
+ }
420
+ applyTo(existingComponents) {
421
+ const finalComponents = new Map(existingComponents);
422
+ for (const componentType of this.removes) {
423
+ finalComponents.delete(componentType);
424
+ }
425
+ for (const [componentType, component2] of this.adds) {
426
+ finalComponents.set(componentType, component2);
427
+ }
428
+ return finalComponents;
429
+ }
430
+ getFinalComponentTypes(existingComponents) {
431
+ const finalComponents = this.applyTo(existingComponents);
432
+ return Array.from(finalComponents.keys()).sort((a, b) => a - b);
433
+ }
434
+ }
435
+
391
436
  // src/command-buffer.ts
392
437
  class CommandBuffer {
393
438
  commands = [];
@@ -435,6 +480,12 @@ class CommandBuffer {
435
480
  }
436
481
 
437
482
  // src/query-filter.ts
483
+ function serializeQueryFilter(filter = {}) {
484
+ const negative = (filter.negativeComponentTypes || []).slice().sort((a, b) => a - b);
485
+ if (negative.length === 0)
486
+ return "";
487
+ return `neg:${negative.join(",")}`;
488
+ }
438
489
  function matchesComponentTypes(archetype, componentTypes) {
439
490
  return componentTypes.every((type) => {
440
491
  const detailedType = getDetailedIdType(type);
@@ -464,17 +515,19 @@ function matchesFilter(archetype, filter) {
464
515
 
465
516
  // src/query.ts
466
517
  class Query {
518
+ key;
467
519
  world;
468
520
  componentTypes;
469
521
  filter;
470
522
  cachedArchetypes = [];
471
523
  isDisposed = false;
472
- constructor(world, componentTypes, filter = {}) {
524
+ constructor(world, componentTypes, filter = {}, key) {
473
525
  this.world = world;
474
526
  this.componentTypes = [...componentTypes].sort((a, b) => a - b);
475
527
  this.filter = filter;
528
+ this.key = key ?? `${this.componentTypes.join(",")}|`;
476
529
  this.updateCache();
477
- world.registerQuery(this);
530
+ world._registerQuery(this);
478
531
  }
479
532
  getEntities() {
480
533
  if (this.isDisposed) {
@@ -528,8 +581,11 @@ class Query {
528
581
  }
529
582
  }
530
583
  dispose() {
584
+ this.world.releaseQuery(this);
585
+ }
586
+ _disposeInternal() {
531
587
  if (!this.isDisposed) {
532
- this.world.unregisterQuery(this);
588
+ this.world._unregisterQuery(this);
533
589
  this.cachedArchetypes = [];
534
590
  this.isDisposed = true;
535
591
  }
@@ -542,18 +598,66 @@ class Query {
542
598
  }
543
599
  }
544
600
 
601
+ // src/system-scheduler.ts
602
+ class SystemScheduler {
603
+ systems = new Set;
604
+ cachedExecutionOrder = null;
605
+ addSystem(system) {
606
+ this.systems.add(system);
607
+ for (const dep of system.dependencies || []) {
608
+ this.systems.add(dep);
609
+ }
610
+ this.cachedExecutionOrder = null;
611
+ }
612
+ getExecutionOrder() {
613
+ if (this.cachedExecutionOrder !== null) {
614
+ return this.cachedExecutionOrder;
615
+ }
616
+ const result = [];
617
+ const visited = new Set;
618
+ const visiting = new Set;
619
+ const visit = (system) => {
620
+ if (visited.has(system))
621
+ return;
622
+ if (visiting.has(system)) {
623
+ throw new Error("Circular dependency detected in system scheduling");
624
+ }
625
+ visiting.add(system);
626
+ for (const dep of system.dependencies || []) {
627
+ visit(dep);
628
+ }
629
+ visiting.delete(system);
630
+ visited.add(system);
631
+ result.push(system);
632
+ };
633
+ for (const system of this.systems) {
634
+ if (!visited.has(system)) {
635
+ visit(system);
636
+ }
637
+ }
638
+ this.cachedExecutionOrder = result;
639
+ return result;
640
+ }
641
+ clear() {
642
+ this.systems.clear();
643
+ this.cachedExecutionOrder = null;
644
+ }
645
+ }
646
+
545
647
  // src/world.ts
546
648
  class World {
547
649
  entityIdManager = new EntityIdManager;
548
650
  archetypes = [];
549
651
  archetypeMap = new Map;
550
652
  entityToArchetype = new Map;
551
- systems = [];
653
+ systemScheduler = new SystemScheduler;
552
654
  queries = [];
655
+ queryCache = new Map;
553
656
  commandBuffer;
554
657
  componentToArchetypes = new Map;
555
658
  lifecycleHooks = new Map;
556
659
  entityReverseIndex = new Map;
660
+ exclusiveComponents = new Set;
557
661
  constructor() {
558
662
  this.commandBuffer = new CommandBuffer((entityId, commands) => this.executeEntityCommands(entityId, commands));
559
663
  }
@@ -641,22 +745,12 @@ class World {
641
745
  getComponent(entityId, componentType) {
642
746
  const archetype = this.entityToArchetype.get(entityId);
643
747
  if (!archetype) {
644
- if (getIdType(componentType) === "wildcard-relation") {
645
- return [];
646
- } else {
647
- return;
648
- }
748
+ throw new Error(`Entity ${entityId} does not exist`);
649
749
  }
650
750
  return archetype.getComponent(entityId, componentType);
651
751
  }
652
752
  registerSystem(system) {
653
- this.systems.push(system);
654
- }
655
- unregisterSystem(system) {
656
- const index = this.systems.indexOf(system);
657
- if (index !== -1) {
658
- this.systems.splice(index, 1);
659
- }
753
+ this.systemScheduler.addSystem(system);
660
754
  }
661
755
  registerLifecycleHook(componentType, hook) {
662
756
  if (!this.lifecycleHooks.has(componentType)) {
@@ -673,8 +767,12 @@ class World {
673
767
  }
674
768
  }
675
769
  }
770
+ setExclusive(componentId) {
771
+ this.exclusiveComponents.add(componentId);
772
+ }
676
773
  update(...params) {
677
- for (const system of this.systems) {
774
+ const systems = this.systemScheduler.getExecutionOrder();
775
+ for (const system of systems) {
678
776
  system.update(this, ...params);
679
777
  }
680
778
  this.commandBuffer.execute();
@@ -683,17 +781,50 @@ class World {
683
781
  this.commandBuffer.execute();
684
782
  }
685
783
  createQuery(componentTypes, filter = {}) {
686
- return new Query(this, componentTypes, filter);
687
- }
688
- registerQuery(query) {
784
+ const sortedTypes = [...componentTypes].sort((a, b) => a - b);
785
+ const filterKey = serializeQueryFilter(filter);
786
+ const key = `${this.getComponentTypesHash(sortedTypes)}${filterKey ? `|${filterKey}` : ""}`;
787
+ const cached = this.queryCache.get(key);
788
+ if (cached) {
789
+ cached.refCount++;
790
+ return cached.query;
791
+ }
792
+ const query = new Query(this, sortedTypes, filter, key);
793
+ this.queryCache.set(key, { query, refCount: 1 });
794
+ return query;
795
+ }
796
+ _registerQuery(query) {
689
797
  this.queries.push(query);
690
798
  }
691
- unregisterQuery(query) {
799
+ _unregisterQuery(query) {
692
800
  const index = this.queries.indexOf(query);
693
801
  if (index !== -1) {
694
802
  this.queries.splice(index, 1);
695
803
  }
696
804
  }
805
+ releaseQuery(query) {
806
+ const key = query.key;
807
+ for (const [k, v] of this.queryCache.entries()) {
808
+ if (v.query === query) {
809
+ v.refCount--;
810
+ if (v.refCount <= 0) {
811
+ this.queryCache.delete(k);
812
+ v.query._disposeInternal();
813
+ }
814
+ return;
815
+ }
816
+ }
817
+ const entry = this.queryCache.get(key);
818
+ if (!entry) {
819
+ this._unregisterQuery(query);
820
+ return;
821
+ }
822
+ entry.refCount--;
823
+ if (entry.refCount <= 0) {
824
+ this.queryCache.delete(key);
825
+ entry.query._disposeInternal();
826
+ }
827
+ }
697
828
  getMatchingArchetypes(componentTypes) {
698
829
  if (componentTypes.length === 0) {
699
830
  return [...this.archetypes];
@@ -763,30 +894,35 @@ class World {
763
894
  }
764
895
  }
765
896
  executeEntityCommands(entityId, commands) {
897
+ const changeset = new ComponentChangeset;
766
898
  const hasDestroy = commands.some((cmd) => cmd.type === "destroyEntity");
767
899
  if (hasDestroy) {
768
900
  this._destroyEntity(entityId);
769
- return;
901
+ return changeset;
770
902
  }
771
903
  const currentArchetype = this.entityToArchetype.get(entityId);
772
904
  if (!currentArchetype) {
773
- return;
905
+ return changeset;
774
906
  }
775
907
  const currentComponents = new Map;
776
908
  for (const componentType of currentArchetype.componentTypes) {
777
909
  const data = currentArchetype.getComponent(entityId, componentType);
778
- if (data !== undefined) {
779
- currentComponents.set(componentType, data);
780
- }
910
+ currentComponents.set(componentType, data);
781
911
  }
782
- const adds = new Map;
783
- const removes = new Set;
784
912
  for (const cmd of commands) {
785
913
  switch (cmd.type) {
786
914
  case "addComponent":
787
- if (cmd.componentType && cmd.component !== undefined) {
788
- adds.set(cmd.componentType, cmd.component);
789
- removes.delete(cmd.componentType);
915
+ if (cmd.componentType) {
916
+ const detailedType = getDetailedIdType(cmd.componentType);
917
+ if ((detailedType.type === "entity-relation" || detailedType.type === "component-relation") && this.exclusiveComponents.has(detailedType.componentId)) {
918
+ for (const componentType of currentArchetype.componentTypes) {
919
+ const componentDetailedType = getDetailedIdType(componentType);
920
+ if ((componentDetailedType.type === "entity-relation" || componentDetailedType.type === "component-relation") && componentDetailedType.componentId === detailedType.componentId) {
921
+ changeset.removeComponent(componentType);
922
+ }
923
+ }
924
+ }
925
+ changeset.addComponent(cmd.componentType, cmd.component);
790
926
  }
791
927
  break;
792
928
  case "removeComponent":
@@ -798,27 +934,19 @@ class World {
798
934
  const componentDetailedType = getDetailedIdType(componentType);
799
935
  if (componentDetailedType.type === "entity-relation" || componentDetailedType.type === "component-relation") {
800
936
  if (componentDetailedType.componentId === baseComponentId) {
801
- removes.add(componentType);
802
- adds.delete(componentType);
937
+ changeset.removeComponent(componentType);
803
938
  }
804
939
  }
805
940
  }
806
941
  } else {
807
- removes.add(cmd.componentType);
808
- adds.delete(cmd.componentType);
942
+ changeset.removeComponent(cmd.componentType);
809
943
  }
810
944
  }
811
945
  break;
812
946
  }
813
947
  }
814
- const finalComponents = new Map(currentComponents);
815
- for (const componentType of removes) {
816
- finalComponents.delete(componentType);
817
- }
818
- for (const [componentType, component2] of adds) {
819
- finalComponents.set(componentType, component2);
820
- }
821
- const finalComponentTypes = Array.from(finalComponents.keys()).sort((a, b) => a - b);
948
+ const finalComponents = changeset.applyTo(currentComponents);
949
+ const finalComponentTypes = changeset.getFinalComponentTypes(currentComponents);
822
950
  const currentComponentTypes = currentArchetype.componentTypes.sort((a, b) => a - b);
823
951
  const needsArchetypeChange = finalComponentTypes.length !== currentComponentTypes.length || !finalComponentTypes.every((type, index) => type === currentComponentTypes[index]);
824
952
  if (needsArchetypeChange) {
@@ -827,11 +955,11 @@ class World {
827
955
  newArchetype.addEntity(entityId, finalComponents);
828
956
  this.entityToArchetype.set(entityId, newArchetype);
829
957
  } else {
830
- for (const [componentType, component2] of adds) {
958
+ for (const [componentType, component2] of changeset.adds) {
831
959
  currentArchetype.setComponent(entityId, componentType, component2);
832
960
  }
833
961
  }
834
- for (const componentType of removes) {
962
+ for (const componentType of changeset.removes) {
835
963
  const detailedType = getDetailedIdType(componentType);
836
964
  if (detailedType.type === "entity-relation") {
837
965
  const targetEntityId = detailedType.targetId;
@@ -840,7 +968,7 @@ class World {
840
968
  this.removeComponentReference(entityId, componentType, componentType);
841
969
  }
842
970
  }
843
- for (const [componentType, component2] of adds) {
971
+ for (const [componentType, component2] of changeset.adds) {
844
972
  const detailedType = getDetailedIdType(componentType);
845
973
  if (detailedType.type === "entity-relation") {
846
974
  const targetEntityId = detailedType.targetId;
@@ -849,7 +977,8 @@ class World {
849
977
  this.addComponentReference(entityId, componentType, componentType);
850
978
  }
851
979
  }
852
- this.executeComponentLifecycleHooks(entityId, adds, removes);
980
+ this.executeComponentLifecycleHooks(entityId, changeset.adds, changeset.removes);
981
+ return changeset;
853
982
  }
854
983
  getOrCreateArchetype(componentTypes) {
855
984
  const sortedTypes = [...componentTypes].sort((a, b) => a - b);
@@ -976,6 +1105,7 @@ export {
976
1105
  component,
977
1106
  World,
978
1107
  WILDCARD_TARGET_ID,
1108
+ SystemScheduler,
979
1109
  RELATION_SHIFT,
980
1110
  Query,
981
1111
  INVALID_COMPONENT_ID,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@codehz/ecs",
3
- "version": "0.0.4",
3
+ "version": "0.0.6",
4
4
  "type": "module",
5
5
  "main": "./index.js",
6
6
  "types": "./index.d.ts",
package/query-filter.d.ts CHANGED
@@ -6,6 +6,11 @@ import type { EntityId } from "./entity";
6
6
  export interface QueryFilter {
7
7
  negativeComponentTypes?: EntityId<any>[];
8
8
  }
9
+ /**
10
+ * Serialize a QueryFilter into a deterministic string suitable for cache keys.
11
+ * Currently only serializes `negativeComponentTypes`.
12
+ */
13
+ export declare function serializeQueryFilter(filter?: QueryFilter): string;
9
14
  /**
10
15
  * Check if an archetype matches the given component types
11
16
  */
package/query.d.ts CHANGED
@@ -1,18 +1,19 @@
1
1
  import { Archetype } from "./archetype";
2
2
  import type { EntityId } from "./entity";
3
- import type { ComponentTuple } from "./types";
4
3
  import { type QueryFilter } from "./query-filter";
4
+ import type { ComponentTuple } from "./types";
5
5
  import type { World } from "./world";
6
6
  /**
7
7
  * Query class for efficient entity queries with cached archetypes
8
8
  */
9
9
  export declare class Query {
10
+ readonly key: string;
10
11
  private world;
11
12
  private componentTypes;
12
13
  private filter;
13
14
  private cachedArchetypes;
14
15
  private isDisposed;
15
- constructor(world: World<any[]>, componentTypes: EntityId<any>[], filter?: QueryFilter);
16
+ constructor(world: World<any[]>, componentTypes: EntityId<any>[], filter?: QueryFilter, key?: string);
16
17
  /**
17
18
  * Get all entities matching the query
18
19
  */
@@ -50,7 +51,16 @@ export declare class Query {
50
51
  /**
51
52
  * Dispose the query and disconnect from world
52
53
  */
54
+ /**
55
+ * Request disposal of this query.
56
+ * This will decrement the world's reference count for the query.
57
+ * The query will only be fully disposed when the ref count reaches zero.
58
+ */
53
59
  dispose(): void;
60
+ /**
61
+ * Internal full dispose called by World when refCount reaches zero.
62
+ */
63
+ _disposeInternal(): void;
54
64
  /**
55
65
  * Symbol.dispose implementation for automatic resource management
56
66
  */
@@ -0,0 +1,22 @@
1
+ import type { System } from "./system";
2
+ /**
3
+ * System Scheduler for managing system dependencies and execution order
4
+ */
5
+ export declare class SystemScheduler<ExtraParams extends any[] = [deltaTime: number]> {
6
+ private systems;
7
+ private cachedExecutionOrder;
8
+ /**
9
+ * Add a system with optional dependencies
10
+ * @param system The system to add
11
+ */
12
+ addSystem(system: System<ExtraParams>): void;
13
+ /**
14
+ * Get the execution order of systems based on dependencies
15
+ * Uses topological sort
16
+ */
17
+ getExecutionOrder(): System<ExtraParams>[];
18
+ /**
19
+ * Clear all systems and dependencies
20
+ */
21
+ clear(): void;
22
+ }
package/system.d.ts CHANGED
@@ -7,4 +7,8 @@ export interface System<ExtraParams extends any[] = [deltaTime: number]> {
7
7
  * Update the system
8
8
  */
9
9
  update(world: World<ExtraParams>, ...params: ExtraParams): void;
10
+ /**
11
+ * Dependencies of this system (systems that must run before this one)
12
+ */
13
+ readonly dependencies?: readonly System<ExtraParams>[];
10
14
  }
package/world.d.ts CHANGED
@@ -1,8 +1,9 @@
1
1
  import { Archetype } from "./archetype";
2
+ import { ComponentChangeset } from "./changeset";
2
3
  import { type Command } from "./command-buffer";
3
4
  import type { EntityId, WildcardRelationId } from "./entity";
4
5
  import { Query } from "./query";
5
- import type { QueryFilter } from "./query-filter";
6
+ import { type QueryFilter } from "./query-filter";
6
7
  import type { System } from "./system";
7
8
  import type { ComponentTuple, LifecycleHook } from "./types";
8
9
  /**
@@ -14,8 +15,9 @@ export declare class World<ExtraParams extends any[] = [deltaTime: number]> {
14
15
  private archetypes;
15
16
  private archetypeMap;
16
17
  private entityToArchetype;
17
- private systems;
18
+ private systemScheduler;
18
19
  private queries;
20
+ private queryCache;
19
21
  private commandBuffer;
20
22
  private componentToArchetypes;
21
23
  /**
@@ -28,6 +30,11 @@ export declare class World<ExtraParams extends any[] = [deltaTime: number]> {
28
30
  * This includes both relation components and direct usage of entities as component types
29
31
  */
30
32
  private entityReverseIndex;
33
+ /**
34
+ * Set of component IDs that are marked as exclusive relations
35
+ * For exclusive relations, an entity can have at most one relation per base component
36
+ */
37
+ private exclusiveComponents;
31
38
  constructor();
32
39
  /**
33
40
  * Generate a hash key for component types array
@@ -48,7 +55,8 @@ export declare class World<ExtraParams extends any[] = [deltaTime: number]> {
48
55
  /**
49
56
  * Add a component to an entity (deferred)
50
57
  */
51
- addComponent<T>(entityId: EntityId, componentType: EntityId<T>, component: T): void;
58
+ addComponent(entityId: EntityId, componentType: EntityId<void>): void;
59
+ addComponent<T>(entityId: EntityId, componentType: EntityId<T>, component: NoInfer<T>): void;
52
60
  /**
53
61
  * Remove a component from an entity (deferred)
54
62
  */
@@ -64,19 +72,15 @@ export declare class World<ExtraParams extends any[] = [deltaTime: number]> {
64
72
  /**
65
73
  * Get wildcard relations from an entity
66
74
  */
67
- getComponent<T>(entityId: EntityId, componentType: WildcardRelationId<T>): [EntityId<unknown>, any][] | undefined;
75
+ getComponent<T>(entityId: EntityId, componentType: WildcardRelationId<T>): [EntityId<unknown>, any][];
68
76
  /**
69
77
  * Get a component from an entity
70
78
  */
71
- getComponent<T>(entityId: EntityId, componentType: EntityId<T>): T | undefined;
79
+ getComponent<T>(entityId: EntityId, componentType: EntityId<T>): T;
72
80
  /**
73
- * Register a system
81
+ * Register a system with optional dependencies
74
82
  */
75
83
  registerSystem(system: System<ExtraParams>): void;
76
- /**
77
- * Unregister a system
78
- */
79
- unregisterSystem(system: System<ExtraParams>): void;
80
84
  /**
81
85
  * Register a lifecycle hook for component or wildcard relation events
82
86
  */
@@ -86,7 +90,12 @@ export declare class World<ExtraParams extends any[] = [deltaTime: number]> {
86
90
  */
87
91
  unregisterLifecycleHook<T>(componentType: EntityId<T>, hook: LifecycleHook<T>): void;
88
92
  /**
89
- * Update the world (run all systems)
93
+ * Mark a component as exclusive relation
94
+ * For exclusive relations, an entity can have at most one relation per base component
95
+ */
96
+ setExclusive(componentId: EntityId): void;
97
+ /**
98
+ * Update the world (run all systems in dependency order)
90
99
  */
91
100
  update(...params: ExtraParams): void;
92
101
  /**
@@ -100,11 +109,16 @@ export declare class World<ExtraParams extends any[] = [deltaTime: number]> {
100
109
  /**
101
110
  * @internal Register a query for archetype update notifications
102
111
  */
103
- registerQuery(query: Query): void;
112
+ _registerQuery(query: Query): void;
104
113
  /**
105
114
  * @internal Unregister a query
106
115
  */
107
- unregisterQuery(query: Query): void;
116
+ _unregisterQuery(query: Query): void;
117
+ /**
118
+ * Release a query reference obtained from createQuery.
119
+ * Decrements the refCount and fully disposes the query when it reaches zero.
120
+ */
121
+ releaseQuery(query: Query): void;
108
122
  /**
109
123
  * @internal Get archetypes that match specific component types (for internal use by queries)
110
124
  */
@@ -120,7 +134,7 @@ export declare class World<ExtraParams extends any[] = [deltaTime: number]> {
120
134
  /**
121
135
  * @internal Execute commands for a single entity (for internal use by CommandBuffer)
122
136
  */
123
- executeEntityCommands(entityId: EntityId, commands: Command[]): void;
137
+ executeEntityCommands(entityId: EntityId, commands: Command[]): ComponentChangeset;
124
138
  /**
125
139
  * Get or create an archetype for the given component types
126
140
  */