@codehz/ecs 0.0.3 → 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 +14 -1
- package/index.d.ts +1 -0
- package/index.js +112 -15
- package/package.json +1 -1
- package/query-filter.d.ts +5 -0
- package/query.d.ts +12 -2
- package/system-scheduler.d.ts +28 -0
- package/world.d.ts +13 -7
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
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.
|
|
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.
|
|
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
|
-
|
|
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.
|
|
717
|
+
registerSystem(system, dependencies = []) {
|
|
718
|
+
this.systemScheduler.addSystem(system, dependencies);
|
|
654
719
|
}
|
|
655
720
|
unregisterSystem(system) {
|
|
656
|
-
|
|
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
|
-
|
|
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
|
-
|
|
687
|
-
|
|
688
|
-
|
|
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
|
-
|
|
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
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
|
|
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
|
|
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
|
-
|
|
104
|
+
_registerQuery(query: Query): void;
|
|
104
105
|
/**
|
|
105
106
|
* @internal Unregister a query
|
|
106
107
|
*/
|
|
107
|
-
|
|
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
|
*/
|