diff --git a/src/engine/engine.ts b/src/engine/engine.ts index d6a009ba13..54168e2918 100644 --- a/src/engine/engine.ts +++ b/src/engine/engine.ts @@ -38,7 +38,7 @@ 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 } from './graphics'; +import type { AntialiasOptions, ExcaliburGraphicsContext, ExcaliburGraphicsContextWebGLOptions } from './graphics'; import { DefaultAntialiasOptions, DefaultPixelArtOptions, @@ -64,6 +64,7 @@ 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; @@ -118,6 +119,10 @@ export enum ScrollPreventionMode { * Defines the available options to configure the Excalibur engine at constructor time. */ export interface EngineOptions { + /** + * Optionally configure Excalibur plugins + */ + plugins?: Plugin[]; /** * Optionally configure the width of the viewport in css pixels */ @@ -428,6 +433,11 @@ export class Engine implements CanInitialize, public global: GlobalEventHandlers; + private _plugins: Plugin[] = []; + public get plugins(): readonly Plugin[] { + return this._plugins; + } + private _garbageCollector: GarbageCollector; public readonly garbageCollectorConfig: GarbageCollectionOptions | null; @@ -820,6 +830,15 @@ export class Engine implements CanInitialize, Flags.freeze(); + if (options.plugins && options.plugins.length > 0) { + this._plugins = [...options.plugins]; + this._plugins.sort((a, b) => a.priority - b.priority); + } + + for (const plugin of this._plugins) { + plugin.onEnginePreConfig?.(this, options); + } + // Initialize browser events facade this.browser = new BrowserEvents(window, document); @@ -988,6 +1007,36 @@ O|===|* >________________>\n\ if (!useCanvasGraphicsContext) { // Attempt webgl first try { + let onGraphicsPreConfig: (context: ExcaliburGraphicsContext, options: ExcaliburGraphicsContextWebGLOptions) => void; + let onGraphicsPostConfig: (context: ExcaliburGraphicsContext, options: ExcaliburGraphicsContextWebGLOptions) => void; + let onGraphicsPreInitialize: (context: ExcaliburGraphicsContext) => void; + let onGraphicsPostInitialize: (context: ExcaliburGraphicsContext) => void; + if (this._plugins.length > 0) { + onGraphicsPreConfig = (context: ExcaliburGraphicsContext, options: ExcaliburGraphicsContextWebGLOptions) => { + for (const plugin of this._plugins) { + plugin.onGraphicsPreConfig?.(context, options); + } + }; + + onGraphicsPostConfig = (context: ExcaliburGraphicsContext, options: ExcaliburGraphicsContextWebGLOptions) => { + for (const plugin of this._plugins) { + plugin.onGraphicsPostConfig?.(context, options); + } + }; + + onGraphicsPreInitialize = (context: ExcaliburGraphicsContext) => { + for (const plugin of this._plugins) { + plugin.onGraphicsPreInitialize?.(context); + } + }; + + onGraphicsPostInitialize = (context: ExcaliburGraphicsContext) => { + for (const plugin of this._plugins) { + plugin.onGraphicsPostInitialize?.(context); + } + }; + } + this.graphicsContext = new ExcaliburGraphicsContextWebGL({ canvasElement: this.canvas, enableTransparency: this.enableCanvasTransparency, @@ -1006,7 +1055,11 @@ O|===|* >________________>\n\ } : null, handleContextLost: options.handleContextLost ?? this._handleWebGLContextLost, - handleContextRestored: options.handleContextRestored + handleContextRestored: options.handleContextRestored, + onGraphicsPreConfig, + onGraphicsPostConfig, + onGraphicsPreInitialize, + onGraphicsPostInitialize }); } catch (e) { this._logger.warn( @@ -1241,6 +1294,11 @@ O|===|* >________________>\n\ this.stop(); this._garbageCollector.forceCollectAll(); this.input.toggleEnabled(false); + + for (const plugin of this.plugins) { + plugin.dispose?.(); + } + if (this._hasCreatedCanvas) { this.canvas.parentNode.removeChild(this.canvas); } @@ -1507,6 +1565,10 @@ O|===|* >________________>\n\ if (!this.canvasElementId && !options.canvasElement) { document.body.appendChild(this.canvas); } + + for (const plugin of this._plugins) { + plugin.onEnginePostConfig?.(this, options); + } } public toggleInputEnabled(enabled: boolean) { @@ -1527,10 +1589,18 @@ O|===|* >________________>\n\ private async _overrideInitialize(engine: Engine) { if (!this.isInitialized) { + for (const plugin of this._plugins) { + 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) { + plugin.onEnginePostInitialize(this); + } } } diff --git a/src/engine/graphics/context/excalibur-graphics-context-webgl.ts b/src/engine/graphics/context/excalibur-graphics-context-webgl.ts index 0fc68998d0..91fa59ffd0 100644 --- a/src/engine/graphics/context/excalibur-graphics-context-webgl.ts +++ b/src/engine/graphics/context/excalibur-graphics-context-webgl.ts @@ -205,6 +205,10 @@ export class ExcaliburGraphicsContextWebGL implements ExcaliburGraphicsContext { public materialScreenTexture!: WebGLTexture | null; + private _onGraphicsPreInitialize?: (context: ExcaliburGraphicsContext) => void; + + private _onGraphicsPostInitialize?: (context: ExcaliburGraphicsContext) => void; + public get z(): number { return this._state.current.z; } @@ -260,6 +264,9 @@ export class ExcaliburGraphicsContextWebGL implements ExcaliburGraphicsContext { private _isContextLost = false; constructor(options: ExcaliburGraphicsContextWebGLOptions) { + if (options.onGraphicsPreConfig) { + options.onGraphicsPreConfig(this, options); + } const { canvasElement, context, @@ -274,8 +281,12 @@ export class ExcaliburGraphicsContextWebGL implements ExcaliburGraphicsContext { useDrawSorting, garbageCollector, handleContextLost, - handleContextRestored + handleContextRestored, + onGraphicsPostConfig, + onGraphicsPreInitialize, + onGraphicsPostInitialize } = options; + this.__gl = context ?? (canvasElement.getContext('webgl2', { @@ -296,6 +307,12 @@ export class ExcaliburGraphicsContextWebGL implements ExcaliburGraphicsContext { if (handleContextRestored) { this.__gl.canvas.addEventListener('webglcontextrestored', handleContextRestored, false); } + if (onGraphicsPreInitialize) { + this._onGraphicsPreInitialize = onGraphicsPreInitialize; + } + if (onGraphicsPostInitialize) { + this._onGraphicsPostInitialize = onGraphicsPostInitialize; + } this.__gl.canvas.addEventListener('webglcontextlost', () => { this._isContextLost = true; @@ -318,6 +335,9 @@ export class ExcaliburGraphicsContextWebGL implements ExcaliburGraphicsContext { this._drawCallPool.disableWarnings = true; this._drawCallPool.preallocate(); this._init(); + if (onGraphicsPostConfig) { + onGraphicsPostConfig(this, options); + } } private _disposed = false; @@ -336,7 +356,12 @@ export class ExcaliburGraphicsContextWebGL implements ExcaliburGraphicsContext { } private _init() { + // add pre/post init here const gl = this.__gl; + if (this._onGraphicsPreInitialize) { + this._onGraphicsPreInitialize(this); + } + // Setup viewport and view matrix this._ortho = Matrix.ortho(0, gl.canvas.width, gl.canvas.height, 0, 400, -400); gl.viewport(0, 0, gl.canvas.width, gl.canvas.height); @@ -420,6 +445,10 @@ export class ExcaliburGraphicsContextWebGL implements ExcaliburGraphicsContext { }); this.debug = new ExcaliburGraphicsContextWebGLDebug(this); + + if (this._onGraphicsPostInitialize) { + this._onGraphicsPostInitialize(this); + } } public register(renderer: T) { diff --git a/src/engine/graphics/context/excalibur-graphics-context.ts b/src/engine/graphics/context/excalibur-graphics-context.ts index d46796da52..2dc6ef483b 100644 --- a/src/engine/graphics/context/excalibur-graphics-context.ts +++ b/src/engine/graphics/context/excalibur-graphics-context.ts @@ -125,6 +125,12 @@ export interface ExcaliburGraphicsContextOptions { * Feature flag that enables draw sorting will removed in v0.29 */ useDrawSorting?: boolean; + + onGraphicsPreConfig?: (context: ExcaliburGraphicsContextState, options: ExcaliburGraphicsContextOptions) => void; + onGraphicsPostConfig?: (context: ExcaliburGraphicsContextState, options: ExcaliburGraphicsContextOptions) => void; + + onGraphicsPreInitialize?: (context: ExcaliburGraphicsContextState) => void; + onGraphicsPostInitialize?: (context: ExcaliburGraphicsContextState) => void; } export interface ExcaliburGraphicsContextState { diff --git a/src/engine/index.ts b/src/engine/index.ts index 3056e12c66..afddd75d98 100644 --- a/src/engine/index.ts +++ b/src/engine/index.ts @@ -112,6 +112,8 @@ export * from './util/coroutine'; export * from './util/assert'; export * from './util/rental-pool'; +export * from './plugin'; + // ex.Deprecated // import * as deprecated from './deprecated'; // export { deprecated as Deprecated }; diff --git a/src/engine/plugin.ts b/src/engine/plugin.ts new file mode 100644 index 0000000000..b8d4a01137 --- /dev/null +++ b/src/engine/plugin.ts @@ -0,0 +1,95 @@ +import type { Engine, EngineOptions } from './engine'; +import type { ExcaliburGraphicsContext, ExcaliburGraphicsContextOptions } from './graphics'; +import type { Scene } from './scene'; + +// TODO should these support async flows??? + +/** + * + * An Excalibur plugin packages up changes to excalibur in a convenient package such as + * * Custom config intercepting and implementation + * * Engine initialization customization + * * Graphics Context configuration including + * * Custom RendererPlugins + * * Custom PostProcessors + * * Scene customization including default ECS Systems, Components + */ +export abstract class Plugin { + /** + * Unique name of the plugin + */ + name: string; + /** + * Plugin priority determines the order they're run, lower is first, higher is last, default is 0 + */ + priority: number = 0; + + /** + * Perform any async loading + */ + async onLoad?(): Promise { + return await Promise.resolve(); + } + + /** + * Perform any extras when load complete + */ + async onLoadComplete?(): Promise { + return await Promise.resolve(); + } + + /** + * Optionally intercept and mutate the {@param options} passed into the Engine, and modify the engine + */ + onEnginePreConfig?(engine: Engine, options: EngineOptions): void; + + /** + * Optionally intercept and mutate the {@param options} passed into the Engine, and modify the engine + */ + onEnginePostConfig?(engine: Engine, options: EngineOptions): void; + + /** + * Optinally intercept the engine and modify before initialize + */ + onEnginePreInitialize?(engine: Engine): void; + + /** + * Optionally intercept the engine and modify after initialize + */ + onEnginePostInitialize?(engine: Engine): void; + + /** + * Optionally intercept the grpahics context before configuration and modify either the context or options + */ + onGraphicsPreConfig?(context: ExcaliburGraphicsContext, options: ExcaliburGraphicsContextOptions): void; + + /** + * Optionally intercept the grpahics context after configuration and modify either the context or options + */ + onGraphicsPostConfig?(context: ExcaliburGraphicsContext, options: ExcaliburGraphicsContextOptions): void; + + /** + * Optionally intercetp the graphics context and modify before initialize + */ + onGraphicsPreInitialize?(context: ExcaliburGraphicsContext): void; + + /** + * Optionally intercetp the graphics context and modify after initialize + */ + onGraphicsPostInitialize?(context: ExcaliburGraphicsContext): void; + + /** + * Optionally intercept a scene and modify before initialize + */ + onScenePreInitialize?(scene: Scene): void; + + /** + * Optionally intercept a scene and modify after initialize + */ + onScenePostInitialize?(scene: Scene): void; + + /** + * Optinally perform any cleanup necessary when the engine is disposed + */ + dispose?(): void; +} diff --git a/src/engine/scene.ts b/src/engine/scene.ts index 67da29576e..ac0dfdc98b 100644 --- a/src/engine/scene.ts +++ b/src/engine/scene.ts @@ -352,11 +352,19 @@ export class Scene implements CanInitialize, CanActiv this.world.systemManager.initialize(); + for (const plugin of engine.plugins) { + plugin.onScenePreInitialize?.(this); + } + // This order is important! we want to be sure any custom init that add actors // fire before the actor init await this.onInitialize(engine); this._initializeChildren(); + for (const plugin of engine.plugins) { + plugin.onScenePostInitialize?.(this); + } + this._logger.debug('Scene.onInitialize', this, engine); this.events.emit('initialize', new InitializeEvent(engine, this)); } catch (e) {