• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

excaliburjs / Excalibur / 17382328181

01 Sep 2025 03:52PM UTC coverage: 87.365% (+0.006%) from 87.359%
17382328181

Pull #3511

github

web-flow
Merge e5f6ec63d into e8be5e077
Pull Request #3511: fix: Clean up memory leaks from Entities in Maps [#3510]

5180 of 7245 branches covered (71.5%)

14029 of 16058 relevant lines covered (87.36%)

24482.16 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

98.67
/src/engine/EntityComponentSystem/EntityManager.ts
1
import { Entity } from './Entity';
2
import type { World } from './World';
3
import { removeItemFromArray } from '../Util/Util';
4
import type { Scene } from '../Scene';
5

6
// Add/Remove entities and components
7

8
export class EntityManager {
9
  public entities: Entity[] = [];
1,318✔
10
  public _entityIndex: { [entityId: string]: Entity } = {};
1,318✔
11
  private _childAddedHandlerMap = new Map<Entity, (entity: Entity) => void>();
1,318✔
12
  private _childRemovedHandlerMap = new Map<Entity, (entity: Entity) => void>();
1,318✔
13

14
  constructor(private _world: World) {}
1,318✔
15

16
  /**
17
   * Runs the entity lifecycle
18
   * @param scene
19
   * @param elapsed
20
   */
21
  public updateEntities(scene: Scene, elapsed: number) {
22
    for (let entityIndex = 0; entityIndex < this.entities.length; entityIndex++) {
1,792✔
23
      const entity = this.entities[entityIndex];
3,249✔
24
      entity.update(scene.engine, elapsed);
3,249✔
25
      if (!entity.isActive) {
3,249✔
26
        this.removeEntity(entity);
17✔
27
      }
28
    }
29
  }
30

31
  public findEntitiesForRemoval() {
32
    for (let entityIndex = 0; entityIndex < this.entities.length; entityIndex++) {
2,727✔
33
      const entity = this.entities[entityIndex];
5,621✔
34
      if (!entity.isActive) {
5,621✔
35
        this.removeEntity(entity);
22✔
36
      }
37
    }
38
  }
39

40
  private _createChildAddedHandler = () => (e: Entity) => {
1,953✔
41
    this.addEntity(e);
42✔
42
  };
43

44
  private _createChildRemovedHandler = () => (e: Entity) => {
1,953✔
45
    this.removeEntity(e, false);
2✔
46
  };
47

48
  /**
49
   * Adds an entity to be tracked by the EntityManager
50
   * @param entity
51
   */
52
  public addEntity(entity: Entity): void {
53
    entity.isActive = true;
1,963✔
54
    entity.scene = this._world.scene;
1,963✔
55
    if (entity && !this._entityIndex[entity.id]) {
1,963✔
56
      this._entityIndex[entity.id] = entity;
1,953✔
57
      this.entities.push(entity);
1,953✔
58
      this._world.queryManager.addEntity(entity);
1,953✔
59

60
      // if entity has children
61
      entity.children.forEach((c) => {
1,953✔
62
        c.scene = entity.scene;
1,307✔
63
        this.addEntity(c);
1,307✔
64
      });
65
      const childAdded = this._createChildAddedHandler();
1,953✔
66
      this._childAddedHandlerMap.set(entity, childAdded);
1,953✔
67
      const childRemoved = this._createChildRemovedHandler();
1,953✔
68
      this._childRemovedHandlerMap.set(entity, childRemoved);
1,953✔
69
      entity.childrenAdded$.subscribe(childAdded);
1,953✔
70
      entity.childrenRemoved$.subscribe(childRemoved);
1,953✔
71
    }
72
  }
73

74
  public removeEntity(entity: Entity, deferred?: boolean): void;
75
  public removeEntity(id: number, deferred?: boolean): void;
76
  public removeEntity(idOrEntity: number | Entity, deferred = true): void {
41✔
77
    let id = 0;
131✔
78
    if (idOrEntity instanceof Entity) {
131✔
79
      id = idOrEntity.id;
130✔
80
    } else {
81
      id = idOrEntity;
1✔
82
    }
83
    const entity = this._entityIndex[id];
131✔
84
    if (entity && entity.isActive) {
131✔
85
      entity.isActive = false;
33✔
86
    }
87

88
    if (entity && deferred) {
131✔
89
      this._entitiesToRemove.push(entity);
54✔
90
      return;
54✔
91
    }
92

93
    delete this._entityIndex[id];
77✔
94
    if (entity) {
77✔
95
      entity.scene = null;
50✔
96
      removeItemFromArray(entity, this.entities);
50✔
97
      this._world.queryManager.removeEntity(entity);
50✔
98

99
      // if entity has children
100
      entity.children.forEach((c) => {
50✔
101
        c.scene = null;
5✔
102
        this.removeEntity(c, deferred);
5✔
103
      });
104
      const childAddedHandler = this._childAddedHandlerMap.get(entity);
50✔
105
      if (childAddedHandler) {
50!
106
        entity.childrenAdded$.unsubscribe(childAddedHandler);
50✔
107
        this._childAddedHandlerMap.delete(entity);
50✔
108
      }
109
      const childRemovedHandler = this._childRemovedHandlerMap.get(entity);
50✔
110
      if (childRemovedHandler) {
50!
111
        entity.childrenRemoved$.unsubscribe(childRemovedHandler);
50✔
112
        this._childRemovedHandlerMap.delete(entity);
50✔
113
      }
114

115
      // stats
116
      if (this._world?.scene?.engine) {
50!
117
        this._world.scene.engine.stats.currFrame.actors.killed++;
33✔
118
      }
119
    }
120
  }
121

122
  private _entitiesToRemove: Entity[] = [];
1,318✔
123
  public processEntityRemovals(): void {
124
    for (let entityIndex = 0; entityIndex < this._entitiesToRemove.length; entityIndex++) {
2,729✔
125
      const entity = this._entitiesToRemove[entityIndex];
52✔
126
      if (entity.isActive) {
52!
127
        continue;
×
128
      }
129
      this.removeEntity(entity, false);
52✔
130
    }
131
    this._entitiesToRemove.length = 0;
2,729✔
132
  }
133

134
  public processComponentRemovals(): void {
135
    for (let entityIndex = 0; entityIndex < this.entities.length; entityIndex++) {
2,727✔
136
      const entity = this.entities[entityIndex];
5,621✔
137
      entity.processComponentRemoval();
5,621✔
138
    }
139
  }
140

141
  public getById(id: number): Entity | undefined {
142
    return this._entityIndex[id];
21✔
143
  }
144

145
  public getByName(name: string): Entity[] {
146
    return this.entities.filter((e) => e.name === name);
4✔
147
  }
148

149
  public clear(): void {
150
    for (let i = this.entities.length - 1; i >= 0; i--) {
2✔
151
      this.removeEntity(this.entities[i]);
2✔
152
    }
153
  }
154
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc