From e603adb800b886b7b504b8b23396ceef35506cd6 Mon Sep 17 00:00:00 2001 From: Erik Onarheim Date: Fri, 5 Nov 2021 20:39:37 -0500 Subject: [PATCH] fix: [#2076] Defer initialization until after final resolution calculated (#2093) ===:clipboard: PR Checklist :clipboard:=== - [x] :pushpin: issue exists in github for these changes - [x] :microscope: existing tests still pass - [x] :see_no_evil: code conforms to the [style guide](https://github.com/excaliburjs/Excalibur/blob/main/STYLEGUIDE.md) - [x] :triangular_ruler: new tests written and passing / old tests updated with new scenario(s) - [x] :page_facing_up: changelog entry added (or not needed) ================== TODO: * [x] WebGL will silently fail at high resolutions which is easy to get to with hidpi scaling * [x] Warn when l.drawingBufferHeight/l.drawingBufferWidth don't match height/width * [x] Documentation improvement for original issue for different displaymodes * [x] Fix tests Closes #2076 ## Changes: - Deferred initialization of scene/camera until final resolution is calculated --- sandbox/tests/fitscreen/fitscreen.ts | 34 ++++++++++++++++++ sandbox/tests/fitscreen/index.html | 24 +++++++++++++ sandbox/tests/gif/animatedGif.ts | 2 ++ src/engine/Camera.ts | 20 ++++++++--- src/engine/Engine.ts | 22 ++++++++---- .../Context/ExcaliburGraphicsContext.ts | 2 +- .../Context/ExcaliburGraphicsContextWebGL.ts | 17 +++++++++ src/engine/Screen.ts | 34 +++++++++++++++--- src/spec/CameraSpec.ts | 34 ++++++++++++++++++ src/spec/ScreenSpec.ts | 35 +++++++++++++++++++ 10 files changed, 208 insertions(+), 16 deletions(-) create mode 100644 sandbox/tests/fitscreen/fitscreen.ts create mode 100644 sandbox/tests/fitscreen/index.html diff --git a/sandbox/tests/fitscreen/fitscreen.ts b/sandbox/tests/fitscreen/fitscreen.ts new file mode 100644 index 000000000..17d85e907 --- /dev/null +++ b/sandbox/tests/fitscreen/fitscreen.ts @@ -0,0 +1,34 @@ + +async function main() { + const game = new ex.Engine({ + width: 3000, + height: 3000, + suppressHiDPIScaling: true, + displayMode: ex.DisplayMode.FitScreen, + }); + + await game.start(); + + const paddle = new ex.Actor({ + x: 220, + y: 220, + width: 400, + height: 400, + color: ex.Color.White, + }); + + console.log('Game:', game.drawWidth, '·', game.drawHeight); + console.log('Canv:', game.canvasWidth, '·', game.canvasHeight); + // console.log('Camera: ', game.currentScene.camera.pos); + // console.log('Center: ', game.screen.center); + // game.currentScene.camera.pos = game.screen.center; + + game.add(paddle); + return [game, paddle]; +} + +main().then(([game, paddle]) => { + console.log('Promise from main()'); + (window as any).game = game; + (window as any).paddle = paddle; +}); \ No newline at end of file diff --git a/sandbox/tests/fitscreen/index.html b/sandbox/tests/fitscreen/index.html new file mode 100644 index 000000000..f79f1d27a --- /dev/null +++ b/sandbox/tests/fitscreen/index.html @@ -0,0 +1,24 @@ + + + + + + + FitScreen + + + + + + + \ No newline at end of file diff --git a/sandbox/tests/gif/animatedGif.ts b/sandbox/tests/gif/animatedGif.ts index 8cd00426b..e8639be22 100644 --- a/sandbox/tests/gif/animatedGif.ts +++ b/sandbox/tests/gif/animatedGif.ts @@ -8,6 +8,8 @@ var game = new ex.Engine({ displayMode: ex.DisplayMode.FitScreen }); +game.currentScene.camera.zoom = 2; + var gif: ex.Gif = new ex.Gif('./sword.gif', ex.Color.Black); var loader = new ex.Loader([gif]); game.start(loader).then(() => { diff --git a/src/engine/Camera.ts b/src/engine/Camera.ts index 6f5a1211e..e4d5da9de 100644 --- a/src/engine/Camera.ts +++ b/src/engine/Camera.ts @@ -586,12 +586,22 @@ export class Camera extends Class implements CanUpdate, CanInitialize { public _initialize(_engine: Engine) { if (!this.isInitialized) { this._engine = _engine; - this._halfWidth = _engine.halfDrawWidth; - this._halfHeight = _engine.halfDrawHeight; - // pos unset apply default position is center screen + + const currentRes = this._engine.screen.resolution; + let center = vec(currentRes.width / 2, currentRes.height / 2); + if (!this._engine.loadingComplete) { + // If there was a loading screen, we peek the configured resolution + const res = this._engine.screen.peekResolution(); + if (res) { + center = vec(res.width / 2, res.height / 2); + } + } + this._halfWidth = center.x; + this._halfHeight = center.x; + + // If the user has not set the camera pos, apply default center screen position if (!this._posChanged) { - this.pos.x = _engine.halfDrawWidth; - this.pos.y = _engine.halfDrawHeight; + this.pos = center; } this.onInitialize(_engine); diff --git a/src/engine/Engine.ts b/src/engine/Engine.ts index f1a788c77..4099d4bd5 100644 --- a/src/engine/Engine.ts +++ b/src/engine/Engine.ts @@ -607,8 +607,6 @@ O|===|* >________________>\n\ pixelRatio: options.suppressHiDPIScaling ? 1 : null }); - this.screen.applyResolutionAndViewport(); - if (options.backgroundColor) { this.backgroundColor = options.backgroundColor.clone(); } @@ -844,7 +842,7 @@ O|===|* >________________>\n\ * named scene. Calls the [[Scene]] lifecycle events. * @param key The key of the scene to transition to. */ - public goToScene(key: string) { + public goToScene(key: string): void { // if not yet initialized defer goToScene if (!this.isInitialized) { this._deferredGoTo = key; @@ -995,7 +993,9 @@ O|===|* >________________>\n\ this.input.gamepads.update(); return; } + this._overrideInitialize(this); + // Publish preupdate events this._preupdate(delta); @@ -1129,21 +1129,30 @@ O|===|* >________________>\n\ return this._isDebug; } + private _loadingComplete: boolean = false; + /** + * Returns true when loading is totally complete and the player has clicked start + */ + public get loadingComplete() { + return this._loadingComplete; + } /** * Starts the internal game loop for Excalibur after loading * any provided assets. * @param loader Optional [[Loader]] to use to load resources. The default loader is [[Loader]], override to provide your own * custom loader. */ - public start(loader?: Loader): Promise { + public start(loader?: Loader): Promise { if (!this._compatible) { return Promise.reject('Excalibur is incompatible with your browser'); } + let loadingComplete: Promise; + // Push the current user entered resolution/viewport this.screen.pushResolutionAndViewport(); + // Configure resolution for loader this.screen.resolution = this.screen.viewport; this.screen.applyResolutionAndViewport(); this.graphicsContext.updateViewport(); - let loadingComplete: Promise; if (loader) { this._loader = loader; this._loader.suppressPlayButton = this._suppressPlayButton || this._loader.suppressPlayButton; @@ -1158,14 +1167,15 @@ O|===|* >________________>\n\ this.screen.applyResolutionAndViewport(); this.graphicsContext.updateViewport(); this.emit('start', new GameStartEvent(this)); + this._loadingComplete = true; }); if (!this._hasStarted) { + // has started is a slight misnomer, it's really mainloop started this._hasStarted = true; this._logger.debug('Starting game...'); this.browser.resume(); Engine.createMainLoop(this, window.requestAnimationFrame, Date.now)(); - this._logger.debug('Game started'); } else { // Game already started; diff --git a/src/engine/Graphics/Context/ExcaliburGraphicsContext.ts b/src/engine/Graphics/Context/ExcaliburGraphicsContext.ts index 83f73358b..584962f2c 100644 --- a/src/engine/Graphics/Context/ExcaliburGraphicsContext.ts +++ b/src/engine/Graphics/Context/ExcaliburGraphicsContext.ts @@ -89,7 +89,7 @@ export interface ExcaliburGraphicsContext { resetTransform(): void; /** - * Update the context with the curren tviewport dimensions (used in resizing) + * Update the context with the current viewport dimensions (used in resizing) */ updateViewport(): void; diff --git a/src/engine/Graphics/Context/ExcaliburGraphicsContextWebGL.ts b/src/engine/Graphics/Context/ExcaliburGraphicsContextWebGL.ts index f0a48ac3a..6d7f7826f 100644 --- a/src/engine/Graphics/Context/ExcaliburGraphicsContextWebGL.ts +++ b/src/engine/Graphics/Context/ExcaliburGraphicsContextWebGL.ts @@ -20,6 +20,7 @@ import { PointRenderer } from './point-renderer'; import { Canvas } from '../Canvas'; import { GraphicsDiagnostics } from '../GraphicsDiagnostics'; import { DebugText } from './debug-text'; +import { ScreenDimension } from '../../Screen'; class ExcaliburGraphicsContextWebGLDebug implements DebugDraw { private _debugText = new DebugText(); @@ -130,6 +131,22 @@ export class ExcaliburGraphicsContextWebGL implements ExcaliburGraphicsContext { return this.__gl.canvas.height; } + /** + * Checks the underlying webgl implementation if the requested internal resolution is supported + * @param dim + */ + public checkIfResolutionSupported(dim: ScreenDimension): boolean { + // Slight hack based on this thread https://groups.google.com/g/webgl-dev-list/c/AHONvz3oQTo + const gl = this.__gl; + // If any dimension is greater than max texture size (divide by 4 bytes per pixel) + const maxDim = gl.getParameter(gl.MAX_TEXTURE_SIZE) / 4; + let supported = true; + if (dim.width > maxDim ||dim.height > maxDim) { + supported = false; + } + return supported; + } + constructor(options: ExcaliburGraphicsContextOptions) { const { canvasElement, enableTransparency, smoothing, snapToPixel, backgroundColor } = options; this.__gl = canvasElement.getContext('webgl', { diff --git a/src/engine/Screen.ts b/src/engine/Screen.ts index 52879cf32..d5ae83a9a 100644 --- a/src/engine/Screen.ts +++ b/src/engine/Screen.ts @@ -5,6 +5,7 @@ import { BrowserEvents } from './Util/Browser'; import { BoundingBox } from './Collision/Index'; import { ExcaliburGraphicsContext } from './Graphics/Context/ExcaliburGraphicsContext'; import { getPosition } from './Util/Util'; +import { ExcaliburGraphicsContextWebGL } from './Graphics/Context/ExcaliburGraphicsContextWebGL'; /** * Enum representing the different display modes available to Excalibur. @@ -216,6 +217,7 @@ export class Screen { this._mediaQueryList.addEventListener('change', this._pixelRatioChangeHandler); this._canvas.addEventListener('fullscreenchange', this._fullscreenChangeHandler); + this.applyResolutionAndViewport(); } public dispose(): void { @@ -334,15 +336,39 @@ export class Screen { this.viewport = { ...this.viewport }; } + public peekViewport(): ScreenDimension { + return this._viewportStack[this._viewportStack.length - 1]; + } + + public peekResolution(): ScreenDimension { + return this._resolutionStack[this._resolutionStack.length - 1]; + } + public popResolutionAndViewport() { this.resolution = this._resolutionStack.pop(); this.viewport = this._viewportStack.pop(); } + private _alreadyWarned = false; public applyResolutionAndViewport() { this._canvas.width = this.scaledWidth; this._canvas.height = this.scaledHeight; + if (this._ctx instanceof ExcaliburGraphicsContextWebGL) { + const supported = this._ctx.checkIfResolutionSupported({ + width: this.scaledWidth, + height: this.scaledHeight + }); + if (!supported && !this._alreadyWarned) { + this._alreadyWarned = true; // warn once + this._logger.warn( + `The currently configured resolution (${this.resolution.width}x${this.resolution.height})` + + ' is too large for the platform WebGL implementation, this may work but cause WebGL rendering to behave oddly.' + + ' Try reducing the resolution or disabling Hi DPI scaling to avoid this' + + ' (read more here https://excaliburjs.com/docs/screens#understanding-viewport--resolution).'); + } + } + if (this._antialiasing) { this._canvas.style.imageRendering = 'auto'; } else { @@ -581,9 +607,9 @@ export class Screen { */ public get drawWidth(): number { if (this._camera) { - return this.scaledWidth / this._camera.zoom / this.pixelRatio; + return this.resolution.width / this._camera.zoom; } - return this.scaledWidth / this.pixelRatio; + return this.resolution.width; } /** @@ -598,9 +624,9 @@ export class Screen { */ public get drawHeight(): number { if (this._camera) { - return this.scaledHeight / this._camera.zoom / this.pixelRatio; + return this.resolution.height / this._camera.zoom; } - return this.scaledHeight / this.pixelRatio; + return this.resolution.height; } /** diff --git a/src/spec/CameraSpec.ts b/src/spec/CameraSpec.ts index cb61d7921..deffdcf51 100644 --- a/src/spec/CameraSpec.ts +++ b/src/spec/CameraSpec.ts @@ -39,6 +39,40 @@ describe('A camera', () => { engine.stop(); }); + it('should be center screen by default (when loading not complete)', () => { + engine = TestUtils.engine({ + viewport: {width: 100, height: 100}, + resolution: {width: 1000, height: 1200 } + }); + engine.screen.pushResolutionAndViewport(); + engine.screen.resolution = {width: 100, height: 1000}; + spyOnProperty(engine, 'loadingComplete', 'get').and.returnValue(false); + spyOn(engine.screen, 'peekResolution').and.callThrough(); + + const sut = new ex.Camera(); + sut.zoom = 2; // zoom should not change the center position + sut._initialize(engine); + + expect(sut.pos).toBeVector(ex.vec(500, 600)); + expect(engine.screen.peekResolution).toHaveBeenCalled(); + }); + + it('should be center screen by default (when loading complete)', () => { + engine = TestUtils.engine({ + viewport: {width: 100, height: 100}, + resolution: {width: 1000, height: 1200 } + }); + + spyOnProperty(engine, 'loadingComplete', 'get').and.returnValue(true); + spyOn(engine.screen, 'peekResolution').and.callThrough(); + const sut = new ex.Camera(); + sut.zoom = 2; // zoom should not change the center position + sut._initialize(engine); + + expect(sut.pos).toBeVector(ex.vec(500, 600)); + expect(engine.screen.peekResolution).not.toHaveBeenCalled(); + }); + it('can focus on a point', () => { // set the focus with positional attributes Camera.x = 10; diff --git a/src/spec/ScreenSpec.ts b/src/spec/ScreenSpec.ts index cd8f2f158..b573638e8 100644 --- a/src/spec/ScreenSpec.ts +++ b/src/spec/ScreenSpec.ts @@ -578,4 +578,39 @@ describe('A Screen', () => { expect(sut.center).toBeVector(ex.vec(200, 150)); }); + + it('will warn if the resolution is too large', () => { + const logger = ex.Logger.getInstance(); + spyOn(logger, 'warn'); + + const canvasElement = document.createElement('canvas'); + canvasElement.width = 100; + canvasElement.height = 100; + + const context = new ex.ExcaliburGraphicsContextWebGL({ + canvasElement: canvasElement, + enableTransparency: false, + snapToPixel: true, + backgroundColor: ex.Color.White + }); + + const sut = new ex.Screen({ + canvas, + context, + browser, + viewport: { width: 800, height: 600 }, + pixelRatio: 2 + }); + + spyOn(context, 'checkIfResolutionSupported').and.returnValue(false); + sut.resolution = { width: 3000, height: 3000 }; + sut.applyResolutionAndViewport(); + expect(context.checkIfResolutionSupported).toHaveBeenCalled(); + expect(logger.warn).toHaveBeenCalledOnceWith( + `The currently configured resolution (${sut.resolution.width}x${sut.resolution.height})` + + ' is too large for the platform WebGL implementation, this may work but cause WebGL rendering to behave oddly.' + + ' Try reducing the resolution or disabling Hi DPI scaling to avoid this' + + ' (read more here https://excaliburjs.com/docs/screens#understanding-viewport--resolution).' + ); + }); });