@codehz/ecs 0.8.2 → 0.9.0
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.en.md +26 -3
- package/README.md +28 -3
- package/dist/builder.d.mts +296 -46
- package/dist/index.d.mts +2 -2
- package/dist/index.mjs +2 -2
- package/dist/testing.d.mts +1 -1
- package/dist/testing.mjs +1 -1
- package/dist/world.mjs +452 -179
- package/dist/world.mjs.map +1 -1
- package/examples/debug-observability.ts +92 -0
- package/examples/inventory-system-relations.ts +1 -1
- package/examples/parent-child-hierarchy.ts +18 -38
- package/package.json +1 -1
- package/skills/ecs/SKILL.md +4 -4
- package/src/__tests__/component/singleton.test.ts +40 -1
- package/src/__tests__/core/archetype.test.ts +155 -13
- package/src/__tests__/core/bitset.test.ts +12 -0
- package/src/__tests__/entity/entity.test.ts +33 -0
- package/src/__tests__/entity/id-system.test.ts +40 -0
- package/src/__tests__/perf/comprehensive.perf.test.ts +6 -9
- package/src/__tests__/perf/serialization.perf.test.ts +242 -0
- package/src/__tests__/perf/{dontfragment-wildcard.perf.test.ts → sparse-wildcard.perf.test.ts} +13 -16
- package/src/__tests__/query/caching.test.ts +62 -0
- package/src/__tests__/query/filter.test.ts +16 -22
- package/src/__tests__/query/perf.test.ts +3 -5
- package/src/__tests__/relations/hierarchy.test.ts +208 -0
- package/src/__tests__/relations/{dont-fragment → sparse}/basic.test.ts +64 -69
- package/src/__tests__/relations/{dont-fragment → sparse}/query-notification.test.ts +17 -9
- package/src/__tests__/serialization/bounds.test.ts +134 -1
- package/src/__tests__/world/commands.test.ts +337 -0
- package/src/__tests__/world/debug-stats.test.ts +206 -0
- package/src/__tests__/world/multi-component-hooks.test.ts +44 -0
- package/src/__tests__/world/serialize.test.ts +17 -0
- package/src/__tests__/world/wildcard-relation-hooks.test.ts +127 -0
- package/src/archetype/archetype.ts +96 -46
- package/src/archetype/helpers.ts +7 -29
- package/src/archetype/store.ts +35 -20
- package/src/commands/buffer.ts +5 -2
- package/src/commands/changeset.ts +0 -31
- package/src/component/registry.ts +64 -63
- package/src/entity/index.ts +6 -3
- package/src/index.ts +13 -0
- package/src/query/filter.ts +4 -10
- package/src/query/query.ts +12 -12
- package/src/storage/serialization.ts +29 -2
- package/src/types/index.ts +71 -0
- package/src/world/commands.ts +44 -56
- package/src/world/hooks.ts +8 -0
- package/src/world/serialization.ts +32 -18
- package/src/world/world.ts +387 -20
package/src/world/world.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Archetype } from "../archetype/archetype";
|
|
2
|
-
import {
|
|
2
|
+
import { SparseStoreImpl } from "../archetype/store";
|
|
3
3
|
import { CommandBuffer, type Command } from "../commands/buffer";
|
|
4
4
|
import { ComponentChangeset } from "../commands/changeset";
|
|
5
5
|
import { ComponentEntityStore } from "../component/entity-store";
|
|
@@ -13,17 +13,26 @@ import {
|
|
|
13
13
|
getDetailedIdType,
|
|
14
14
|
getTargetIdFromRelationId,
|
|
15
15
|
isCascadeDeleteRelation,
|
|
16
|
-
isDontFragmentRelation,
|
|
17
|
-
isDontFragmentWildcard,
|
|
18
16
|
isEntityRelation,
|
|
19
17
|
isExclusiveComponent,
|
|
18
|
+
isSparseRelation,
|
|
19
|
+
isSparseWildcard,
|
|
20
20
|
isWildcardRelationId,
|
|
21
|
+
relation,
|
|
21
22
|
} from "../entity";
|
|
22
23
|
import { matchesFilter, serializeQueryFilter, type QueryFilter } from "../query/filter";
|
|
23
24
|
import type { Query } from "../query/query";
|
|
24
25
|
import { QueryRegistry } from "../query/registry";
|
|
25
26
|
import type { SerializedWorld } from "../storage/serialization";
|
|
26
|
-
import type {
|
|
27
|
+
import type {
|
|
28
|
+
ComponentTuple,
|
|
29
|
+
ComponentType,
|
|
30
|
+
DebugStatsCollector,
|
|
31
|
+
LifecycleCallback,
|
|
32
|
+
LifecycleHook,
|
|
33
|
+
LifecycleHookEntry,
|
|
34
|
+
SyncDebugStats,
|
|
35
|
+
} from "../types";
|
|
27
36
|
import { isOptionalEntityId } from "../types";
|
|
28
37
|
import { getOrCompute } from "../utils/utils";
|
|
29
38
|
import { EntityBuilder } from "./builder";
|
|
@@ -37,6 +46,7 @@ import {
|
|
|
37
46
|
} from "./commands";
|
|
38
47
|
import {
|
|
39
48
|
collectMultiHookComponents,
|
|
49
|
+
debugHookExecutionCounter,
|
|
40
50
|
triggerLifecycleHooks,
|
|
41
51
|
triggerRemoveHooksForEntityDeletion,
|
|
42
52
|
type HooksContext,
|
|
@@ -63,8 +73,8 @@ export class World {
|
|
|
63
73
|
private entityReferences: EntityReferencesMap = new Map();
|
|
64
74
|
/** Reverse index: entity ID → set of archetypes whose componentTypes include that entity ID */
|
|
65
75
|
private entityToReferencingArchetypes = new Map<EntityId, Set<Archetype>>();
|
|
66
|
-
/**
|
|
67
|
-
private readonly
|
|
76
|
+
/** Sparse relation storage (for components created with `sparse: true`), shared with all Archetype instances */
|
|
77
|
+
private readonly sparseStore = new SparseStoreImpl();
|
|
68
78
|
/** Component entity (singleton) storage */
|
|
69
79
|
private readonly componentEntities = new ComponentEntityStore();
|
|
70
80
|
|
|
@@ -74,6 +84,14 @@ export class World {
|
|
|
74
84
|
// Lifecycle hooks (declared before cached contexts that reference them)
|
|
75
85
|
private hooks: Set<LifecycleHookEntry> = new Set();
|
|
76
86
|
|
|
87
|
+
// Debug observability collectors (armed only when non-empty)
|
|
88
|
+
private readonly _debugCollectors = new Set<(stats: SyncDebugStats) => void>();
|
|
89
|
+
|
|
90
|
+
// Transient counters for the current armed sync (reset each time)
|
|
91
|
+
private _debugMigrations = 0;
|
|
92
|
+
private _debugArchetypesCreated = 0;
|
|
93
|
+
private _debugArchetypesRemoved = 0;
|
|
94
|
+
|
|
77
95
|
// Command execution
|
|
78
96
|
private commandBuffer = new CommandBuffer((entityId, commands) => this.executeEntityCommands(entityId, commands));
|
|
79
97
|
|
|
@@ -82,7 +100,7 @@ export class World {
|
|
|
82
100
|
private readonly _removeChangeset = new ComponentChangeset();
|
|
83
101
|
/** Cached command processor context to avoid per-entity object allocation */
|
|
84
102
|
private readonly _commandCtx: CommandProcessorContext = {
|
|
85
|
-
|
|
103
|
+
sparseStore: this.sparseStore,
|
|
86
104
|
ensureArchetype: (ct) => this.ensureArchetype(ct),
|
|
87
105
|
};
|
|
88
106
|
/** Cached hooks context to avoid per-entity object allocation */
|
|
@@ -198,7 +216,7 @@ export class World {
|
|
|
198
216
|
}
|
|
199
217
|
}
|
|
200
218
|
|
|
201
|
-
// Remove entity from archetype - this also cleans up
|
|
219
|
+
// Remove entity from archetype - this also cleans up sparse relations
|
|
202
220
|
// and returns all removed component data
|
|
203
221
|
this.entityReferences.delete(cur);
|
|
204
222
|
const removedComponents = archetype.removeEntity(cur)!;
|
|
@@ -443,11 +461,11 @@ export class World {
|
|
|
443
461
|
|
|
444
462
|
if (archetype.componentTypeSet.has(componentType)) return true;
|
|
445
463
|
|
|
446
|
-
if (
|
|
464
|
+
if (isSparseRelation(componentType)) {
|
|
447
465
|
// Use getValue; presence check via getAllForEntity only if value can legitimately be undefined
|
|
448
|
-
const val = this.
|
|
466
|
+
const val = this.sparseStore.getValue(entityId, componentType);
|
|
449
467
|
if (val !== undefined) return true;
|
|
450
|
-
return this.
|
|
468
|
+
return this.sparseStore.getAllForEntity(entityId).some(([t]) => t === componentType);
|
|
451
469
|
}
|
|
452
470
|
|
|
453
471
|
return false;
|
|
@@ -495,12 +513,12 @@ export class World {
|
|
|
495
513
|
|
|
496
514
|
if (componentType >= 0 || componentType % RELATION_SHIFT !== 0) {
|
|
497
515
|
const inArchetype = archetype.componentTypeSet.has(componentType);
|
|
498
|
-
const
|
|
516
|
+
const hasSparse = isSparseRelation(componentType);
|
|
499
517
|
const hasComponent =
|
|
500
518
|
inArchetype ||
|
|
501
|
-
(
|
|
502
|
-
(this.
|
|
503
|
-
this.
|
|
519
|
+
(hasSparse &&
|
|
520
|
+
(this.sparseStore.getValue(entityId, componentType) !== undefined ||
|
|
521
|
+
this.sparseStore.getAllForEntity(entityId).some(([t]) => t === componentType)));
|
|
504
522
|
|
|
505
523
|
if (!hasComponent) {
|
|
506
524
|
throw new Error(
|
|
@@ -571,6 +589,213 @@ export class World {
|
|
|
571
589
|
return archetype.getOptional(entityId, componentType);
|
|
572
590
|
}
|
|
573
591
|
|
|
592
|
+
// ==========================================================================
|
|
593
|
+
// Relation & Hierarchy Companion Tools (public API)
|
|
594
|
+
// ==========================================================================
|
|
595
|
+
|
|
596
|
+
/**
|
|
597
|
+
* Retrieves all targets (and their associated data) for relations of a given
|
|
598
|
+
* base component on an entity.
|
|
599
|
+
*
|
|
600
|
+
* This is the ergonomic replacement for the common pattern:
|
|
601
|
+
* world.get(entity, relation(Comp, "*"))
|
|
602
|
+
*
|
|
603
|
+
* @example
|
|
604
|
+
* const ChildOf = component({ exclusive: true, sparse: true });
|
|
605
|
+
* const children = world.getRelationTargets(parent, ChildOf); // usually []
|
|
606
|
+
* const items = world.getRelationTargets(player, InInventory);
|
|
607
|
+
*
|
|
608
|
+
* // For common hierarchy use cases, prefer the higher-level helpers:
|
|
609
|
+
* // world.getChildren(parent, ChildOf), world.getParent(child, ChildOf)
|
|
610
|
+
*/
|
|
611
|
+
getRelationTargets<T = void>(
|
|
612
|
+
entityId: EntityId,
|
|
613
|
+
relationComp: ComponentId<T>,
|
|
614
|
+
): [target: EntityId<unknown>, data: T | undefined][] {
|
|
615
|
+
this.assertEntityExists(entityId, "Entity");
|
|
616
|
+
|
|
617
|
+
const wildcard = relation(relationComp, "*") as WildcardRelationId<T>;
|
|
618
|
+
|
|
619
|
+
// For component entities (singletons) the path is different; they rarely host relations
|
|
620
|
+
if (this.componentEntities.exists(entityId)) {
|
|
621
|
+
return this.componentEntities.getWildcard(entityId, wildcard);
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
// Regular entity path — archetype.get for wildcard always materializes the array
|
|
625
|
+
// (even if empty for a sparse relation that only has the marker)
|
|
626
|
+
const data = this.get(entityId, wildcard);
|
|
627
|
+
return data as [EntityId<unknown>, T | undefined][];
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
/**
|
|
631
|
+
* Returns every entity that currently holds a relation of the given base
|
|
632
|
+
* component pointing at `targetId`.
|
|
633
|
+
*
|
|
634
|
+
* This is the efficient **reverse** lookup. For common hierarchy cases,
|
|
635
|
+
* prefer the higher-level `world.getChildren(parent, ChildOf)` instead.
|
|
636
|
+
*
|
|
637
|
+
* @example
|
|
638
|
+
* const ChildOf = component({ exclusive: true, sparse: true });
|
|
639
|
+
* const directChildren = world.getRelationSources(ship, ChildOf);
|
|
640
|
+
*/
|
|
641
|
+
getRelationSources(targetId: EntityId, relationComp: ComponentId<any>): EntityId[] {
|
|
642
|
+
const refs = getEntityReferences(this.entityReferences, targetId);
|
|
643
|
+
const result: EntityId[] = [];
|
|
644
|
+
|
|
645
|
+
for (const [source, relType] of refs) {
|
|
646
|
+
// Only consider still-living sources
|
|
647
|
+
if (!this.entityToArchetype.has(source) && !this.componentEntities.exists(source)) continue;
|
|
648
|
+
|
|
649
|
+
const decodedComp = getComponentIdFromRelationId(relType);
|
|
650
|
+
if (decodedComp === relationComp) {
|
|
651
|
+
result.push(source);
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
return result;
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
/**
|
|
658
|
+
* Returns true if the entity has any (or a specific-target) relation of the
|
|
659
|
+
* given base component.
|
|
660
|
+
*/
|
|
661
|
+
hasRelation(entityId: EntityId, relationComp: ComponentId<any>, targetId?: EntityId): boolean {
|
|
662
|
+
this.assertEntityExists(entityId, "Entity");
|
|
663
|
+
|
|
664
|
+
if (targetId !== undefined) {
|
|
665
|
+
const specific = relation(relationComp, targetId);
|
|
666
|
+
return this.has(entityId, specific);
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
// Any target of this relation kind?
|
|
670
|
+
const targets = this.getRelationTargets(entityId, relationComp);
|
|
671
|
+
return targets.length > 0;
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
/**
|
|
675
|
+
* Returns the number of relations of the given base component held by the entity.
|
|
676
|
+
*/
|
|
677
|
+
countRelations(entityId: EntityId, relationComp: ComponentId<any>): number {
|
|
678
|
+
this.assertEntityExists(entityId, "Entity");
|
|
679
|
+
const targets = this.getRelationTargets(entityId, relationComp);
|
|
680
|
+
return targets.length;
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
/**
|
|
684
|
+
* For an *exclusive* relation (e.g. ChildOf, Owner), returns the single
|
|
685
|
+
* target entity (or undefined if none).
|
|
686
|
+
*
|
|
687
|
+
* When the component was declared `exclusive: true`, this is the preferred
|
|
688
|
+
* accessor (clearer intent than array destructuring).
|
|
689
|
+
*/
|
|
690
|
+
getSingleRelationTarget<T = void>(entityId: EntityId, relationComp: ComponentId<T>): EntityId | undefined {
|
|
691
|
+
const targets = this.getRelationTargets(entityId, relationComp);
|
|
692
|
+
return targets.length > 0 ? (targets[0]![0] as EntityId) : undefined;
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
// --------------------------------------------------------------------------
|
|
696
|
+
// High-level hierarchy helpers (convenience methods on World)
|
|
697
|
+
// --------------------------------------------------------------------------
|
|
698
|
+
|
|
699
|
+
/**
|
|
700
|
+
* Returns the direct children of `parent` for the given relationship component
|
|
701
|
+
* (typically a `ChildOf` or similar exclusive `sparse` relation).
|
|
702
|
+
*
|
|
703
|
+
* This is the recommended high-level API for hierarchy traversal.
|
|
704
|
+
* It uses the internal reverse reference index for efficiency.
|
|
705
|
+
*
|
|
706
|
+
* @example
|
|
707
|
+
* const ChildOf = component({ exclusive: true, sparse: true });
|
|
708
|
+
* const kids = world.getChildren(ship, ChildOf);
|
|
709
|
+
*/
|
|
710
|
+
getChildren(parent: EntityId, childOf: ComponentId<any>): EntityId[] {
|
|
711
|
+
return this.getRelationSources(parent, childOf);
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
/**
|
|
715
|
+
* Returns the parent of `child` for the given relationship component
|
|
716
|
+
* (typically an exclusive `ChildOf` relation).
|
|
717
|
+
*
|
|
718
|
+
* @example
|
|
719
|
+
* const ChildOf = component({ exclusive: true, sparse: true });
|
|
720
|
+
* const parent = world.getParent(turret, ChildOf);
|
|
721
|
+
*/
|
|
722
|
+
getParent(child: EntityId, childOf: ComponentId<any>): EntityId | undefined {
|
|
723
|
+
return this.getSingleRelationTarget(child, childOf);
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
/**
|
|
727
|
+
* Returns the ancestor chain from the immediate parent up to (but not
|
|
728
|
+
* including) the root for the given relationship component.
|
|
729
|
+
*
|
|
730
|
+
* @example
|
|
731
|
+
* const ChildOf = component({ exclusive: true, sparse: true });
|
|
732
|
+
* const ancestors = world.getAncestors(muzzle, ChildOf); // [turret, ship]
|
|
733
|
+
*/
|
|
734
|
+
getAncestors(entity: EntityId, childOf: ComponentId<any>): EntityId[] {
|
|
735
|
+
const ancestors: EntityId[] = [];
|
|
736
|
+
let cur = this.getParent(entity, childOf);
|
|
737
|
+
while (cur !== undefined) {
|
|
738
|
+
ancestors.push(cur);
|
|
739
|
+
cur = this.getParent(cur, childOf);
|
|
740
|
+
}
|
|
741
|
+
return ancestors;
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
/**
|
|
745
|
+
* Iteratively traverses all descendants of `root` in DFS pre-order.
|
|
746
|
+
* This is a generator and is safe for very deep hierarchies.
|
|
747
|
+
*
|
|
748
|
+
* @example
|
|
749
|
+
* for (const { entity, depth, parent } of world.iterateDescendants(root, ChildOf)) {
|
|
750
|
+
* console.log(depth, entity);
|
|
751
|
+
* }
|
|
752
|
+
*/
|
|
753
|
+
*iterateDescendants(
|
|
754
|
+
root: EntityId,
|
|
755
|
+
childOf: ComponentId<any>,
|
|
756
|
+
opts: { includeSelf?: boolean; maxDepth?: number } = {},
|
|
757
|
+
): IterableIterator<{ entity: EntityId; depth: number; parent: EntityId | null }> {
|
|
758
|
+
const { includeSelf = false, maxDepth } = opts;
|
|
759
|
+
const stack: Array<{ entity: EntityId; depth: number; parent: EntityId | null }> = [];
|
|
760
|
+
|
|
761
|
+
if (includeSelf) {
|
|
762
|
+
stack.push({ entity: root, depth: 0, parent: null });
|
|
763
|
+
} else {
|
|
764
|
+
for (const child of this.getChildren(root, childOf)) {
|
|
765
|
+
stack.push({ entity: child, depth: 1, parent: root });
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
while (stack.length > 0) {
|
|
770
|
+
const current = stack.pop()!;
|
|
771
|
+
if (maxDepth !== undefined && current.depth > maxDepth) continue;
|
|
772
|
+
|
|
773
|
+
yield current;
|
|
774
|
+
|
|
775
|
+
const kids = this.getChildren(current.entity, childOf);
|
|
776
|
+
for (let i = kids.length - 1; i >= 0; i--) {
|
|
777
|
+
const k = kids[i]!;
|
|
778
|
+
stack.push({ entity: k, depth: current.depth + 1, parent: current.entity });
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
/**
|
|
784
|
+
* Callback-based descendant traversal (hot path friendly).
|
|
785
|
+
* Return `false` from the visitor to stop early.
|
|
786
|
+
*/
|
|
787
|
+
traverseDescendants(
|
|
788
|
+
root: EntityId,
|
|
789
|
+
childOf: ComponentId<any>,
|
|
790
|
+
visitor: (entity: EntityId, depth: number, parent: EntityId | null) => void | boolean,
|
|
791
|
+
opts: { includeSelf?: boolean; maxDepth?: number } = {},
|
|
792
|
+
): void {
|
|
793
|
+
for (const { entity, depth, parent } of this.iterateDescendants(root, childOf, opts)) {
|
|
794
|
+
const res = visitor(entity, depth, parent);
|
|
795
|
+
if (res === false) return;
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
|
|
574
799
|
/**
|
|
575
800
|
* Registers a lifecycle hook that responds to component changes.
|
|
576
801
|
* The hook callback is invoked when components matching the specified types are added, updated, or removed.
|
|
@@ -684,6 +909,103 @@ export class World {
|
|
|
684
909
|
};
|
|
685
910
|
}
|
|
686
911
|
|
|
912
|
+
/**
|
|
913
|
+
* Creates a debug stats collector that will receive a `SyncDebugStats` payload
|
|
914
|
+
* after every subsequent `sync()`.
|
|
915
|
+
*
|
|
916
|
+
* The returned object is a pure lifecycle handle. It does not store data.
|
|
917
|
+
* Collection stops when you call `[Symbol.dispose]()` (or use a `using` declaration).
|
|
918
|
+
*
|
|
919
|
+
* All active collectors receive the exact same stats object for a given sync.
|
|
920
|
+
* Exceptions thrown by callbacks are ignored.
|
|
921
|
+
*
|
|
922
|
+
* This is intended for development/debugging and leak detection.
|
|
923
|
+
*/
|
|
924
|
+
createDebugStatsCollector(callback: (stats: SyncDebugStats) => void): DebugStatsCollector {
|
|
925
|
+
this._debugCollectors.add(callback);
|
|
926
|
+
|
|
927
|
+
return {
|
|
928
|
+
[Symbol.dispose]: () => {
|
|
929
|
+
this._debugCollectors.delete(callback);
|
|
930
|
+
},
|
|
931
|
+
};
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
private _resetDebugActivityCounters(): void {
|
|
935
|
+
this._debugMigrations = 0;
|
|
936
|
+
this._debugArchetypesCreated = 0;
|
|
937
|
+
this._debugArchetypesRemoved = 0;
|
|
938
|
+
debugHookExecutionCounter.value = 0;
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
private _deliverDebugStats(timings: {
|
|
942
|
+
syncStart: number;
|
|
943
|
+
syncEnd: number;
|
|
944
|
+
commandBufferStart: number;
|
|
945
|
+
commandBufferEnd: number;
|
|
946
|
+
commandIterations: number;
|
|
947
|
+
}): void {
|
|
948
|
+
// Build structural counts (post-sync)
|
|
949
|
+
// Note: singletons (component-as-entity) are not included in the main archetype map.
|
|
950
|
+
// For debug purposes the dominant number is regular entities; we keep it simple here.
|
|
951
|
+
const entityCount = this.entityToArchetype.size;
|
|
952
|
+
let emptyArchetypes = 0;
|
|
953
|
+
for (const arch of this.archetypes) {
|
|
954
|
+
if (arch.size === 0) emptyArchetypes++;
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
let archetypesByComponentSize = 0;
|
|
958
|
+
for (const set of this.archetypesByComponent.values()) {
|
|
959
|
+
archetypesByComponentSize += set.size;
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
const stats: SyncDebugStats = {
|
|
963
|
+
timestamps: {
|
|
964
|
+
syncStart: timings.syncStart,
|
|
965
|
+
syncEnd: timings.syncEnd,
|
|
966
|
+
commandBufferStart: timings.commandBufferStart,
|
|
967
|
+
commandBufferEnd: timings.commandBufferEnd,
|
|
968
|
+
},
|
|
969
|
+
commandIterations: timings.commandIterations,
|
|
970
|
+
|
|
971
|
+
entities: {
|
|
972
|
+
total: entityCount,
|
|
973
|
+
freelistSize: this.entityIdManager.getFreelistSize(),
|
|
974
|
+
nextId: this.entityIdManager.getNextId(),
|
|
975
|
+
},
|
|
976
|
+
archetypes: {
|
|
977
|
+
total: this.archetypes.length,
|
|
978
|
+
empty: emptyArchetypes,
|
|
979
|
+
},
|
|
980
|
+
queries: {
|
|
981
|
+
cached: (this.queryRegistry as any).cache?.size ?? 0,
|
|
982
|
+
registered: (this.queryRegistry as any).queries?.size ?? 0,
|
|
983
|
+
},
|
|
984
|
+
hooks: {
|
|
985
|
+
total: this.hooks.size,
|
|
986
|
+
},
|
|
987
|
+
indices: {
|
|
988
|
+
entityReferences: this.entityReferences.size,
|
|
989
|
+
entityToReferencingArchetypes: this.entityToReferencingArchetypes.size,
|
|
990
|
+
archetypesByComponent: archetypesByComponentSize,
|
|
991
|
+
},
|
|
992
|
+
activity: {
|
|
993
|
+
migrations: this._debugMigrations,
|
|
994
|
+
hooksExecuted: debugHookExecutionCounter.value,
|
|
995
|
+
archetypesCreated: this._debugArchetypesCreated,
|
|
996
|
+
archetypesRemoved: this._debugArchetypesRemoved,
|
|
997
|
+
},
|
|
998
|
+
};
|
|
999
|
+
|
|
1000
|
+
for (const cb of this._debugCollectors) {
|
|
1001
|
+
try {
|
|
1002
|
+
cb(stats);
|
|
1003
|
+
} catch {
|
|
1004
|
+
// Intentionally ignore user callback errors
|
|
1005
|
+
}
|
|
1006
|
+
}
|
|
1007
|
+
}
|
|
1008
|
+
|
|
687
1009
|
/**
|
|
688
1010
|
* Synchronizes all buffered commands (set/remove/delete) to the world.
|
|
689
1011
|
* This method must be called after making changes via `set()`, `remove()`, or `delete()` for them to take effect.
|
|
@@ -695,7 +1017,29 @@ export class World {
|
|
|
695
1017
|
* world.sync(); // Apply all buffered changes
|
|
696
1018
|
*/
|
|
697
1019
|
sync(): void {
|
|
698
|
-
this.
|
|
1020
|
+
const hasCollectors = this._debugCollectors.size > 0;
|
|
1021
|
+
|
|
1022
|
+
const syncStart = hasCollectors ? performance.now() : 0;
|
|
1023
|
+
|
|
1024
|
+
if (hasCollectors) {
|
|
1025
|
+
this._resetDebugActivityCounters();
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
const commandBufferStart = hasCollectors ? performance.now() : 0;
|
|
1029
|
+
const commandIterations = this.commandBuffer.execute();
|
|
1030
|
+
const commandBufferEnd = hasCollectors ? performance.now() : 0;
|
|
1031
|
+
|
|
1032
|
+
const syncEnd = hasCollectors ? performance.now() : 0;
|
|
1033
|
+
|
|
1034
|
+
if (hasCollectors) {
|
|
1035
|
+
this._deliverDebugStats({
|
|
1036
|
+
syncStart,
|
|
1037
|
+
syncEnd,
|
|
1038
|
+
commandBufferStart,
|
|
1039
|
+
commandBufferEnd,
|
|
1040
|
+
commandIterations,
|
|
1041
|
+
});
|
|
1042
|
+
}
|
|
699
1043
|
}
|
|
700
1044
|
|
|
701
1045
|
/**
|
|
@@ -958,10 +1302,20 @@ export class World {
|
|
|
958
1302
|
|
|
959
1303
|
if (this.hooks.size === 0) {
|
|
960
1304
|
// Fast path: no hooks, skip removedComponents map allocation and hook triggering
|
|
961
|
-
|
|
1305
|
+
const newArchetype = applyChangeset(
|
|
1306
|
+
this._commandCtx,
|
|
1307
|
+
entityId,
|
|
1308
|
+
currentArchetype,
|
|
1309
|
+
changeset,
|
|
1310
|
+
this.entityToArchetype,
|
|
1311
|
+
null,
|
|
1312
|
+
);
|
|
962
1313
|
if (hasStructuralChange) {
|
|
963
1314
|
this.updateEntityReferences(entityId, changeset);
|
|
964
1315
|
}
|
|
1316
|
+
if (this._debugCollectors.size > 0 && newArchetype !== currentArchetype) {
|
|
1317
|
+
this._debugMigrations++;
|
|
1318
|
+
}
|
|
965
1319
|
return;
|
|
966
1320
|
}
|
|
967
1321
|
|
|
@@ -978,6 +1332,11 @@ export class World {
|
|
|
978
1332
|
if (hasStructuralChange) {
|
|
979
1333
|
this.updateEntityReferences(entityId, changeset);
|
|
980
1334
|
}
|
|
1335
|
+
|
|
1336
|
+
if (this._debugCollectors.size > 0 && newArchetype !== currentArchetype) {
|
|
1337
|
+
this._debugMigrations++;
|
|
1338
|
+
}
|
|
1339
|
+
|
|
981
1340
|
triggerLifecycleHooks(
|
|
982
1341
|
this.createHooksContext(),
|
|
983
1342
|
entityId,
|
|
@@ -1099,9 +1458,13 @@ export class World {
|
|
|
1099
1458
|
}
|
|
1100
1459
|
|
|
1101
1460
|
private createNewArchetype(componentTypes: EntityId<any>[]): Archetype {
|
|
1102
|
-
const newArchetype = new Archetype(componentTypes, this.
|
|
1461
|
+
const newArchetype = new Archetype(componentTypes, this.sparseStore);
|
|
1103
1462
|
this.archetypes.push(newArchetype);
|
|
1104
1463
|
|
|
1464
|
+
if (this._debugCollectors.size > 0) {
|
|
1465
|
+
this._debugArchetypesCreated++;
|
|
1466
|
+
}
|
|
1467
|
+
|
|
1105
1468
|
for (const componentType of componentTypes) {
|
|
1106
1469
|
let archetypes = this.archetypesByComponent.get(componentType);
|
|
1107
1470
|
if (!archetypes) {
|
|
@@ -1135,11 +1498,11 @@ export class World {
|
|
|
1135
1498
|
return (
|
|
1136
1499
|
entry.requiredComponents.every((c: EntityId<any>) => {
|
|
1137
1500
|
if (isWildcardRelationId(c)) {
|
|
1138
|
-
if (
|
|
1501
|
+
if (isSparseWildcard(c)) return true;
|
|
1139
1502
|
const componentId = getComponentIdFromRelationId(c);
|
|
1140
1503
|
return componentId !== undefined && archetype.hasRelationWithComponentId(componentId);
|
|
1141
1504
|
}
|
|
1142
|
-
return archetype.componentTypeSet.has(c) ||
|
|
1505
|
+
return archetype.componentTypeSet.has(c) || isSparseRelation(c);
|
|
1143
1506
|
}) && matchesFilter(archetype, entry.filter)
|
|
1144
1507
|
);
|
|
1145
1508
|
}
|
|
@@ -1166,6 +1529,10 @@ export class World {
|
|
|
1166
1529
|
this.archetypes.pop();
|
|
1167
1530
|
}
|
|
1168
1531
|
|
|
1532
|
+
if (this._debugCollectors.size > 0) {
|
|
1533
|
+
this._debugArchetypesRemoved++;
|
|
1534
|
+
}
|
|
1535
|
+
|
|
1169
1536
|
this.archetypeBySignature.delete(this.createArchetypeSignature(archetype.componentTypes));
|
|
1170
1537
|
|
|
1171
1538
|
for (const componentType of archetype.componentTypes) {
|