Skip to content

Commit

Permalink
fix: [#2076] Defer initialization until after final resolution calcul…
Browse files Browse the repository at this point in the history
…ated (#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] 📐 new tests written and passing / old tests updated with new scenario(s)
- [x] 📄 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
  • Loading branch information
eonarheim authored Nov 6, 2021
1 parent cdd08cd commit e603adb
Show file tree
Hide file tree
Showing 10 changed files with 208 additions and 16 deletions.
34 changes: 34 additions & 0 deletions sandbox/tests/fitscreen/fitscreen.ts
Original file line number Diff line number Diff line change
@@ -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;
});
24 changes: 24 additions & 0 deletions sandbox/tests/fitscreen/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>FitScreen</title>
<style>
body {
background: #000;
color: #F8F9FA;
display: flex;
align-items: center;
justify-content: center;
height: 100%;
width: 100%;
}
</style>
</head>
<body>
<script src="../../lib/excalibur.js"></script>
<script src="./fitscreen.js"></script>
</body>
</html>
2 changes: 2 additions & 0 deletions sandbox/tests/gif/animatedGif.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(() => {
Expand Down
20 changes: 15 additions & 5 deletions src/engine/Camera.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
22 changes: 16 additions & 6 deletions src/engine/Engine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -607,8 +607,6 @@ O|===|* >________________>\n\
pixelRatio: options.suppressHiDPIScaling ? 1 : null
});

this.screen.applyResolutionAndViewport();

if (options.backgroundColor) {
this.backgroundColor = options.backgroundColor.clone();
}
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -995,7 +993,9 @@ O|===|* >________________>\n\
this.input.gamepads.update();
return;
}

this._overrideInitialize(this);

// Publish preupdate events
this._preupdate(delta);

Expand Down Expand Up @@ -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<any> {
public start(loader?: Loader): Promise<void> {
if (!this._compatible) {
return Promise.reject('Excalibur is incompatible with your browser');
}
let loadingComplete: Promise<void>;
// 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<any>;
if (loader) {
this._loader = loader;
this._loader.suppressPlayButton = this._suppressPlayButton || this._loader.suppressPlayButton;
Expand All @@ -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;
Expand Down
2 changes: 1 addition & 1 deletion src/engine/Graphics/Context/ExcaliburGraphicsContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
17 changes: 17 additions & 0 deletions src/engine/Graphics/Context/ExcaliburGraphicsContextWebGL.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -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', {
Expand Down
34 changes: 30 additions & 4 deletions src/engine/Screen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -216,6 +217,7 @@ export class Screen {
this._mediaQueryList.addEventListener('change', this._pixelRatioChangeHandler);

this._canvas.addEventListener('fullscreenchange', this._fullscreenChangeHandler);
this.applyResolutionAndViewport();
}

public dispose(): void {
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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;
}

/**
Expand All @@ -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;
}

/**
Expand Down
34 changes: 34 additions & 0 deletions src/spec/CameraSpec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
35 changes: 35 additions & 0 deletions src/spec/ScreenSpec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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).'
);
});
});

0 comments on commit e603adb

Please sign in to comment.