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

excaliburjs / Excalibur / 21539417834

31 Jan 2026 05:22AM UTC coverage: 88.61% (-0.1%) from 88.757%
21539417834

Pull #3670

github

web-flow
Merge 7773440cb into 8cba88917
Pull Request #3670: feat: Implement Plugin System

5402 of 7377 branches covered (73.23%)

17 of 47 new or added lines in 4 files covered. (36.17%)

86 existing lines in 4 files now uncovered.

14882 of 16795 relevant lines covered (88.61%)

24427.76 hits per line

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

85.46
/src/engine/engine.ts
1
import { EX_VERSION } from './';
2
import { Future } from './util/future';
3
import type { EventKey, Handler, Subscription } from './event-emitter';
4
import { EventEmitter } from './event-emitter';
5
import { PointerScope } from './input/pointer-scope';
6
import { Flags } from './flags';
7
import { polyfill } from './polyfill';
8
polyfill();
248✔
9
import type { CanUpdate, CanDraw, CanInitialize } from './interfaces/lifecycle-events';
10
import type { Vector } from './math/vector';
11
import type { ViewportDimension } from './screen';
12
import { Screen, DisplayMode, Resolution } from './screen';
13
import type { ScreenElement } from './screen-element';
14
import type { Actor } from './actor';
15
import type { Timer } from './timer';
16
import type { TileMap } from './tile-map';
17
import { DefaultLoader } from './director/default-loader';
18
import { Loader } from './director/loader';
19
import { Detector } from './util/detector';
20
import {
21
  VisibleEvent,
22
  HiddenEvent,
23
  GameStartEvent,
24
  GameStopEvent,
25
  PreUpdateEvent,
26
  PostUpdateEvent,
27
  PreFrameEvent,
28
  PostFrameEvent,
29
  PreDrawEvent,
30
  PostDrawEvent,
31
  InitializeEvent
32
} from './events';
33
import { Logger, LogLevel } from './util/log';
34
import { Color } from './color';
35
import type { SceneConstructor } from './scene';
36
import { Scene, isSceneConstructor } from './scene';
37
import { Entity } from './entity-component-system/entity';
38
import type { DebugStats } from './debug/debug-config';
39
import { DebugConfig } from './debug/debug-config';
40
import { BrowserEvents } from './util/browser';
41
import type { AntialiasOptions, ExcaliburGraphicsContext, ExcaliburGraphicsContextWebGLOptions } from './graphics';
42
import {
43
  DefaultAntialiasOptions,
44
  DefaultPixelArtOptions,
45
  ExcaliburGraphicsContext2DCanvas,
46
  ExcaliburGraphicsContextWebGL,
47
  TextureLoader
48
} from './graphics';
49
import type { Clock } from './util/clock';
50
import { StandardClock } from './util/clock';
51
import { ImageFiltering } from './graphics/filtering';
52
import { GraphicsDiagnostics } from './graphics/graphics-diagnostics';
53
import { Toaster } from './util/toaster';
54
import type { InputMapper } from './input/input-mapper';
55
import type { GoToOptions, SceneMap, StartOptions, SceneWithOptions, WithRoot } from './director/director';
56
import { Director, DirectorEvents } from './director/director';
57
import { InputHost } from './input/input-host';
58
import type { PhysicsConfig } from './collision/physics-config';
59
import { getDefaultPhysicsConfig } from './collision/physics-config';
60
import type { DeepRequired } from './util/required';
61
import type { Context } from './context';
62
import { createContext, useContext } from './context';
63
import type { GarbageCollectionOptions } from './garbage-collector';
64
import { DefaultGarbageCollectionOptions, GarbageCollector } from './garbage-collector';
65
import { mergeDeep } from './util/util';
66
import { getDefaultGlobal } from './util/iframe';
67
import type { Plugin } from './plugin';
68

69
export interface EngineEvents extends DirectorEvents {
70
  fallbackgraphicscontext: ExcaliburGraphicsContext2DCanvas;
71
  initialize: InitializeEvent<Engine>;
72
  visible: VisibleEvent;
73
  hidden: HiddenEvent;
74
  start: GameStartEvent;
75
  stop: GameStopEvent;
76
  preupdate: PreUpdateEvent<Engine>;
77
  postupdate: PostUpdateEvent<Engine>;
78
  preframe: PreFrameEvent;
79
  postframe: PostFrameEvent;
80
  predraw: PreDrawEvent;
81
  postdraw: PostDrawEvent;
82
}
83

84
export const EngineEvents = {
248✔
85
  FallbackGraphicsContext: 'fallbackgraphicscontext',
86
  Initialize: 'initialize',
87
  Visible: 'visible',
88
  Hidden: 'hidden',
89
  Start: 'start',
90
  Stop: 'stop',
91
  PreUpdate: 'preupdate',
92
  PostUpdate: 'postupdate',
93
  PreFrame: 'preframe',
94
  PostFrame: 'postframe',
95
  PreDraw: 'predraw',
96
  PostDraw: 'postdraw',
97
  ...DirectorEvents
98
} as const;
99

100
/**
101
 * Enum representing the different mousewheel event bubble prevention
102
 */
103
export enum ScrollPreventionMode {
248✔
104
  /**
105
   * Do not prevent any page scrolling
106
   */
107
  None,
248✔
108
  /**
109
   * Prevent page scroll if mouse is over the game canvas
110
   */
111
  Canvas,
248✔
112
  /**
113
   * Prevent all page scrolling via mouse wheel
114
   */
115
  All
248✔
116
}
117

118
/**
119
 * Defines the available options to configure the Excalibur engine at constructor time.
120
 */
121
export interface EngineOptions<TKnownScenes extends string = any> {
122
  /**
123
   * Optionally configure Excalibur plugins
124
   */
125
  plugins?: Plugin[];
126
  /**
127
   * Optionally configure the width of the viewport in css pixels
128
   */
129
  width?: number;
130

131
  /**
132
   * Optionally configure the height of the viewport in css pixels
133
   */
134
  height?: number;
135

136
  /**
137
   * Optionally configure the width & height of the viewport in css pixels.
138
   * Use `viewport` instead of {@apilink EngineOptions.width} and {@apilink EngineOptions.height}, or vice versa.
139
   */
140
  viewport?: ViewportDimension;
141

142
  /**
143
   * Optionally specify the size the logical pixel resolution, if not specified it will be width x height.
144
   * See {@apilink Resolution} for common presets.
145
   */
146
  resolution?: Resolution;
147

148
  /**
149
   * Optionally specify antialiasing (smoothing), by default true (smooth pixels)
150
   *
151
   *  * `true` - useful for high resolution art work you would like smoothed, this also hints excalibur to load images
152
   * with default blending {@apilink ImageFiltering.Blended}
153
   *
154
   *  * `false` - useful for pixel art style art work you would like sharp, this also hints excalibur to load images
155
   * with default blending {@apilink ImageFiltering.Pixel}
156
   *
157
   * * {@apilink AntialiasOptions} Optionally deeply configure the different antialiasing settings, **WARNING** thar be dragons here.
158
   * It is recommended you stick to `true` or `false` unless you understand what you're doing and need to control rendering to
159
   * a high degree.
160
   */
161
  antialiasing?: boolean | AntialiasOptions;
162

163
  /**
164
   * Optionally specify excalibur garbage collection, by default true.
165
   *
166
   * * `true` - garbage collection defaults are enabled (default)
167
   *
168
   * * `false` - garbage collection is completely disabled (not recommended)
169
   *
170
   * * {@apilink GarbageCollectionOptions} Optionally deeply configure garbage collection settings, **WARNING** thar be dragons here.
171
   * It is recommended you stick to `true` or `false` unless you understand what you're doing, it is possible to get into a downward
172
   * spiral if collection timings are set too low where you are stuck in repeated collection.
173
   */
174
  garbageCollection?: boolean | GarbageCollectionOptions;
175

176
  /**
177
   * Quick convenience property to configure Excalibur to use special settings for "pretty" anti-aliased pixel art
178
   *
179
   * 1. Turns on special shader condition to blend for pixel art and enables various antialiasing settings,
180
   *  notice blending is ON for this special mode.
181
   *
182
   * Equivalent to:
183
   * ```javascript
184
   * antialiasing: {
185
   *  pixelArtSampler: true,
186
   *  canvasImageRendering: 'auto',
187
   *  filtering: ImageFiltering.Blended,
188
   *  webglAntialiasing: true
189
   * }
190
   * ```
191
   */
192
  pixelArt?: boolean;
193

194
  /**
195
   * Specify any UV padding you want use in pixels, this brings sampling into the texture if you're using
196
   * a sprite sheet in one image to prevent sampling bleed.
197
   *
198
   * Defaults:
199
   * * `antialiasing: false` or `filtering: ImageFiltering.Pixel` - 0.0;
200
   * * `pixelArt: true` - 0.25
201
   * * All else 0.01
202
   */
203
  uvPadding?: number;
204

205
  /**
206
   * Optionally hint the graphics context into a specific power profile
207
   *
208
   * Default "high-performance"
209
   */
210
  powerPreference?: 'default' | 'high-performance' | 'low-power';
211

212
  /**
213
   * Optionally upscale the number of pixels in the canvas. Normally only useful if you need a smoother look to your assets, especially
214
   * {@apilink Text} or Pixel Art assets.
215
   *
216
   * **WARNING** It is recommended you try using `antialiasing: true` before adjusting pixel ratio. Pixel ratio will consume more memory
217
   * and on mobile may break if the internal size of the canvas exceeds 4k pixels in width or height.
218
   *
219
   * Default is based the display's pixel ratio, for example a HiDPI screen might have the value 2;
220
   */
221
  pixelRatio?: number;
222

223
  /**
224
   * Optionally configure the native canvas transparent backdrop
225
   */
226
  enableCanvasTransparency?: boolean;
227

228
  /**
229
   * Optionally specify the target canvas DOM element to render the game in
230
   */
231
  canvasElementId?: string;
232

233
  /**
234
   * Optionally specify the target canvas DOM element directly
235
   */
236
  canvasElement?: HTMLCanvasElement;
237

238
  /**
239
   * Optionally enable the right click context menu on the canvas
240
   *
241
   * Default if unset is false
242
   */
243
  enableCanvasContextMenu?: boolean;
244

245
  /**
246
   * Optionally snap graphics to nearest pixel, default is false
247
   */
248
  snapToPixel?: boolean;
249

250
  /**
251
   * The {@apilink DisplayMode} of the game, by default {@apilink DisplayMode.FitScreen} with aspect ratio 4:3 (800x600).
252
   * Depending on this value, {@apilink width} and {@apilink height} may be ignored.
253
   */
254
  displayMode?: DisplayMode;
255

256
  /**
257
   * Optionally configure the global, or a factory to produce it to listen to for browser events for Excalibur to listen to
258
   */
259
  global?: GlobalEventHandlers | (() => GlobalEventHandlers);
260

261
  /**
262
   * Configures the pointer scope. Pointers scoped to the 'Canvas' can only fire events within the canvas viewport; whereas, 'Document'
263
   * (default) scoped will fire anywhere on the page.
264
   */
265
  pointerScope?: PointerScope;
266

267
  /**
268
   * Suppress boot up console message, which contains the "powered by Excalibur message"
269
   */
270
  suppressConsoleBootMessage?: boolean;
271

272
  /**
273
   * Suppress minimum browser feature detection, it is not recommended users of excalibur switch this off. This feature ensures that
274
   * the currently running browser meets the minimum requirements for running excalibur. This can be useful if running on non-standard
275
   * browsers or if there is a bug in excalibur preventing execution.
276
   */
277
  suppressMinimumBrowserFeatureDetection?: boolean;
278

279
  /**
280
   * Suppress HiDPI auto detection and scaling, it is not recommended users of excalibur switch off this feature. This feature detects
281
   * and scales the drawing canvas appropriately to accommodate HiDPI screens.
282
   */
283
  suppressHiDPIScaling?: boolean;
284

285
  /**
286
   * Suppress play button, it is not recommended users of excalibur switch this feature. Some browsers require a user gesture (like a click)
287
   * for certain browser features to work like web audio.
288
   */
289
  suppressPlayButton?: boolean;
290

291
  /**
292
   * Sets the focus of the window, this is needed when hosting excalibur in a cross-origin/same-origin iframe in order for certain events
293
   * (like keyboard) to work. You can use
294
   * For example: itch.io or codesandbox.io
295
   *
296
   * By default set to true,
297
   */
298
  grabWindowFocus?: boolean;
299

300
  /**
301
   * Scroll prevention method.
302
   */
303
  scrollPreventionMode?: ScrollPreventionMode;
304

305
  /**
306
   * Optionally set the background color
307
   */
308
  backgroundColor?: Color;
309

310
  /**
311
   * Optionally set the maximum fps if not set Excalibur will go as fast as the device allows.
312
   *
313
   * You may want to constrain max fps if your game cannot maintain fps consistently, it can look and feel better to have a 30fps game than
314
   * one that bounces between 30fps and 60fps
315
   */
316
  maxFps?: number;
317

318
  /**
319
   * Optionally configure a fixed update timestep in milliseconds, this can be desirable if you need the physics simulation to be very stable. When
320
   * set the update step and physics will use the same elapsed time for each tick even if the graphical framerate drops. In order for the
321
   * simulation to be correct, excalibur will run multiple updates in a row (at the configured update elapsed) to catch up, for example
322
   * there could be X updates and 1 draw each clock step.
323
   *
324
   * **NOTE:** This does come at a potential perf cost because each catch-up update will need to be run if the fixed rate is greater than
325
   * the current instantaneous framerate, or perf gain if the fixed rate is less than the current framerate.
326
   *
327
   * By default is unset and updates will use the current instantaneous framerate with 1 update and 1 draw each clock step.
328
   *
329
   * **WARN:** `fixedUpdateTimestep` takes precedence over `fixedUpdateFps` use whichever is most convenient.
330
   */
331
  fixedUpdateTimestep?: number;
332

333
  /**
334
   * Optionally configure a fixed update fps, this can be desirable if you need the physics simulation to be very stable. When set
335
   * the update step and physics will use the same elapsed time for each tick even if the graphical framerate drops. In order for the
336
   * simulation to be correct, excalibur will run multiple updates in a row (at the configured update elapsed) to catch up, for example
337
   * there could be X updates and 1 draw each clock step.
338
   *
339
   * **NOTE:** This does come at a potential perf cost because each catch-up update will need to be run if the fixed rate is greater than
340
   * the current instantaneous framerate, or perf gain if the fixed rate is less than the current framerate.
341
   *
342
   * By default is unset and updates will use the current instantaneous framerate with 1 update and 1 draw each clock step.
343
   *
344
   * **WARN:** `fixedUpdateTimestep` takes precedence over `fixedUpdateFps` use whichever is most convenient.
345
   */
346
  fixedUpdateFps?: number;
347

348
  /**
349
   * Default `true`, optionally configure excalibur to use optimal draw call sorting, to opt out set this to `false`.
350
   *
351
   * Excalibur will automatically sort draw calls by z and priority into renderer batches for maximal draw performance,
352
   * this can disrupt a specific desired painter order.
353
   *
354
   */
355
  useDrawSorting?: boolean;
356

357
  /**
358
   * Optionally provide a custom handler for the webgl context lost event
359
   */
360
  handleContextLost?: (e: Event) => void;
361

362
  /**
363
   * Optionally provide a custom handler for the webgl context restored event
364
   */
365
  handleContextRestored?: (e: Event) => void;
366

367
  /**
368
   * Optionally configure how excalibur handles poor performance on a player's browser
369
   */
370
  configurePerformanceCanvas2DFallback?: {
371
    /**
372
     * By default `false`, this will switch the internal graphics context to Canvas2D which can improve performance on non hardware
373
     * accelerated browsers.
374
     */
375
    allow: boolean;
376
    /**
377
     * By default `false`, if set to `true` a dialogue will be presented to the player about their browser and how to potentially
378
     * address any issues.
379
     */
380
    showPlayerMessage?: boolean;
381
    /**
382
     * Default `{ numberOfFrames: 100, fps: 20 }`, optionally configure excalibur to fallback to the 2D Canvas renderer
383
     * if bad performance is detected.
384
     *
385
     * In this example of the default if excalibur is running at 20fps or less for 100 frames it will trigger the fallback to the 2D
386
     * Canvas renderer.
387
     */
388
    threshold?: { numberOfFrames: number; fps: number };
389
  };
390

391
  /**
392
   * Optionally configure the physics simulation in excalibur
393
   *
394
   * If false, Excalibur will not produce a physics simulation.
395
   *
396
   * Default is configured to use {@apilink SolverStrategy.Arcade} physics simulation
397
   */
398
  physics?: boolean | PhysicsConfig;
399

400
  /**
401
   * Optionally specify scenes with their transitions and loaders to excalibur's scene {@apilink Director}
402
   *
403
   * Scene transitions can can overridden dynamically by the `Scene` or by the call to `.goToScene`
404
   */
405
  scenes?: SceneMap<TKnownScenes>;
406
}
407

408
/**
409
 * The Excalibur Engine
410
 *
411
 * The {@apilink Engine} is the main driver for a game. It is responsible for
412
 * starting/stopping the game, maintaining state, transmitting events,
413
 * loading resources, and managing the scene.
414
 */
