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

excaliburjs / Excalibur / 15926884062

27 Jun 2025 12:59PM UTC coverage: 87.851% (-0.02%) from 87.869%
15926884062

push

github

web-flow
feat: support any, all and not component/tag filters on Query (#3380)

Improves `ex.Query` to support "any" and "not" filters for components, as well as all/any/not tags filters. The intention is for this to replace `ex.TagQuery`, but that is not (yet) in this PR.

With this change, this would much easily allow systems to exclude entities with tags. For example, GraphicsSystem could now exclude any Entity with an `ex.offscreen` tag instead of manually checking in its update. This could also be used for #3076 by adding a tag to an Entity while paused that excludes it from systems.

I also removed the restriction of throwing on empty component queries, as I think it could make sense to have a system that applies for every Entity. I can change this, though.

--- 

There's also some code quality changes here. 

- I added an eslint rule to prevent `fit` tests (as I've accidentally committed those before)
- an easy way to enable console logs in karma by doing `CAPTURE_CONSOLE=true npm run test` as I'd previously have to edit the karma config file.
- fixed some ts errors in collider tests

===:clipboard: PR Checklist :clipboard:===

- [ ] :pushpin: issue exists in github for these changes
- [x] :microscope: existing tests still pass
- [x] :see_no_evil: code conforms to the [style guide](https://github.com/excaliburjs/Excalibur/blob/main/STYLEGUIDE.md)
- [x] :triangular_ruler: new tests written and passing / old tests updated with new scenario(s)
- [x] :page_facing_up: changelog entry added (or not needed)

==================

<!-- If you're closing an issue with this pull request, or contributing a significant change, please include your changes in the appropriate section of CHANGELOG.md as outlined in https://github.com/excaliburjs/Excalibur/blob/main/.github/CONTRIBUTING.md#creating-a-pull-request. -->

<!--Please format your pull request title according to our commit message styleguide: https://github.com/excaliburjs/Excalibur/blob/main/.github/CONTRIBUTI... (continued)

5083 of 7051 branches covered (72.09%)

59 of 63 new or added lines in 3 files covered. (93.65%)

4 existing lines in 1 file now uncovered.

13710 of 15606 relevant lines covered (87.85%)

25170.58 hits per line

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

92.42
/src/engine/EntityComponentSystem/Entity.ts
1
import type { ComponentCtor } from './Component';
2
import { Component, isComponentCtor } from './Component';
3

4
import type { Message } from '../Util/Observable';
5
import { Observable } from '../Util/Observable';
6
import type { OnInitialize, OnPreUpdate, OnPostUpdate, OnAdd, OnRemove } from '../Interfaces/LifecycleEvents';
7
import type { Engine } from '../Engine';
8
import { InitializeEvent, PreUpdateEvent, PostUpdateEvent, AddEvent, RemoveEvent } from '../Events';
9
import { KillEvent } from '../Events';
10
import type { EventKey, Handler, Subscription } from '../EventEmitter';
11
import { EventEmitter } from '../EventEmitter';
12
import type { Scene } from '../Scene';
13
import { removeItemFromArray } from '../Util/Util';
14
import type { MaybeKnownComponent } from './Types';
15
import { Logger } from '../Util/Log';
16

17
/**
18
 * Interface holding an entity component pair
19
 */
20
export interface EntityComponent {
21
  component: Component;
22
  entity: Entity;
23
}
24

25
/**
26
 * AddedComponent message
27
 */
28
export class AddedComponent implements Message<EntityComponent> {
29
  readonly type: 'Component Added' = 'Component Added';
×
30
  constructor(public data: EntityComponent) {}
×
31
}
32

33
/**
34
 * Type guard to know if message is f an Added Component
35
 */
36
export function isAddedComponent(x: Message<EntityComponent>): x is AddedComponent {
37
  return !!x && x.type === 'Component Added';
×
38
}
39

40
/**
41
 * RemovedComponent message
42
 */
43
export class RemovedComponent implements Message<EntityComponent> {
44
  readonly type: 'Component Removed' = 'Component Removed';
×
45
  constructor(public data: EntityComponent) {}
×
46
}
47

48
/**
49
 * Type guard to know if message is for a Removed Component
50
 */
51
export function isRemovedComponent(x: Message<EntityComponent>): x is RemovedComponent {
52
  return !!x && x.type === 'Component Removed';
×
53
}
54

55
/**
56
 * Built in events supported by all entities
57
 */
58
export type EntityEvents = {
59
  initialize: InitializeEvent;
60
  //@ts-ignore
61
  add: AddEvent;
62
  //@ts-ignore
63
  remove: RemoveEvent;
64
  preupdate: PreUpdateEvent;
65
  postupdate: PostUpdateEvent;
66
  kill: KillEvent;
67
};
68

69
export const EntityEvents = {
118✔
70
  Add: 'add',
71
  Remove: 'remove',
72
  Initialize: 'initialize',
73
  PreUpdate: 'preupdate',
74
  PostUpdate: 'postupdate',
75
  Kill: 'kill'
76
} as const;
77

78
export interface EntityOptions<TComponents extends Component> {
79
  name?: string;
80
  components?: TComponents[];
81
  silenceWarnings?: boolean;
82
}
83

84
/**
85
 * An Entity is the base type of anything that can have behavior in Excalibur, they are part of the built in entity component system
86
 *
87
 * Entities can be strongly typed with the components they contain
88
 *
89
 * ```typescript
90
 * const entity = new Entity<ComponentA | ComponentB>();
91
 * entity.components.a; // Type ComponentA
92
 * entity.components.b; // Type ComponentB
93
 * ```
94
 */
95
export class Entity<TKnownComponents extends Component = any> implements OnInitialize, OnPreUpdate, OnPostUpdate, OnAdd, OnRemove {
118✔
96
  private static _ID = 0;
97
  /**
98
   * The unique identifier for the entity
99
   */
100
  public id: number = Entity._ID++;
7,536✔
101

102
  public name = `Entity#${this.id}`;
7,536✔
103

104
  /**
105
   * Listen to or emit events for an entity
106
   */
107
  public events = new EventEmitter<EntityEvents>();
7,536✔
108
  private _tags = new Set<string>();
7,536✔
109
  public componentAdded$ = new Observable<Component>();
7,536✔
110
  public componentRemoved$ = new Observable<Component>();
7,536✔
111
  public tagAdded$ = new Observable<string>();
7,536✔
112
  public tagRemoved$ = new Observable<string>();
7,536✔
113
  /**
114
   * Current components on the entity
115
   *
116
   * **Do not modify**
117
   *
118
   * Use addComponent/removeComponent otherwise the ECS will not be notified of changes.
119
   */
120
  public readonly components = new Map<Function, Component>();
7,536✔
121
  public componentValues: Component[] = [];
7,536✔
122
  private _componentsToRemove: ComponentCtor[] = [];
7,536✔
123

124
  constructor(options: EntityOptions<TKnownComponents>);
125
  constructor(components?: TKnownComponents[], name?: string);
126
  constructor(componentsOrOptions?: TKnownComponents[] | EntityOptions<TKnownComponents>, name?: string) {
127
    let componentsToAdd!: TKnownComponents[];
128
    let nameToAdd: string | undefined;
129
    let silence = false;
7,536✔
130
    if (Array.isArray(componentsOrOptions)) {
7,536✔
131
      componentsToAdd = componentsOrOptions;
2,934✔
132
      nameToAdd = name;
2,934✔
133
    } else if (componentsOrOptions && typeof componentsOrOptions === 'object') {
4,602✔
134
      const { components, name, silenceWarnings } = componentsOrOptions;
3,500✔
135
      componentsToAdd = components ?? [];
3,500!
136
      nameToAdd = name;
3,500✔
137
      silence = !!silenceWarnings;
3,500✔
138
    }
139
    if (nameToAdd) {
7,536✔
140
      this.name = nameToAdd;
10✔
141
    }
142
    if (componentsToAdd) {
7,536✔
143
      for (const component of componentsToAdd) {
6,434✔
144
        this.addComponent(component);
8,656✔
145
      }
146
    }
147

148
    if (process.env.NODE_ENV === 'development') {
7,536!
149
      if (!silence) {
7,536✔
150
        setTimeout(() => {
4,036✔
151
          if (!this.scene && !this.isInitialized) {
184✔
152
            Logger.getInstance().warn(`Entity "${this.name || this.id}" was not added to a scene.`);
85!
153
          }
154
        }, 5000);
155
      }
156
    }
157
  }
158

159
  /**
160
   * The current scene that the entity is in, if any
161
   */
162
  public scene: Scene | null = null;
7,536✔
163

164
  /**
165
   * Whether this entity is active, if set to false it will be reclaimed
166
   * @deprecated use isActive
167
   */
168
  public get active(): boolean {
169
    return this.isActive;
1✔
170
  }
171

172
  /**
173
   * Whether this entity is active, if set to false it will be reclaimed
174
   * @deprecated use isActive
175
   */
176
  public set active(val: boolean) {
177
    this.isActive = val;
1✔
178
  }
179

180
  /**
181
   * Whether this entity is active, if set to false it will be reclaimed
182
   */
183
  public isActive: boolean = true;
7,536✔
184

185
  /**
186
   * Kill the entity, means it will no longer be updated. Kills are deferred to the end of the update.
187
   * If parented it will be removed from the parent when killed.
188
   */
189
  public kill() {
190
    if (this.isActive) {
30!
191
      this.isActive = false;
30✔
192
      this.unparent();
30✔
193
    }
194
    this.emit('kill', new KillEvent(this));
30✔
195
  }
196

197
  public isKilled() {
198
    return !this.isActive;
2✔
199
  }
200

201
  /**
202
   * Specifically get the tags on the entity from {@apilink TagsComponent}
203
   */
204
  public get tags(): Set<string> {
205
    return this._tags;
30✔
206
  }
207

208
  /**
209
   * Check if a tag exists on the entity
210
   * @param tag name to check for
211
   */
212
  public hasTag(tag: string): boolean {
213
    return this._tags.has(tag);
5,022✔
214
  }
215

216
  /**
217
   * Adds a tag to an entity
218
   * @param tag
219
   */
220
  public addTag(tag: string): Entity<TKnownComponents> {
221
    this._tags.add(tag);
58✔
222
    this.tagAdded$.notifyAll(tag);
58✔
223
    return this;
58✔
224
  }
225

226
  /**
227
   * Removes a tag on the entity
228
   *
229
   * Removals are deferred until the end of update
230
   * @param tag
231
   */
232
  public removeTag(tag: string): Entity<TKnownComponents> {
233
    this._tags.delete(tag);
4✔
234
    this.tagRemoved$.notifyAll(tag);
4✔
235
    return this;
4✔
236
  }
237

238
  /**
239
   * The types of the components on the Entity
240
   */
241
  public get types(): ComponentCtor[] {
242
    return Array.from(this.components.keys()) as ComponentCtor[];
24✔
243
  }
244

245
  /**
246
   * Returns all component instances on entity
247
   */
248
  public getComponents(): Component[] {
249
    return Array.from(this.components.values());
8✔
250
  }
251

252
  /**
253
   * Verifies that an entity has all the required types
254
   * @param requiredTypes
255
   */
256
  hasAll<TComponent extends Component>(requiredTypes: ComponentCtor<TComponent>[]): boolean {
UNCOV
257
    for (let i = 0; i < requiredTypes.length; i++) {
×
UNCOV
258
      if (!this.components.has(requiredTypes[i])) {
×
UNCOV
259
        return false;
×
260
      }
261
    }
UNCOV
262
    return true;
×
263
  }
264

265
  /**
266
   * Verifies that an entity has all the required tags
267
   * @param requiredTags
268
   */
269
  hasAllTags(requiredTags: string[]): boolean {
270
    for (let i = 0; i < requiredTags.length; i++) {
13✔
271
      if (!this.tags.has(requiredTags[i])) {
26✔
272
        return false;
2✔
273
      }
274
    }
275
    return true;
11✔
276
  }
277

278
  get<TComponent extends Component>(type: ComponentCtor<TComponent>): MaybeKnownComponent<TComponent, TKnownComponents> {
279
    return this.components.get(type) as MaybeKnownComponent<TComponent, TKnownComponents>;
68,490✔
280
  }
281

282
  private _parent: Entity | null = null;
7,536✔
283
  public get parent(): Entity | null {
284
    return this._parent;
15,217✔
285
  }
286

287
  public childrenAdded$ = new Observable<Entity>();
7,536✔
288
  public childrenRemoved$ = new Observable<Entity>();
7,536✔
289

290
  private _children: Entity[] = [];
7,536✔
291
  /**
292
   * Get the direct children of this entity
293
   */
294
  public get children(): readonly Entity[] {
295
    return this._children;
20,001✔
296
  }
297

298
  /**
299
   * Unparents this entity, if there is a parent. Otherwise it does nothing.
300
   */
301
  public unparent() {
302
    if (this._parent) {
31✔
303
      this._parent.removeChild(this);
7✔
304
      this._parent = null;
7✔
305
    }
306
  }
307

308
  /**
309
   * Adds an entity to be a child of this entity
310
   * @param entity
311
   */
312
  public addChild(entity: Entity): Entity {
313
    if (entity.parent === null) {
2,969✔
314
      if (this.getAncestors().includes(entity)) {
2,968✔
315
        throw new Error('Cycle detected, cannot add entity');
1✔
316
      }
317
      this._children.push(entity);
2,967✔
318
      entity._parent = this;
2,967✔
319
      this.childrenAdded$.notifyAll(entity);
2,967✔
320
    } else {
321
      throw new Error('Entity already has a parent, cannot add without unparenting');
1✔
322
    }
323
    return this;
2,967✔
324
  }
325

326
  /**
327
   * Remove an entity from children if it exists
328
   * @param entity
329
   */
330
  public removeChild(entity: Entity): Entity {
331
    if (entity.parent === this) {
22!
332
      removeItemFromArray(entity, this._children);
22✔
333
      entity._parent = null;
22✔
334
      this.childrenRemoved$.notifyAll(entity);
22✔
335
    }
336
    return this;
22✔
337
  }
338

339
  /**
340
   * Removes all children from this entity
341
   */
342
  public removeAllChildren(): Entity {
343
    // Avoid modifying the array issue by walking backwards
344
    for (let i = this.children.length - 1; i >= 0; i--) {
1✔
345
      this.removeChild(this.children[i]);
6✔
346
    }
347
    return this;
1✔
348
  }
349

350
  /**
351
   * Returns a list of parent entities starting with the topmost parent. Includes the current entity.
352
   */
353
  public getAncestors(): Entity[] {
354
    const result: Entity[] = [this];
7,808✔
355
    let current = this.parent;
7,808✔
356
    while (current) {
7,808✔
357
      result.push(current);
2,052✔
358
      current = current.parent;
2,052✔
359
    }
360
    return result.reverse();
7,808✔
361
  }
362

363
  /**
364
   * Returns a list of all the entities that descend from this entity. Includes the current entity.
365
   */
366
  public getDescendants(): Entity[] {
367
    let result: Entity[] = [this];
1✔
368
    let queue: Entity[] = [this];
1✔
369
    while (queue.length > 0) {
1✔
370
      const curr = queue.pop();
3✔
371
      if (curr) {
3!
372
        queue = queue.concat(curr.children);
3✔
373
        result = result.concat(curr.children);
3✔
374
      }
375
    }
376
    return result;
1✔
377
  }
378

379
  /**
380
   * Creates a deep copy of the entity and a copy of all its components
381
   */
382
  public clone(): Entity {
383
    const newEntity = new Entity();
10✔
384
    for (const c of this.types) {
10✔
385
      const componentInstance = this.get(c);
14✔
386
      if (componentInstance) {
14!
387
        newEntity.addComponent(componentInstance.clone());
14✔
388
      }
389
    }
390
    for (const child of this.children) {
10✔
391
      newEntity.addChild(child.clone());
2✔
392
    }
393
    return newEntity;
10✔
394
  }
395

396
  /**
397
   * Adds a copy of all the components from another template entity as a "prefab"
398
   * @param templateEntity Entity to use as a template
399
   * @param force Force component replacement if it already exists on the target entity
400
   */
401
  public addTemplate(templateEntity: Entity, force: boolean = false): Entity {
3✔
402
    for (const c of templateEntity.getComponents()) {
3✔
403
      this.addComponent(c.clone(), force);
5✔
404
    }
405
    for (const child of templateEntity.children) {
3✔
406
      this.addChild(child.clone().addTemplate(child));
2✔
407
    }
408
    return this;
3✔
409
  }
410

411
  private _getClassHierarchyRoot(componentType: ComponentCtor): ComponentCtor {
412
    let current = componentType;
25,881✔
413
    let parent = Object.getPrototypeOf(current.prototype)?.constructor;
25,881!
414

415
    while (parent && parent !== Object && parent !== Component) {
25,881✔
416
      current = parent;
2✔
417
      parent = Object.getPrototypeOf(current.prototype)?.constructor;
2!
418
    }
419
    return current;
25,881✔
420
  }
421

422
  /**
423
   * Adds a component to the entity
424
   * @param component Component or Entity to add copy of components from
425
   * @param force Optionally overwrite any existing components of the same type
426
   */
427
  public addComponent<TComponent extends Component>(component: TComponent, force: boolean = false): Entity<TKnownComponents | TComponent> {
29,455✔
428
    // if component already exists, skip if not forced
429
    if (this.has(component.constructor as ComponentCtor)) {
29,480✔
430
      if (force) {
3,638✔
431
        // Remove existing component type if exists when forced
432
        this.removeComponent(component.constructor as ComponentCtor, true);
7✔
433
      } else {
434
        // early exit component exits
435
        return this as Entity<TKnownComponents | TComponent>;
3,631✔
436
      }
437
    }
438

439
    // TODO circular dependencies will be a problem
440
    if (component.dependencies && component.dependencies.length) {
25,849✔
441
      for (const ctor of component.dependencies) {
1,823✔
442
        this.addComponent(new ctor());
3,645✔
443
      }
444
    }
445

446
    component.owner = this;
25,849✔
447
    const rootComponent = this._getClassHierarchyRoot(component.constructor as ComponentCtor);
25,849✔
448
    this.components.set(rootComponent, component);
25,849✔
449
    this.components.set(component.constructor, component);
25,849✔
450
    this.componentValues.push(component);
25,849✔
451
    if (component.onAdd) {
25,849✔
452
      component.onAdd(this);
10,150✔
453
    }
454

455
    this.componentAdded$.notifyAll(component);
25,849✔
456
    return this as Entity<TKnownComponents | TComponent>;
25,849✔
457
  }
458

459
  /**
460
   * Removes a component from the entity, by default removals are deferred to the end of entity update to avoid consistency issues
461
   *
462
   * Components can be force removed with the `force` flag, the removal is not deferred and happens immediately
463
   * @param typeOrInstance
464
   * @param force
465
   */
466
  public removeComponent<TComponent extends Component>(
467
    typeOrInstance: ComponentCtor<TComponent> | TComponent,
468
    force = false
20✔
469
  ): Entity<Exclude<TKnownComponents, TComponent>> {
470
    let type: ComponentCtor<TComponent>;
471
    if (isComponentCtor(typeOrInstance)) {
52✔
472
      type = typeOrInstance;
51✔
473
    } else {
474
      type = typeOrInstance.constructor as ComponentCtor<TComponent>;
1✔
475
    }
476

477
    if (force) {
52✔
478
      const componentToRemove = this.components.get(type);
32✔
479
      if (componentToRemove) {
32!
480
        this.componentRemoved$.notifyAll(componentToRemove);
32✔
481
        componentToRemove.owner = undefined;
32✔
482
        if (componentToRemove.onRemove) {
32✔
483
          componentToRemove.onRemove(this);
8✔
484
        }
485
        const componentIndex = this.componentValues.indexOf(componentToRemove);
32✔
486
        if (componentIndex > -1) {
32!
487
          this.componentValues.splice(componentIndex, 1);
32✔
488
        }
489
      }
490

491
      const rootComponent = this._getClassHierarchyRoot(type);
32✔
492
      this.components.delete(rootComponent);
32✔
493
      this.components.delete(type); // remove after the notify to preserve typing
32✔
494
    } else {
495
      this._componentsToRemove.push(type);
20✔
496
    }
497

498
    return this as any;
52✔
499
  }
500

501
  public clearComponents() {
502
    const components = this.types;
2✔
503
    for (const c of components) {
2✔
504
      this.removeComponent(c);
14✔
505
    }
506
  }
507

508
  /**
509
   * @hidden
510
   * @internal
511
   */
512
  public processComponentRemoval() {
513
    for (const type of this._componentsToRemove) {
5,628✔
514
      this.removeComponent(type, true);
18✔
515
    }
516
    this._componentsToRemove.length = 0;
5,628✔
517
  }
518

519
  /**
520
   * Check if a component type exists
521
   * @param type
522
   */
523
  public has<TComponent extends Component>(type: ComponentCtor<TComponent>): boolean {
524
    return this.components.has(type);
52,694✔
525
  }
526

527
  private _isInitialized = false;
7,536✔
528
  private _isAdded = false;
7,536✔
529

530
  /**
531
   * Gets whether the actor is Initialized
532
   */
533
  public get isInitialized(): boolean {
534
    return this._isInitialized;
3,487✔
535
  }
536

537
  public get isAdded(): boolean {
538
    return this._isAdded;
6,192✔
539
  }
540

541
  /**
542
   * Initializes this entity, meant to be called by the Scene before first update not by users of Excalibur.
543
   *
544
   * It is not recommended that internal excalibur methods be overridden, do so at your own risk.
545
   * @internal
546
   */
547
  public _initialize(engine: Engine) {
548
    if (!this.isInitialized) {
3,353✔
549
      this.onInitialize(engine);
1,309✔
550
      this.events.emit('initialize', new InitializeEvent(engine, this));
1,309✔
551
      this._isInitialized = true;
1,309✔
552
    }
553
  }
554

555
  /**
556
   * Adds this Actor, meant to be called by the Scene when Actor is added.
557
   *
558
   * It is not recommended that internal excalibur methods be overridden, do so at your own risk.
559
   * @internal
560
   */
561
  public _add(engine: Engine) {
562
    if (!this.isAdded && this.isActive) {
3,096✔
563
      this.onAdd(engine);
1,271✔
564
      this.events.emit('add', new AddEvent(engine, this));
1,271✔
565
      this._isAdded = true;
1,271✔
566
    }
567
  }
568

569
  /**
570
   * Removes Actor, meant to be called by the Scene when Actor is added.
571
   *
572
   * It is not recommended that internal excalibur methods be overridden, do so at your own risk.
573
   * @internal
574
   */
575
  public _remove(engine: Engine) {
576
    if (this.isAdded && !this.isActive) {
3,096✔
577
      this.onRemove(engine);
5✔
578
      this.events.emit('remove', new RemoveEvent(engine, this));
5✔
579
      this._isAdded = false;
5✔
580
    }
581
  }
582

583
  /**
584
   * It is not recommended that internal excalibur methods be overridden, do so at your own risk.
585
   *
586
   * Internal _preupdate handler for {@apilink onPreUpdate} lifecycle event
587
   * @internal
588
   */
589
  public _preupdate(engine: Engine, elapsed: number): void {
590
    this.events.emit('preupdate', new PreUpdateEvent(engine, elapsed, this));
923✔
591
    this.onPreUpdate(engine, elapsed);
923✔
592
  }
593

594
  /**
595
   * It is not recommended that internal excalibur methods be overridden, do so at your own risk.
596
   *
597
   * Internal _preupdate handler for {@apilink onPostUpdate} lifecycle event
598
   * @internal
599
   */
600
  public _postupdate(engine: Engine, elapsed: number): void {
601
    this.events.emit('postupdate', new PostUpdateEvent(engine, elapsed, this));
923✔
602
    this.onPostUpdate(engine, elapsed);
923✔
603
  }
604

605
  /**
606
   * `onInitialize` is called before the first update of the entity. This method is meant to be
607
   * overridden.
608
   *
609
   * Synonymous with the event handler `.on('initialize', (evt) => {...})`
610
   */
611
  public onInitialize(engine: Engine): void {
612
    // Override me
613
  }
614

615
  /**
616
   * `onAdd` is called when Actor is added to scene. This method is meant to be
617
   * overridden.
618
   *
619
   * Synonymous with the event handler `.on('add', (evt) => {...})`
620
   */
621
  public onAdd(engine: Engine): void {
622
    // Override me
623
  }
624

625
  /**
626
   * `onRemove` is called when Actor is added to scene. This method is meant to be
627
   * overridden.
628
   *
629
   * Synonymous with the event handler `.on('remove', (evt) => {...})`
630
   */
631
  public onRemove(engine: Engine): void {
632
    // Override me
633
  }
634

635
  /**
636
   * Safe to override onPreUpdate lifecycle event handler. Synonymous with `.on('preupdate', (evt) =>{...})`
637
   *
638
   * `onPreUpdate` is called directly before an entity is updated.
639
   */
640
  public onPreUpdate(engine: Engine, elapsed: number): void {
641
    // Override me
642
  }
643

644
  /**
645
   * Safe to override onPostUpdate lifecycle event handler. Synonymous with `.on('postupdate', (evt) =>{...})`
646
   *
647
   * `onPostUpdate` is called directly after an entity is updated.
648
   */
649
  public onPostUpdate(engine: Engine, elapsed: number): void {
650
    // Override me
651
  }
652

653
  /**
654
   *
655
   * Entity update lifecycle, called internally
656
   * @internal
657
   * @param engine
658
   * @param elapsed
659
   */
660
  public update(engine: Engine, elapsed: number): void {
661
    this._initialize(engine);
921✔
662
    this._add(engine);
921✔
663
    this._preupdate(engine, elapsed);
921✔
664
    for (const child of this.children) {
921✔
665
      child.update(engine, elapsed);
×
666
    }
667
    this._postupdate(engine, elapsed);
921✔
668
    this._remove(engine);
921✔
669
  }
670

671
  public emit<TEventName extends EventKey<EntityEvents>>(eventName: TEventName, event: EntityEvents[TEventName]): void;
672
  public emit(eventName: string, event?: any): void;
673
  public emit<TEventName extends EventKey<EntityEvents> | string>(eventName: TEventName, event?: any): void {
674
    this.events.emit(eventName, event);
8✔
675
  }
676

677
  public on<TEventName extends EventKey<EntityEvents>>(eventName: TEventName, handler: Handler<EntityEvents[TEventName]>): Subscription;
678
  public on(eventName: string, handler: Handler<unknown>): Subscription;
679
  public on<TEventName extends EventKey<EntityEvents> | string>(eventName: TEventName, handler: Handler<any>): Subscription {
680
    return this.events.on(eventName, handler);
9✔
681
  }
682

683
  public once<TEventName extends EventKey<EntityEvents>>(eventName: TEventName, handler: Handler<EntityEvents[TEventName]>): Subscription;
684
  public once(eventName: string, handler: Handler<unknown>): Subscription;
685
  public once<TEventName extends EventKey<EntityEvents> | string>(eventName: TEventName, handler: Handler<any>): Subscription {
686
    return this.events.once(eventName, handler);
×
687
  }
688

689
  public off<TEventName extends EventKey<EntityEvents>>(eventName: TEventName, handler: Handler<EntityEvents[TEventName]>): void;
690
  public off(eventName: string, handler: Handler<unknown>): void;
691
  public off(eventName: string): void;
692
  public off<TEventName extends EventKey<EntityEvents> | string>(eventName: TEventName, handler?: Handler<any>): void {
693
    if (handler) {
×
694
      this.events.off(eventName, handler);
×
695
    } else {
696
      this.events.off(eventName);
×
697
    }
698
  }
699
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc