@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 +47 -0
- package/archetype.d.ts +2 -2
- package/changeset.d.ts +36 -0
- package/command-buffer.d.ts +2 -1
- package/entity.d.ts +1 -1
- package/index.d.ts +1 -0
- package/index.js +194 -64
- package/package.json +1 -1
- package/query-filter.d.ts +5 -0
- package/query.d.ts +12 -2
- package/system-scheduler.d.ts +22 -0
- package/system.d.ts +4 -0
- package/world.d.ts +28 -14
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][]
|
|
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
|
|
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
|
+
}
|
package/command-buffer.d.ts
CHANGED
|
@@ -21,7 +21,8 @@ export declare class CommandBuffer {
|
|
|
21
21
|
/**
|
|
22
22
|
* Add a component to an entity (deferred)
|
|
23
23
|
*/
|
|
24
|
-
addComponent
|
|
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
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
687
|
-
|
|
688
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
788
|
-
|
|
789
|
-
|
|
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
|
-
|
|
802
|
-
adds.delete(componentType);
|
|
937
|
+
changeset.removeComponent(componentType);
|
|
803
938
|
}
|
|
804
939
|
}
|
|
805
940
|
}
|
|
806
941
|
} else {
|
|
807
|
-
|
|
808
|
-
adds.delete(cmd.componentType);
|
|
942
|
+
changeset.removeComponent(cmd.componentType);
|
|
809
943
|
}
|
|
810
944
|
}
|
|
811
945
|
break;
|
|
812
946
|
}
|
|
813
947
|
}
|
|
814
|
-
const finalComponents =
|
|
815
|
-
|
|
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
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
|
|
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
|
|
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
|
|
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][]
|
|
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
|
|
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
|
-
*
|
|
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
|
-
|
|
112
|
+
_registerQuery(query: Query): void;
|
|
104
113
|
/**
|
|
105
114
|
* @internal Unregister a query
|
|
106
115
|
*/
|
|
107
|
-
|
|
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[]):
|
|
137
|
+
executeEntityCommands(entityId: EntityId, commands: Command[]): ComponentChangeset;
|
|
124
138
|
/**
|
|
125
139
|
* Get or create an archetype for the given component types
|
|
126
140
|
*/
|