@codehz/ecs 0.0.4 → 0.0.5

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
 
@@ -139,7 +140,7 @@ bun run examples/simple/demo.ts
139
140
  - `addComponent(entity, componentId, data)`: 向实体添加组件
140
141
  - `removeComponent(entity, componentId)`: 从实体移除组件
141
142
  - `createQuery(componentIds)`: 创建查询
142
- - `registerSystem(system)`: 注册系统
143
+ - `registerSystem(system, dependencies?)`: 注册系统,可选指定依赖系统列表
143
144
  - `registerLifecycleHook(componentId, hook)`: 注册组件或通配符关系生命周期钩子
144
145
  - `unregisterLifecycleHook(componentId, hook)`: 注销组件或通配符关系生命周期钩子
145
146
  - `update(deltaTime)`: 更新世界
@@ -167,6 +168,17 @@ class MySystem implements System {
167
168
  }
168
169
  ```
169
170
 
171
+ 系统支持依赖关系排序,确保正确的执行顺序:
172
+
173
+ ```typescript
174
+ // 注册系统时指定依赖
175
+ world.registerSystem(inputSystem);
176
+ world.registerSystem(movementSystem, [inputSystem]); // movementSystem 依赖 inputSystem
177
+ world.registerSystem(renderSystem, [movementSystem]); // renderSystem 依赖 movementSystem
178
+ ```
179
+
180
+ 系统将按照拓扑排序执行,依赖系统始终在被依赖系统之前运行。
181
+
170
182
  ## 性能特点
171
183
 
172
184
  - **Archetype 系统**:实体按组件组合分组,实现连续内存访问
@@ -199,6 +211,7 @@ src/
199
211
  ├── query.ts # 查询系统
200
212
  ├── query-filter.ts # 查询过滤器
201
213
  ├── system.ts # 系统接口
214
+ ├── system-scheduler.ts # 系统调度器
202
215
  ├── command-buffer.ts # 命令缓冲区
203
216
  ├── types.ts # 类型定义
204
217
  ├── utils.ts # 工具函数
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
@@ -435,6 +435,12 @@ class CommandBuffer {
435
435
  }
436
436
 
437
437
  // src/query-filter.ts
438
+ function serializeQueryFilter(filter = {}) {
439
+ const negative = (filter.negativeComponentTypes || []).slice().sort((a, b) => a - b);
440
+ if (negative.length === 0)
441
+ return "";
442
+ return `neg:${negative.join(",")}`;
443
+ }
438
444
  function matchesComponentTypes(archetype, componentTypes) {
439
445
  return componentTypes.every((type) => {
440
446
  const detailedType = getDetailedIdType(type);
@@ -464,17 +470,19 @@ function matchesFilter(archetype, filter) {
464
470
 
465
471
  // src/query.ts
466
472
  class Query {
473
+ key;
467
474
  world;
468
475
  componentTypes;
469
476
  filter;
470
477
  cachedArchetypes = [];
471
478
  isDisposed = false;
472
- constructor(world, componentTypes, filter = {}) {
479
+ constructor(world, componentTypes, filter = {}, key) {
473
480
  this.world = world;
474
481
  this.componentTypes = [...componentTypes].sort((a, b) => a - b);
475
482
  this.filter = filter;
483
+ this.key = key ?? `${this.componentTypes.join(",")}|`;
476
484
  this.updateCache();
477
- world.registerQuery(this);
485
+ world._registerQuery(this);
478
486
  }
479
487
  getEntities() {
480
488
  if (this.isDisposed) {
@@ -528,8 +536,11 @@ class Query {
528
536
  }
529
537
  }
530
538
  dispose() {
539
+ this.world.releaseQuery(this);
540
+ }
541
+ _disposeInternal() {
531
542
  if (!this.isDisposed) {
532
- this.world.unregisterQuery(this);
543
+ this.world._unregisterQuery(this);
533
544
  this.cachedArchetypes = [];
534
545
  this.isDisposed = true;
535
546
  }
@@ -542,14 +553,68 @@ class Query {
542
553
  }
543
554
  }
544
555
 
556
+ // src/system-scheduler.ts
557
+ class SystemScheduler {
558
+ systems = new Map;
559
+ allSystems = new Set;
560
+ addSystem(system, dependencies = []) {
561
+ this.systems.set(system, dependencies);
562
+ this.allSystems.add(system);
563
+ for (const dep of dependencies) {
564
+ this.allSystems.add(dep);
565
+ }
566
+ }
567
+ removeSystem(system) {
568
+ this.systems.delete(system);
569
+ this.allSystems.delete(system);
570
+ for (const [sys, deps] of this.systems) {
571
+ const index = deps.indexOf(system);
572
+ if (index !== -1) {
573
+ deps.splice(index, 1);
574
+ }
575
+ }
576
+ }
577
+ getExecutionOrder() {
578
+ const result = [];
579
+ const visited = new Set;
580
+ const visiting = new Set;
581
+ const visit = (system) => {
582
+ if (visited.has(system))
583
+ return;
584
+ if (visiting.has(system)) {
585
+ throw new Error("Circular dependency detected in system scheduling");
586
+ }
587
+ visiting.add(system);
588
+ const dependencies = this.systems.get(system) || [];
589
+ for (const dep of dependencies) {
590
+ visit(dep);
591
+ }
592
+ visiting.delete(system);
593
+ visited.add(system);
594
+ result.push(system);
595
+ };
596
+ for (const system of this.allSystems) {
597
+ if (!visited.has(system)) {
598
+ visit(system);
599
+ }
600
+ }
601
+ return result;
602
+ }
603
+ clear() {
604
+ this.systems.clear();
605
+ this.allSystems.clear();
606
+ }
607
+ }
608
+
545
609
  // src/world.ts
546
610
  class World {
547
611
  entityIdManager = new EntityIdManager;
548
612
  archetypes = [];
549
613
  archetypeMap = new Map;
550
614
  entityToArchetype = new Map;
551
- systems = [];
615
+ systemScheduler = new SystemScheduler;
552
616
  queries = [];
617
+ queryCache = new Map;
553
618
  commandBuffer;
554
619
  componentToArchetypes = new Map;
555
620
  lifecycleHooks = new Map;
@@ -649,14 +714,11 @@ class World {
649
714
  }
650
715
  return archetype.getComponent(entityId, componentType);
651
716
  }
652
- registerSystem(system) {
653
- this.systems.push(system);
717
+ registerSystem(system, dependencies = []) {
718
+ this.systemScheduler.addSystem(system, dependencies);
654
719
  }
655
720
  unregisterSystem(system) {
656
- const index = this.systems.indexOf(system);
657
- if (index !== -1) {
658
- this.systems.splice(index, 1);
659
- }
721
+ this.systemScheduler.removeSystem(system);
660
722
  }
661
723
  registerLifecycleHook(componentType, hook) {
662
724
  if (!this.lifecycleHooks.has(componentType)) {
@@ -674,7 +736,8 @@ class World {
674
736
  }
675
737
  }
676
738
  update(...params) {
677
- for (const system of this.systems) {
739
+ const systems = this.systemScheduler.getExecutionOrder();
740
+ for (const system of systems) {
678
741
  system.update(this, ...params);
679
742
  }
680
743
  this.commandBuffer.execute();
@@ -683,17 +746,50 @@ class World {
683
746
  this.commandBuffer.execute();
684
747
  }
685
748
  createQuery(componentTypes, filter = {}) {
686
- return new Query(this, componentTypes, filter);
687
- }
688
- registerQuery(query) {
749
+ const sortedTypes = [...componentTypes].sort((a, b) => a - b);
750
+ const filterKey = serializeQueryFilter(filter);
751
+ const key = `${this.getComponentTypesHash(sortedTypes)}${filterKey ? `|${filterKey}` : ""}`;
752
+ const cached = this.queryCache.get(key);
753
+ if (cached) {
754
+ cached.refCount++;
755
+ return cached.query;
756
+ }
757
+ const query = new Query(this, sortedTypes, filter, key);
758
+ this.queryCache.set(key, { query, refCount: 1 });
759
+ return query;
760
+ }
761
+ _registerQuery(query) {
689
762
  this.queries.push(query);
690
763
  }
691
- unregisterQuery(query) {
764
+ _unregisterQuery(query) {
692
765
  const index = this.queries.indexOf(query);
693
766
  if (index !== -1) {
694
767
  this.queries.splice(index, 1);
695
768
  }
696
769
  }
770
+ releaseQuery(query) {
771
+ const key = query.key;
772
+ for (const [k, v] of this.queryCache.entries()) {
773
+ if (v.query === query) {
774
+ v.refCount--;
775
+ if (v.refCount <= 0) {
776
+ this.queryCache.delete(k);
777
+ v.query._disposeInternal();
778
+ }
779
+ return;
780
+ }
781
+ }
782
+ const entry = this.queryCache.get(key);
783
+ if (!entry) {
784
+ this._unregisterQuery(query);
785
+ return;
786
+ }
787
+ entry.refCount--;
788
+ if (entry.refCount <= 0) {
789
+ this.queryCache.delete(key);
790
+ entry.query._disposeInternal();
791
+ }
792
+ }
697
793
  getMatchingArchetypes(componentTypes) {
698
794
  if (componentTypes.length === 0) {
699
795
  return [...this.archetypes];
@@ -976,6 +1072,7 @@ export {
976
1072
  component,
977
1073
  World,
978
1074
  WILDCARD_TARGET_ID,
1075
+ SystemScheduler,
979
1076
  RELATION_SHIFT,
980
1077
  Query,
981
1078
  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.5",
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,28 @@
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 allSystems;
8
+ /**
9
+ * Add a system with optional dependencies
10
+ * @param system The system to add
11
+ * @param dependencies Systems that this system depends on (must run before this system)
12
+ */
13
+ addSystem(system: System<ExtraParams>, dependencies?: System<ExtraParams>[]): void;
14
+ /**
15
+ * Remove a system
16
+ * @param system The system to remove
17
+ */
18
+ removeSystem(system: System<ExtraParams>): void;
19
+ /**
20
+ * Get the execution order of systems based on dependencies
21
+ * Uses topological sort
22
+ */
23
+ getExecutionOrder(): System<ExtraParams>[];
24
+ /**
25
+ * Clear all systems and dependencies
26
+ */
27
+ clear(): void;
28
+ }
package/world.d.ts CHANGED
@@ -2,7 +2,7 @@ import { Archetype } from "./archetype";
2
2
  import { type Command } from "./command-buffer";
3
3
  import type { EntityId, WildcardRelationId } from "./entity";
4
4
  import { Query } from "./query";
5
- import type { QueryFilter } from "./query-filter";
5
+ import { type QueryFilter } from "./query-filter";
6
6
  import type { System } from "./system";
7
7
  import type { ComponentTuple, LifecycleHook } from "./types";
8
8
  /**
@@ -14,8 +14,9 @@ export declare class World<ExtraParams extends any[] = [deltaTime: number]> {
14
14
  private archetypes;
15
15
  private archetypeMap;
16
16
  private entityToArchetype;
17
- private systems;
17
+ private systemScheduler;
18
18
  private queries;
19
+ private queryCache;
19
20
  private commandBuffer;
20
21
  private componentToArchetypes;
21
22
  /**
@@ -70,9 +71,9 @@ export declare class World<ExtraParams extends any[] = [deltaTime: number]> {
70
71
  */
71
72
  getComponent<T>(entityId: EntityId, componentType: EntityId<T>): T | undefined;
72
73
  /**
73
- * Register a system
74
+ * Register a system with optional dependencies
74
75
  */
75
- registerSystem(system: System<ExtraParams>): void;
76
+ registerSystem(system: System<ExtraParams>, dependencies?: System<ExtraParams>[]): void;
76
77
  /**
77
78
  * Unregister a system
78
79
  */
@@ -86,7 +87,7 @@ export declare class World<ExtraParams extends any[] = [deltaTime: number]> {
86
87
  */
87
88
  unregisterLifecycleHook<T>(componentType: EntityId<T>, hook: LifecycleHook<T>): void;
88
89
  /**
89
- * Update the world (run all systems)
90
+ * Update the world (run all systems in dependency order)
90
91
  */
91
92
  update(...params: ExtraParams): void;
92
93
  /**
@@ -100,11 +101,16 @@ export declare class World<ExtraParams extends any[] = [deltaTime: number]> {
100
101
  /**
101
102
  * @internal Register a query for archetype update notifications
102
103
  */
103
- registerQuery(query: Query): void;
104
+ _registerQuery(query: Query): void;
104
105
  /**
105
106
  * @internal Unregister a query
106
107
  */
107
- unregisterQuery(query: Query): void;
108
+ _unregisterQuery(query: Query): void;
109
+ /**
110
+ * Release a query reference obtained from createQuery.
111
+ * Decrements the refCount and fully disposes the query when it reaches zero.
112
+ */
113
+ releaseQuery(query: Query): void;
108
114
  /**
109
115
  * @internal Get archetypes that match specific component types (for internal use by queries)
110
116
  */