415
export class Engine<TKnownScenes extends string = any> implements CanInitialize, CanUpdate, CanDraw {
248✔
416
  static Context: Context<Engine | null> = createContext<Engine | null>();
417
  static useEngine(): Engine {
418
    const value = useContext(Engine.Context);
20✔
419

420
    if (!value) {
20✔
421
      throw new Error('Cannot inject engine with `useEngine()`, `useEngine()` was called outside of Engine lifecycle scope.');
1✔
422
    }
423

424
    return value;
19✔
425
  }
426
  static InstanceCount = 0;
427

428
  /**
429
   * Anything run under scope can use `useEngine()` to inject the current engine
430
   * @param cb
431
   */
432
  scope = <TReturn>(cb: () => TReturn) => Engine.Context.scope(this, cb);
3,781✔
433

434
  public global: GlobalEventHandlers;
435

436
  private _plugins: Plugin[] = [];
746✔
437
  public get plugins(): readonly Plugin[] {
438
    return this._plugins;
2,132✔
439
  }
440

441
  private _garbageCollector: GarbageCollector;
442

443
  public readonly garbageCollectorConfig: GarbageCollectionOptions | null;
444

445
  /**
446
   * Current Excalibur version string
447
   *
448
   * Useful for plugins or other tools that need to know what features are available
449
   */
450
  public readonly version = EX_VERSION;
746✔
451

452
  /**
453
   * Listen to and emit events on the Engine
454
   */
455
  public events = new EventEmitter<EngineEvents>();
746✔
456

457
  /**
458
   * Excalibur browser events abstraction used for wiring to native browser events safely
459
   */
460
  public browser: BrowserEvents;
461

462
  /**
463
   * Screen abstraction
464
   */
465
  public screen: Screen;
466

467
  /**
468
   * Scene director, manages all scenes, scene transitions, and loaders in excalibur
469
   */
470
  public director: Director<TKnownScenes>;
471

472
  /**
473
   * Direct access to the engine's canvas element
474
   */
475
  public canvas: HTMLCanvasElement;
476

477
  /**
478
   * Direct access to the ExcaliburGraphicsContext used for drawing things to the screen
479
   */
480
  public graphicsContext: ExcaliburGraphicsContext;
481

482
  /**
483
   * Direct access to the canvas element ID, if an ID exists
484
   */
485
  public canvasElementId: string;
486

487
  /**
488
   * Direct access to the physics configuration for excalibur
489
   */
490
  public physics: DeepRequired<PhysicsConfig>;
491

492
  /**
493
   * Optionally set the maximum fps if not set Excalibur will go as fast as the device allows.
494
   *
495
   * You may want to constrain max fps if your game cannot maintain fps consistently, it can look and feel better to have a 30fps game than
496
   * one that bounces between 30fps and 60fps
497
   */
498
  public maxFps: number = Number.POSITIVE_INFINITY;
746✔
499

500
  /**
501
   * Optionally configure a fixed update fps, this can be desirable if you need the physics simulation to be very stable. When set
502
   * the update step and physics will use the same elapsed time for each tick even if the graphical framerate drops. In order for the
503
   * simulation to be correct, excalibur will run multiple updates in a row (at the configured update elapsed) to catch up, for example
504
   * there could be X updates and 1 draw each clock step.
505
   *
506
   * **NOTE:** This does come at a potential perf cost because each catch-up update will need to be run if the fixed rate is greater than
507
   * the current instantaneous framerate, or perf gain if the fixed rate is less than the current framerate.
508
   *
509
   * By default is unset and updates will use the current instantaneous framerate with 1 update and 1 draw each clock step.
510
   *
511
   * **WARN:** `fixedUpdateTimestep` takes precedence over `fixedUpdateFps` use whichever is most convenient.
512
   */
513
  public readonly fixedUpdateFps?: number;
514

515
  /**
516
   * Optionally configure a fixed update timestep in milliseconds, this can be desirable if you need the physics simulation to be very stable. When
517
   * set the update step and physics will use the same elapsed time for each tick even if the graphical framerate drops. In order for the
518
   * simulation to be correct, excalibur will run multiple updates in a row (at the configured update elapsed) to catch up, for example
519
   * there could be X updates and 1 draw each clock step.
520
   *
521
   * **NOTE:** This does come at a potential perf cost because each catch-up update will need to be run if the fixed rate is greater than
522
   * the current instantaneous framerate, or perf gain if the fixed rate is less than the current framerate.
523
   *
524
   * By default is unset and updates will use the current instantaneous framerate with 1 update and 1 draw each clock step.
525
   *
526
   * **WARN:** `fixedUpdateTimestep` takes precedence over `fixedUpdateFps` use whichever is most convenient.
527
   */
528
  public readonly fixedUpdateTimestep?: number;
529

530
  /**
531
   * Direct access to the excalibur clock
532
   */
533
  public clock: Clock;
534

535
  public readonly pointerScope: PointerScope;
536
  public readonly grabWindowFocus: boolean;
537

538
  /**
539
   * The width of the game canvas in pixels (physical width component of the
540
   * resolution of the canvas element)
541
   */
542
  public get canvasWidth(): number {
543
    return this.screen.canvasWidth;
837✔
544
  }
545

546
  /**
547
   * Returns half width of the game canvas in pixels (half physical width component)
548
   */
549
  public get halfCanvasWidth(): number {
550
    return this.screen.halfCanvasWidth;
3✔
551
  }
552

553
  /**
554
   * The height of the game canvas in pixels, (physical height component of
555
   * the resolution of the canvas element)
556
   */
557
  public get canvasHeight(): number {
558
    return this.screen.canvasHeight;
837✔
559
  }
560

561
  /**
562
   * Returns half height of the game canvas in pixels (half physical height component)
563
   */
564
  public get halfCanvasHeight(): number {
565
    return this.screen.halfCanvasHeight;
3✔
566
  }
567

568
  /**
569
   * Returns the width of the engine's visible drawing surface in pixels including zoom and device pixel ratio.
570
   */
571
  public get drawWidth(): number {
572
    return this.screen.drawWidth;
5✔
573
  }
574

575
  /**
576
   * Returns half the width of the engine's visible drawing surface in pixels including zoom and device pixel ratio.
577
   */
578
  public get halfDrawWidth(): number {
579
    return this.screen.halfDrawWidth;
1,905✔
580
  }
581

582
  /**
583
   * Returns the height of the engine's visible drawing surface in pixels including zoom and device pixel ratio.
584
   */
585
  public get drawHeight(): number {
586
    return this.screen.drawHeight;
5✔
587
  }
588

589
  /**
590
   * Returns half the height of the engine's visible drawing surface in pixels including zoom and device pixel ratio.
591
   */
592
  public get halfDrawHeight(): number {
593
    return this.screen.halfDrawHeight;
1,905✔
594
  }
595

596
  /**
597
   * Returns whether excalibur detects the current screen to be HiDPI
598
   */
599
  public get isHiDpi(): boolean {
600
    return this.screen.isHiDpi;
3✔
601
  }
602

603
  /**
604
   * Access engine input like pointer, keyboard, or gamepad
605
   */
606
  public input: InputHost;
607

608
  /**
609
   * Map multiple input sources to specific game actions actions
610
   */
611
  public inputMapper: InputMapper;
612

613
  private _inputEnabled: boolean = true;
746✔
614

615
  /**
616
   * Access Excalibur debugging functionality.
617
   *
618
   * Useful when you want to debug different aspects of built in engine features like
619
   *   * Transform
620
   *   * Graphics
621
   *   * Colliders
622
   */
623
  public debug: DebugConfig;
624

625
  /**
626
   * Access {@apilink stats} that holds frame statistics.
627
   */
628
  public get stats(): DebugStats {
629
    return this.debug.stats;
29,395✔
630
  }
631

632
  /**
633
   * The current {@apilink Scene} being drawn and updated on screen
634
   */
635
  public get currentScene(): Scene {
636
    return this.director.currentScene;
7,845✔
637
  }
638

639
  /**
640
   * The current {@apilink Scene} being drawn and updated on screen
641
   */
642
  public get currentSceneName(): string {
643
    return this.director.currentSceneName;
34✔
644
  }
645

646
  /**
647
   * The default {@apilink Scene} of the game, use {@apilink Engine.goToScene} to transition to different scenes.
648
   */
649
  public get rootScene(): Scene {
650
    return this.director.rootScene;
1✔
651
  }
652

653
  /**
654
   * Contains all the scenes currently registered with Excalibur
655
   */
656
  public get scenes(): { [key: string]: Scene | SceneConstructor | SceneWithOptions } {
657
    return this.director.scenes;
4✔
658
  }
659

660
  /**
661
   * Indicates whether the engine is set to fullscreen or not
662
   */
663
  public get isFullscreen(): boolean {
664
    return this.screen.isFullScreen;
1✔
665
  }
666

667
  /**
668
   * Indicates the current {@apilink DisplayMode} of the engine.
669
   */
670
  public get displayMode(): DisplayMode {
UNCOV
671
    return this.screen.displayMode;
×
672
  }
673

674
  private _suppressPlayButton: boolean = false;
746✔
675
  /**
676
   * Returns the calculated pixel ration for use in rendering
677
   */
678
  public get pixelRatio(): number {
679
    return this.screen.pixelRatio;
1,673✔
680
  }
681

682
  /**
683
   * Indicates whether audio should be paused when the game is no longer visible.
684
   */
685
  public pauseAudioWhenHidden: boolean = true;
746✔
686

687
  /**
688
   * Indicates whether the engine should draw with debug information
689
   */
690
  private _isDebug: boolean = false;
746✔
691
  public get isDebug(): boolean {
692
    return this._isDebug;
3,887✔
693
  }
694

695
  /**
696
   * Sets the background color for the engine.
697
   */
698
  public backgroundColor: Color;
699

700
  /**
701
   * Sets the Transparency for the engine.
702
   */
703
  public enableCanvasTransparency: boolean = true;
746✔
704

705
  /**
706
   * Hints the graphics context to truncate fractional world space coordinates
707
   */
708
  public get snapToPixel(): boolean {
709
    return this.graphicsContext.snapToPixel;
2✔
710
  }
711

712
  public set snapToPixel(shouldSnapToPixel: boolean) {
713
    this.graphicsContext.snapToPixel = shouldSnapToPixel;
1✔
714
  }
715

716
  /**
717
   * The action to take when a fatal exception is thrown
718
   */
719
  public onFatalException = (e: any) => {
746✔
UNCOV
720
    Logger.getInstance().fatal(e, e.stack);
×
721
  };
722

723
  /**
724
   * The mouse wheel scroll prevention mode
725
   */
726
  public pageScrollPreventionMode: ScrollPreventionMode;
727

728
  private _logger: Logger;
729

730
  private _toaster: Toaster = new Toaster();
746✔
731

732
  // this determines whether excalibur is compatible with your browser
733
  private _compatible: boolean;
734

735
  private _timescale: number = 1.0;
746✔
736

737
  // loading
738
  private _loader: DefaultLoader;
739

740
  private _isInitialized: boolean = false;
746✔
741

742
  private _hasCreatedCanvas: boolean = false;
746✔
743

744
  public emit<TEventName extends EventKey<EngineEvents>>(eventName: TEventName, event: EngineEvents[TEventName]): void;
745
  public emit(eventName: string, event?: any): void;
746
  public emit<TEventName extends EventKey<EngineEvents> | string>(eventName: TEventName, event?: any): void {
747
    this.events.emit(eventName, event);
9,155✔
748
  }
749

750
  public on<TEventName extends EventKey<EngineEvents>>(eventName: TEventName, handler: Handler<EngineEvents[TEventName]>): Subscription;
751
  public on(eventName: string, handler: Handler<unknown>): Subscription;
752
  public on<TEventName extends EventKey<EngineEvents> | string>(eventName: TEventName, handler: Handler<any>): Subscription {
753
    return this.events.on(eventName, handler);
29✔
754
  }
755

756
  public once<TEventName extends EventKey<EngineEvents>>(eventName: TEventName, handler: Handler<EngineEvents[TEventName]>): Subscription;
757
  public once(eventName: string, handler: Handler<unknown>): Subscription;
758
  public once<TEventName extends EventKey<EngineEvents> | string>(eventName: TEventName, handler: Handler<any>): Subscription {
759
    return this.events.once(eventName, handler);
10✔
760
  }
761

762
  public off<TEventName extends EventKey<EngineEvents>>(eventName: TEventName, handler: Handler<EngineEvents[TEventName]>): void;
763
  public off(eventName: string, handler: Handler<unknown>): void;
764
  public off(eventName: string): void;
765
  public off<TEventName extends EventKey<EngineEvents> | string>(eventName: TEventName, handler?: Handler<any>): void {
UNCOV
766
    this.events.off(eventName, handler);
×
767
  }
768

769
  /**
770
   * Default {@apilink EngineOptions}
771
   */
772
  private static _DEFAULT_ENGINE_OPTIONS: EngineOptions = {
773
    width: 0,
774
    height: 0,
775
    enableCanvasTransparency: true,
776
    useDrawSorting: true,
777
    configurePerformanceCanvas2DFallback: {
778
      allow: false,
779
      showPlayerMessage: false,
780
      threshold: { fps: 20, numberOfFrames: 100 }
781
    },
782
    canvasElementId: '',
783
    canvasElement: undefined,
784
    enableCanvasContextMenu: false,
785
    snapToPixel: false,
786
    antialiasing: true,
787
    pixelArt: false,
788
    garbageCollection: true,
789
    powerPreference: 'high-performance',
790
    pointerScope: PointerScope.Canvas,
791
    suppressConsoleBootMessage: null,
792
    suppressMinimumBrowserFeatureDetection: null,
793
    suppressHiDPIScaling: null,
794
    suppressPlayButton: null,
795
    grabWindowFocus: true,
796
    scrollPreventionMode: ScrollPreventionMode.Canvas,
797
    backgroundColor: Color.fromHex('#2185d0') // Excalibur blue
798
  };
799

800
  private _originalOptions: EngineOptions = {};
746✔
801
  public readonly _originalDisplayMode: DisplayMode;
802

803
  /**
804
   * Creates a new game using the given {@apilink EngineOptions}. By default, if no options are provided,
805
   * the game will be rendered full screen (taking up all available browser window space).
806
   * You can customize the game rendering through {@apilink EngineOptions}.
807
   *
808
   * Example:
809
   *
810
   * ```js
811
   * var game = new ex.Engine({
812
   *   width: 0, // the width of the canvas
813
   *   height: 0, // the height of the canvas
814
   *   enableCanvasTransparency: true, // the transparencySection of the canvas
815
   *   canvasElementId: '', // the DOM canvas element ID, if you are providing your own
816
   *   displayMode: ex.DisplayMode.FullScreen, // the display mode
817
   *   pointerScope: ex.PointerScope.Document, // the scope of capturing pointer (mouse/touch) events
818
   *   backgroundColor: ex.Color.fromHex('#2185d0') // background color of the engine
819
   * });
820
   *
821
   * // call game.start, which is a Promise
822
   * game.start().then(function () {
823
   *   // ready, set, go!
824
   * });
825
   * ```
826
   */
827
  constructor(options?: EngineOptions<TKnownScenes>) {
828
    options = { ...Engine._DEFAULT_ENGINE_OPTIONS, ...options };
746✔
829
    this._originalOptions = options;
746✔
830

831
    Flags.freeze();
746✔
832

833
    if (options.plugins && options.plugins.length > 0) {
746!
NEW
UNCOV
834
      this._plugins = [...options.plugins];
×
NEW
UNCOV
835
      this._plugins.sort((a, b) => a.priority - b.priority);
×
836
    }
837

838
    for (const plugin of this._plugins) {
746✔
NEW
UNCOV
839
      plugin.onEnginePreConfig?.(this, options);
×
840
    }
841

842
    // Initialize browser events facade
843
    this.browser = new BrowserEvents(window, document);
746✔
844

845
    // Check compatibility
846
    const detector = new Detector();
746✔
847
    if (!options.suppressMinimumBrowserFeatureDetection && !(this._compatible = detector.test())) {
746!
848
      const message = document.createElement('div');
×
UNCOV
849
      message.innerText = 'Sorry, your browser does not support all the features needed for Excalibur';
×
UNCOV
850
      document.body.appendChild(message);
×
851

UNCOV
852
      detector.failedTests.forEach(function (test) {
×
UNCOV
853
        const testMessage = document.createElement('div');
×
UNCOV
854
        testMessage.innerText = 'Browser feature missing ' + test;
×
UNCOV
855
        document.body.appendChild(testMessage);
×
856
      });
857

UNCOV
858
      if (options.canvasElementId) {
×
UNCOV
859
        const canvas = document.getElementById(options.canvasElementId);
×
UNCOV
860
        if (canvas) {
×
UNCOV
861
          canvas.parentElement.removeChild(canvas);
×
862
        }
863
      }
864

UNCOV
865
      return;
×
866
    } else {
867
      this._compatible = true;
746✔
868
    }
869

870
    // Use native console API for color fun
871
    // eslint-disable-next-line no-console
872
    if (console.log && !options.suppressConsoleBootMessage) {
746✔
873
      // eslint-disable-next-line no-console
874
      console.log(
2✔
875
        `%cPowered by Excalibur.js (v${EX_VERSION})`,
876
        'background: #176BAA; color: white; border-radius: 5px; padding: 15px; font-size: 1.5em; line-height: 80px;'
877
      );
878
      // eslint-disable-next-line no-console
879
      console.log(
2✔
880
        '\n\
881
      /| ________________\n\
882
O|===|* >________________>\n\
883
      \\|'
884
      );
885
      // eslint-disable-next-line no-console
886
      console.log('Visit', 'http://excaliburjs.com', 'for more information');
2✔
887
    }
888

889
    // Suppress play button
890
    if (options.suppressPlayButton) {
746✔
891
      this._suppressPlayButton = true;
734✔
892
    }
893

894
    this._logger = Logger.getInstance();
746✔
895

896
    this.debug = new DebugConfig(this);
746✔
897

898
    // If debug is enabled, let's log browser features to the console.
899
    if (this._logger.defaultLevel === LogLevel.Debug) {
746!
UNCOV
900
      detector.logBrowserFeatures();
×
901
    }
902

903
    this._logger.debug('Building engine...');
746✔
904
    if (options.garbageCollection === true) {
746!
905
      this.garbageCollectorConfig = {
746✔
906
        ...DefaultGarbageCollectionOptions
907
      };
UNCOV
908
    } else if (options.garbageCollection === false) {
×
UNCOV
909
      this._logger.warn(
×
910
        'WebGL Garbage Collection Disabled!!! If you leak any images over time your game will crash when GPU memory is exhausted'
911
      );
UNCOV
912
      this.garbageCollectorConfig = null;
×
913
    } else {
914
      this.garbageCollectorConfig = {
×
915
        ...DefaultGarbageCollectionOptions,
916
        ...options.garbageCollection
917
      };
918
    }
919
    this._garbageCollector = new GarbageCollector({ getTimestamp: Date.now });
746✔
920

921
    this.canvasElementId = options.canvasElementId;
746✔
922

923
    if (options.canvasElementId) {
746!
924
      this._logger.debug('Using Canvas element specified: ' + options.canvasElementId);
×
925

926
      //test for existence of element
927
      if (document.getElementById(options.canvasElementId) === null) {
×
928
        throw new Error('Cannot find existing element in the DOM, please ensure element is created prior to engine creation.');
×
929
      }
930

UNCOV
931
      this.canvas = <HTMLCanvasElement>document.getElementById(options.canvasElementId);
×
UNCOV
932
      this._hasCreatedCanvas = false;
×
933
    } else if (options.canvasElement) {
746!
UNCOV
934
      this._logger.debug('Using Canvas element specified:', options.canvasElement);
×
UNCOV
935
      this.canvas = options.canvasElement;
×
UNCOV
936
      this._hasCreatedCanvas = false;
×
937
    } else {
938
      this._logger.debug('Using generated canvas element');
746✔
939
      this.canvas = <HTMLCanvasElement>document.createElement('canvas');
746✔
940
      this._hasCreatedCanvas = true;
746✔
941
    }
942

943
    if (this.canvas && !options.enableCanvasContextMenu) {
746✔
944
      this.canvas.addEventListener('contextmenu', (evt) => {
745✔
945
        evt.preventDefault();
1✔
946
      });
947
    }
948

949
    let displayMode = options.displayMode ?? DisplayMode.Fixed;
746✔
950
    if ((options.width && options.height) || options.viewport) {
746✔
951
      if (options.displayMode === undefined) {
741✔
952
        displayMode = DisplayMode.Fixed;
6✔
953
      }
954
      this._logger.debug('Engine viewport is size ' + options.width + ' x ' + options.height);
741✔
955
    } else if (!options.displayMode) {
5✔
956
      this._logger.debug('Engine viewport is fit');
2✔
957
      displayMode = DisplayMode.FitScreen;
2✔
958
    }
959

960
    const global = (options.global && typeof options.global === 'function' ? options.global() : options.global) as GlobalEventHandlers;
746!
961

962
    this.global = global ?? getDefaultGlobal();
746!
963
    this.grabWindowFocus = options.grabWindowFocus;
746✔
964
    this.pointerScope = options.pointerScope;
746✔
965

966
    this._originalDisplayMode = displayMode;
746✔
967

968
    let pixelArtSampler: boolean;
969
    let uvPadding: number;
970
    let nativeContextAntialiasing: boolean;
971
    let canvasImageRendering: 'pixelated' | 'auto';
972
    let filtering: ImageFiltering;
973
    let multiSampleAntialiasing: boolean | { samples: number };
974
    if (typeof options.antialiasing === 'object') {
746!
UNCOV
975
      ({ pixelArtSampler, nativeContextAntialiasing, multiSampleAntialiasing, filtering, canvasImageRendering } = {
×
976
        ...(options.pixelArt ? DefaultPixelArtOptions : DefaultAntialiasOptions),
×
977
        ...options.antialiasing
978
      });
979
    } else {
980
      pixelArtSampler = !!options.pixelArt;
746✔
981
      nativeContextAntialiasing = false;
746✔
982
      multiSampleAntialiasing = options.antialiasing;
746✔
983
      canvasImageRendering = options.antialiasing ? 'auto' : 'pixelated';
746✔
984
      filtering = options.antialiasing ? ImageFiltering.Blended : ImageFiltering.Pixel;
746✔
985
    }
986

987
    if (nativeContextAntialiasing && multiSampleAntialiasing) {
746!
UNCOV
988
      this._logger.warnOnce(
×
989
        `Cannot use antialias setting nativeContextAntialiasing and multiSampleAntialiasing` +
990
          ` at the same time, they are incompatible settings. If you aren\'t sure use multiSampleAntialiasing`
991
      );
992
    }
993

994
    if (options.pixelArt) {
746✔
995
      uvPadding = 0.25;
1✔
996
    }
997

998
    if (!options.antialiasing || filtering === ImageFiltering.Pixel) {
746✔
999
      uvPadding = 0;
737✔
1000
    }
1001

1002
    // Override with any user option, if non default to .25 for pixel art, 0.01 for everything else
1003
    uvPadding = options.uvPadding ?? uvPadding ?? 0.01;
746!
1004

1005
    // Canvas 2D fallback can be flagged on
1006
    let useCanvasGraphicsContext = Flags.isEnabled('use-canvas-context');
746✔
1007
    if (!useCanvasGraphicsContext) {
746✔
1008
      // Attempt webgl first
1009
      try {
744✔
1010
        let onGraphicsPreConfig: (context: ExcaliburGraphicsContext, options: ExcaliburGraphicsContextWebGLOptions) => void;
1011
        let onGraphicsPostConfig: (context: ExcaliburGraphicsContext, options: ExcaliburGraphicsContextWebGLOptions) => void;
1012
        let onGraphicsPreInitialize: (context: ExcaliburGraphicsContext) => void;
1013
        let onGraphicsPostInitialize: (context: ExcaliburGraphicsContext) => void;
1014
        if (this._plugins.length > 0) {
744!
NEW
UNCOV
1015
          onGraphicsPreConfig = (context: ExcaliburGraphicsContext, options: ExcaliburGraphicsContextWebGLOptions) => {
×
NEW
1016
            for (const plugin of this._plugins) {
×
NEW
1017
              plugin.onGraphicsPreConfig?.(context, options);
×
1018
            }
1019
          };
1020

NEW
1021
          onGraphicsPostConfig = (context: ExcaliburGraphicsContext, options: ExcaliburGraphicsContextWebGLOptions) => {
×
NEW
UNCOV
1022
            for (const plugin of this._plugins) {
×
NEW
1023
              plugin.onGraphicsPostConfig?.(context, options);
×
1024
            }
1025
          };
1026

NEW
UNCOV
1027
          onGraphicsPreInitialize = (context: ExcaliburGraphicsContext) => {
×
NEW
UNCOV
1028
            for (const plugin of this._plugins) {
×
NEW
UNCOV
1029
              plugin.onGraphicsPreInitialize?.(context);
×
1030
            }
1031
          };
1032

NEW
UNCOV
1033
          onGraphicsPostInitialize = (context: ExcaliburGraphicsContext) => {
×
NEW
UNCOV
1034
            for (const plugin of this._plugins) {
×
NEW
UNCOV
1035
              plugin.onGraphicsPostInitialize?.(context);
×
1036
            }
1037
          };
1038
        }
1039

1040
        this.graphicsContext = new ExcaliburGraphicsContextWebGL({
744✔
1041
          canvasElement: this.canvas,
1042
          enableTransparency: this.enableCanvasTransparency,
1043
          pixelArtSampler: pixelArtSampler,
1044
          antialiasing: nativeContextAntialiasing,
1045
          multiSampleAntialiasing: multiSampleAntialiasing,
1046
          uvPadding: uvPadding,
1047
          powerPreference: options.powerPreference,
1048
          backgroundColor: options.backgroundColor,
1049
          snapToPixel: options.snapToPixel,
1050
          useDrawSorting: options.useDrawSorting,
1051
          garbageCollector: this.garbageCollectorConfig
744!
1052
            ? {
1053
                garbageCollector: this._garbageCollector,
1054
                collectionInterval: this.garbageCollectorConfig.textureCollectInterval
1055
              }
1056
            : null,
1057
          handleContextLost: options.handleContextLost ?? this._handleWebGLContextLost,
744!
1058
          handleContextRestored: options.handleContextRestored,
1059
          onGraphicsPreConfig,
1060
          onGraphicsPostConfig,
1061
          onGraphicsPreInitialize,
1062
          onGraphicsPostInitialize
1063
        });
1064
      } catch (e) {
UNCOV
1065
        this._logger.warn(
×
1066
          `Excalibur could not load webgl for some reason (${(e as Error).message}) and loaded a Canvas 2D fallback. ` +
1067
            `Some features of Excalibur will not work in this mode. \n\n` +
1068
            'Read more about this issue at https://excaliburjs.com/docs/performance'
1069
        );
1070
        // fallback to canvas in case of failure
UNCOV
1071
        useCanvasGraphicsContext = true;
×
1072
      }
1073
    }
1074

1075
    if (useCanvasGraphicsContext) {
746✔
1076
      this.graphicsContext = new ExcaliburGraphicsContext2DCanvas({
2✔
1077
        canvasElement: this.canvas,
1078
        enableTransparency: this.enableCanvasTransparency,
1079
        antialiasing: nativeContextAntialiasing,
1080
        backgroundColor: options.backgroundColor,
1081
        snapToPixel: options.snapToPixel,
1082
        useDrawSorting: options.useDrawSorting
1083
      });
1084
    }
1085

1086
    this.screen = new Screen({
746✔
1087
      canvas: this.canvas,
1088
      context: this.graphicsContext,
1089
      antialiasing: nativeContextAntialiasing,
1090
      canvasImageRendering: canvasImageRendering,
1091
      browser: this.browser,
1092
      viewport: options.viewport ?? (options.width && options.height ? { width: options.width, height: options.height } : Resolution.SVGA),
2,967✔
1093
      resolution: options.resolution,
1094
      displayMode,
1095
      pixelRatio: options.suppressHiDPIScaling ? 1 : (options.pixelRatio ?? null)
759✔
1096
    });
1097

1098
    // TODO REMOVE STATIC!!!
1099
    // Set default filtering based on antialiasing
1100
    TextureLoader.filtering = filtering;
746✔
1101

1102
    if (options.backgroundColor) {
746!
1103
      this.backgroundColor = options.backgroundColor.clone();
746✔
1104
    }
1105

1106
    this.maxFps = options.maxFps ?? this.maxFps;
746!
1107

1108
    this.fixedUpdateTimestep = options.fixedUpdateTimestep ?? this.fixedUpdateTimestep;
746!
1109
    this.fixedUpdateFps = options.fixedUpdateFps ?? this.fixedUpdateFps;
746✔
1110
    this.fixedUpdateTimestep = this.fixedUpdateTimestep || 1000 / this.fixedUpdateFps;
746✔
1111

1112
    this.clock = new StandardClock({
746✔
1113
      maxFps: this.maxFps,
1114
      tick: this._mainloop.bind(this),
1115
      onFatalException: (e) => this.onFatalException(e)
×
1116
    });
1117

1118
    this.enableCanvasTransparency = options.enableCanvasTransparency;
746✔
1119

1120
    if (typeof options.physics === 'boolean') {
746!
1121
      this.physics = {
×
1122
        ...getDefaultPhysicsConfig(),
1123
        enabled: options.physics
1124
      };
1125
    } else {
1126
      this.physics = {
746✔
1127
        ...getDefaultPhysicsConfig()
1128
      };
1129
      mergeDeep(this.physics, options.physics);
746✔
1130
    }
1131

1132
    this.director = new Director(this, options.scenes);
746✔
1133
    this.director.events.pipe(this.events);
746✔
1134

1135
    this._initialize(options);
746✔
1136

1137
    (window as any).___EXCALIBUR_DEVTOOL = this;
746✔
1138
    Engine.InstanceCount++;
746✔
1139
  }
1140

1141
  private _handleWebGLContextLost = (e: Event) => {
746✔
1142
    e.preventDefault();
734✔
1143
    this.clock.stop();
734✔
1144
    this._logger.fatalOnce('WebGL Graphics Lost', e);
734✔
1145
    const container = document.createElement('div');
734✔
1146
    container.id = 'ex-webgl-graphics-context-lost';
734✔
1147
    container.style.position = 'absolute';
734✔
1148
    container.style.zIndex = '99';
734✔
1149
    container.style.left = '50%';
734✔
1150
    container.style.top = '50%';
734✔
1151
    container.style.display = 'flex';
734✔
1152
    container.style.flexDirection = 'column';
734✔
1153
    container.style.transform = 'translate(-50%, -50%)';
734✔
1154
    container.style.backgroundColor = 'white';
734✔
1155
    container.style.padding = '10px';
734✔
1156
    container.style.borderStyle = 'solid 1px';
734✔
1157

1158
    const div = document.createElement('div');
734✔
1159
    div.innerHTML = `
734✔
1160
      <h1>There was an issue rendering, please refresh the page.</h1>
1161
      <div>
1162
        <p>WebGL Graphics Context Lost</p>
1163

1164
        <button id="ex-webgl-graphics-reload">Refresh Page</button>
1165

1166
        <p>There are a few reasons this might happen:</p>
1167
        <ul>
1168
          <li>Two or more pages are placing a high demand on the GPU</li>
1169
          <li>Another page or operation has stalled the GPU and the browser has decided to reset the GPU</li>
1170
          <li>The computer has multiple GPUs and the user has switched between them</li>
1171
          <li>Graphics driver has crashed or restarted</li>
1172
          <li>Graphics driver was updated</li>
1173
        </ul>
1174
      </div>
1175
    `;
1176
    container.appendChild(div);
734✔
1177
    if (this.canvas?.parentElement) {
734!
UNCOV
1178
      this.canvas.parentElement.appendChild(container);
×
UNCOV
1179
      const button = div.querySelector('#ex-webgl-graphics-reload');
×
UNCOV
1180
      button?.addEventListener('click', () => location.reload());
×
1181
    }
1182
  };
1183

1184
  private _performanceThresholdTriggered = false;
746✔
1185
  private _fpsSamples: number[] = [];
746✔
1186
  private _monitorPerformanceThresholdAndTriggerFallback() {
1187
    const { allow } = this._originalOptions.configurePerformanceCanvas2DFallback;
1,746✔
1188
    let { threshold, showPlayerMessage } = this._originalOptions.configurePerformanceCanvas2DFallback;
1,746✔
1189
    if (threshold === undefined) {
1,746✔
1190
      threshold = Engine._DEFAULT_ENGINE_OPTIONS.configurePerformanceCanvas2DFallback.threshold;
100✔
1191
    }
1192
    if (showPlayerMessage === undefined) {
1,746✔
1193
      showPlayerMessage = Engine._DEFAULT_ENGINE_OPTIONS.configurePerformanceCanvas2DFallback.showPlayerMessage;
100✔
1194
    }
1195
    if (!Flags.isEnabled('use-canvas-context') && allow && this.ready && !this._performanceThresholdTriggered) {
1,746✔
1196
      // Calculate Average fps for last X number of frames after start
1197
      if (this._fpsSamples.length === threshold.numberOfFrames) {
100!
UNCOV
1198
        this._fpsSamples.splice(0, 1);
×
1199
      }
1200
      this._fpsSamples.push(this.clock.fpsSampler.fps);
100✔
1201
      let total = 0;
100✔
1202
      for (let i = 0; i < this._fpsSamples.length; i++) {
100✔
1203
        total += this._fpsSamples[i];
5,050✔
1204
      }
1205
      const average = total / this._fpsSamples.length;
100✔
1206

1207
      if (this._fpsSamples.length === threshold.numberOfFrames) {
100✔
1208
        if (average <= threshold.fps) {
1!
1209
          this._performanceThresholdTriggered = true;
1✔
1210
          this._logger.warn(
1✔
1211
            `Switching to browser 2D Canvas fallback due to performance. Some features of Excalibur will not work in this mode.\n` +
1212
              "this might mean your browser doesn't have webgl enabled or hardware acceleration is unavailable.\n\n" +
1213
              'If in Chrome:\n' +
1214
              '  * Visit Settings > Advanced > System, and ensure "Use Hardware Acceleration" is checked.\n' +
1215
              '  * Visit chrome://flags/#ignore-gpu-blocklist and ensure "Override software rendering list" is "enabled"\n' +
1216
              'If in Firefox, visit about:config\n' +
1217
              '  * Ensure webgl.disabled = false\n' +
1218
              '  * Ensure webgl.force-enabled = true\n' +
1219
              '  * Ensure layers.acceleration.force-enabled = true\n\n' +
1220
              'Read more about this issue at https://excaliburjs.com/docs/performance'
1221
          );
1222

1223
          if (showPlayerMessage) {
1!
UNCOV
1224
            this._toaster.toast(
×
1225
              'Excalibur is encountering performance issues. ' +
1226
                "It's possible that your browser doesn't have hardware acceleration enabled. " +
1227
                'Visit [LINK] for more information and potential solutions.',
1228
              'https://excaliburjs.com/docs/performance'
1229
            );
1230
          }
1231
          this.useCanvas2DFallback();
1✔
1232
          this.emit('fallbackgraphicscontext', this.graphicsContext);
1✔
1233
        }
1234
      }
1235
    }
1236
  }
1237

1238
  /**
1239
   * Switches the engine's graphics context to the 2D Canvas.
1240
   * @warning Some features of Excalibur will not work in this mode.
1241
   */
1242
  public useCanvas2DFallback() {
1243
    // Swap out the canvas
1244
    const newCanvas = this.canvas.cloneNode(false) as HTMLCanvasElement;
2✔
1245
    this.canvas.parentNode.replaceChild(newCanvas, this.canvas);
2✔
1246
    this.canvas = newCanvas;
2✔
1247

1248
    const options = { ...this._originalOptions, antialiasing: this.screen.antialiasing };
2✔
1249
    const displayMode = this._originalDisplayMode;
2✔
1250

1251
    // New graphics context
1252
    this.graphicsContext = new ExcaliburGraphicsContext2DCanvas({
2✔
1253
      canvasElement: this.canvas,
1254
      enableTransparency: this.enableCanvasTransparency,
1255
      antialiasing: options.antialiasing,
1256
      backgroundColor: options.backgroundColor,
1257
      snapToPixel: options.snapToPixel,
1258
      useDrawSorting: options.useDrawSorting
1259
    });
1260

1261
    // Reset screen
1262
    if (this.screen) {
2!
1263
      this.screen.dispose();
2✔
1264
    }
1265

1266
    this.screen = new Screen({
2✔
1267
      canvas: this.canvas,
1268
      context: this.graphicsContext,
1269
      antialiasing: options.antialiasing ?? true,
2!
1270
      browser: this.browser,
1271
      viewport: options.viewport ?? (options.width && options.height ? { width: options.width, height: options.height } : Resolution.SVGA),
8!
1272
      resolution: options.resolution,
1273
      displayMode,
1274
      pixelRatio: options.suppressHiDPIScaling ? 1 : (options.pixelRatio ?? null)
2!
1275
    });
1276
    this.screen.setCurrentCamera(this.currentScene.camera);
2✔
1277

1278
    // Reset pointers
1279
    this.input.pointers.detach();
2✔
1280
    const pointerTarget = options && options.pointerScope === PointerScope.Document ? document : this.canvas;
2!
1281
    this.input.pointers = this.input.pointers.recreate(pointerTarget, this);
2✔
1282
    this.input.pointers.init();
2✔
1283
  }
1284

1285
  private _disposed = false;
746✔
1286
  /**
1287
   * Attempts to completely clean up excalibur resources, including removing the canvas from the dom.
1288
   *
1289
   * To start again you will need to new up an Engine.
1290
   */
1291
  public dispose() {
1292
    if (!this._disposed) {
748✔
1293
      this._disposed = true;
744✔
1294
      this.stop();
744✔
1295
      this._garbageCollector.forceCollectAll();
744✔
1296
      this.input.toggleEnabled(false);
744✔
1297

1298
      for (const plugin of this.plugins) {
744✔
NEW
UNCOV
1299
        plugin.dispose?.();
×
1300
      }
1301

1302
      if (this._hasCreatedCanvas) {
744!
1303
        this.canvas.parentNode.removeChild(this.canvas);
744✔
1304
      }
1305
      this.canvas = null;
744✔
1306
      this.screen.dispose();
744✔
1307
      this.graphicsContext.dispose();
744✔
1308
      this.graphicsContext = null;
744✔
1309
      Engine.InstanceCount--;
744✔
1310
    }
1311
  }
1312

1313
  public isDisposed() {
1314
    return this._disposed;
1,442✔
1315
  }
1316

1317
  /**
1318
   * Returns a BoundingBox of the top left corner of the screen
1319
   * and the bottom right corner of the screen.
1320
   */
1321
  public getWorldBounds() {
1322
    return this.screen.getWorldBounds();
1✔
1323
  }
1324

1325
  /**
1326
   * Gets the current engine timescale factor (default is 1.0 which is 1:1 time)
1327
   */
1328
  public get timescale() {
1329
    return this._timescale;
1,747✔
1330
  }
1331

1332
  /**
1333
   * Sets the current engine timescale factor. Useful for creating slow-motion effects or fast-forward effects
1334
   * when using time-based movement.
1335
   */
1336
  public set timescale(value: number) {
1337
    if (value < 0) {
2!
UNCOV
1338
      Logger.getInstance().warnOnce('engine.timescale to a value less than 0 are ignored');
×
UNCOV
1339
      return;
×
1340
    }
1341

1342
    this._timescale = value;
2✔
1343
  }
1344

1345
  /**
1346
   * Adds a {@apilink Timer} to the {@apilink currentScene}.
1347
   * @param timer  The timer to add to the {@apilink currentScene}.
1348
   */
1349
  public addTimer(timer: Timer): Timer {
UNCOV
1350
    return this.currentScene.addTimer(timer);
×
1351
  }
1352

1353
  /**
1354
   * Removes a {@apilink Timer} from the {@apilink currentScene}.
1355
   * @param timer  The timer to remove to the {@apilink currentScene}.
1356
   */
1357
  public removeTimer(timer: Timer): Timer {
UNCOV
1358
    return this.currentScene.removeTimer(timer);
×
1359
  }
1360

1361
  /**
1362
   * Adds a {@apilink Scene} to the engine, think of scenes in Excalibur as you
1363
   * would levels or menus.
1364
   * @param key  The name of the scene, must be unique
1365
   * @param scene The scene to add to the engine
1366
   */
1367
  public addScene<TScene extends string>(key: TScene, scene: Scene | SceneConstructor | SceneWithOptions): Engine<TKnownScenes | TScene> {
1368
    this.director.add(key, scene);
419✔
1369
    return this as Engine<TKnownScenes | TScene>;
419✔
1370
  }
1371

1372
  /**
1373
   * Removes a {@apilink Scene} instance from the engine
1374
   * @param scene  The scene to remove
1375
   */
1376
  public removeScene(scene: Scene | SceneConstructor): void;
1377
  /**
1378
   * Removes a scene from the engine by key
1379
   * @param key  The scene key to remove
1380
   */
1381
  public removeScene(key: string): void;
1382
  /**
1383
   * @internal
1384
   */
1385
  public removeScene(entity: any): void {
1386
    this.director.remove(entity);
8✔
1387
  }
1388

1389
  /**
1390
   * Adds a {@apilink Scene} to the engine, think of scenes in Excalibur as you
1391
   * would levels or menus.
1392
   * @param sceneKey  The key of the scene, must be unique
1393
   * @param scene     The scene to add to the engine
1394
   */
1395
  public add(sceneKey: string, scene: Scene | SceneConstructor | SceneWithOptions): void;
1396
  /**
1397
   * Adds a {@apilink Timer} to the {@apilink currentScene}.
1398
   * @param timer  The timer to add to the {@apilink currentScene}.
1399
   */
1400
  public add(timer: Timer): void;
1401
  /**
1402
   * Adds a {@apilink TileMap} to the {@apilink currentScene}, once this is done the TileMap
1403
   * will be drawn and updated.
1404
   */
1405
  public add(tileMap: TileMap): void;
1406
  /**
1407
   * Adds an actor to the {@apilink currentScene} of the game. This is synonymous
1408
   * to calling `engine.currentScene.add(actor)`.
1409
   *
1410
   * Actors can only be drawn if they are a member of a scene, and only
1411
   * the {@apilink currentScene} may be drawn or updated.
1412
   * @param actor  The actor to add to the {@apilink currentScene}
1413
   */
1414
  public add(actor: Actor): void;
1415

1416
  public add(entity: Entity): void;
1417

1418
  /**
1419
   * Adds a {@apilink ScreenElement} to the {@apilink currentScene} of the game,
1420
   * ScreenElements do not participate in collisions, instead the
1421
   * remain in the same place on the screen.
1422
   * @param screenElement  The ScreenElement to add to the {@apilink currentScene}
1423
   */
1424
  public add(screenElement: ScreenElement): void;
1425
  public add(entity: any): void {
1426
    if (arguments.length === 2) {
265✔
1427
      this.director.add(<string>arguments[0], <Scene | SceneConstructor | SceneWithOptions>arguments[1]);
95✔
1428
      return;
95✔
1429
    }
1430
    const maybeDeferred = this.director.getDeferredScene();
170✔
1431
    if (maybeDeferred instanceof Scene) {
170!
UNCOV
1432
      maybeDeferred.add(entity);
×
1433
    } else {
1434
      this.currentScene.add(entity);
170✔
1435
    }
1436
  }
1437

1438
  /**
1439
   * Removes a scene instance from the engine
1440
   * @param scene  The scene to remove
1441
   */
1442
  public remove(scene: Scene | SceneConstructor): void;
1443
  /**
1444
   * Removes a scene from the engine by key
1445
   * @param sceneKey  The scene to remove
1446
   */
1447
  public remove(sceneKey: string): void;
1448
  /**
1449
   * Removes a {@apilink Timer} from the {@apilink currentScene}.
1450
   * @param timer  The timer to remove to the {@apilink currentScene}.
1451
   */
1452
  public remove(timer: Timer): void;
1453
  /**
1454
   * Removes a {@apilink TileMap} from the {@apilink currentScene}, it will no longer be drawn or updated.
1455
   */
1456
  public remove(tileMap: TileMap): void;
1457
  /**
1458
   * Removes an actor from the {@apilink currentScene} of the game. This is synonymous
1459
   * to calling `engine.currentScene.removeChild(actor)`.
1460
   * Actors that are removed from a scene will no longer be drawn or updated.
1461
   * @param actor  The actor to remove from the {@apilink currentScene}.
1462
   */
1463
  public remove(actor: Actor): void;
1464
  /**
1465
   * Removes a {@apilink ScreenElement} to the scene, it will no longer be drawn or updated
1466
   * @param screenElement  The ScreenElement to remove from the {@apilink currentScene}
1467
   */
1468
  public remove(screenElement: ScreenElement): void;
1469
  public remove(entity: any): void {
1470
    if (entity instanceof Entity) {
4✔
1471
      this.currentScene.remove(entity);
2✔
1472
    }
1473

1474
    if (entity instanceof Scene || isSceneConstructor(entity)) {
4✔
1475
      this.removeScene(entity);
1✔
1476
    }
1477

1478
    if (typeof entity === 'string') {
4✔
1479
      this.removeScene(entity);
1✔
1480
    }
1481
  }
1482

1483
  /**
1484
   * Changes the current scene with optionally supplied:
1485
   * * Activation data
1486
   * * Transitions
1487
   * * Loaders
1488
   *
1489
   * Example:
1490
   * ```typescript
1491
   * game.goToScene('myScene', {
1492
   *   sceneActivationData: {any: 'thing at all'},
1493
   *   destinationIn: new FadeInOut({duration: 1000, direction: 'in'}),
1494
   *   sourceOut: new FadeInOut({duration: 1000, direction: 'out'}),
1495
   *   loader: MyLoader
1496
   * });
1497
   * ```
1498
   *
1499
   * Scenes are defined in the Engine constructor
1500
   * ```typescript
1501
   * const engine = new ex.Engine({
1502
      scenes: {...}
1503
    });
1504
   * ```
1505
   * Or by adding dynamically
1506
   *
1507
   * ```typescript
1508
   * engine.addScene('myScene', new ex.Scene());
1509
   * ```
1510
   * @param destinationScene
1511
   * @param options
1512
   */
1513
  public async goToScene<TData = undefined>(destinationScene: WithRoot<TKnownScenes>, options?: GoToOptions<TData>): Promise<void> {
1514
    await this.scope(async () => {
424✔
1515
      await this.director.goToScene(destinationScene, options);
424✔
1516
    });
1517
  }
1518

1519
  /**
1520
   * Transforms the current x, y from screen coordinates to world coordinates
1521
   * @param point  Screen coordinate to convert
1522
   */
1523
  public screenToWorldCoordinates(point: Vector): Vector {
1524
    return this.screen.screenToWorldCoordinates(point);
2✔
1525
  }
1526

1527
  /**
1528
   * Transforms a world coordinate, to a screen coordinate
1529
   * @param point  World coordinate to convert
1530
   */
1531
  public worldToScreenCoordinates(point: Vector): Vector {
1532
    return this.screen.worldToScreenCoordinates(point);
3✔
1533
  }
1534

1535
  /**
1536
   * Initializes the internal canvas, rendering context, display mode, and native event listeners
1537
   */
1538
  private _initialize(options?: EngineOptions) {
1539
    this.pageScrollPreventionMode = options.scrollPreventionMode;
746✔
1540

1541
    // initialize inputs
1542
    const pointerTarget = options && options.pointerScope === PointerScope.Document ? document : this.canvas;
746✔
1543
    const grabWindowFocus = this._originalOptions?.grabWindowFocus ?? true;
746!
1544
    this.input = new InputHost({
746✔
1545
      global: this.global,
1546
      pointerTarget,
1547
      grabWindowFocus,
1548
      engine: this
1549
    });
1550
    this.inputMapper = this.input.inputMapper;
746✔
1551

1552
    // Issue #385 make use of the visibility api
1553
    // https://developer.mozilla.org/en-US/docs/Web/Guide/User_experience/Using_the_Page_Visibility_API
1554

1555
    this.browser.document.on('visibilitychange', () => {
746✔
1556
      if (document.visibilityState === 'hidden') {
20✔
1557
        this.events.emit('hidden', new HiddenEvent(this));
10✔
1558
        this._logger.debug('Window hidden');
10✔
1559
      } else if (document.visibilityState === 'visible') {
10!
1560
        this.events.emit('visible', new VisibleEvent(this));
10✔
1561
        this._logger.debug('Window visible');
10✔
1562
      }
1563
    });
1564

1565
    if (!this.canvasElementId && !options.canvasElement) {
746!
1566
      document.body.appendChild(this.canvas);
746✔
1567
    }
1568

1569
    for (const plugin of this._plugins) {
746✔
NEW
UNCOV
1570
      plugin.onEnginePostConfig?.(this, options);
×
1571
    }
1572
  }
1573

1574
  public toggleInputEnabled(enabled: boolean) {
UNCOV
1575
    this._inputEnabled = enabled;
×
UNCOV
1576
    this.input.toggleEnabled(this._inputEnabled);
×
1577
  }
1578

1579
  public onInitialize(engine: Engine) {
1580
    // Override me
1581
  }
1582

1583
  /**
1584
   * Gets whether the actor is Initialized
1585
   */
1586
  public get isInitialized(): boolean {
1587
    return this._isInitialized;
579✔
1588
  }
1589

1590
  private async _overrideInitialize(engine: Engine) {
1591
    if (!this.isInitialized) {
579✔
1592
      for (const plugin of this._plugins) {
570✔
NEW
UNCOV
1593
        plugin.onEnginePreInitialize(this);
×
1594
      }
1595

1596
      await this.director.onInitialize();
570✔
1597
      await this.onInitialize(engine);
570✔
1598
      this.events.emit('initialize', new InitializeEvent(engine, this));
570✔
1599
      this._isInitialized = true;
570✔
1600

1601
      for (const plugin of this._plugins) {
570✔
NEW
UNCOV
1602
        plugin.onEnginePostInitialize(this);
×
1603
      }
1604
    }
1605
  }
1606

1607
  /**
1608
   * Updates the entire state of the game
1609
   * @param elapsed  Number of milliseconds elapsed since the last update.
1610
   */
1611
  private _update(elapsed: number) {
1612
    if (this._isLoading) {
1,744✔
1613
      // suspend updates until loading is finished
1614
      this._loader?.onUpdate(this, elapsed);
829!
1615
      // Update input listeners
1616
      this.input.update();
829✔
1617
      return;
829✔
1618
    }
1619

1620
    // Publish preupdate events
1621
    this.clock.__runScheduledCbs('preupdate');
915✔
1622
    this._preupdate(elapsed);
915✔
1623

1624
    // process engine level events
1625
    this.currentScene.update(this, elapsed);
915✔
1626

1627
    // Update graphics postprocessors
1628
    this.graphicsContext.updatePostProcessors(elapsed);
915✔
1629

1630
    // Publish update event
1631
    this.clock.__runScheduledCbs('postupdate');
915✔
1632
    this._postupdate(elapsed);
915✔
1633

1634
    // Update input listeners
1635
    this.input.update();
915✔
1636
  }
1637

1638
  /**
1639
   * @internal
1640
   */
1641
  public _preupdate(elapsed: number) {
1642
    this.emit('preupdate', new PreUpdateEvent(this, elapsed, this));
915✔
1643
    this.onPreUpdate(this, elapsed);
915✔
1644
  }
1645

1646
  /**
1647
   * Safe to override method
1648
   * @param engine The reference to the current game engine
1649
   * @param elapsed  The time elapsed since the last update in milliseconds
1650
   */
1651
  public onPreUpdate(engine: Engine, elapsed: number) {
1652
    // Override me
1653
  }
1654

1655
  /**
1656
   * @internal
1657
   */
1658
  public _postupdate(elapsed: number) {
1659
    this.emit('postupdate', new PostUpdateEvent(this, elapsed, this));
915✔
1660
    this.onPostUpdate(this, elapsed);
915✔
1661
  }
1662

1663
  /**
1664
   * Safe to override method
1665
   * @param engine The reference to the current game engine
1666
   * @param elapsed  The time elapsed since the last update in milliseconds
1667
   */
1668
  public onPostUpdate(engine: Engine, elapsed: number) {
1669
    // Override me
1670
  }
1671

1672
  /**
1673
   * Draws the entire game
1674
   * @param elapsed  Number of milliseconds elapsed since the last draw.
1675
   */
1676
  private _draw(elapsed: number) {
1677
    // Use scene background color if present, fallback to engine
1678
    this.graphicsContext.backgroundColor = this.currentScene.backgroundColor ?? this.backgroundColor;
1,747✔
1679
    this.graphicsContext.beginDrawLifecycle();
1,747✔
1680
    this.graphicsContext.clear();
1,747✔
1681
    this.clock.__runScheduledCbs('predraw');
1,747✔
1682
    this._predraw(this.graphicsContext, elapsed);
1,747✔
1683

1684
    // Drawing nothing else while loading
1685
    if (this._isLoading) {
1,747✔
1686
      if (!this._hideLoader) {
829!
1687
        this._loader?.canvas.draw(this.graphicsContext, 0, 0);
829!
1688
        this.clock.__runScheduledCbs('postdraw');
829✔
1689
        this.graphicsContext.flush();
829✔
1690
        this.graphicsContext.endDrawLifecycle();
829✔
1691
      }
1692
      return;
829✔
1693
    }
1694

1695
    this.currentScene.draw(this.graphicsContext, elapsed);
918✔
1696

1697
    this.clock.__runScheduledCbs('postdraw');
918✔
1698
    this._postdraw(this.graphicsContext, elapsed);
918✔
1699

1700
    // Flush any pending drawings
1701
    this.graphicsContext.flush();
918✔
1702
    this.graphicsContext.endDrawLifecycle();
918✔
1703

1704
    this._checkForScreenShots();
918✔
1705
  }
1706

1707
  /**
1708
   * @internal
1709
   */
1710
  public _predraw(ctx: ExcaliburGraphicsContext, elapsed: number) {
1711
    this.emit('predraw', new PreDrawEvent(ctx, elapsed, this));
1,747✔
1712
    this.onPreDraw(ctx, elapsed);
1,747✔
1713
  }
1714

1715
  /**
1716
   * Safe to override method to hook into pre draw
1717
   * @param ctx {@link ExcaliburGraphicsContext} for drawing
1718
   * @param elapsed  Number of milliseconds elapsed since the last draw.
1719
   */
1720
  public onPreDraw(ctx: ExcaliburGraphicsContext, elapsed: number) {
1721
    // Override me
1722
  }
1723

1724
  /**
1725
   * @internal
1726
   */
1727
  public _postdraw(ctx: ExcaliburGraphicsContext, elapsed: number) {
1728
    this.emit('postdraw', new PostDrawEvent(ctx, elapsed, this));
918✔
1729
    this.onPostDraw(ctx, elapsed);
918✔
1730
  }
1731

1732
  /**
1733
   * Safe to override method to hook into pre draw
1734
   * @param ctx {@link ExcaliburGraphicsContext} for drawing
1735
   * @param elapsed  Number of milliseconds elapsed since the last draw.
1736
   */
1737
  public onPostDraw(ctx: ExcaliburGraphicsContext, elapsed: number) {
1738
    // Override me
1739
  }
1740

1741
  /**
1742
   * Enable or disable Excalibur debugging functionality.
1743
   * @param toggle a value that debug drawing will be changed to
1744
   */
1745
  public showDebug(toggle: boolean): void {
1746
    this._isDebug = toggle;
2✔
1747
  }
1748

1749
  /**
1750
   * Toggle Excalibur debugging functionality.
1751
   */
1752
  public toggleDebug(): boolean {
1753
    this._isDebug = !this._isDebug;
14✔
1754
    return this._isDebug;
14✔
1755
  }
1756

1757
  /**
1758
   * Returns true when loading is totally complete and the player has clicked start
1759
   */
1760
  public get loadingComplete() {
1761
    return !this._isLoading;
734✔
1762
  }
1763

1764
  private _isLoading = false;
746✔
1765
  private _hideLoader = false;
746✔
1766
  private _isReadyFuture = new Future<void>();
746✔
1767
  public get ready() {
1768
    return this._isReadyFuture.isCompleted;
100✔
1769
  }
1770
  public isReady(): Promise<void> {
1771
    return this._isReadyFuture.promise;
14✔
1772
  }
1773

1774
  /**
1775
   * Starts the internal game loop for Excalibur after loading
1776
   * any provided assets.
1777
   * @param loader  Optional {@apilink Loader} to use to load resources. The default loader is {@apilink Loader},
1778
   * override to provide your own custom loader.
1779
   *
1780
   * Note: start() only resolves AFTER the user has clicked the play button
1781
   */
1782
  public async start(loader?: DefaultLoader): Promise<void>;
1783
  /**
1784
   * Starts the internal game loop for Excalibur after configuring any routes, loaders, or transitions
1785
   * @param startOptions Optional {@apilink StartOptions} to configure the routes for scenes in Excalibur
1786
   *
1787
   * Note: start() only resolves AFTER the user has clicked the play button
1788
   */
1789
  public async start(sceneName: WithRoot<TKnownScenes>, options?: StartOptions): Promise<void>;
1790
  /**
1791
   * Starts the internal game loop after any loader is finished
1792
   * @param loader
1793
   */
1794
  public async start(loader?: DefaultLoader): Promise<void>;
1795
  public async start(sceneNameOrLoader?: WithRoot<TKnownScenes> | DefaultLoader, options?: StartOptions): Promise<void> {
1796
    await this.scope(async () => {
578✔
1797
      if (!this._compatible) {
578!
UNCOV
1798
        throw new Error('Excalibur is incompatible with your browser');
×
1799
      }
1800
      this._isLoading = true;
578✔
1801
      let loader: DefaultLoader;
1802
      if (sceneNameOrLoader instanceof DefaultLoader) {
578✔
1803
        loader = sceneNameOrLoader;
15✔
1804
      } else if (typeof sceneNameOrLoader === 'string') {
563✔
1805
        this.director.configureStart(sceneNameOrLoader, options);
1✔
1806
        loader = this.director.mainLoader;
1✔
1807
      }
1808

1809
      // Start the excalibur clock which drives the mainloop
1810
      this._logger.debug('Starting game clock...');
578✔
1811
      this.browser.resume();
578✔
1812
      this.clock.start();
578✔
1813
      if (this.garbageCollectorConfig) {
578!
1814
        this._garbageCollector.start();
578✔
1815
      }
1816
      this._logger.debug('Game clock started');
578✔
1817

1818
      await this.load(loader ?? new Loader());
578✔
1819

1820
      // Initialize before ready
1821
      await this._overrideInitialize(this);
577✔
1822

1823
      this._isReadyFuture.resolve();
577✔
1824
      this.emit('start', new GameStartEvent(this));
577✔
1825
      return this._isReadyFuture.promise;
577✔
1826
    });
1827
  }
1828

1829
  /**
1830
   * Returns the current frames elapsed milliseconds
1831
   */
1832
  public currentFrameElapsedMs = 0;
746✔
1833

1834
  /**
1835
   * Returns the current frame lag when in fixed update mode
1836
   */
1837
  public currentFrameLagMs = 0;
746✔
1838

1839
  private _lagMs = 0;
746✔
1840
  private _mainloop(elapsed: number) {
1841
    this.scope(() => {
1,746✔
1842
      this.emit('preframe', new PreFrameEvent(this, this.stats.prevFrame));
1,746✔
1843
      const elapsedMs = elapsed * this.timescale;
1,746✔
1844
      this.currentFrameElapsedMs = elapsedMs;
1,746✔
1845

1846
      // reset frame stats (reuse existing instances)
1847
      const frameId = this.stats.prevFrame.id + 1;
1,746✔
1848
      this.stats.currFrame.reset();
1,746✔
1849
      this.stats.currFrame.id = frameId;
1,746✔
1850
      this.stats.currFrame.elapsedMs = elapsedMs;
1,746✔
1851
      this.stats.currFrame.fps = this.clock.fpsSampler.fps;
1,746✔
1852
      GraphicsDiagnostics.clear();
1,746✔
1853

1854
      const beforeUpdate = this.clock.now();
1,746✔
1855
      const fixedTimestepMs = this.fixedUpdateTimestep;
1,746✔
1856
      if (this.fixedUpdateTimestep) {
1,746✔
1857
        this._lagMs += elapsedMs;
44✔
1858
        while (this._lagMs >= fixedTimestepMs) {
44✔
1859
          this._update(fixedTimestepMs);
42✔
1860
          this._lagMs -= fixedTimestepMs;
42✔
1861
        }
1862
      } else {
1863
        this._update(elapsedMs);
1,702✔
1864
      }
1865
      const afterUpdate = this.clock.now();
1,746✔
1866
      this.currentFrameLagMs = this._lagMs;
1,746✔
1867
      this._draw(elapsedMs);
1,746✔
1868
      const afterDraw = this.clock.now();
1,746✔
1869

1870
      this.stats.currFrame.duration.update = afterUpdate - beforeUpdate;
1,746✔
1871
      this.stats.currFrame.duration.draw = afterDraw - afterUpdate;
1,746✔
1872
      this.stats.currFrame.graphics.drawnImages = GraphicsDiagnostics.DrawnImagesCount;
1,746✔
1873
      this.stats.currFrame.graphics.drawCalls = GraphicsDiagnostics.DrawCallCount;
1,746✔
1874
      this.stats.currFrame.graphics.rendererSwaps = GraphicsDiagnostics.RendererSwaps;
1,746✔
1875

1876
      this.emit('postframe', new PostFrameEvent(this, this.stats.currFrame));
1,746✔
1877
      this.stats.prevFrame.reset(this.stats.currFrame);
1,746✔
1878

1879
      this._monitorPerformanceThresholdAndTriggerFallback();
1,746✔
1880
    });
1881
  }
1882

1883
  /**
1884
   * Stops Excalibur's main loop, useful for pausing the game.
1885
   */
1886
  public stop() {
1887
    if (this.clock.isRunning()) {
1,491✔
1888
      this.emit('stop', new GameStopEvent(this));
587✔
1889
      this.browser.pause();
587✔
1890
      this.clock.stop();
587✔
1891
      this._garbageCollector.stop();
587✔
1892
      this._logger.debug('Game stopped');
587✔
1893
    }
1894
  }
1895

1896
  /**
1897
   * Returns the Engine's running status, Useful for checking whether engine is running or paused.
1898
   */
1899
  public isRunning() {
1900
    return this.clock.isRunning();
3✔
1901
  }
1902

1903
  private _screenShotRequests: { preserveHiDPIResolution: boolean; resolve: (image: HTMLImageElement) => void }[] = [];
746✔
1904
  /**
1905
   * Takes a screen shot of the current viewport and returns it as an
1906
   * HTML Image Element.
1907
   * @param preserveHiDPIResolution in the case of HiDPI return the full scaled backing image, by default false
1908
   */
1909
  public screenshot(preserveHiDPIResolution = false): Promise<HTMLImageElement> {
3✔
1910
    const screenShotPromise = new Promise<HTMLImageElement>((resolve) => {
9✔
1911
      this._screenShotRequests.push({ preserveHiDPIResolution, resolve });
9✔
1912
    });
1913
    return screenShotPromise;
9✔
1914
  }
1915

1916
  private _checkForScreenShots() {
1917
    // We must grab the draw buffer before we yield to the browser
1918
    // the draw buffer is cleared after compositing
1919
    // the reason for the asynchrony is setting `preserveDrawingBuffer: true`
1920
    // forces the browser to copy buffers which can have a mass perf impact on mobile
1921
    for (const request of this._screenShotRequests) {
918✔
1922
      const finalWidth = request.preserveHiDPIResolution ? this.canvas.width : this.screen.resolution.width;
9✔
1923
      const finalHeight = request.preserveHiDPIResolution ? this.canvas.height : this.screen.resolution.height;
9✔
1924
      const screenshot = document.createElement('canvas');
9✔
1925
      screenshot.width = finalWidth;
9✔
1926
      screenshot.height = finalHeight;
9✔
1927
      const ctx = screenshot.getContext('2d');
9✔
1928
      ctx.imageSmoothingEnabled = this.screen.antialiasing;
9✔
1929
      ctx.drawImage(this.canvas, 0, 0, finalWidth, finalHeight);
9✔
1930

1931
      const result = new Image();
9✔
1932
      const raw = screenshot.toDataURL('image/png');
9✔
1933
      result.onload = () => {
9✔
1934
        request.resolve(result);
9✔
1935
      };
1936
      result.src = raw;
9✔
1937
    }
1938
    // Reset state
1939
    this._screenShotRequests.length = 0;
918✔
1940
  }
1941

1942
  /**
1943
   * Another option available to you to load resources into the game.
1944
   * Immediately after calling this the game will pause and the loading screen
1945
   * will appear.
1946
   * @param loader  Some {@apilink Loadable} such as a {@apilink Loader} collection, {@apilink Sound}, or {@apilink Texture}.
1947
   */
1948
  public async load(loader: DefaultLoader, hideLoader = false): Promise<void> {
1,020✔
1949
    await this.scope(async () => {
1,020✔
1950
      try {
1,020✔
1951
        // early exit if loaded
1952
        if (loader.isLoaded()) {
1,020✔
1953
          return;
1,002✔
1954
        }
1955
        this._loader = loader;
18✔
1956
        this._isLoading = true;
18✔
1957
        this._hideLoader = hideLoader;
18✔
1958

1959
        if (loader instanceof Loader) {
18✔
1960
          loader.suppressPlayButton = loader.suppressPlayButton || this._suppressPlayButton;
15✔
1961
        }
1962
        this._loader.onInitialize(this);
18✔
1963

1964
        await loader.load();
18✔
1965
      } catch (e) {
1966
        this._logger.error('Error loading resources, things may not behave properly', e);
1✔
1967
        await Promise.resolve();
1✔
1968
      } finally {
1969
        this._isLoading = false;
1,019✔
1970
        this._hideLoader = false;
1,019✔
1971
        this._loader = null;
1,019✔
1972
      }
1973
    });
1974
  }
1975
}
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