Skip to content

Commit b952bda

Browse files
committed
feat(world): enhance component cleanup on entity destruction
- Added a test to verify that components are cleaned up when an entity is used directly as a component type and subsequently destroyed. - Updated the World class to manage the reverse index for components, ensuring proper cleanup of references when entities are destroyed. - Refactored related methods to handle component references instead of relation references, improving clarity and functionality.
1 parent b86fecd commit b952bda

File tree

2 files changed

+84
-39
lines changed

2 files changed

+84
-39
lines changed

src/world.test.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -296,5 +296,41 @@ describe("World", () => {
296296
expect(world.hasEntity(entity2)).toBe(true);
297297
expect(world.hasEntity(entity3)).toBe(true);
298298
});
299+
300+
it("should clean up components when entity is used directly as component type and destroyed", () => {
301+
const world = new World();
302+
303+
// Create entities
304+
const entity1 = world.createEntity(); // This will be used as component type
305+
const entity2 = world.createEntity(); // This will have entity1 as component
306+
307+
// Add entity1 directly as a component type to entity2
308+
world.addComponent(entity2, entity1, null);
309+
world.flushCommands();
310+
311+
// Verify the component exists
312+
expect(world.hasComponent(entity2, entity1)).toBe(true);
313+
314+
// Query entities that have entity1 as component
315+
const entitiesWithComponent = world.queryEntities([entity1]);
316+
expect(entitiesWithComponent).toContain(entity2);
317+
318+
// Destroy entity1
319+
world.destroyEntity(entity1);
320+
world.flushCommands();
321+
322+
// Verify entity1 is destroyed
323+
expect(world.hasEntity(entity1)).toBe(false);
324+
325+
// Verify the component is cleaned up
326+
expect(world.hasComponent(entity2, entity1)).toBe(false);
327+
328+
// Query should now return empty
329+
const entitiesWithComponentAfterDestroy = world.queryEntities([entity1]);
330+
expect(entitiesWithComponentAfterDestroy).toHaveLength(0);
331+
332+
// entity2 should still exist
333+
expect(world.hasEntity(entity2)).toBe(true);
334+
});
299335
});
300336
});

src/world.ts

Lines changed: 48 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,11 @@ export class World<ExtraParams extends any[] = [deltaTime: number]> {
2323
private componentToArchetypes = new Map<EntityId<any>, Archetype[]>();
2424

2525
/**
26-
* Reverse index tracking which entities have relation components pointing to each target entity
27-
* Maps target entity ID to set of {sourceEntityId, relationType} pairs
26+
* Reverse index tracking which entities use each entity as a component type
27+
* Maps entity ID to set of {sourceEntityId, componentType} pairs where componentType uses this entity
28+
* This includes both relation components and direct usage of entities as component types
2829
*/
29-
private relationReverseIndex = new Map<EntityId, Set<{ sourceEntityId: EntityId; relationType: EntityId }>>();
30+
private entityAsComponentReverseIndex = new Map<EntityId, Set<{ sourceEntityId: EntityId; componentType: EntityId }>>();
3031

3132
constructor() {
3233
this.commandBuffer = new CommandBuffer((entityId, commands) => this.executeEntityCommands(entityId, commands));
@@ -60,19 +61,19 @@ export class World<ExtraParams extends any[] = [deltaTime: number]> {
6061
return; // Entity doesn't exist, nothing to do
6162
}
6263

63-
// Clean up relation components pointing to this entity
64-
const relationReferences = this.getRelationReferences(entityId);
65-
for (const { sourceEntityId, relationType } of relationReferences) {
66-
// Directly remove the relation component from the source entity
64+
// Clean up components that use this entity as a component type
65+
const componentReferences = this.getComponentReferences(entityId);
66+
for (const { sourceEntityId, componentType } of componentReferences) {
67+
// Directly remove the component from the source entity
6768
const sourceArchetype = this.entityToArchetype.get(sourceEntityId);
68-
if (sourceArchetype && sourceArchetype.componentTypes.includes(relationType)) {
69+
if (sourceArchetype && sourceArchetype.componentTypes.includes(componentType)) {
6970
// Remove from current archetype and move to new archetype without this component
7071
const currentComponents = new Map<EntityId<any>, any>();
71-
for (const componentType of sourceArchetype.componentTypes) {
72-
if (componentType !== relationType) {
73-
const data = sourceArchetype.getComponent(sourceEntityId, componentType);
72+
for (const compType of sourceArchetype.componentTypes) {
73+
if (compType !== componentType) {
74+
const data = sourceArchetype.getComponent(sourceEntityId, compType);
7475
if (data !== undefined) {
75-
currentComponents.set(componentType, data);
76+
currentComponents.set(compType, data);
7677
}
7778
}
7879
}
@@ -87,13 +88,13 @@ export class World<ExtraParams extends any[] = [deltaTime: number]> {
8788
newArchetype.addEntity(sourceEntityId, currentComponents);
8889
this.entityToArchetype.set(sourceEntityId, newArchetype);
8990

90-
// Remove from relation reverse index
91-
this.removeRelationReference(sourceEntityId, relationType, entityId);
91+
// Remove from component reverse index
92+
this.removeComponentReference(sourceEntityId, componentType, entityId);
9293
}
9394
}
9495

9596
// Clean up the reverse index for this entity
96-
this.relationReverseIndex.delete(entityId);
97+
this.entityAsComponentReverseIndex.delete(entityId);
9798

9899
archetype.removeEntity(entityId);
99100
this.entityToArchetype.delete(entityId);
@@ -409,21 +410,29 @@ export class World<ExtraParams extends any[] = [deltaTime: number]> {
409410
// Removals are already handled by not including them in finalComponents
410411
}
411412

412-
// Update relation reverse index for removed components
413+
// Update component reverse index for removed components
413414
for (const componentType of removes) {
414415
const detailedType = getDetailedIdType(componentType);
415416
if (detailedType.type === "entity-relation") {
417+
// For relation components, track the target entity
416418
const targetEntityId = detailedType.targetId!;
417-
this.removeRelationReference(entityId, componentType, targetEntityId);
419+
this.removeComponentReference(entityId, componentType, targetEntityId);
420+
} else if (detailedType.type === "entity") {
421+
// For direct entity usage as component type, track the component type itself
422+
this.removeComponentReference(entityId, componentType, componentType);
418423
}
419424
}
420425

421-
// Update relation reverse index for added components
426+
// Update component reverse index for added components
422427
for (const [componentType, component] of adds) {
423428
const detailedType = getDetailedIdType(componentType);
424429
if (detailedType.type === "entity-relation") {
430+
// For relation components, track the target entity
425431
const targetEntityId = detailedType.targetId!;
426-
this.addRelationReference(entityId, componentType, targetEntityId);
432+
this.addComponentReference(entityId, componentType, targetEntityId);
433+
} else if (detailedType.type === "entity") {
434+
// For direct entity usage as component type, track the component type itself
435+
this.addComponentReference(entityId, componentType, componentType);
427436
}
428437
}
429438
}
@@ -457,45 +466,45 @@ export class World<ExtraParams extends any[] = [deltaTime: number]> {
457466
}
458467

459468
/**
460-
* Add a relation reference to the reverse index
461-
* @param sourceEntityId The entity that has the relation component
462-
* @param relationType The relation component type
463-
* @param targetEntityId The entity being pointed to
469+
* Add a component reference to the reverse index when an entity is used as a component type
470+
* @param sourceEntityId The entity that has the component
471+
* @param componentType The component type (which may be an entity ID used as component type)
472+
* @param targetEntityId The entity being used as component type
464473
*/
465-
private addRelationReference(sourceEntityId: EntityId, relationType: EntityId, targetEntityId: EntityId): void {
466-
if (!this.relationReverseIndex.has(targetEntityId)) {
467-
this.relationReverseIndex.set(targetEntityId, new Set());
474+
private addComponentReference(sourceEntityId: EntityId, componentType: EntityId, targetEntityId: EntityId): void {
475+
if (!this.entityAsComponentReverseIndex.has(targetEntityId)) {
476+
this.entityAsComponentReverseIndex.set(targetEntityId, new Set());
468477
}
469-
this.relationReverseIndex.get(targetEntityId)!.add({ sourceEntityId, relationType });
478+
this.entityAsComponentReverseIndex.get(targetEntityId)!.add({ sourceEntityId, componentType });
470479
}
471480

472481
/**
473-
* Remove a relation reference from the reverse index
474-
* @param sourceEntityId The entity that has the relation component
475-
* @param relationType The relation component type
476-
* @param targetEntityId The entity being pointed to
482+
* Remove a component reference from the reverse index
483+
* @param sourceEntityId The entity that has the component
484+
* @param componentType The component type
485+
* @param targetEntityId The entity being used as component type
477486
*/
478-
private removeRelationReference(sourceEntityId: EntityId, relationType: EntityId, targetEntityId: EntityId): void {
479-
const references = this.relationReverseIndex.get(targetEntityId);
487+
private removeComponentReference(sourceEntityId: EntityId, componentType: EntityId, targetEntityId: EntityId): void {
488+
const references = this.entityAsComponentReverseIndex.get(targetEntityId);
480489
if (references) {
481490
references.forEach(ref => {
482-
if (ref.sourceEntityId === sourceEntityId && ref.relationType === relationType) {
491+
if (ref.sourceEntityId === sourceEntityId && ref.componentType === componentType) {
483492
references.delete(ref);
484493
}
485494
});
486495
if (references.size === 0) {
487-
this.relationReverseIndex.delete(targetEntityId);
496+
this.entityAsComponentReverseIndex.delete(targetEntityId);
488497
}
489498
}
490499
}
491500

492501
/**
493-
* Get all relation references pointing to a target entity
502+
* Get all component references where a target entity is used as a component type
494503
* @param targetEntityId The target entity
495-
* @returns Array of {sourceEntityId, relationType} pairs
504+
* @returns Array of {sourceEntityId, componentType} pairs
496505
*/
497-
private getRelationReferences(targetEntityId: EntityId): Array<{ sourceEntityId: EntityId; relationType: EntityId }> {
498-
const references = this.relationReverseIndex.get(targetEntityId);
506+
private getComponentReferences(targetEntityId: EntityId): Array<{ sourceEntityId: EntityId; componentType: EntityId }> {
507+
const references = this.entityAsComponentReverseIndex.get(targetEntityId);
499508
return references ? Array.from(references) : [];
500509
}
501510
}

0 commit comments

Comments
 (0)