@codehz/ecs 0.0.5 → 0.0.7
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 +35 -1
- 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.js +107 -74
- package/package.json +1 -1
- package/system-scheduler.d.ts +2 -8
- package/system.d.ts +4 -0
- package/world.d.ts +24 -11
package/README.md
CHANGED
|
@@ -120,6 +120,39 @@ world.addComponent(entity, positionRelation, { x: 10, y: 20 });
|
|
|
120
120
|
world.flushCommands(); // 通配符钩子会被触发
|
|
121
121
|
```
|
|
122
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
|
+
|
|
123
156
|
### 运行示例
|
|
124
157
|
|
|
125
158
|
```bash
|
|
@@ -139,8 +172,9 @@ bun run examples/simple/demo.ts
|
|
|
139
172
|
- `createEntity()`: 创建新实体
|
|
140
173
|
- `addComponent(entity, componentId, data)`: 向实体添加组件
|
|
141
174
|
- `removeComponent(entity, componentId)`: 从实体移除组件
|
|
175
|
+
- `setExclusive(componentId)`: 将组件标记为独占关系
|
|
142
176
|
- `createQuery(componentIds)`: 创建查询
|
|
143
|
-
- `registerSystem(system
|
|
177
|
+
- `registerSystem(system)`: 注册系统
|
|
144
178
|
- `registerLifecycleHook(componentId, hook)`: 注册组件或通配符关系生命周期钩子
|
|
145
179
|
- `unregisterLifecycleHook(componentId, hook)`: 注销组件或通配符关系生命周期钩子
|
|
146
180
|
- `update(deltaTime)`: 更新世界
|
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.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 = [];
|
|
@@ -555,26 +600,19 @@ class Query {
|
|
|
555
600
|
|
|
556
601
|
// src/system-scheduler.ts
|
|
557
602
|
class SystemScheduler {
|
|
558
|
-
systems = new
|
|
559
|
-
|
|
560
|
-
addSystem(system
|
|
561
|
-
this.systems.
|
|
562
|
-
|
|
563
|
-
|
|
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
|
-
}
|
|
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);
|
|
575
609
|
}
|
|
610
|
+
this.cachedExecutionOrder = null;
|
|
576
611
|
}
|
|
577
612
|
getExecutionOrder() {
|
|
613
|
+
if (this.cachedExecutionOrder !== null) {
|
|
614
|
+
return this.cachedExecutionOrder;
|
|
615
|
+
}
|
|
578
616
|
const result = [];
|
|
579
617
|
const visited = new Set;
|
|
580
618
|
const visiting = new Set;
|
|
@@ -585,24 +623,24 @@ class SystemScheduler {
|
|
|
585
623
|
throw new Error("Circular dependency detected in system scheduling");
|
|
586
624
|
}
|
|
587
625
|
visiting.add(system);
|
|
588
|
-
const
|
|
589
|
-
for (const dep of dependencies) {
|
|
626
|
+
for (const dep of system.dependencies || []) {
|
|
590
627
|
visit(dep);
|
|
591
628
|
}
|
|
592
629
|
visiting.delete(system);
|
|
593
630
|
visited.add(system);
|
|
594
631
|
result.push(system);
|
|
595
632
|
};
|
|
596
|
-
for (const system of this.
|
|
633
|
+
for (const system of this.systems) {
|
|
597
634
|
if (!visited.has(system)) {
|
|
598
635
|
visit(system);
|
|
599
636
|
}
|
|
600
637
|
}
|
|
638
|
+
this.cachedExecutionOrder = result;
|
|
601
639
|
return result;
|
|
602
640
|
}
|
|
603
641
|
clear() {
|
|
604
642
|
this.systems.clear();
|
|
605
|
-
this.
|
|
643
|
+
this.cachedExecutionOrder = null;
|
|
606
644
|
}
|
|
607
645
|
}
|
|
608
646
|
|
|
@@ -619,6 +657,7 @@ class World {
|
|
|
619
657
|
componentToArchetypes = new Map;
|
|
620
658
|
lifecycleHooks = new Map;
|
|
621
659
|
entityReverseIndex = new Map;
|
|
660
|
+
exclusiveComponents = new Set;
|
|
622
661
|
constructor() {
|
|
623
662
|
this.commandBuffer = new CommandBuffer((entityId, commands) => this.executeEntityCommands(entityId, commands));
|
|
624
663
|
}
|
|
@@ -706,19 +745,12 @@ class World {
|
|
|
706
745
|
getComponent(entityId, componentType) {
|
|
707
746
|
const archetype = this.entityToArchetype.get(entityId);
|
|
708
747
|
if (!archetype) {
|
|
709
|
-
|
|
710
|
-
return [];
|
|
711
|
-
} else {
|
|
712
|
-
return;
|
|
713
|
-
}
|
|
748
|
+
throw new Error(`Entity ${entityId} does not exist`);
|
|
714
749
|
}
|
|
715
750
|
return archetype.getComponent(entityId, componentType);
|
|
716
751
|
}
|
|
717
|
-
registerSystem(system
|
|
718
|
-
this.systemScheduler.addSystem(system
|
|
719
|
-
}
|
|
720
|
-
unregisterSystem(system) {
|
|
721
|
-
this.systemScheduler.removeSystem(system);
|
|
752
|
+
registerSystem(system) {
|
|
753
|
+
this.systemScheduler.addSystem(system);
|
|
722
754
|
}
|
|
723
755
|
registerLifecycleHook(componentType, hook) {
|
|
724
756
|
if (!this.lifecycleHooks.has(componentType)) {
|
|
@@ -735,6 +767,9 @@ class World {
|
|
|
735
767
|
}
|
|
736
768
|
}
|
|
737
769
|
}
|
|
770
|
+
setExclusive(componentId) {
|
|
771
|
+
this.exclusiveComponents.add(componentId);
|
|
772
|
+
}
|
|
738
773
|
update(...params) {
|
|
739
774
|
const systems = this.systemScheduler.getExecutionOrder();
|
|
740
775
|
for (const system of systems) {
|
|
@@ -859,30 +894,35 @@ class World {
|
|
|
859
894
|
}
|
|
860
895
|
}
|
|
861
896
|
executeEntityCommands(entityId, commands) {
|
|
897
|
+
const changeset = new ComponentChangeset;
|
|
862
898
|
const hasDestroy = commands.some((cmd) => cmd.type === "destroyEntity");
|
|
863
899
|
if (hasDestroy) {
|
|
864
900
|
this._destroyEntity(entityId);
|
|
865
|
-
return;
|
|
901
|
+
return changeset;
|
|
866
902
|
}
|
|
867
903
|
const currentArchetype = this.entityToArchetype.get(entityId);
|
|
868
904
|
if (!currentArchetype) {
|
|
869
|
-
return;
|
|
905
|
+
return changeset;
|
|
870
906
|
}
|
|
871
907
|
const currentComponents = new Map;
|
|
872
908
|
for (const componentType of currentArchetype.componentTypes) {
|
|
873
909
|
const data = currentArchetype.getComponent(entityId, componentType);
|
|
874
|
-
|
|
875
|
-
currentComponents.set(componentType, data);
|
|
876
|
-
}
|
|
910
|
+
currentComponents.set(componentType, data);
|
|
877
911
|
}
|
|
878
|
-
const adds = new Map;
|
|
879
|
-
const removes = new Set;
|
|
880
912
|
for (const cmd of commands) {
|
|
881
913
|
switch (cmd.type) {
|
|
882
914
|
case "addComponent":
|
|
883
|
-
if (cmd.componentType
|
|
884
|
-
|
|
885
|
-
|
|
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);
|
|
886
926
|
}
|
|
887
927
|
break;
|
|
888
928
|
case "removeComponent":
|
|
@@ -894,27 +934,19 @@ class World {
|
|
|
894
934
|
const componentDetailedType = getDetailedIdType(componentType);
|
|
895
935
|
if (componentDetailedType.type === "entity-relation" || componentDetailedType.type === "component-relation") {
|
|
896
936
|
if (componentDetailedType.componentId === baseComponentId) {
|
|
897
|
-
|
|
898
|
-
adds.delete(componentType);
|
|
937
|
+
changeset.removeComponent(componentType);
|
|
899
938
|
}
|
|
900
939
|
}
|
|
901
940
|
}
|
|
902
941
|
} else {
|
|
903
|
-
|
|
904
|
-
adds.delete(cmd.componentType);
|
|
942
|
+
changeset.removeComponent(cmd.componentType);
|
|
905
943
|
}
|
|
906
944
|
}
|
|
907
945
|
break;
|
|
908
946
|
}
|
|
909
947
|
}
|
|
910
|
-
const finalComponents =
|
|
911
|
-
|
|
912
|
-
finalComponents.delete(componentType);
|
|
913
|
-
}
|
|
914
|
-
for (const [componentType, component2] of adds) {
|
|
915
|
-
finalComponents.set(componentType, component2);
|
|
916
|
-
}
|
|
917
|
-
const finalComponentTypes = Array.from(finalComponents.keys()).sort((a, b) => a - b);
|
|
948
|
+
const finalComponents = changeset.applyTo(currentComponents);
|
|
949
|
+
const finalComponentTypes = changeset.getFinalComponentTypes(currentComponents);
|
|
918
950
|
const currentComponentTypes = currentArchetype.componentTypes.sort((a, b) => a - b);
|
|
919
951
|
const needsArchetypeChange = finalComponentTypes.length !== currentComponentTypes.length || !finalComponentTypes.every((type, index) => type === currentComponentTypes[index]);
|
|
920
952
|
if (needsArchetypeChange) {
|
|
@@ -923,11 +955,11 @@ class World {
|
|
|
923
955
|
newArchetype.addEntity(entityId, finalComponents);
|
|
924
956
|
this.entityToArchetype.set(entityId, newArchetype);
|
|
925
957
|
} else {
|
|
926
|
-
for (const [componentType, component2] of adds) {
|
|
958
|
+
for (const [componentType, component2] of changeset.adds) {
|
|
927
959
|
currentArchetype.setComponent(entityId, componentType, component2);
|
|
928
960
|
}
|
|
929
961
|
}
|
|
930
|
-
for (const componentType of removes) {
|
|
962
|
+
for (const componentType of changeset.removes) {
|
|
931
963
|
const detailedType = getDetailedIdType(componentType);
|
|
932
964
|
if (detailedType.type === "entity-relation") {
|
|
933
965
|
const targetEntityId = detailedType.targetId;
|
|
@@ -936,7 +968,7 @@ class World {
|
|
|
936
968
|
this.removeComponentReference(entityId, componentType, componentType);
|
|
937
969
|
}
|
|
938
970
|
}
|
|
939
|
-
for (const [componentType, component2] of adds) {
|
|
971
|
+
for (const [componentType, component2] of changeset.adds) {
|
|
940
972
|
const detailedType = getDetailedIdType(componentType);
|
|
941
973
|
if (detailedType.type === "entity-relation") {
|
|
942
974
|
const targetEntityId = detailedType.targetId;
|
|
@@ -945,7 +977,8 @@ class World {
|
|
|
945
977
|
this.addComponentReference(entityId, componentType, componentType);
|
|
946
978
|
}
|
|
947
979
|
}
|
|
948
|
-
this.executeComponentLifecycleHooks(entityId, adds, removes);
|
|
980
|
+
this.executeComponentLifecycleHooks(entityId, changeset.adds, changeset.removes);
|
|
981
|
+
return changeset;
|
|
949
982
|
}
|
|
950
983
|
getOrCreateArchetype(componentTypes) {
|
|
951
984
|
const sortedTypes = [...componentTypes].sort((a, b) => a - b);
|
package/package.json
CHANGED
package/system-scheduler.d.ts
CHANGED
|
@@ -4,18 +4,12 @@ import type { System } from "./system";
|
|
|
4
4
|
*/
|
|
5
5
|
export declare class SystemScheduler<ExtraParams extends any[] = [deltaTime: number]> {
|
|
6
6
|
private systems;
|
|
7
|
-
private
|
|
7
|
+
private cachedExecutionOrder;
|
|
8
8
|
/**
|
|
9
9
|
* Add a system with optional dependencies
|
|
10
10
|
* @param system The system to add
|
|
11
|
-
* @param dependencies Systems that this system depends on (must run before this system)
|
|
12
11
|
*/
|
|
13
|
-
addSystem(system: System<ExtraParams
|
|
14
|
-
/**
|
|
15
|
-
* Remove a system
|
|
16
|
-
* @param system The system to remove
|
|
17
|
-
*/
|
|
18
|
-
removeSystem(system: System<ExtraParams>): void;
|
|
12
|
+
addSystem(system: System<ExtraParams>): void;
|
|
19
13
|
/**
|
|
20
14
|
* Get the execution order of systems based on dependencies
|
|
21
15
|
* Uses topological sort
|
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,4 +1,5 @@
|
|
|
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";
|
|
@@ -29,6 +30,11 @@ export declare class World<ExtraParams extends any[] = [deltaTime: number]> {
|
|
|
29
30
|
* This includes both relation components and direct usage of entities as component types
|
|
30
31
|
*/
|
|
31
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;
|
|
32
38
|
constructor();
|
|
33
39
|
/**
|
|
34
40
|
* Generate a hash key for component types array
|
|
@@ -49,7 +55,8 @@ export declare class World<ExtraParams extends any[] = [deltaTime: number]> {
|
|
|
49
55
|
/**
|
|
50
56
|
* Add a component to an entity (deferred)
|
|
51
57
|
*/
|
|
52
|
-
addComponent
|
|
58
|
+
addComponent(entityId: EntityId, componentType: EntityId<void>): void;
|
|
59
|
+
addComponent<T>(entityId: EntityId, componentType: EntityId<T>, component: NoInfer<T>): void;
|
|
53
60
|
/**
|
|
54
61
|
* Remove a component from an entity (deferred)
|
|
55
62
|
*/
|
|
@@ -63,21 +70,22 @@ export declare class World<ExtraParams extends any[] = [deltaTime: number]> {
|
|
|
63
70
|
*/
|
|
64
71
|
hasComponent<T>(entityId: EntityId, componentType: EntityId<T>): boolean;
|
|
65
72
|
/**
|
|
66
|
-
* Get
|
|
73
|
+
* Get component data for a specific entity and wildcard relation type
|
|
74
|
+
* Returns an array of all matching relation instances
|
|
75
|
+
* @param entityId The entity
|
|
76
|
+
* @param componentType The wildcard relation type
|
|
67
77
|
*/
|
|
68
|
-
getComponent<T>(entityId: EntityId, componentType: WildcardRelationId<T>): [EntityId<unknown>,
|
|
78
|
+
getComponent<T>(entityId: EntityId, componentType: WildcardRelationId<T>): [EntityId<unknown>, T][];
|
|
69
79
|
/**
|
|
70
|
-
* Get a
|
|
80
|
+
* Get component data for a specific entity and component type
|
|
81
|
+
* @param entityId The entity
|
|
82
|
+
* @param componentType The component type
|
|
71
83
|
*/
|
|
72
|
-
getComponent<T>(entityId: EntityId, componentType: EntityId<T>): T
|
|
84
|
+
getComponent<T>(entityId: EntityId, componentType: EntityId<T>): T;
|
|
73
85
|
/**
|
|
74
86
|
* Register a system with optional dependencies
|
|
75
87
|
*/
|
|
76
|
-
registerSystem(system: System<ExtraParams
|
|
77
|
-
/**
|
|
78
|
-
* Unregister a system
|
|
79
|
-
*/
|
|
80
|
-
unregisterSystem(system: System<ExtraParams>): void;
|
|
88
|
+
registerSystem(system: System<ExtraParams>): void;
|
|
81
89
|
/**
|
|
82
90
|
* Register a lifecycle hook for component or wildcard relation events
|
|
83
91
|
*/
|
|
@@ -86,6 +94,11 @@ export declare class World<ExtraParams extends any[] = [deltaTime: number]> {
|
|
|
86
94
|
* Unregister a lifecycle hook for component or wildcard relation events
|
|
87
95
|
*/
|
|
88
96
|
unregisterLifecycleHook<T>(componentType: EntityId<T>, hook: LifecycleHook<T>): void;
|
|
97
|
+
/**
|
|
98
|
+
* Mark a component as exclusive relation
|
|
99
|
+
* For exclusive relations, an entity can have at most one relation per base component
|
|
100
|
+
*/
|
|
101
|
+
setExclusive(componentId: EntityId): void;
|
|
89
102
|
/**
|
|
90
103
|
* Update the world (run all systems in dependency order)
|
|
91
104
|
*/
|
|
@@ -126,7 +139,7 @@ export declare class World<ExtraParams extends any[] = [deltaTime: number]> {
|
|
|
126
139
|
/**
|
|
127
140
|
* @internal Execute commands for a single entity (for internal use by CommandBuffer)
|
|
128
141
|
*/
|
|
129
|
-
executeEntityCommands(entityId: EntityId, commands: Command[]):
|
|
142
|
+
executeEntityCommands(entityId: EntityId, commands: Command[]): ComponentChangeset;
|
|
130
143
|
/**
|
|
131
144
|
* Get or create an archetype for the given component types
|
|
132
145
|
*/
|