Something went wrong. Try again.
[READ-ONLY] Mirror of https://github.com/excaliburjs/Excalibur. 🎮 Your friendly TypeScript 2D game engine for the web 🗡️ excaliburjs.com
excalibur excaliburjs game-development game-engine game-framework gamedev games html5-canvas typescript
Something went wrong. Try again.
TypeScript
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103210421052106210721082109211021112112211321142115211621172118211921202121212221232124212521262127import { EX_VERSION } from './';import { Future } from './util/future';import type { EventKey, Handler, Subscription } from './event-emitter';import { EventEmitter } from './event-emitter';import { PointerScope } from './input/pointer-scope';import { Flags } from './flags';import { polyfill } from './polyfill';polyfill();import type { CanUpdate, CanDraw, CanInitialize } from './interfaces/lifecycle-events';import type { Vector } from './math/vector';import type { ViewportDimension } from './screen';import { Screen, DisplayMode, Resolution } from './screen';import type { ScreenElement } from './screen-element';import type { Actor } from './actor';import type { Timer } from './timer';import type { TileMap } from './tile-map';import { DefaultLoader } from './director/default-loader';import { Loader } from './director/loader';import { Detector } from './util/detector';import { VisibleEvent, HiddenEvent, GameStartEvent, GameStopEvent, PreUpdateEvent, PostUpdateEvent, PreFrameEvent, PostFrameEvent, PreDrawEvent, PostDrawEvent, InitializeEvent} from './events';import { Logger, LogLevel } from './util/log';import { Color } from './color';import type { SceneConstructor } from './scene';import { Scene, isSceneConstructor } from './scene';import { Entity } from './entity-component-system/entity';import type { DebugStats } from './debug/debug-config';import { DebugConfig } from './debug/debug-config';import { BrowserEvents } from './util/browser';import type { AntialiasOptions, ExcaliburGraphicsContext, ExcaliburGraphicsContextOptions, ExcaliburGraphicsContextWebGLOptions} from './graphics';import { DefaultAntialiasOptions, DefaultPixelArtOptions, ExcaliburGraphicsContext2DCanvas, ExcaliburGraphicsContextWebGL, TextureLoader} from './graphics';import type { Clock } from './util/clock';import { StandardClock } from './util/clock';import { ImageFiltering } from './graphics/filtering';import { GraphicsDiagnostics } from './graphics/graphics-diagnostics';import { Toaster } from './util/toaster';import type { InputMapper } from './input/input-mapper';import type { GoToOptions, SceneMap, StartOptions, SceneWithOptions, WithRoot } from './director/director';import { Director, DirectorEvents } from './director/director';import { InputHost } from './input/input-host';import type { PhysicsConfig } from './collision/physics-config';import { getDefaultPhysicsConfig } from './collision/physics-config';import type { LightingConfig } from './lighting/lighting-config';import { getDefaultLightingConfig } from './lighting/lighting-config';import type { DeepRequired } from './util/required';import type { Context } from './context';import { createContext, useContext } from './context';import type { GarbageCollectionOptions } from './garbage-collector';import { DefaultGarbageCollectionOptions, GarbageCollector } from './garbage-collector';import { mergeDeep } from './util/util';import { getDefaultGlobal } from './util/iframe';import type { Plugin } from './plugin';
export interface EngineEvents extends DirectorEvents { fallbackgraphicscontext: ExcaliburGraphicsContext2DCanvas; initialize: InitializeEvent<Engine>; visible: VisibleEvent; hidden: HiddenEvent; start: GameStartEvent; stop: GameStopEvent; preupdate: PreUpdateEvent<Engine>; postupdate: PostUpdateEvent<Engine>; preframe: PreFrameEvent; postframe: PostFrameEvent; predraw: PreDrawEvent; postdraw: PostDrawEvent;}
export const EngineEvents = { FallbackGraphicsContext: 'fallbackgraphicscontext', Initialize: 'initialize', Visible: 'visible', Hidden: 'hidden', Start: 'start', Stop: 'stop', PreUpdate: 'preupdate', PostUpdate: 'postupdate', PreFrame: 'preframe', PostFrame: 'postframe', PreDraw: 'predraw', PostDraw: 'postdraw', ...DirectorEvents} as const;
/** * Enum representing the different mousewheel event bubble prevention */export enum ScrollPreventionMode { /** * Do not prevent any page scrolling */ None, /** * Prevent page scroll if mouse is over the game canvas */ Canvas, /** * Prevent all page scrolling via mouse wheel */ All}
/** * Defines the available options to configure the Excalibur engine at constructor time. */export interface EngineOptions<TKnownScenes extends string = any> { /** * Optionally configure Excalibur plugins */ plugins?: Plugin[]; /** * Optionally configure the width of the viewport in css pixels */ width?: number;
/** * Optionally configure the height of the viewport in css pixels */ height?: number;
/** * Optionally configure the width & height of the viewport in css pixels. * Use `viewport` instead of {@apilink EngineOptions.width} and {@apilink EngineOptions.height}, or vice versa. */ viewport?: ViewportDimension;
/** * Optionally specify the size the logical pixel resolution, if not specified it will be width x height. * See {@apilink Resolution} for common presets. */ resolution?: Resolution;
/** * Optionally specify antialiasing (smoothing), by default true (smooth pixels) * * * `true` - useful for high resolution art work you would like smoothed, this also hints excalibur to load images * with default blending {@apilink ImageFiltering.Blended} * * * `false` - useful for pixel art style art work you would like sharp, this also hints excalibur to load images * with default blending {@apilink ImageFiltering.Pixel} * * * {@apilink AntialiasOptions} Optionally deeply configure the different antialiasing settings, **WARNING** thar be dragons here. * It is recommended you stick to `true` or `false` unless you understand what you're doing and need to control rendering to * a high degree. */ antialiasing?: boolean | AntialiasOptions;
/** * Optionally specify excalibur garbage collection, by default true. * * * `true` - garbage collection defaults are enabled (default) * * * `false` - garbage collection is completely disabled (not recommended) * * * {@apilink GarbageCollectionOptions} Optionally deeply configure garbage collection settings, **WARNING** thar be dragons here. * It is recommended you stick to `true` or `false` unless you understand what you're doing, it is possible to get into a downward * spiral if collection timings are set too low where you are stuck in repeated collection. */ garbageCollection?: boolean | GarbageCollectionOptions;
/** * Quick convenience property to configure Excalibur to use special settings for "pretty" anti-aliased pixel art * * 1. Turns on special shader condition to blend for pixel art and enables various antialiasing settings, * notice blending is ON for this special mode. * * Equivalent to: * ```javascript * antialiasing: { * pixelArtSampler: true, * canvasImageRendering: 'auto', * filtering: ImageFiltering.Blended, * webglAntialiasing: true * } * ``` */ pixelArt?: boolean;
/** * Specify any UV padding you want use in pixels, this brings sampling into the texture if you're using * a sprite sheet in one image to prevent sampling bleed. * * Defaults: * * `antialiasing: false` or `filtering: ImageFiltering.Pixel` - 0.0; * * `pixelArt: true` - 0.25 * * All else 0.01 */ uvPadding?: number;
/** * Optionally hint the graphics context into a specific power profile * * Default "high-performance" */ powerPreference?: 'default' | 'high-performance' | 'low-power';
/** * Optionally upscale the number of pixels in the canvas. Normally only useful if you need a smoother look to your assets, especially * {@apilink Text} or Pixel Art assets. * * **WARNING** It is recommended you try using `antialiasing: true` before adjusting pixel ratio. Pixel ratio will consume more memory * and on mobile may break if the internal size of the canvas exceeds 4k pixels in width or height. * * Default is based the display's pixel ratio, for example a HiDPI screen might have the value 2; */ pixelRatio?: number;
/** * Optionally configure the native canvas transparent backdrop */ enableCanvasTransparency?: boolean;
/** * Optionally specify the target canvas DOM element to render the game in */ canvasElementId?: string;
/** * Optionally specify the target canvas DOM element directly */ canvasElement?: HTMLCanvasElement;
/** * Optionally enable the right click context menu on the canvas * * Default if unset is false */ enableCanvasContextMenu?: boolean;
/** * Optionally snap graphics to nearest pixel, default is false */ snapToPixel?: boolean;
/** * The {@apilink DisplayMode} of the game, by default {@apilink DisplayMode.FitScreen} with aspect ratio 4:3 (800x600). * Depending on this value, {@apilink width} and {@apilink height} may be ignored. */ displayMode?: DisplayMode;
/** * Optionally configure the global, or a factory to produce it to listen to for browser events for Excalibur to listen to */ global?: GlobalEventHandlers | (() => GlobalEventHandlers);
/** * Configures the pointer scope. Pointers scoped to the 'Canvas' can only fire events within the canvas viewport; whereas, 'Document' * (default) scoped will fire anywhere on the page. */ pointerScope?: PointerScope;
/** * Suppress boot up console message, which contains the "powered by Excalibur message" */ suppressConsoleBootMessage?: boolean;
/** * Suppress minimum browser feature detection, it is not recommended users of excalibur switch this off. This feature ensures that * the currently running browser meets the minimum requirements for running excalibur. This can be useful if running on non-standard * browsers or if there is a bug in excalibur preventing execution. */ suppressMinimumBrowserFeatureDetection?: boolean;
/** * Suppress HiDPI auto detection and scaling, it is not recommended users of excalibur switch off this feature. This feature detects * and scales the drawing canvas appropriately to accommodate HiDPI screens. */ suppressHiDPIScaling?: boolean;
/** * Suppress play button, it is not recommended users of excalibur switch this feature. Some browsers require a user gesture (like a click) * for certain browser features to work like web audio. */ suppressPlayButton?: boolean;
/** * Sets the focus of the window, this is needed when hosting excalibur in a cross-origin/same-origin iframe in order for certain events * (like keyboard) to work. You can use * For example: itch.io or codesandbox.io * * By default set to true, */ grabWindowFocus?: boolean;
/** * Scroll prevention method. */ scrollPreventionMode?: ScrollPreventionMode;
/** * Optionally set the background color */ backgroundColor?: Color;
/** * Optionally set the maximum fps if not set Excalibur will go as fast as the device allows. * * 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 * one that bounces between 30fps and 60fps */ maxFps?: number;
/** * Optionally configure a fixed update timestep in milliseconds, this can be desirable if you need the physics simulation to be very stable. When * 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 * simulation to be correct, excalibur will run multiple updates in a row (at the configured update elapsed) to catch up, for example * there could be X updates and 1 draw each clock step. * * **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 * the current instantaneous framerate, or perf gain if the fixed rate is less than the current framerate. * * By default is unset and updates will use the current instantaneous framerate with 1 update and 1 draw each clock step. * * **WARN:** `fixedUpdateTimestep` takes precedence over `fixedUpdateFps` use whichever is most convenient. */ fixedUpdateTimestep?: number;
/** * Optionally configure a fixed update fps, this can be desirable if you need the physics simulation to be very stable. When 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 * simulation to be correct, excalibur will run multiple updates in a row (at the configured update elapsed) to catch up, for example * there could be X updates and 1 draw each clock step. * * **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 * the current instantaneous framerate, or perf gain if the fixed rate is less than the current framerate. * * By default is unset and updates will use the current instantaneous framerate with 1 update and 1 draw each clock step. * * **WARN:** `fixedUpdateTimestep` takes precedence over `fixedUpdateFps` use whichever is most convenient. */ fixedUpdateFps?: number;
/** * Default `true`, optionally configure excalibur to use optimal draw call sorting, to opt out set this to `false`. * * Excalibur will automatically sort draw calls by z and priority into renderer batches for maximal draw performance, * this can disrupt a specific desired painter order. * */ useDrawSorting?: boolean;
/** * Optionally provide a custom handler for the webgl context lost event */ handleContextLost?: (e: Event) => void;
/** * Optionally provide a custom handler for the webgl context restored event */ handleContextRestored?: (e: Event) => void;
/** * Optionally configure how excalibur handles poor performance on a player's browser */ configurePerformanceCanvas2DFallback?: { /** * By default `false`, this will switch the internal graphics context to Canvas2D which can improve performance on non hardware * accelerated browsers. */ allow: boolean; /** * By default `false`, if set to `true` a dialogue will be presented to the player about their browser and how to potentially * address any issues. */ showPlayerMessage?: boolean; /** * Default `{ numberOfFrames: 100, fps: 20 }`, optionally configure excalibur to fallback to the 2D Canvas renderer * if bad performance is detected. * * 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 * Canvas renderer. */ threshold?: { numberOfFrames: number; fps: number }; };
/** * Optionally configure the physics simulation in excalibur * * If false, Excalibur will not produce a physics simulation. * * Default is configured to use {@apilink SolverStrategy.Arcade} physics simulation */ physics?: boolean | PhysicsConfig;
/** * Optionally enable the 2D lighting simulation in excalibur, adding the {@apilink LightingSystem} and * {@apilink FlickerSystem} to every scene. * * **Potential performance impact** — the lighting overlay is rasterized with the 2D Canvas API and * re-uploaded to the GPU every frame, which carries a performance penalty. * * Pass a {@apilink LightingConfig} to tune the lighting simulation's defaults instead of just enabling it. * * Default is false */ lighting?: boolean | LightingConfig;
/** * Optionally specify scenes with their transitions and loaders to excalibur's scene {@apilink Director} * * Scene transitions can can overridden dynamically by the `Scene` or by the call to `.goToScene` */ scenes?: SceneMap<TKnownScenes>;}
/** * The Excalibur Engine * * The {@apilink Engine} is the main driver for a game. It is responsible for * starting/stopping the game, maintaining state, transmitting events, * loading resources, and managing the scene. */export class Engine<TKnownScenes extends string = any> implements CanInitialize, CanUpdate, CanDraw { static Context: Context<Engine | null> = createContext<Engine | null>(); static useEngine(): Engine { const value = useContext(Engine.Context);
if (!value) { throw new Error('Cannot inject engine with `useEngine()`, `useEngine()` was called outside of Engine lifecycle scope.'); }
return value; } static InstanceCount = 0;
/** * Anything run under scope can use `useEngine()` to inject the current engine * @param cb */ scope = <TReturn>(cb: () => TReturn) => Engine.Context.scope(this, cb);
public global!: GlobalEventHandlers;
private _plugins: Plugin[] = []; public get plugins(): readonly Plugin[] { return this._plugins; }
/** * Check whether a plugin with the given name is currently installed. * * @param name The unique plugin name to check * @returns `true` if a plugin with that name is installed */ public hasPlugin(name: string): boolean { return this._plugins.some((p) => p.name === name); }
/** * Add a plugin to the engine after construction. * * If the engine has already passed certain lifecycle stages, the plugin's hooks * for those stages will be called immediately so the plugin can "catch up". * * If a plugin with the same {@apilink Plugin.name} is already installed, a warning * is logged and the plugin is not added. * * @param plugin The plugin to add * @returns `true` if the plugin was added, `false` if it was skipped (duplicate name) */ public addPlugin(plugin: Plugin): boolean { if (this._plugins.some((p) => p.name === plugin.name)) { this._logger.warn(`Plugin with name "${plugin.name}" is already installed, skipping duplicate.`); return false; }
// Insert maintaining priority sort order const insertIndex = this._plugins.findIndex((p) => p.priority > plugin.priority); if (insertIndex === -1) { this._plugins.push(plugin); } else { this._plugins.splice(insertIndex, 0, plugin); }
// Catch up on lifecycle stages the engine has already passed // Engine config hooks plugin.onEnginePreConfig?.(this, this._originalOptions); plugin.onEnginePostConfig?.(this, this._originalOptions);
// Graphics hooks - if the graphics context already exists if (this.graphicsContext) { const graphicsOptions = this._originalOptions as unknown as ExcaliburGraphicsContextOptions; plugin.onGraphicsPreConfig?.(this.graphicsContext, graphicsOptions); plugin.onGraphicsPostConfig?.(this.graphicsContext, graphicsOptions); plugin.onGraphicsPreInitialize?.(this.graphicsContext); plugin.onGraphicsPostInitialize?.(this.graphicsContext); }
// Engine initialization hooks - if the engine is already initialized if (this.isInitialized) { plugin.onEnginePreInitialize?.(this); plugin.onEnginePostInitialize?.(this); }
return true; }
/** * Remove a plugin from the engine by name. * * The plugin's {@apilink Plugin.dispose} method will be called if present. * * @param name The unique plugin name to remove * @returns `true` if the plugin was found and removed */ public removePlugin(name: string): boolean { const index = this._plugins.findIndex((p) => p.name === name); if (index === -1) { return false; } const [removed] = this._plugins.splice(index, 1); removed?.dispose?.(); return true; }
private _garbageCollector!: GarbageCollector;
public readonly garbageCollectorConfig!: GarbageCollectionOptions | null;
/** * Current Excalibur version string * * Useful for plugins or other tools that need to know what features are available */ public readonly version = EX_VERSION;
/** * Listen to and emit events on the Engine */ public events = new EventEmitter<EngineEvents>();
/** * Excalibur browser events abstraction used for wiring to native browser events safely */ public browser: BrowserEvents;
/** * Screen abstraction */ public screen!: Screen;
/** * Scene director, manages all scenes, scene transitions, and loaders in excalibur */ public director!: Director<TKnownScenes>;
/** * Direct access to the engine's canvas element */ public canvas!: HTMLCanvasElement;
/** * Direct access to the ExcaliburGraphicsContext used for drawing things to the screen */ public graphicsContext!: ExcaliburGraphicsContext;
/** * Direct access to the canvas element ID, if an ID exists */ public canvasElementId?: string;
/** * Direct access to the physics configuration for excalibur */ public physics!: DeepRequired<PhysicsConfig>;
/** * Direct access to the lighting configuration for excalibur */ public lighting!: DeepRequired<LightingConfig>;
/** * Optionally set the maximum fps if not set Excalibur will go as fast as the device allows. * * 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 * one that bounces between 30fps and 60fps */ public maxFps: number = Number.POSITIVE_INFINITY;
/** * Optionally configure a fixed update fps, this can be desirable if you need the physics simulation to be very stable. When 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 * simulation to be correct, excalibur will run multiple updates in a row (at the configured update elapsed) to catch up, for example * there could be X updates and 1 draw each clock step. * * **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 * the current instantaneous framerate, or perf gain if the fixed rate is less than the current framerate. * * By default is unset and updates will use the current instantaneous framerate with 1 update and 1 draw each clock step. * * **WARN:** `fixedUpdateTimestep` takes precedence over `fixedUpdateFps` use whichever is most convenient. */ public readonly fixedUpdateFps?: number;
/** * Optionally configure a fixed update timestep in milliseconds, this can be desirable if you need the physics simulation to be very stable. When * 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 * simulation to be correct, excalibur will run multiple updates in a row (at the configured update elapsed) to catch up, for example * there could be X updates and 1 draw each clock step. * * **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 * the current instantaneous framerate, or perf gain if the fixed rate is less than the current framerate. * * By default is unset and updates will use the current instantaneous framerate with 1 update and 1 draw each clock step. * * **WARN:** `fixedUpdateTimestep` takes precedence over `fixedUpdateFps` use whichever is most convenient. */ public readonly fixedUpdateTimestep?: number;
/** * Direct access to the excalibur clock */ public clock!: Clock;
public readonly pointerScope!: PointerScope; public readonly grabWindowFocus!: boolean;
/** * The width of the game canvas in pixels (physical width component of the * resolution of the canvas element) */ public get canvasWidth(): number { return this.screen.canvasWidth; }
/** * Returns half width of the game canvas in pixels (half physical width component) */ public get halfCanvasWidth(): number { return this.screen.halfCanvasWidth; }
/** * The height of the game canvas in pixels, (physical height component of * the resolution of the canvas element) */ public get canvasHeight(): number { return this.screen.canvasHeight; }
/** * Returns half height of the game canvas in pixels (half physical height component) */ public get halfCanvasHeight(): number { return this.screen.halfCanvasHeight; }
/** * Returns the width of the engine's visible drawing surface in pixels including zoom and device pixel ratio. */ public get drawWidth(): number { return this.screen.drawWidth; }
/** * Returns half the width of the engine's visible drawing surface in pixels including zoom and device pixel ratio. */ public get halfDrawWidth(): number { return this.screen.halfDrawWidth; }
/** * Returns the height of the engine's visible drawing surface in pixels including zoom and device pixel ratio. */ public get drawHeight(): number { return this.screen.drawHeight; }
/** * Returns half the height of the engine's visible drawing surface in pixels including zoom and device pixel ratio. */ public get halfDrawHeight(): number { return this.screen.halfDrawHeight; }
/** * Returns whether excalibur detects the current screen to be HiDPI */ public get isHiDpi(): boolean { return this.screen.isHiDpi; }
/** * Access engine input like pointer, keyboard, or gamepad */ public input!: InputHost;
/** * Map multiple input sources to specific game actions actions */ public inputMapper!: InputMapper;
private _inputEnabled: boolean = true;
/** * Access Excalibur debugging functionality. * * Useful when you want to debug different aspects of built in engine features like * * Transform * * Graphics * * Colliders */ public debug!: DebugConfig;
/** * Access {@apilink stats} that holds frame statistics. */ public get stats(): DebugStats { return this.debug.stats; }
/** * The current {@apilink Scene} being drawn and updated on screen */ public get currentScene(): Scene { return this.director.currentScene; }
/** * The current {@apilink Scene} being drawn and updated on screen */ public get currentSceneName(): string { return this.director.currentSceneName; }
/** * The default {@apilink Scene} of the game, use {@apilink Engine.goToScene} to transition to different scenes. */ public get rootScene(): Scene { return this.director.rootScene; }
/** * Contains all the scenes currently registered with Excalibur */ public get scenes(): { [key: string]: Scene | SceneConstructor | SceneWithOptions } { return this.director.scenes; }
/** * Indicates whether the engine is set to fullscreen or not */ public get isFullscreen(): boolean { return this.screen.isFullscreen; }
/** * Indicates the current {@apilink DisplayMode} of the engine. */ public get displayMode(): DisplayMode { return this.screen.displayMode; }
private _suppressPlayButton: boolean = false; /** * Returns the calculated pixel ration for use in rendering */ public get pixelRatio(): number { return this.screen.pixelRatio; }
/** * Indicates whether audio should be paused when the game is no longer visible. */ public pauseAudioWhenHidden: boolean = true;
/** * Indicates whether the engine should draw with debug information */ private _isDebug: boolean = false; public get isDebug(): boolean { return this._isDebug; }
/** * Sets the background color for the engine. */ public backgroundColor!: Color;
/** * Sets the Transparency for the engine. */ public enableCanvasTransparency: boolean = true;
/** * Hints the graphics context to truncate fractional world space coordinates */ public get snapToPixel(): boolean { return this.graphicsContext.snapToPixel; }
public set snapToPixel(shouldSnapToPixel: boolean) { this.graphicsContext.snapToPixel = shouldSnapToPixel; }
/** * The action to take when a fatal exception is thrown */ public onFatalException = (e: any) => { Logger.getInstance().fatal(e, e.stack); };
/** * The mouse wheel scroll prevention mode */ public pageScrollPreventionMode!: ScrollPreventionMode;
private _logger!: Logger;
private _toaster: Toaster = new Toaster();
// this determines whether excalibur is compatible with your browser private _compatible: boolean;
private _timescale: number = 1.0;
// loading private _loader!: DefaultLoader;
private _isInitialized: boolean = false;
private _hasCreatedCanvas: boolean = false;
public emit<TEventName extends EventKey<EngineEvents>>(eventName: TEventName, event: EngineEvents[TEventName]): void; public emit(eventName: string, event?: any): void; public emit<TEventName extends EventKey<EngineEvents> | string>(eventName: TEventName, event?: any): void { this.events.emit(eventName, event); }
public on<TEventName extends EventKey<EngineEvents>>(eventName: TEventName, handler: Handler<EngineEvents[TEventName]>): Subscription; public on(eventName: string, handler: Handler<unknown>): Subscription; public on<TEventName extends EventKey<EngineEvents> | string>(eventName: TEventName, handler: Handler<any>): Subscription { return this.events.on(eventName, handler); }
public once<TEventName extends EventKey<EngineEvents>>(eventName: TEventName, handler: Handler<EngineEvents[TEventName]>): Subscription; public once(eventName: string, handler: Handler<unknown>): Subscription; public once<TEventName extends EventKey<EngineEvents> | string>(eventName: TEventName, handler: Handler<any>): Subscription { return this.events.once(eventName, handler); }
public off<TEventName extends EventKey<EngineEvents>>(eventName: TEventName, handler: Handler<EngineEvents[TEventName]>): void; public off(eventName: string, handler: Handler<unknown>): void; public off(eventName: string): void; public off<TEventName extends EventKey<EngineEvents> | string>(eventName: TEventName, handler?: Handler<any>): void { this.events.off(eventName, handler as any); }
/** * Default {@apilink EngineOptions} */ private static _DEFAULT_ENGINE_OPTIONS: EngineOptions = { width: 0, height: 0, enableCanvasTransparency: true, useDrawSorting: true, configurePerformanceCanvas2DFallback: { allow: false, showPlayerMessage: false, threshold: { fps: 20, numberOfFrames: 100 } }, canvasElementId: '', canvasElement: undefined, enableCanvasContextMenu: false, snapToPixel: false, antialiasing: true, pixelArt: false, garbageCollection: true, lighting: false, powerPreference: 'high-performance', pointerScope: PointerScope.Canvas, suppressConsoleBootMessage: undefined, suppressMinimumBrowserFeatureDetection: undefined, suppressHiDPIScaling: undefined, suppressPlayButton: undefined, grabWindowFocus: true, scrollPreventionMode: ScrollPreventionMode.Canvas, backgroundColor: Color.fromHex('#2185d0') // Excalibur blue };
private _originalOptions: EngineOptions = {}; public readonly _originalDisplayMode!: DisplayMode;
/** * Creates a new game using the given {@apilink EngineOptions}. By default, if no options are provided, * the game will be rendered full screen (taking up all available browser window space). * You can customize the game rendering through {@apilink EngineOptions}. * * Example: * * ```js * var game = new ex.Engine({ * width: 0, // the width of the canvas * height: 0, // the height of the canvas * enableCanvasTransparency: true, // the transparencySection of the canvas * canvasElementId: '', // the DOM canvas element ID, if you are providing your own * displayMode: ex.DisplayMode.FullScreen, // the display mode * pointerScope: ex.PointerScope.Document, // the scope of capturing pointer (mouse/touch) events * backgroundColor: ex.Color.fromHex('#2185d0') // background color of the engine * }); * * // call game.start, which is a Promise * game.start().then(function () { * // ready, set, go! * }); * ``` */ constructor(options?: EngineOptions<TKnownScenes>) { options = { ...Engine._DEFAULT_ENGINE_OPTIONS, ...options }; this._originalOptions = options;
if (options.plugins && options.plugins.length > 0) { const logger = Logger.getInstance(); for (const plugin of options.plugins) { if (this._plugins.some((p) => p.name === plugin.name)) { logger.warn(`Plugin with name "${plugin.name}" is already installed, skipping duplicate.`); continue; } this._plugins.push(plugin); } this._plugins.sort((a, b) => a.priority - b.priority); }
for (const plugin of this._plugins) { plugin.onEnginePreConfig?.(this, options); }
Flags.freeze();
// Initialize browser events facade this.browser = new BrowserEvents(window, document);
// Check compatibility const detector = new Detector(); if (!options.suppressMinimumBrowserFeatureDetection && !(this._compatible = detector.test())) { const message = document.createElement('div'); message.innerText = 'Sorry, your browser does not support all the features needed for Excalibur'; document.body.appendChild(message);
detector.failedTests.forEach(function (test) { const testMessage = document.createElement('div'); testMessage.innerText = 'Browser feature missing ' + test; document.body.appendChild(testMessage); });
if (options.canvasElementId) { const canvas = document.getElementById(options.canvasElementId); if (canvas) { canvas.parentElement!.removeChild(canvas); } }
return; } else { this._compatible = true; }
// Use native console API for color fun // eslint-disable-next-line no-console if (console.log && !options.suppressConsoleBootMessage) { // eslint-disable-next-line no-console console.log( `%cPowered by Excalibur.js (v${EX_VERSION})`, 'background: #176BAA; color: white; border-radius: 5px; padding: 15px; font-size: 1.5em; line-height: 80px;' ); // eslint-disable-next-line no-console console.log( '\n\ /| ________________\n\O|===|* >________________>\n\ \\|' ); // eslint-disable-next-line no-console console.log('Visit', 'http://excaliburjs.com', 'for more information'); }
// Suppress play button if (options.suppressPlayButton) { this._suppressPlayButton = true; }
this._logger = Logger.getInstance();
this.debug = new DebugConfig(this);
// If debug is enabled, let's log browser features to the console. if (this._logger.defaultLevel === LogLevel.Debug) { detector.logBrowserFeatures(); }
this._logger.debug('Building engine...'); if (options.garbageCollection === true) { this.garbageCollectorConfig = { ...DefaultGarbageCollectionOptions }; } else if (options.garbageCollection === false) { this._logger.warn( 'WebGL Garbage Collection Disabled!!! If you leak any images over time your game will crash when GPU memory is exhausted' ); this.garbageCollectorConfig = null; } else { this.garbageCollectorConfig = { ...DefaultGarbageCollectionOptions, ...options.garbageCollection }; } this._garbageCollector = new GarbageCollector({ getTimestamp: Date.now });
this.canvasElementId = options.canvasElementId;
if (options.canvasElementId) { this._logger.debug('Using Canvas element specified: ' + options.canvasElementId);
//test for existence of element if (document.getElementById(options.canvasElementId) === null) { throw new Error('Cannot find existing element in the DOM, please ensure element is created prior to engine creation.'); }
this.canvas = <HTMLCanvasElement>document.getElementById(options.canvasElementId); this._hasCreatedCanvas = false; } else if (options.canvasElement) { this._logger.debug('Using Canvas element specified:', options.canvasElement); this.canvas = options.canvasElement; this._hasCreatedCanvas = false; } else { this._logger.debug('Using generated canvas element'); this.canvas = <HTMLCanvasElement>document.createElement('canvas'); this._hasCreatedCanvas = true; }
if (this.canvas && !options.enableCanvasContextMenu) { this.canvas.addEventListener('contextmenu', (evt) => { evt.preventDefault(); }); }
let displayMode = options.displayMode ?? DisplayMode.Fixed; if ((options.width && options.height) || options.viewport) { if (options.displayMode === undefined) { displayMode = DisplayMode.Fixed; } this._logger.debug('Engine viewport is size ' + options.width + ' x ' + options.height); } else if (!options.displayMode) { this._logger.debug('Engine viewport is fit'); displayMode = DisplayMode.FitScreen; }
const global = (options.global && typeof options.global === 'function' ? options.global() : options.global) as GlobalEventHandlers;
this.global = global ?? getDefaultGlobal(); this.grabWindowFocus = options.grabWindowFocus!; this.pointerScope = options.pointerScope!;
this._originalDisplayMode = displayMode;
let pixelArtSampler: boolean; let uvPadding: number; let nativeContextAntialiasing: boolean; let canvasImageRendering: 'pixelated' | 'auto'; let filtering: ImageFiltering; let multiSampleAntialiasing: boolean | { samples: number }; if (typeof options.antialiasing === 'object') { ({ pixelArtSampler, nativeContextAntialiasing, multiSampleAntialiasing, filtering, canvasImageRendering } = { ...(options.pixelArt ? DefaultPixelArtOptions : DefaultAntialiasOptions), ...options.antialiasing }); } else { pixelArtSampler = !!options.pixelArt; nativeContextAntialiasing = false; multiSampleAntialiasing = options.antialiasing!; canvasImageRendering = options.antialiasing ? 'auto' : 'pixelated'; filtering = options.antialiasing ? ImageFiltering.Blended : ImageFiltering.Pixel; }
if (nativeContextAntialiasing && multiSampleAntialiasing) { this._logger.warnOnce( `Cannot use antialias setting nativeContextAntialiasing and multiSampleAntialiasing` + ` at the same time, they are incompatible settings. If you aren\'t sure use multiSampleAntialiasing` ); }
if (options.pixelArt) { uvPadding = 0.25; }
if (!options.antialiasing || filtering === ImageFiltering.Pixel) { uvPadding = 0; }
// Override with any user option, if non default to .25 for pixel art, 0.01 for everything else uvPadding = options.uvPadding ?? uvPadding! ?? 0.01;
// Canvas 2D fallback can be flagged on let useCanvasGraphicsContext = Flags.isEnabled('use-canvas-context'); if (!useCanvasGraphicsContext) { // Attempt webgl first try { const onGraphicsPreConfig = (context: ExcaliburGraphicsContext, options: ExcaliburGraphicsContextWebGLOptions) => { for (const plugin of this._plugins) { plugin.onGraphicsPreConfig?.(context, options); } };
const onGraphicsPostConfig = (context: ExcaliburGraphicsContext, options: ExcaliburGraphicsContextWebGLOptions) => { for (const plugin of this._plugins) { plugin.onGraphicsPostConfig?.(context, options); } };
const onGraphicsPreInitialize = (context: ExcaliburGraphicsContext) => { for (const plugin of this._plugins) { plugin.onGraphicsPreInitialize?.(context); } };
const onGraphicsPostInitialize = (context: ExcaliburGraphicsContext) => { for (const plugin of this._plugins) { plugin.onGraphicsPostInitialize?.(context); } };
this.graphicsContext = new ExcaliburGraphicsContextWebGL({ canvasElement: this.canvas, enableTransparency: this.enableCanvasTransparency, pixelArtSampler: pixelArtSampler, antialiasing: nativeContextAntialiasing, multiSampleAntialiasing: multiSampleAntialiasing, uvPadding: uvPadding, powerPreference: options.powerPreference, backgroundColor: options.backgroundColor, snapToPixel: options.snapToPixel, useDrawSorting: options.useDrawSorting, garbageCollector: this.garbageCollectorConfig ? { garbageCollector: this._garbageCollector!, collectionInterval: this.garbageCollectorConfig!.textureCollectInterval! } : undefined, handleContextLost: options.handleContextLost ?? this._handleWebGLContextLost, handleContextRestored: options.handleContextRestored ?? this._handleWebGLContextRestored, onGraphicsPreConfig, onGraphicsPostConfig, onGraphicsPreInitialize, onGraphicsPostInitialize }); } catch (e) { this._logger.warn( `Excalibur could not load webgl for some reason (${(e as Error).message}) and loaded a Canvas 2D fallback. ` + `Some features of Excalibur will not work in this mode. \n\n` + 'Read more about this issue at https://excaliburjs.com/docs/performance' ); // fallback to canvas in case of failure useCanvasGraphicsContext = true; } }
if (useCanvasGraphicsContext) { this.graphicsContext = new ExcaliburGraphicsContext2DCanvas({ canvasElement: this.canvas, enableTransparency: this.enableCanvasTransparency, antialiasing: nativeContextAntialiasing, backgroundColor: options.backgroundColor, snapToPixel: options.snapToPixel, useDrawSorting: options.useDrawSorting }); }
this.screen = new Screen({ canvas: this.canvas, context: this.graphicsContext, antialiasing: nativeContextAntialiasing, canvasImageRendering: canvasImageRendering, browser: this.browser, viewport: options.viewport ?? (options.width && options.height ? { width: options.width, height: options.height } : Resolution.SVGA), resolution: options.resolution, displayMode, pixelRatio: options.suppressHiDPIScaling ? 1 : (options.pixelRatio ?? undefined) });
// TODO REMOVE STATIC!!! // Set default filtering based on antialiasing TextureLoader.filtering = filtering;
if (options.backgroundColor) { this.backgroundColor = options.backgroundColor.clone(); }
this.maxFps = options.maxFps ?? this.maxFps;
this.fixedUpdateTimestep = options.fixedUpdateTimestep ?? this.fixedUpdateTimestep; this.fixedUpdateFps = options.fixedUpdateFps ?? this.fixedUpdateFps; this.fixedUpdateTimestep = this.fixedUpdateTimestep || 1000 / this.fixedUpdateFps!;
this.clock = new StandardClock({ maxFps: this.maxFps, tick: this._mainloop.bind(this), onFatalException: (e) => this.onFatalException(e) });
this.enableCanvasTransparency = options.enableCanvasTransparency!;
if (typeof options.physics === 'boolean') { this.physics = { ...getDefaultPhysicsConfig(), enabled: options.physics }; } else { this.physics = { ...getDefaultPhysicsConfig() }; mergeDeep(this.physics, options.physics!); }
if (typeof options.lighting === 'boolean') { this.lighting = { ...getDefaultLightingConfig(), enabled: options.lighting }; } else { this.lighting = { ...getDefaultLightingConfig() }; mergeDeep(this.lighting, options.lighting!); }
this.director = new Director(this, options.scenes!); this.director.events.pipe(this.events);
this._initialize(options);
(window as any).___EXCALIBUR_DEVTOOL = this; Engine.InstanceCount++; }
private _handleWebGLContextLost = (e: Event) => { e.preventDefault(); this.clock.stop(); this._logger.fatalOnce('WebGL Graphics Lost', e);
// Notify plugins that the graphics context was lost for (const plugin of this._plugins) { plugin.onGraphicsContextLost?.(this.graphicsContext); }
const container = document.createElement('div'); container.id = 'ex-webgl-graphics-context-lost'; container.style.position = 'absolute'; container.style.zIndex = '99'; container.style.left = '50%'; container.style.top = '50%'; container.style.display = 'flex'; container.style.flexDirection = 'column'; container.style.transform = 'translate(-50%, -50%)'; container.style.backgroundColor = 'white'; container.style.padding = '10px'; container.style.borderStyle = 'solid 1px';
const div = document.createElement('div'); div.innerHTML = ` <h1>There was an issue rendering, please refresh the page.</h1> <div> <p>WebGL Graphics Context Lost</p>
<button id="ex-webgl-graphics-reload">Refresh Page</button>
<p>There are a few reasons this might happen:</p> <ul> <li>Two or more pages are placing a high demand on the GPU</li> <li>Another page or operation has stalled the GPU and the browser has decided to reset the GPU</li> <li>The computer has multiple GPUs and the user has switched between them</li> <li>Graphics driver has crashed or restarted</li> <li>Graphics driver was updated</li> </ul> </div> `; container.appendChild(div); if (this.canvas?.parentElement) { this.canvas.parentElement.appendChild(container); const button = div.querySelector('#ex-webgl-graphics-reload'); button?.addEventListener('click', () => location.reload()); } };
private _handleWebGLContextRestored = () => { this._logger.debug('WebGL Graphics Context Restored');
// Notify plugins that the graphics context was restored. // The graphics context's _init() will re-run and call onGraphicsPreInitialize/onGraphicsPostInitialize // so plugins can rebuild their WebGL resources. for (const plugin of this._plugins) { plugin.onGraphicsContextRestored?.(this.graphicsContext); }
// Restart the clock this.browser.resume(); this.clock.start(); };
private _performanceThresholdTriggered = false; private _fpsSamples: number[] = []; private _monitorPerformanceThresholdAndTriggerFallback() { const { allow } = this._originalOptions.configurePerformanceCanvas2DFallback!; let { threshold, showPlayerMessage } = this._originalOptions.configurePerformanceCanvas2DFallback!; if (threshold === undefined) { threshold = Engine._DEFAULT_ENGINE_OPTIONS.configurePerformanceCanvas2DFallback!.threshold; } if (showPlayerMessage === undefined) { showPlayerMessage = Engine._DEFAULT_ENGINE_OPTIONS.configurePerformanceCanvas2DFallback!.showPlayerMessage; } if (!Flags.isEnabled('use-canvas-context') && allow && this.ready && !this._performanceThresholdTriggered) { // Calculate Average fps for last X number of frames after start if (this._fpsSamples.length === threshold!.numberOfFrames) { this._fpsSamples.splice(0, 1); } this._fpsSamples.push(this.clock.fpsSampler.fps); let total = 0; for (let i = 0; i < this._fpsSamples.length; i++) { total += this._fpsSamples[i]; } const average = total / this._fpsSamples.length;
if (this._fpsSamples.length === threshold!.numberOfFrames) { if (average <= threshold!.fps) { this._performanceThresholdTriggered = true; this._logger.warn( `Switching to browser 2D Canvas fallback due to performance. Some features of Excalibur will not work in this mode.\n` + "this might mean your browser doesn't have webgl enabled or hardware acceleration is unavailable.\n\n" + 'If in Chrome:\n' + ' * Visit Settings > Advanced > System, and ensure "Use Hardware Acceleration" is checked.\n' + ' * Visit chrome://flags/#ignore-gpu-blocklist and ensure "Override software rendering list" is "enabled"\n' + 'If in Firefox, visit about:config\n' + ' * Ensure webgl.disabled = false\n' + ' * Ensure webgl.force-enabled = true\n' + ' * Ensure layers.acceleration.force-enabled = true\n\n' + 'Read more about this issue at https://excaliburjs.com/docs/performance' );
if (showPlayerMessage) { this._toaster.toast( 'Excalibur is encountering performance issues. ' + "It's possible that your browser doesn't have hardware acceleration enabled. " + 'Visit [LINK] for more information and potential solutions.', 'https://excaliburjs.com/docs/performance' ); } this.useCanvas2DFallback(); this.emit('fallbackgraphicscontext', this.graphicsContext); } } } }
/** * Switches the engine's graphics context to the 2D Canvas. * @warning Some features of Excalibur will not work in this mode. */ public useCanvas2DFallback() { // Swap out the canvas const newCanvas = this.canvas.cloneNode(false) as HTMLCanvasElement; this.canvas!.parentNode!.replaceChild(newCanvas, this.canvas); this.canvas = newCanvas;
const options = { ...this._originalOptions, antialiasing: this.screen.antialiasing }; const displayMode = this._originalDisplayMode;
// New graphics context this.graphicsContext = new ExcaliburGraphicsContext2DCanvas({ canvasElement: this.canvas, enableTransparency: this.enableCanvasTransparency, antialiasing: options.antialiasing, backgroundColor: options.backgroundColor, snapToPixel: options.snapToPixel, useDrawSorting: options.useDrawSorting });
// Reset screen if (this.screen) { this.screen.dispose(); }
this.screen = new Screen({ canvas: this.canvas, context: this.graphicsContext, antialiasing: options.antialiasing ?? true, browser: this.browser, viewport: options.viewport ?? (options.width && options.height ? { width: options.width, height: options.height } : Resolution.SVGA), resolution: options.resolution, displayMode, pixelRatio: options.suppressHiDPIScaling ? 1 : (options.pixelRatio ?? undefined) }); this.screen.setCurrentCamera(this.currentScene.camera);
// Reset pointers this.input.pointers.detach(); const pointerTarget = options && options.pointerScope === PointerScope.Document ? document : this.canvas; this.input.pointers = this.input.pointers.recreate(pointerTarget, this); this.input.pointers.init(); }
private _disposed = false; /** * Attempts to completely clean up excalibur resources, including removing the canvas from the dom. * * To start again you will need to new up an Engine. */ public dispose() { if (!this._disposed) { this._disposed = true; this.stop(); this._garbageCollector.forceCollectAll(); this.input.toggleEnabled(false); this.input.pointers.detach(); this.director.dispose();
for (const plugin of this.plugins) { plugin.dispose?.(); }
if (this._hasCreatedCanvas) { this.canvas!.parentNode!.removeChild(this.canvas); } this.canvas = null as any; this.screen.dispose(); this.graphicsContext.dispose(); this.graphicsContext = null as any; Engine.InstanceCount--; } }
public isDisposed() { return this._disposed; }
/** * Returns a BoundingBox of the top left corner of the screen * and the bottom right corner of the screen. */ public getWorldBounds() { return this.screen.getWorldBounds(); }
/** * Gets the current engine timescale factor (default is 1.0 which is 1:1 time) */ public get timescale() { return this._timescale; }
/** * Sets the current engine timescale factor. Useful for creating slow-motion effects or fast-forward effects * when using time-based movement. */ public set timescale(value: number) { if (value < 0) { Logger.getInstance().warnOnce('engine.timescale to a value less than 0 are ignored'); return; }
this._timescale = value; }
/** * Adds a {@apilink Timer} to the {@apilink currentScene}. * @param timer The timer to add to the {@apilink currentScene}. */ public addTimer(timer: Timer): Timer { return this.currentScene.addTimer(timer); }
/** * Removes a {@apilink Timer} from the {@apilink currentScene}. * @param timer The timer to remove to the {@apilink currentScene}. */ public removeTimer(timer: Timer): Timer { return this.currentScene.removeTimer(timer); }
/** * Adds a {@apilink Scene} to the engine, think of scenes in Excalibur as you * would levels or menus. * @param key The name of the scene, must be unique * @param scene The scene to add to the engine */ public addScene<TScene extends string>(key: TScene, scene: Scene | SceneConstructor | SceneWithOptions): Engine<TKnownScenes | TScene> { this.director.add(key, scene); return this as Engine<TKnownScenes | TScene>; }
/** * Removes a {@apilink Scene} instance from the engine * @param scene The scene to remove */ public removeScene(scene: Scene | SceneConstructor): void; /** * Removes a scene from the engine by key * @param key The scene key to remove */ public removeScene(key: string): void; /** * @internal */ public removeScene(entity: any): void { this.director.remove(entity); }
/** * Adds a {@apilink Scene} to the engine, think of scenes in Excalibur as you * would levels or menus. * @param sceneKey The key of the scene, must be unique * @param scene The scene to add to the engine */ public add(sceneKey: string, scene: Scene | SceneConstructor | SceneWithOptions): void; /** * Adds a {@apilink Timer} to the {@apilink currentScene}. * @param timer The timer to add to the {@apilink currentScene}. */ public add(timer: Timer): void; /** * Adds a {@apilink TileMap} to the {@apilink currentScene}, once this is done the TileMap * will be drawn and updated. */ public add(tileMap: TileMap): void; /** * Adds an actor to the {@apilink currentScene} of the game. This is synonymous * to calling `engine.currentScene.add(actor)`. * * Actors can only be drawn if they are a member of a scene, and only * the {@apilink currentScene} may be drawn or updated. * @param actor The actor to add to the {@apilink currentScene} */ public add(actor: Actor): void;
public add(entity: Entity): void;
/** * Adds a {@apilink ScreenElement} to the {@apilink currentScene} of the game, * ScreenElements do not participate in collisions, instead the * remain in the same place on the screen. * @param screenElement The ScreenElement to add to the {@apilink currentScene} */ public add(screenElement: ScreenElement): void; public add(entity: any): void { if (arguments.length === 2) { this.director.add(<string>arguments[0], <Scene | SceneConstructor | SceneWithOptions>arguments[1]); return; } const maybeDeferred = this.director.getDeferredScene(); if (maybeDeferred instanceof Scene) { maybeDeferred.add(entity); } else { this.currentScene.add(entity); } }
/** * Removes a scene instance from the engine * @param scene The scene to remove */ public remove(scene: Scene | SceneConstructor): void; /** * Removes a scene from the engine by key * @param sceneKey The scene to remove */ public remove(sceneKey: string): void; /** * Removes a {@apilink Timer} from the {@apilink currentScene}. * @param timer The timer to remove to the {@apilink currentScene}. */ public remove(timer: Timer): void; /** * Removes a {@apilink TileMap} from the {@apilink currentScene}, it will no longer be drawn or updated. */ public remove(tileMap: TileMap): void; /** * Removes an actor from the {@apilink currentScene} of the game. This is synonymous * to calling `engine.currentScene.removeChild(actor)`. * Actors that are removed from a scene will no longer be drawn or updated. * @param actor The actor to remove from the {@apilink currentScene}. */ public remove(actor: Actor): void; /** * Removes a {@apilink ScreenElement} to the scene, it will no longer be drawn or updated * @param screenElement The ScreenElement to remove from the {@apilink currentScene} */ public remove(screenElement: ScreenElement): void; public remove(entity: any): void { if (entity instanceof Entity) { this.currentScene.remove(entity); }
if (entity instanceof Scene || isSceneConstructor(entity)) { this.removeScene(entity); }
if (typeof entity === 'string') { this.removeScene(entity); } }
/** * Changes the current scene with optionally supplied: * * Activation data * * Transitions * * Loaders * * Example: * ```typescript * game.goToScene('myScene', { * sceneActivationData: {any: 'thing at all'}, * destinationIn: new FadeInOut({duration: 1000, direction: 'in'}), * sourceOut: new FadeInOut({duration: 1000, direction: 'out'}), * loader: MyLoader * }); * ``` * * Scenes are defined in the Engine constructor * ```typescript * const engine = new ex.Engine({ scenes: {...} }); * ``` * Or by adding dynamically * * ```typescript * engine.addScene('myScene', new ex.Scene()); * ``` * @param destinationScene * @param options */ public async goToScene<TData = undefined>(destinationScene: WithRoot<TKnownScenes>, options?: GoToOptions<TData>): Promise<void> { await this.scope(async () => { await this.director.goToScene(destinationScene, options); }); }
/** * Transforms the current x, y from screen coordinates to world coordinates * @param point Screen coordinate to convert */ public screenToWorldCoordinates(point: Vector): Vector { return this.screen.screenToWorldCoordinates(point); }
/** * Transforms a world coordinate, to a screen coordinate * @param point World coordinate to convert */ public worldToScreenCoordinates(point: Vector): Vector { return this.screen.worldToScreenCoordinates(point); }
/** * Initializes the internal canvas, rendering context, display mode, and native event listeners */ private _initialize(options?: EngineOptions) { this.pageScrollPreventionMode = options?.scrollPreventionMode!;
// initialize inputs const pointerTarget = options && options.pointerScope === PointerScope.Document ? document : this.canvas; const grabWindowFocus = this._originalOptions?.grabWindowFocus ?? true; this.input = new InputHost({ global: this.global, pointerTarget, grabWindowFocus, engine: this }); this.inputMapper = this.input.inputMapper;
// Issue #385 make use of the visibility api // https://developer.mozilla.org/en-US/docs/Web/Guide/User_experience/Using_the_Page_Visibility_API
this.browser.document.on('visibilitychange', () => { if (document.visibilityState === 'hidden') { this.events.emit('hidden', new HiddenEvent(this)); this._logger.debug('Window hidden'); } else if (document.visibilityState === 'visible') { this.events.emit('visible', new VisibleEvent(this)); this._logger.debug('Window visible'); } });
if (!this.canvasElementId && !options?.canvasElement) { document.body.appendChild(this.canvas); }
for (const plugin of this._plugins) { if (plugin.onEnginePostConfig) { plugin.onEnginePostConfig?.(this, options ?? {}); } } }
public toggleInputEnabled(enabled: boolean) { this._inputEnabled = enabled; this.input.toggleEnabled(this._inputEnabled); }
public onInitialize(engine: Engine) { // Override me }
/** * Gets whether the actor is Initialized */ public get isInitialized(): boolean { return this._isInitialized; }
private async _overrideInitialize(engine: Engine) { if (!this.isInitialized) { for (const plugin of this._plugins) { if (plugin.onEnginePreInitialize) { plugin.onEnginePreInitialize(this); } }
await this.director.onInitialize(); await this.onInitialize(engine); this.events.emit('initialize', new InitializeEvent(engine, this)); this._isInitialized = true;
for (const plugin of this._plugins) { if (plugin.onEnginePostInitialize) { plugin.onEnginePostInitialize(this); } } } }
/** * Updates the entire state of the game * @param elapsed Number of milliseconds elapsed since the last update. */ private _update(elapsed: number) { if (this._isLoading) { // suspend updates until loading is finished this._loader?.onUpdate(this, elapsed); // Update input listeners this.input.update(); return; }
// Publish preupdate events this.clock.__runScheduledCbs('preupdate'); this._preupdate(elapsed);
// process engine level events this.currentScene.update(this, elapsed);
// Update graphics postprocessors this.graphicsContext.updatePostProcessors(elapsed);
// Publish update event this.clock.__runScheduledCbs('postupdate'); this._postupdate(elapsed);
// Update input listeners this.input.update(); }
/** * @internal */ public _preupdate(elapsed: number) { this.emit('preupdate', new PreUpdateEvent(this, elapsed, this)); this.onPreUpdate(this, elapsed); }
/** * Safe to override method * @param engine The reference to the current game engine * @param elapsed The time elapsed since the last update in milliseconds */ public onPreUpdate(engine: Engine, elapsed: number) { // Override me }
/** * @internal */ public _postupdate(elapsed: number) { this.emit('postupdate', new PostUpdateEvent(this, elapsed, this)); this.onPostUpdate(this, elapsed); }
/** * Safe to override method * @param engine The reference to the current game engine * @param elapsed The time elapsed since the last update in milliseconds */ public onPostUpdate(engine: Engine, elapsed: number) { // Override me }
/** * Draws the entire game * @param elapsed Number of milliseconds elapsed since the last draw. */ private _draw(elapsed: number) { // Use scene background color if present, fallback to engine this.graphicsContext.backgroundColor = this.currentScene.backgroundColor ?? this.backgroundColor; this.graphicsContext.beginDrawLifecycle(); this.graphicsContext.clear(); this.clock.__runScheduledCbs('predraw'); this._predraw(this.graphicsContext, elapsed);
// Drawing nothing else while loading if (this._isLoading) { if (!this._hideLoader) { this._loader?.canvas.draw(this.graphicsContext, 0, 0); this.clock.__runScheduledCbs('postdraw'); this.graphicsContext.flush(); this.graphicsContext.endDrawLifecycle(); } return; }
this.currentScene.draw(this.graphicsContext, elapsed);
this.clock.__runScheduledCbs('postdraw'); this._postdraw(this.graphicsContext, elapsed);
// Flush any pending drawings this.graphicsContext.flush(); this.graphicsContext.endDrawLifecycle();
this._checkForScreenShots(); }
/** * @internal */ public _predraw(ctx: ExcaliburGraphicsContext, elapsed: number) { this.emit('predraw', new PreDrawEvent(ctx, elapsed, this)); this.onPreDraw(ctx, elapsed); }
/** * Safe to override method to hook into pre draw * @param ctx {@link ExcaliburGraphicsContext} for drawing * @param elapsed Number of milliseconds elapsed since the last draw. */ public onPreDraw(ctx: ExcaliburGraphicsContext, elapsed: number) { // Override me }
/** * @internal */ public _postdraw(ctx: ExcaliburGraphicsContext, elapsed: number) { this.emit('postdraw', new PostDrawEvent(ctx, elapsed, this)); this.onPostDraw(ctx, elapsed); }
/** * Safe to override method to hook into pre draw * @param ctx {@link ExcaliburGraphicsContext} for drawing * @param elapsed Number of milliseconds elapsed since the last draw. */ public onPostDraw(ctx: ExcaliburGraphicsContext, elapsed: number) { // Override me }
/** * Enable or disable Excalibur debugging functionality. * @param toggle a value that debug drawing will be changed to */ public showDebug(toggle: boolean): void { this._isDebug = toggle; }
/** * Toggle Excalibur debugging functionality. */ public toggleDebug(): boolean { this._isDebug = !this._isDebug; return this._isDebug; }
/** * Returns true when loading is totally complete and the player has clicked start */ public get loadingComplete() { return !this._isLoading; }
private _isLoading = false; private _hideLoader = false; private _isReadyFuture = new Future<void>(); public get ready() { return this._isReadyFuture.isCompleted; } public isReady(): Promise<void> { return this._isReadyFuture.promise; }
/** * Starts the internal game loop for Excalibur after loading * any provided assets. * @param loader Optional {@apilink Loader} to use to load resources. The default loader is {@apilink Loader}, * override to provide your own custom loader. * * Note: start() only resolves AFTER the user has clicked the play button */ public async start(loader?: DefaultLoader): Promise<void>; /** * Starts the internal game loop for Excalibur after configuring any routes, loaders, or transitions * @param sceneName * @param options {Optional} {@apilink StartOptions} to configure the routes for scenes in Excalibur * * Note: start() only resolves AFTER the user has clicked the play button */ public async start(sceneName: WithRoot<TKnownScenes>, options?: StartOptions): Promise<void>; public async start(sceneNameOrLoader?: WithRoot<TKnownScenes> | DefaultLoader, options?: StartOptions): Promise<void> { await this.scope(async () => { if (!this._compatible) { throw new Error('Excalibur is incompatible with your browser'); } this._isLoading = true; let loader: DefaultLoader; if (sceneNameOrLoader instanceof DefaultLoader) { loader = sceneNameOrLoader; } else if (typeof sceneNameOrLoader === 'string') { this.director.configureStart(sceneNameOrLoader, options); loader = this.director.mainLoader!; }
// Start the excalibur clock which drives the mainloop this._logger.debug('Starting game clock...'); this.browser.resume(); this.clock.start(); if (this.garbageCollectorConfig) { this._garbageCollector.start(); } this._logger.debug('Game clock started');
// Run plugin onLoad hooks before loading resources for (const plugin of this._plugins) { await plugin.onLoad?.(); }
await this.load(loader! ?? new Loader());
// Run plugin onLoadComplete hooks after resources are loaded for (const plugin of this._plugins) { await plugin.onLoadComplete?.(); }
// Initialize before ready await this._overrideInitialize(this);
this._isReadyFuture.resolve(); this.emit('start', new GameStartEvent(this)); return this._isReadyFuture.promise; }); }
/** * Returns the current frames elapsed milliseconds */ public currentFrameElapsedMs = 0;
/** * Returns the current frame lag when in fixed update mode */ public currentFrameLagMs = 0;
private _lagMs = 0; private _mainloop(elapsed: number) { this.scope(() => { this.emit('preframe', new PreFrameEvent(this, this.stats.prevFrame)); const elapsedMs = elapsed * this.timescale; this.currentFrameElapsedMs = elapsedMs;
// reset frame stats (reuse existing instances) const frameId = this.stats.prevFrame.id + 1; this.stats.currFrame.reset(); this.stats.currFrame.id = frameId; this.stats.currFrame.elapsedMs = elapsedMs; this.stats.currFrame.fps = this.clock.fpsSampler.fps; GraphicsDiagnostics.clear();
const beforeUpdate = this.clock.now(); const fixedTimestepMs = this.fixedUpdateTimestep; if (this.fixedUpdateTimestep) { this._lagMs += elapsedMs; while (this._lagMs >= fixedTimestepMs!) { this._update(fixedTimestepMs!); this._lagMs -= fixedTimestepMs!; } } else { this._update(elapsedMs); } const afterUpdate = this.clock.now(); this.currentFrameLagMs = this._lagMs; this._draw(elapsedMs); const afterDraw = this.clock.now();
this.stats.currFrame.duration.update = afterUpdate - beforeUpdate; this.stats.currFrame.duration.draw = afterDraw - afterUpdate; this.stats.currFrame.graphics.drawnImages = GraphicsDiagnostics.DrawnImagesCount; this.stats.currFrame.graphics.drawCalls = GraphicsDiagnostics.DrawCallCount; this.stats.currFrame.graphics.rendererSwaps = GraphicsDiagnostics.RendererSwaps;
this.emit('postframe', new PostFrameEvent(this, this.stats.currFrame)); this.stats.prevFrame.reset(this.stats.currFrame);
this._monitorPerformanceThresholdAndTriggerFallback(); }); }
/** * Stops Excalibur's main loop, useful for pausing the game. */ public stop() { if (this.clock.isRunning()) { this.emit('stop', new GameStopEvent(this)); this.browser.pause(); this.clock.stop(); this._garbageCollector.stop(); this._logger.debug('Game stopped'); } }
/** * Returns the Engine's running status, Useful for checking whether engine is running or paused. */ public isRunning() { return this.clock.isRunning(); }
private _screenShotRequests: { preserveHiDPIResolution: boolean; resolve: (image: HTMLImageElement) => void }[] = []; /** * Takes a screen shot of the current viewport and returns it as an * HTML Image Element. * @param preserveHiDPIResolution in the case of HiDPI return the full scaled backing image, by default false */ public screenshot(preserveHiDPIResolution = false): Promise<HTMLImageElement> { const screenShotPromise = new Promise<HTMLImageElement>((resolve) => { this._screenShotRequests.push({ preserveHiDPIResolution, resolve }); }); return screenShotPromise; }
private _checkForScreenShots() { // We must grab the draw buffer before we yield to the browser // the draw buffer is cleared after compositing // the reason for the asynchrony is setting `preserveDrawingBuffer: true` // forces the browser to copy buffers which can have a mass perf impact on mobile for (const request of this._screenShotRequests) { const finalWidth = request.preserveHiDPIResolution ? this.canvas.width : this.screen.resolution.width; const finalHeight = request.preserveHiDPIResolution ? this.canvas.height : this.screen.resolution.height; const screenshot = document.createElement('canvas'); screenshot.width = finalWidth; screenshot.height = finalHeight; const ctx = screenshot.getContext('2d'); ctx!.imageSmoothingEnabled = this.screen.antialiasing; ctx!.drawImage(this.canvas, 0, 0, finalWidth, finalHeight);
const result = new Image(); const raw = screenshot.toDataURL('image/png'); result.onload = () => { request.resolve(result); }; result.src = raw; } // Reset state this._screenShotRequests.length = 0; }
/** * Another option available to you to load resources into the game. * Immediately after calling this the game will pause and the loading screen * will appear. * @param loader Some {@apilink Loadable} such as a {@apilink Loader} collection, {@apilink Sound}, or {@apilink Texture}. */ public async load(loader: DefaultLoader, hideLoader = false): Promise<void> { await this.scope(async () => { try { // early exit if loaded if (loader.isLoaded()) { return; } this._loader = loader; this._isLoading = true; this._hideLoader = hideLoader;
if (loader instanceof Loader) { loader.suppressPlayButton = loader.suppressPlayButton || this._suppressPlayButton; } this._loader.onInitialize(this);
await loader.load(); } catch (e) { this._logger.error('Error loading resources, things may not behave properly', e); await Promise.resolve(); } finally { this._isLoading = false; this._hideLoader = false; this._loader = null as any; } }); }}