Skip to content

Commit b86fecd

Browse files
committed
feat(world): add relation management for entities
- Introduced relationReverseIndex to track relations between entities. - Implemented cleanup of relation components when an entity is destroyed. - Added methods to manage relation references in the reverse index. - Updated existing methods to maintain the integrity of relation data.
1 parent 8f7c461 commit b86fecd

File tree

2 files changed

+153
-0
lines changed

2 files changed

+153
-0
lines changed

src/world.test.ts

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -245,5 +245,56 @@ describe("World", () => {
245245
expect(mixedEntities).not.toContain(entity2);
246246
expect(mixedEntities).not.toContain(entity3);
247247
});
248+
249+
it("should clean up relation components when target entity is destroyed", () => {
250+
const world = new World();
251+
252+
// Create component IDs
253+
const positionComponent = createComponentId<{ x: number; y: number }>(1);
254+
const followsComponent = createComponentId<void>(2);
255+
256+
// Create entities
257+
const entity1 = world.createEntity(); // This will be followed
258+
const entity2 = world.createEntity(); // This will follow entity1
259+
const entity3 = world.createEntity(); // This will also follow entity1
260+
261+
// Add position to entity1
262+
world.addComponent(entity1, positionComponent, { x: 10, y: 20 });
263+
world.flushCommands();
264+
265+
// Create relation components (entity2 and entity3 follow entity1)
266+
const followsEntity1 = createRelationId(followsComponent, entity1);
267+
world.addComponent(entity2, followsEntity1, null);
268+
world.addComponent(entity3, followsEntity1, null);
269+
world.flushCommands();
270+
271+
// Verify relations exist
272+
expect(world.hasComponent(entity2, followsEntity1)).toBe(true);
273+
expect(world.hasComponent(entity3, followsEntity1)).toBe(true);
274+
275+
// Query entities that follow entity1
276+
const followers = world.queryEntities([followsEntity1]);
277+
expect(followers).toContain(entity2);
278+
expect(followers).toContain(entity3);
279+
280+
// Destroy entity1
281+
world.destroyEntity(entity1);
282+
world.flushCommands();
283+
284+
// Verify entity1 is destroyed
285+
expect(world.hasEntity(entity1)).toBe(false);
286+
287+
// Verify relation components are cleaned up
288+
expect(world.hasComponent(entity2, followsEntity1)).toBe(false);
289+
expect(world.hasComponent(entity3, followsEntity1)).toBe(false);
290+
291+
// Query should now return empty
292+
const followersAfterDestroy = world.queryEntities([followsEntity1]);
293+
expect(followersAfterDestroy).toHaveLength(0);
294+
295+
// entity2 and entity3 should still exist but without the relation components
296+
expect(world.hasEntity(entity2)).toBe(true);
297+
expect(world.hasEntity(entity3)).toBe(true);
298+
});
248299
});
249300
});

src/world.ts

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,12 @@ export class World<ExtraParams extends any[] = [deltaTime: number]> {
2222
private commandBuffer: CommandBuffer;
2323
private componentToArchetypes = new Map<EntityId<any>, Archetype[]>();
2424

25+
/**
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
28+
*/
29+
private relationReverseIndex = new Map<EntityId, Set<{ sourceEntityId: EntityId; relationType: EntityId }>>();
30+
2531
constructor() {
2632
this.commandBuffer = new CommandBuffer((entityId, commands) => this.executeEntityCommands(entityId, commands));
2733
}
@@ -54,6 +60,41 @@ export class World<ExtraParams extends any[] = [deltaTime: number]> {
5460
return; // Entity doesn't exist, nothing to do
5561
}
5662

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
67+
const sourceArchetype = this.entityToArchetype.get(sourceEntityId);
68+
if (sourceArchetype && sourceArchetype.componentTypes.includes(relationType)) {
69+
// Remove from current archetype and move to new archetype without this component
70+
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);
74+
if (data !== undefined) {
75+
currentComponents.set(componentType, data);
76+
}
77+
}
78+
}
79+
80+
const newComponentTypes = Array.from(currentComponents.keys()).sort((a, b) => a - b);
81+
const newArchetype = this.getOrCreateArchetype(newComponentTypes);
82+
83+
// Remove from current archetype
84+
sourceArchetype.removeEntity(sourceEntityId);
85+
86+
// Add to new archetype
87+
newArchetype.addEntity(sourceEntityId, currentComponents);
88+
this.entityToArchetype.set(sourceEntityId, newArchetype);
89+
90+
// Remove from relation reverse index
91+
this.removeRelationReference(sourceEntityId, relationType, entityId);
92+
}
93+
}
94+
95+
// Clean up the reverse index for this entity
96+
this.relationReverseIndex.delete(entityId);
97+
5798
archetype.removeEntity(entityId);
5899
this.entityToArchetype.delete(entityId);
59100
this.entityIdManager.deallocate(entityId);
@@ -367,6 +408,24 @@ export class World<ExtraParams extends any[] = [deltaTime: number]> {
367408
}
368409
// Removals are already handled by not including them in finalComponents
369410
}
411+
412+
// Update relation reverse index for removed components
413+
for (const componentType of removes) {
414+
const detailedType = getDetailedIdType(componentType);
415+
if (detailedType.type === "entity-relation") {
416+
const targetEntityId = detailedType.targetId!;
417+
this.removeRelationReference(entityId, componentType, targetEntityId);
418+
}
419+
}
420+
421+
// Update relation reverse index for added components
422+
for (const [componentType, component] of adds) {
423+
const detailedType = getDetailedIdType(componentType);
424+
if (detailedType.type === "entity-relation") {
425+
const targetEntityId = detailedType.targetId!;
426+
this.addRelationReference(entityId, componentType, targetEntityId);
427+
}
428+
}
370429
}
371430

372431
/**
@@ -396,4 +455,47 @@ export class World<ExtraParams extends any[] = [deltaTime: number]> {
396455
return newArchetype;
397456
});
398457
}
458+
459+
/**
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
464+
*/
465+
private addRelationReference(sourceEntityId: EntityId, relationType: EntityId, targetEntityId: EntityId): void {
466+
if (!this.relationReverseIndex.has(targetEntityId)) {
467+
this.relationReverseIndex.set(targetEntityId, new Set());
468+
}
469+
this.relationReverseIndex.get(targetEntityId)!.add({ sourceEntityId, relationType });
470+
}
471+
472+
/**
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
477+
*/
478+
private removeRelationReference(sourceEntityId: EntityId, relationType: EntityId, targetEntityId: EntityId): void {
479+
const references = this.relationReverseIndex.get(targetEntityId);
480+
if (references) {
481+
references.forEach(ref => {
482+
if (ref.sourceEntityId === sourceEntityId && ref.relationType === relationType) {
483+
references.delete(ref);
484+
}
485+
});
486+
if (references.size === 0) {
487+
this.relationReverseIndex.delete(targetEntityId);
488+
}
489+
}
490+
}
491+
492+
/**
493+
* Get all relation references pointing to a target entity
494+
* @param targetEntityId The target entity
495+
* @returns Array of {sourceEntityId, relationType} pairs
496+
*/
497+
private getRelationReferences(targetEntityId: EntityId): Array<{ sourceEntityId: EntityId; relationType: EntityId }> {
498+
const references = this.relationReverseIndex.get(targetEntityId);
499+
return references ? Array.from(references) : [];
500+
}
399501
}

0 commit comments

Comments
 (0)