From 0b4dadd3d5e7e7e66b344880016d53ab5fed85a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Davy=20H=C3=A9lard?= Date: Sun, 23 Mar 2025 18:53:35 +0100 Subject: [PATCH 1/8] Add a scale mode for modern pixel-art games --- Core/GDCore/Project/Project.h | 6 +- .../textinputruntimeobject-pixi-renderer.ts | 4 +- .../textruntimeobject-pixi-renderer.ts | 42 ++++++---- Extensions/TextObject/textruntimeobject.ts | 42 +++++----- GDJS/Runtime/CustomRuntimeObject.ts | 4 + GDJS/Runtime/RuntimeCustomObjectLayer.ts | 4 + GDJS/Runtime/RuntimeInstanceContainer.ts | 9 ++ GDJS/Runtime/RuntimeLayer.ts | 2 + GDJS/Runtime/layer.ts | 45 +++++++--- .../pixi-renderers/layer-pixi-renderer.ts | 10 +-- .../runtimegame-pixi-renderer.ts | 32 ++++---- .../runtimescene-pixi-renderer.ts | 4 +- GDJS/Runtime/runtimegame.ts | 82 +++++++++++++++---- GDJS/Runtime/runtimeobject.ts | 5 ++ GDJS/Runtime/runtimescene.ts | 21 +++-- GDJS/Runtime/scenestack.ts | 9 ++ GDJS/Runtime/types/project-data.d.ts | 4 +- .../ProjectManager/ProjectPropertiesDialog.js | 4 + 18 files changed, 230 insertions(+), 99 deletions(-) diff --git a/Core/GDCore/Project/Project.h b/Core/GDCore/Project/Project.h index 59a300206168..6ba0ef36bca2 100644 --- a/Core/GDCore/Project/Project.h +++ b/Core/GDCore/Project/Project.h @@ -374,12 +374,14 @@ class GD_CORE_API Project { void SetVerticalSyncActivatedByDefault(bool enable) { verticalSync = enable; } /** - * Return the scale mode used by the game (usually "linear" or "nearest"). + * Return the scale mode used by the game (usually "linear", "magnified" or + * "nearest"). */ const gd::String& GetScaleMode() const { return scaleMode; } /** - * Set the scale mode used by the game (usually "linear" or "nearest"). + * Set the scale mode used by the game (usually "linear", "magnified" or + * "nearest"). */ void SetScaleMode(const gd::String& scaleMode_) { scaleMode = scaleMode_; } diff --git a/Extensions/TextInput/textinputruntimeobject-pixi-renderer.ts b/Extensions/TextInput/textinputruntimeobject-pixi-renderer.ts index 91f2ae3faf00..c09e697396ea 100644 --- a/Extensions/TextInput/textinputruntimeobject-pixi-renderer.ts +++ b/Extensions/TextInput/textinputruntimeobject-pixi-renderer.ts @@ -211,8 +211,8 @@ namespace gdjs { const isOutsideCanvas = canvasRight < 0 || canvasBottom < 0 || - canvasLeft > runtimeGame.getGameResolutionWidth() || - canvasTop > runtimeGame.getGameResolutionHeight(); + canvasLeft > runtimeGame.getRenderingResolutionWidth() || + canvasTop > runtimeGame.getRenderingResolutionHeight(); if (isOutsideCanvas) { this._form.style.display = 'none'; return; diff --git a/Extensions/TextObject/textruntimeobject-pixi-renderer.ts b/Extensions/TextObject/textruntimeobject-pixi-renderer.ts index 0fe94a9b8085..c6f99c777f27 100644 --- a/Extensions/TextObject/textruntimeobject-pixi-renderer.ts +++ b/Extensions/TextObject/textruntimeobject-pixi-renderer.ts @@ -4,6 +4,7 @@ namespace gdjs { _fontManager: any; _text: PIXI.Text; _justCreated: boolean = true; + _upscaleRatio: float = 1; constructor( runtimeObject: gdjs.TextRuntimeObject, @@ -47,7 +48,7 @@ namespace gdjs { const style = this._text.style; style.fontStyle = this._object._italic ? 'italic' : 'normal'; style.fontWeight = this._object._bold ? 'bold' : 'normal'; - style.fontSize = this._object._characterSize; + style.fontSize = this._object._characterSize * this._upscaleRatio; style.fontFamily = fontName; if (this._object._useGradient) { style.fill = this._getGradientHex(); @@ -62,7 +63,7 @@ namespace gdjs { // @ts-ignore style.align = this._object._textAlign; style.wordWrap = this._object._wrapping; - style.wordWrapWidth = this._object._wrappingWidth; + style.wordWrapWidth = this._object._wrappingWidth * this._upscaleRatio; style.breakWords = true; style.stroke = gdjs.rgbToHexNumber( this._object._outlineColor[0], @@ -70,7 +71,7 @@ namespace gdjs { this._object._outlineColor[2] ); style.strokeThickness = this._object._isOutlineEnabled - ? this._object._outlineThickness + ? this._object._outlineThickness * this._upscaleRatio : 0; style.dropShadow = this._object._shadow; style.dropShadowColor = gdjs.rgbToHexNumber( @@ -79,13 +80,16 @@ namespace gdjs { this._object._shadowColor[2] ); style.dropShadowAlpha = this._object._shadowOpacity / 255; - style.dropShadowBlur = this._object._shadowBlur; + style.dropShadowBlur = this._object._shadowBlur * this._upscaleRatio; style.dropShadowAngle = gdjs.toRad(this._object._shadowAngle); - style.dropShadowDistance = this._object._shadowDistance; + style.dropShadowDistance = + this._object._shadowDistance * this._upscaleRatio; const extraPaddingForShadow = style.dropShadow - ? style.dropShadowDistance + style.dropShadowBlur + ? this._object._shadowDistance + this._object._shadowBlur : 0; - style.padding = Math.ceil(this._object._padding + extraPaddingForShadow); + style.padding = + Math.ceil(this._object._padding + extraPaddingForShadow) * + this._upscaleRatio; // Prevent spikey outlines by adding a miter limit style.miterLimit = 3; @@ -117,7 +121,6 @@ namespace gdjs { this._text.position.x = this._object.x + this._text.width / 2; this._text.anchor.x = 0.5; } - this._text.position.y = this._object.y + this._text.height / 2; const alignmentY = this._object._verticalTextAlignment === 'bottom' @@ -181,18 +184,29 @@ namespace gdjs { return gradient; } + /** + * Set the text object scale. + * @param upscaleRatio The new scale for the text object. + */ + setUpscaleRatio(upscaleRatio: float): void { + this._upscaleRatio = upscaleRatio; + this._text.scale.x = this._object.getScaleX() / this._upscaleRatio; + this._text.scale.y = this._object.getScaleY() / this._upscaleRatio; + this.updateStyle(); + } + /** * Get x-scale of the text. */ getScaleX(): float { - return this._text.scale.x; + return this._object.getScaleX(); } /** * Get y-scale of the text. */ getScaleY(): float { - return this._text.scale.y; + return this._object.getScaleY(); } /** @@ -200,8 +214,8 @@ namespace gdjs { * @param newScale The new scale for the text object. */ setScale(newScale: float): void { - this._text.scale.x = newScale; - this._text.scale.y = newScale; + this._text.scale.x = newScale / this._upscaleRatio; + this._text.scale.y = newScale / this._upscaleRatio; } /** @@ -209,7 +223,7 @@ namespace gdjs { * @param newScale The new x-scale for the text object. */ setScaleX(newScale: float): void { - this._text.scale.x = newScale; + this._text.scale.x = newScale / this._upscaleRatio; } /** @@ -217,7 +231,7 @@ namespace gdjs { * @param newScale The new y-scale for the text object. */ setScaleY(newScale: float): void { - this._text.scale.y = newScale; + this._text.scale.y = newScale / this._upscaleRatio; } destroy() { diff --git a/Extensions/TextObject/textruntimeobject.ts b/Extensions/TextObject/textruntimeobject.ts index abd6ea72d914..e5b6437c2539 100644 --- a/Extensions/TextObject/textruntimeobject.ts +++ b/Extensions/TextObject/textruntimeobject.ts @@ -149,7 +149,7 @@ namespace gdjs { this.onCreated(); } - updateFromObjectData( + override updateFromObjectData( oldObjectData: TextObjectData, newObjectData: TextObjectData ): boolean { @@ -214,7 +214,7 @@ namespace gdjs { return true; } - getNetworkSyncData(): TextObjectNetworkSyncData { + override getNetworkSyncData(): TextObjectNetworkSyncData { return { ...super.getNetworkSyncData(), str: this._str, @@ -242,7 +242,7 @@ namespace gdjs { }; } - updateFromNetworkSyncData( + override updateFromNetworkSyncData( networkSyncData: TextObjectNetworkSyncData ): void { super.updateFromNetworkSyncData(networkSyncData); @@ -317,15 +317,15 @@ namespace gdjs { } } - getRendererObject() { + override getRendererObject() { return this._renderer.getRendererObject(); } - update(instanceContainer: gdjs.RuntimeInstanceContainer): void { + override update(instanceContainer: gdjs.RuntimeInstanceContainer): void { this._renderer.ensureUpToDate(); } - onDestroyed(): void { + override onDestroyed(): void { super.onDestroyed(); this._renderer.destroy(); } @@ -345,6 +345,12 @@ namespace gdjs { } } + override onGameZoomFactorChanged(): void { + this._renderer.setUpscaleRatio( + this.getRuntimeScene().getGame().getZoomFactor() + ); + } + /** * Update the rendered object position. */ @@ -353,27 +359,17 @@ namespace gdjs { this._renderer.updatePosition(); } - /** - * Set object position on X axis. - */ - setX(x: float): void { + override setX(x: float): void { super.setX(x); this._updateTextPosition(); } - /** - * Set object position on Y axis. - */ - setY(y: float): void { + override setY(y: float): void { super.setY(y); this._updateTextPosition(); } - /** - * Set the angle of the object. - * @param angle The new angle of the object - */ - setAngle(angle: float): void { + override setAngle(angle: float): void { super.setAngle(angle); this._renderer.updateAngle(); } @@ -499,14 +495,14 @@ namespace gdjs { /** * Get width of the text. */ - getWidth(): float { + override getWidth(): float { return this._wrapping ? this._wrappingWidth : this._renderer.getWidth(); } /** * Get height of the text. */ - getHeight(): float { + override getHeight(): float { return this._renderer.getHeight(); } @@ -685,11 +681,11 @@ namespace gdjs { } } - setWidth(width: float): void { + override setWidth(width: float): void { this.setWrappingWidth(width); } - getDrawableY(): float { + override getDrawableY(): float { return ( this.getY() - (this._verticalTextAlignment === 'center' diff --git a/GDJS/Runtime/CustomRuntimeObject.ts b/GDJS/Runtime/CustomRuntimeObject.ts index 3c597d8ce06c..f650460d4f63 100644 --- a/GDJS/Runtime/CustomRuntimeObject.ts +++ b/GDJS/Runtime/CustomRuntimeObject.ts @@ -229,6 +229,10 @@ namespace gdjs { */ onDestroy(parent: gdjs.RuntimeInstanceContainer) {} + override onGameZoomFactorChanged() { + this._instanceContainer.onGameZoomFactorChanged(); + } + override updatePreRender(parent: gdjs.RuntimeInstanceContainer): void { this._instanceContainer._updateObjectsPreRender(); this.getRenderer().ensureUpToDate(); diff --git a/GDJS/Runtime/RuntimeCustomObjectLayer.ts b/GDJS/Runtime/RuntimeCustomObjectLayer.ts index 7b20900e5dbd..a39321ab3020 100644 --- a/GDJS/Runtime/RuntimeCustomObjectLayer.ts +++ b/GDJS/Runtime/RuntimeCustomObjectLayer.ts @@ -54,6 +54,10 @@ namespace gdjs { return 1; } + override getCameraRenderingZoom(cameraId?: integer): float { + return 1; + } + override setCameraZ(z: float, fov: float, cameraId?: integer): void {} override getCameraZ(fov: float | null, cameraId?: integer): float { diff --git a/GDJS/Runtime/RuntimeInstanceContainer.ts b/GDJS/Runtime/RuntimeInstanceContainer.ts index e4ef5eee2099..b33e06bddc04 100644 --- a/GDJS/Runtime/RuntimeInstanceContainer.ts +++ b/GDJS/Runtime/RuntimeInstanceContainer.ts @@ -547,6 +547,15 @@ namespace gdjs { return this._allInstancesList; } + /** + * Called when the game zoom factor is changed to adapt to a new resolution. + */ + onGameZoomFactorChanged(): void { + for (const instance of this.getAdhocListOfAllInstances()) { + instance.onGameZoomFactorChanged(); + } + } + /** * Update the objects before launching the events. */ diff --git a/GDJS/Runtime/RuntimeLayer.ts b/GDJS/Runtime/RuntimeLayer.ts index 4e2531b2ea29..d6cd6ff72e11 100644 --- a/GDJS/Runtime/RuntimeLayer.ts +++ b/GDJS/Runtime/RuntimeLayer.ts @@ -272,6 +272,8 @@ namespace gdjs { */ abstract getCameraZoom(cameraId?: integer): float; + abstract getCameraRenderingZoom(cameraId?: integer): float; + /** * Set the camera center Z position. * diff --git a/GDJS/Runtime/layer.ts b/GDJS/Runtime/layer.ts index 493b33923aa7..4473e5dac51a 100644 --- a/GDJS/Runtime/layer.ts +++ b/GDJS/Runtime/layer.ts @@ -81,10 +81,13 @@ namespace gdjs { Math.abs(this._cameraY - oldGameResolutionOriginY) < 1 && this._zoomFactor === 1 ) { + const gameZoomFactor = this.getRuntimeScene().getGame().getZoomFactor(); this._cameraX += - this._runtimeScene.getViewportOriginX() - oldGameResolutionOriginX; + this._runtimeScene.getViewportOriginX() / gameZoomFactor - + oldGameResolutionOriginX; this._cameraY += - this._runtimeScene.getViewportOriginY() - oldGameResolutionOriginY; + this._runtimeScene.getViewportOriginY() / gameZoomFactor - + oldGameResolutionOriginY; } this._renderer.updatePosition(); @@ -145,7 +148,9 @@ namespace gdjs { * @return The width of the camera */ override getCameraWidth(cameraId?: integer): float { - return this.getWidth() / this._zoomFactor; + const zoomFactor = + this._zoomFactor * this.getRuntimeScene().getGame().getZoomFactor(); + return this.getWidth() / zoomFactor; } /** @@ -156,7 +161,9 @@ namespace gdjs { * @return The height of the camera */ override getCameraHeight(cameraId?: integer): float { - return this.getHeight() / this._zoomFactor; + const zoomFactor = + this._zoomFactor * this.getRuntimeScene().getGame().getZoomFactor(); + return this.getHeight() / zoomFactor; } /** @@ -181,6 +188,12 @@ namespace gdjs { return this._zoomFactor; } + override getCameraRenderingZoom(cameraId?: integer): float { + return ( + this._zoomFactor * this.getRuntimeScene().getGame().getZoomFactor() + ); + } + /** * Set the camera center Z position. * @@ -283,8 +296,10 @@ namespace gdjs { x -= this.getRuntimeScene()._cachedGameResolutionWidth / 2; y -= this.getRuntimeScene()._cachedGameResolutionHeight / 2; - x /= Math.abs(this._zoomFactor); - y /= Math.abs(this._zoomFactor); + const zoomFactor = + this._zoomFactor * this.getRuntimeScene().getGame().getZoomFactor(); + x /= Math.abs(zoomFactor); + y /= Math.abs(zoomFactor); // Only compute angle and cos/sin once (allow heavy optimization from JS engines). const angleInRadians = (this._cameraRotation / 180) * Math.PI; @@ -320,8 +335,10 @@ namespace gdjs { ): FloatPoint { x -= this._runtimeScene.getViewportOriginX(); y -= this._runtimeScene.getViewportOriginY(); - x /= Math.abs(this._zoomFactor); - y /= Math.abs(this._zoomFactor); + const zoomFactor = + this._zoomFactor * this.getRuntimeScene().getGame().getZoomFactor(); + x /= Math.abs(zoomFactor); + y /= Math.abs(zoomFactor); // Only compute angle and cos/sin once (allow heavy optimization from JS engines). const angleInRadians = (this._cameraRotation / 180) * Math.PI; @@ -367,8 +384,10 @@ namespace gdjs { const sinValue = Math.sin(-angleInRadians); x = cosValue * x - sinValue * y; y = sinValue * tmp + cosValue * y; - x *= Math.abs(this._zoomFactor); - y *= Math.abs(this._zoomFactor); + const zoomFactor = + this._zoomFactor * this.getRuntimeScene().getGame().getZoomFactor(); + x *= Math.abs(zoomFactor); + y *= Math.abs(zoomFactor); position[0] = x + this.getRuntimeScene()._cachedGameResolutionWidth / 2; position[1] = y + this.getRuntimeScene()._cachedGameResolutionHeight / 2; @@ -404,8 +423,10 @@ namespace gdjs { const sinValue = Math.sin(-angleInRadians); x = cosValue * x - sinValue * y; y = sinValue * tmp + cosValue * y; - x *= Math.abs(this._zoomFactor); - y *= Math.abs(this._zoomFactor); + const zoomFactor = + this._zoomFactor * this.getRuntimeScene().getGame().getZoomFactor(); + x *= Math.abs(zoomFactor); + y *= Math.abs(zoomFactor); x += this._runtimeScene.getViewportOriginX(); y += this._runtimeScene.getViewportOriginY(); diff --git a/GDJS/Runtime/pixi-renderers/layer-pixi-renderer.ts b/GDJS/Runtime/pixi-renderers/layer-pixi-renderer.ts index bdeff384f2ee..16a9addc09a6 100644 --- a/GDJS/Runtime/pixi-renderers/layer-pixi-renderer.ts +++ b/GDJS/Runtime/pixi-renderers/layer-pixi-renderer.ts @@ -262,8 +262,8 @@ namespace gdjs { if (game.getAntialiasingMode() !== 'none') { this._threeEffectComposer.addPass( new THREE_ADDONS.SMAAPass( - game.getGameResolutionWidth(), - game.getGameResolutionHeight() + game.getRenderingResolutionWidth(), + game.getRenderingResolutionHeight() ) ); } @@ -393,7 +393,7 @@ namespace gdjs { */ updatePosition(): void { const angle = -gdjs.toRad(this._layer.getCameraRotation()); - const zoomFactor = this._layer.getCameraZoom(); + const zoomFactor = this._layer.getCameraRenderingZoom(); this._pixiContainer.rotation = angle; this._pixiContainer.scale.x = zoomFactor; this._pixiContainer.scale.y = zoomFactor; @@ -501,8 +501,8 @@ namespace gdjs { if (this._threeEffectComposer) { const game = this._layer.getRuntimeScene().getGame(); this._threeEffectComposer.setSize( - game.getGameResolutionWidth(), - game.getGameResolutionHeight() + game.getRenderingResolutionWidth(), + game.getRenderingResolutionHeight() ); } } diff --git a/GDJS/Runtime/pixi-renderers/runtimegame-pixi-renderer.ts b/GDJS/Runtime/pixi-renderers/runtimegame-pixi-renderer.ts index 9c736c974d2d..e1d35994a2be 100644 --- a/GDJS/Runtime/pixi-renderers/runtimegame-pixi-renderer.ts +++ b/GDJS/Runtime/pixi-renderers/runtimegame-pixi-renderer.ts @@ -100,16 +100,16 @@ namespace gdjs { this._threeRenderer.useLegacyLights = true; this._threeRenderer.autoClear = false; this._threeRenderer.setSize( - this._game.getGameResolutionWidth(), - this._game.getGameResolutionHeight() + this._game.getRenderingResolutionWidth(), + this._game.getRenderingResolutionHeight() ); // Create a PixiJS renderer that use the same GL context as Three.js // so that both can render to the canvas and even have PixiJS rendering // reused in Three.js (by using a RenderTexture and the same internal WebGL texture). this._pixiRenderer = new PIXI.Renderer({ - width: this._game.getGameResolutionWidth(), - height: this._game.getGameResolutionHeight(), + width: this._game.getRenderingResolutionWidth(), + height: this._game.getRenderingResolutionHeight(), view: gameCanvas, // @ts-ignore - reuse the context from Three.js. context: this._threeRenderer.getContext(), @@ -124,8 +124,8 @@ namespace gdjs { // "preserveDrawingBuffer: true" is needed to avoid flickering // and background issues on some mobile phones (see #585 #572 #566 #463). this._pixiRenderer = PIXI.autoDetectRenderer({ - width: this._game.getGameResolutionWidth(), - height: this._game.getGameResolutionHeight(), + width: this._game.getRenderingResolutionWidth(), + height: this._game.getRenderingResolutionHeight(), view: gameCanvas, preserveDrawingBuffer: true, antialias: false, @@ -272,20 +272,20 @@ namespace gdjs { // There is no "smart" resizing to be done here: the rendering of the game // should be done with the size set on the game. if ( - this._pixiRenderer.width !== this._game.getGameResolutionWidth() || - this._pixiRenderer.height !== this._game.getGameResolutionHeight() + this._pixiRenderer.width !== this._game.getRenderingResolutionWidth() || + this._pixiRenderer.height !== this._game.getRenderingResolutionHeight() ) { // TODO (3D): It might be useful to resize pixi view in 3D depending on FOV value // to enable a mode where pixi always fills the whole screen. this._pixiRenderer.resize( - this._game.getGameResolutionWidth(), - this._game.getGameResolutionHeight() + this._game.getRenderingResolutionWidth(), + this._game.getRenderingResolutionHeight() ); if (this._threeRenderer) { this._threeRenderer.setSize( - this._game.getGameResolutionWidth(), - this._game.getGameResolutionHeight() + this._game.getRenderingResolutionWidth(), + this._game.getRenderingResolutionHeight() ); } } @@ -295,8 +295,8 @@ namespace gdjs { // only, so won't create visual artifacts during the rendering. const isFullPage = this._forceFullscreen || this._isFullPage || this._isFullscreen; - let canvasWidth = this._game.getGameResolutionWidth(); - let canvasHeight = this._game.getGameResolutionHeight(); + let canvasWidth = this._game.getRenderingResolutionWidth(); + let canvasHeight = this._game.getRenderingResolutionHeight(); let maxWidth = window.innerWidth - this._marginLeft - this._marginRight; let maxHeight = window.innerHeight - this._marginTop - this._marginBottom; if (maxWidth < 0) { @@ -523,10 +523,10 @@ namespace gdjs { // Handle the fact that the game is stretched to fill the canvas. pageCoords[0] = (canvasCoords[0] * this._canvasWidth) / - this._game.getGameResolutionWidth(); + this._game.getRenderingResolutionWidth(); pageCoords[1] = (canvasCoords[1] * this._canvasHeight) / - this._game.getGameResolutionHeight(); + this._game.getRenderingResolutionHeight(); return pageCoords; } diff --git a/GDJS/Runtime/pixi-renderers/runtimescene-pixi-renderer.ts b/GDJS/Runtime/pixi-renderers/runtimescene-pixi-renderer.ts index 603c1bb3436e..e914f3a8e3c9 100644 --- a/GDJS/Runtime/pixi-renderers/runtimescene-pixi-renderer.ts +++ b/GDJS/Runtime/pixi-renderers/runtimescene-pixi-renderer.ts @@ -47,9 +47,9 @@ namespace gdjs { // TODO (3D): should this be done for each individual layer? // Especially if we remove _pixiContainer entirely. this._pixiContainer.scale.x = - pixiRenderer.width / runtimeGame.getGameResolutionWidth(); + pixiRenderer.width / runtimeGame.getRenderingResolutionWidth(); this._pixiContainer.scale.y = - pixiRenderer.height / runtimeGame.getGameResolutionHeight(); + pixiRenderer.height / runtimeGame.getRenderingResolutionHeight(); for (const runtimeLayer of this._runtimeScene._orderedLayers) { runtimeLayer.getRenderer().onGameResolutionResized(); diff --git a/GDJS/Runtime/runtimegame.ts b/GDJS/Runtime/runtimegame.ts index 7bb9cceabd01..014a2855fe89 100644 --- a/GDJS/Runtime/runtimegame.ts +++ b/GDJS/Runtime/runtimegame.ts @@ -141,7 +141,7 @@ namespace gdjs { _originalHeight: float; _resizeMode: 'adaptWidth' | 'adaptHeight' | string; _adaptGameResolutionAtRuntime: boolean; - _scaleMode: 'linear' | 'nearest'; + _scaleMode: ScaleMode; _pixelsRounding: boolean; _antialiasingMode: 'none' | 'MSAA'; _isAntialisingEnabledOnMobile: boolean; @@ -158,6 +158,11 @@ namespace gdjs { * When set to true, the scenes are notified that game resolution size changed. */ _notifyScenesForGameResolutionResize: boolean = false; + /** + * When set to true, the scenes are notified that game zoom factor changed. + */ + _notifyScenesForGameZoomFactorChange: boolean = false; + _zoomFactor: number = 1; /** * When paused, the game won't step and will be freezed. Useful for debugging. @@ -551,21 +556,41 @@ namespace gdjs { } /** - * Get the game resolution (the size at which the game is played and rendered) width. + * Get the game resolution width for events. * @returns The game resolution width, in pixels. */ getGameResolutionWidth(): float { - return this._gameResolutionWidth; + return this._gameResolutionWidth / this._zoomFactor; } /** - * Get the game resolution (the size at which the game is played and rendered) height. + * Get the game resolution height for events. * @returns The game resolution height, in pixels. */ getGameResolutionHeight(): float { + return this._gameResolutionHeight / this._zoomFactor; + } + + /** + * Get the game resolution width (the size at which the game is rendered). + * @returns The game resolution width, in pixels. + */ + getRenderingResolutionWidth(): float { + return this._gameResolutionWidth; + } + + /** + * Get the game resolution height (the size at which the game is rendered). + * @returns The game resolution height, in pixels. + */ + getRenderingResolutionHeight(): float { return this._gameResolutionHeight; } + getZoomFactor() { + return this._zoomFactor; + } + /** * Change the game resolution. * @@ -577,17 +602,16 @@ namespace gdjs { this._gameResolutionWidth = width; this._gameResolutionHeight = height; - if (this._adaptGameResolutionAtRuntime) { - if ( - gdjs.RuntimeGameRenderer && - gdjs.RuntimeGameRenderer.getWindowInnerWidth && - gdjs.RuntimeGameRenderer.getWindowInnerHeight - ) { - const windowInnerWidth = - gdjs.RuntimeGameRenderer.getWindowInnerWidth(); - const windowInnerHeight = - gdjs.RuntimeGameRenderer.getWindowInnerHeight(); + if ( + gdjs.RuntimeGameRenderer && + gdjs.RuntimeGameRenderer.getWindowInnerWidth && + gdjs.RuntimeGameRenderer.getWindowInnerHeight + ) { + const windowInnerWidth = gdjs.RuntimeGameRenderer.getWindowInnerWidth(); + const windowInnerHeight = + gdjs.RuntimeGameRenderer.getWindowInnerHeight(); + if (this._adaptGameResolutionAtRuntime) { // Enlarge either the width or the eight to fill the inner window space. if (this._resizeMode === 'adaptWidth') { this._gameResolutionWidth = @@ -614,6 +638,28 @@ namespace gdjs { } } } + if (this._scaleMode === 'magnified') { + const pixelSize = Math.max( + 1, + Math.floor( + this._zoomFactor * + Math.min( + windowInnerWidth / this._gameResolutionWidth, + windowInnerHeight / this._gameResolutionHeight + ) + ) + ); + this._gameResolutionWidth = Math.round( + (this._gameResolutionWidth * pixelSize) / this._zoomFactor + ); + this._gameResolutionHeight = Math.round( + (this._gameResolutionHeight * pixelSize) / this._zoomFactor + ); + if (this._zoomFactor !== pixelSize) { + this._zoomFactor = pixelSize; + this._notifyScenesForGameZoomFactorChange = true; + } + } } // Don't alter the game resolution. The renderer @@ -682,9 +728,9 @@ namespace gdjs { } /** - * Return the scale mode of the game ("linear" or "nearest"). + * Return the scale mode of the game ("linear", "magnified" or "nearest"). */ - getScaleMode(): 'linear' | 'nearest' { + getScaleMode(): ScaleMode { return this._scaleMode; } @@ -962,6 +1008,10 @@ namespace gdjs { this._sceneStack.onGameResolutionResized(); this._notifyScenesForGameResolutionResize = false; } + if (this._notifyScenesForGameZoomFactorChange) { + this._sceneStack.onGameZoomFactorChanged(); + this._notifyScenesForGameZoomFactorChange = false; + } // Render and step the scene. if (this._sceneStack.step(elapsedTime)) { diff --git a/GDJS/Runtime/runtimeobject.ts b/GDJS/Runtime/runtimeobject.ts index 47179079b686..0434373b14d5 100644 --- a/GDJS/Runtime/runtimeobject.ts +++ b/GDJS/Runtime/runtimeobject.ts @@ -658,6 +658,11 @@ namespace gdjs { */ onSceneResumed(runtimeScene: gdjs.RuntimeScene): void {} + /** + * Called when the game zoom factor is changed to adapt to a new resolution. + */ + onGameZoomFactorChanged(): void {} + //Rendering: /** * @return The internal object for a 2D rendering (PIXI.DisplayObject...) diff --git a/GDJS/Runtime/runtimescene.ts b/GDJS/Runtime/runtimescene.ts index c32de70f2599..15b6922727f7 100644 --- a/GDJS/Runtime/runtimescene.ts +++ b/GDJS/Runtime/runtimescene.ts @@ -45,6 +45,7 @@ namespace gdjs { _cachedGameResolutionWidth: integer; _cachedGameResolutionHeight: integer; + _cachedGameZoomFactor: float; /** * A network ID associated to the scene to be used @@ -69,10 +70,13 @@ namespace gdjs { this._onceTriggers = new gdjs.OnceTriggers(); this._requestedChange = SceneChangeRequest.CONTINUE; this._cachedGameResolutionWidth = runtimeGame - ? runtimeGame.getGameResolutionWidth() + ? runtimeGame.getRenderingResolutionWidth() : 0; this._cachedGameResolutionHeight = runtimeGame - ? runtimeGame.getGameResolutionHeight() + ? runtimeGame.getRenderingResolutionHeight() + : 0; + this._cachedGameZoomFactor = runtimeGame + ? runtimeGame.getZoomFactor() : 0; this._renderer = new gdjs.RuntimeSceneRenderer( @@ -99,13 +103,18 @@ namespace gdjs { * See gdjs.RuntimeGame.startGameLoop in particular. */ onGameResolutionResized() { - const oldGameResolutionOriginX = this.getViewportOriginX(); - const oldGameResolutionOriginY = this.getViewportOriginY(); + const oldGameResolutionOriginX = + this.getViewportOriginX() / this._cachedGameZoomFactor; + const oldGameResolutionOriginY = + this.getViewportOriginY() / this._cachedGameZoomFactor; this._cachedGameResolutionWidth = this._runtimeGame - ? this._runtimeGame.getGameResolutionWidth() + ? this._runtimeGame.getRenderingResolutionWidth() : 0; this._cachedGameResolutionHeight = this._runtimeGame - ? this._runtimeGame.getGameResolutionHeight() + ? this._runtimeGame.getRenderingResolutionHeight() + : 0; + this._cachedGameZoomFactor = this._runtimeGame + ? this._runtimeGame.getZoomFactor() : 0; for (const name in this._layers.items) { if (this._layers.items.hasOwnProperty(name)) { diff --git a/GDJS/Runtime/scenestack.ts b/GDJS/Runtime/scenestack.ts index b04bd5976187..1c3831b4fb9d 100644 --- a/GDJS/Runtime/scenestack.ts +++ b/GDJS/Runtime/scenestack.ts @@ -34,6 +34,15 @@ namespace gdjs { } } + /** + * Called when the game zoom factor is changed to adapt to a new resolution. + */ + onGameZoomFactorChanged(): void { + for (let i = 0; i < this._stack.length; ++i) { + this._stack[i].onGameZoomFactorChanged(); + } + } + step(elapsedTime: float): boolean { this._throwIfDisposed(); if (this._isNextLayoutLoading || this._stack.length === 0) { diff --git a/GDJS/Runtime/types/project-data.d.ts b/GDJS/Runtime/types/project-data.d.ts index 8f4947b17911..024939051aed 100644 --- a/GDJS/Runtime/types/project-data.d.ts +++ b/GDJS/Runtime/types/project-data.d.ts @@ -329,13 +329,15 @@ declare interface EffectNetworkSyncData { }; } +declare type ScaleMode = 'linear' | 'magnified' | 'nearest'; + declare interface ProjectPropertiesData { adaptGameResolutionAtRuntime: boolean; folderProject: boolean; orientation: string; packageName: string; projectFile: string; - scaleMode: 'linear' | 'nearest'; + scaleMode: ScaleMode; pixelsRounding: boolean; antialiasingMode: 'none' | 'MSAA'; antialisingEnabledOnMobile: boolean; diff --git a/newIDE/app/src/ProjectManager/ProjectPropertiesDialog.js b/newIDE/app/src/ProjectManager/ProjectPropertiesDialog.js index 46a01a06b6aa..006c21855cbc 100644 --- a/newIDE/app/src/ProjectManager/ProjectPropertiesDialog.js +++ b/newIDE/app/src/ProjectManager/ProjectPropertiesDialog.js @@ -731,6 +731,10 @@ const ProjectPropertiesDialog = (props: Props) => { value="linear" label={t`Linear (antialiased rendering, good for most games)`} /> + Date: Sun, 23 Mar 2025 19:40:31 +0100 Subject: [PATCH 2/8] Disable resource smoothing by default for the new scale mode. --- newIDE/app/src/AssetStore/InstallAsset.js | 4 +++- .../src/InstancesEditor/InstancesRenderer/LayerRenderer.js | 3 ++- newIDE/app/src/ProjectManager/ProjectPropertiesDialog.js | 2 +- newIDE/app/src/ResourcesList/ResourceUtils.js | 5 ++++- 4 files changed, 10 insertions(+), 4 deletions(-) diff --git a/newIDE/app/src/AssetStore/InstallAsset.js b/newIDE/app/src/AssetStore/InstallAsset.js index 86bfb6e90861..5b4c674cb63d 100644 --- a/newIDE/app/src/AssetStore/InstallAsset.js +++ b/newIDE/app/src/AssetStore/InstallAsset.js @@ -129,7 +129,9 @@ export const installResource = ( if (newResource.getKind() === 'image') { // $FlowExpectedError[prop-missing] - We know the resource is an ImageResource and has the setSmooth method. newResource.setSmooth( - project.getScaleMode() !== 'nearest' && !isPixelArt(asset) + project.getScaleMode() !== 'nearest' && + project.getScaleMode() !== 'magnified' && + !isPixelArt(asset) ); } diff --git a/newIDE/app/src/InstancesEditor/InstancesRenderer/LayerRenderer.js b/newIDE/app/src/InstancesEditor/InstancesRenderer/LayerRenderer.js index 625fce456066..53321d6387c6 100644 --- a/newIDE/app/src/InstancesEditor/InstancesRenderer/LayerRenderer.js +++ b/newIDE/app/src/InstancesEditor/InstancesRenderer/LayerRenderer.js @@ -655,7 +655,8 @@ export default class LayerRenderer { threePlaneTexture.generateMipmaps = false; const filter = - this.project.getScaleMode() === 'nearest' + this.project.getScaleMode() === 'nearest' || + this.project.getScaleMode() === 'magnified' ? THREE.NearestFilter : THREE.LinearFilter; threePlaneTexture.minFilter = filter; diff --git a/newIDE/app/src/ProjectManager/ProjectPropertiesDialog.js b/newIDE/app/src/ProjectManager/ProjectPropertiesDialog.js index 006c21855cbc..66779df8495a 100644 --- a/newIDE/app/src/ProjectManager/ProjectPropertiesDialog.js +++ b/newIDE/app/src/ProjectManager/ProjectPropertiesDialog.js @@ -753,7 +753,7 @@ const ProjectPropertiesDialog = (props: Props) => { notifyOfChange(); }} /> - {scaleMode === 'nearest' && ( + {(scaleMode === 'nearest' || scaleMode === 'magnified') && ( { if (newResource instanceof gd.ImageResource) { - newResource.setSmooth(project.getScaleMode() !== 'nearest'); + newResource.setSmooth( + project.getScaleMode() !== 'nearest' && + project.getScaleMode() !== 'magnified' + ); } }; From 1438b28cab0b99224923d1623bd9253733a7b2e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Davy=20H=C3=A9lard?= Date: Tue, 25 Mar 2025 17:05:29 +0100 Subject: [PATCH 3/8] Fix transformation. --- GDJS/Runtime/RuntimeCustomObjectLayer.ts | 4 -- GDJS/Runtime/RuntimeLayer.ts | 2 - GDJS/Runtime/layer.ts | 45 +++++-------------- .../pixi-renderers/layer-pixi-renderer.ts | 9 ++-- .../runtimescene-pixi-renderer.ts | 4 +- GDJS/Runtime/runtimegame.ts | 10 +++-- GDJS/Runtime/runtimescene.ts | 14 +++--- 7 files changed, 33 insertions(+), 55 deletions(-) diff --git a/GDJS/Runtime/RuntimeCustomObjectLayer.ts b/GDJS/Runtime/RuntimeCustomObjectLayer.ts index a39321ab3020..7b20900e5dbd 100644 --- a/GDJS/Runtime/RuntimeCustomObjectLayer.ts +++ b/GDJS/Runtime/RuntimeCustomObjectLayer.ts @@ -54,10 +54,6 @@ namespace gdjs { return 1; } - override getCameraRenderingZoom(cameraId?: integer): float { - return 1; - } - override setCameraZ(z: float, fov: float, cameraId?: integer): void {} override getCameraZ(fov: float | null, cameraId?: integer): float { diff --git a/GDJS/Runtime/RuntimeLayer.ts b/GDJS/Runtime/RuntimeLayer.ts index d6cd6ff72e11..4e2531b2ea29 100644 --- a/GDJS/Runtime/RuntimeLayer.ts +++ b/GDJS/Runtime/RuntimeLayer.ts @@ -272,8 +272,6 @@ namespace gdjs { */ abstract getCameraZoom(cameraId?: integer): float; - abstract getCameraRenderingZoom(cameraId?: integer): float; - /** * Set the camera center Z position. * diff --git a/GDJS/Runtime/layer.ts b/GDJS/Runtime/layer.ts index 4473e5dac51a..493b33923aa7 100644 --- a/GDJS/Runtime/layer.ts +++ b/GDJS/Runtime/layer.ts @@ -81,13 +81,10 @@ namespace gdjs { Math.abs(this._cameraY - oldGameResolutionOriginY) < 1 && this._zoomFactor === 1 ) { - const gameZoomFactor = this.getRuntimeScene().getGame().getZoomFactor(); this._cameraX += - this._runtimeScene.getViewportOriginX() / gameZoomFactor - - oldGameResolutionOriginX; + this._runtimeScene.getViewportOriginX() - oldGameResolutionOriginX; this._cameraY += - this._runtimeScene.getViewportOriginY() / gameZoomFactor - - oldGameResolutionOriginY; + this._runtimeScene.getViewportOriginY() - oldGameResolutionOriginY; } this._renderer.updatePosition(); @@ -148,9 +145,7 @@ namespace gdjs { * @return The width of the camera */ override getCameraWidth(cameraId?: integer): float { - const zoomFactor = - this._zoomFactor * this.getRuntimeScene().getGame().getZoomFactor(); - return this.getWidth() / zoomFactor; + return this.getWidth() / this._zoomFactor; } /** @@ -161,9 +156,7 @@ namespace gdjs { * @return The height of the camera */ override getCameraHeight(cameraId?: integer): float { - const zoomFactor = - this._zoomFactor * this.getRuntimeScene().getGame().getZoomFactor(); - return this.getHeight() / zoomFactor; + return this.getHeight() / this._zoomFactor; } /** @@ -188,12 +181,6 @@ namespace gdjs { return this._zoomFactor; } - override getCameraRenderingZoom(cameraId?: integer): float { - return ( - this._zoomFactor * this.getRuntimeScene().getGame().getZoomFactor() - ); - } - /** * Set the camera center Z position. * @@ -296,10 +283,8 @@ namespace gdjs { x -= this.getRuntimeScene()._cachedGameResolutionWidth / 2; y -= this.getRuntimeScene()._cachedGameResolutionHeight / 2; - const zoomFactor = - this._zoomFactor * this.getRuntimeScene().getGame().getZoomFactor(); - x /= Math.abs(zoomFactor); - y /= Math.abs(zoomFactor); + x /= Math.abs(this._zoomFactor); + y /= Math.abs(this._zoomFactor); // Only compute angle and cos/sin once (allow heavy optimization from JS engines). const angleInRadians = (this._cameraRotation / 180) * Math.PI; @@ -335,10 +320,8 @@ namespace gdjs { ): FloatPoint { x -= this._runtimeScene.getViewportOriginX(); y -= this._runtimeScene.getViewportOriginY(); - const zoomFactor = - this._zoomFactor * this.getRuntimeScene().getGame().getZoomFactor(); - x /= Math.abs(zoomFactor); - y /= Math.abs(zoomFactor); + x /= Math.abs(this._zoomFactor); + y /= Math.abs(this._zoomFactor); // Only compute angle and cos/sin once (allow heavy optimization from JS engines). const angleInRadians = (this._cameraRotation / 180) * Math.PI; @@ -384,10 +367,8 @@ namespace gdjs { const sinValue = Math.sin(-angleInRadians); x = cosValue * x - sinValue * y; y = sinValue * tmp + cosValue * y; - const zoomFactor = - this._zoomFactor * this.getRuntimeScene().getGame().getZoomFactor(); - x *= Math.abs(zoomFactor); - y *= Math.abs(zoomFactor); + x *= Math.abs(this._zoomFactor); + y *= Math.abs(this._zoomFactor); position[0] = x + this.getRuntimeScene()._cachedGameResolutionWidth / 2; position[1] = y + this.getRuntimeScene()._cachedGameResolutionHeight / 2; @@ -423,10 +404,8 @@ namespace gdjs { const sinValue = Math.sin(-angleInRadians); x = cosValue * x - sinValue * y; y = sinValue * tmp + cosValue * y; - const zoomFactor = - this._zoomFactor * this.getRuntimeScene().getGame().getZoomFactor(); - x *= Math.abs(zoomFactor); - y *= Math.abs(zoomFactor); + x *= Math.abs(this._zoomFactor); + y *= Math.abs(this._zoomFactor); x += this._runtimeScene.getViewportOriginX(); y += this._runtimeScene.getViewportOriginY(); diff --git a/GDJS/Runtime/pixi-renderers/layer-pixi-renderer.ts b/GDJS/Runtime/pixi-renderers/layer-pixi-renderer.ts index 16a9addc09a6..d8709d6b2c6d 100644 --- a/GDJS/Runtime/pixi-renderers/layer-pixi-renderer.ts +++ b/GDJS/Runtime/pixi-renderers/layer-pixi-renderer.ts @@ -304,9 +304,12 @@ namespace gdjs { this._threePlaneTexture = texture; this._threePlaneTexture.generateMipmaps = false; + const scaleMode = this._layer + .getRuntimeScene() + .getGame() + .getScaleMode(); const filter = - this._layer.getRuntimeScene().getGame().getScaleMode() === - 'nearest' + scaleMode === 'nearest' || scaleMode === 'magnified' ? THREE.NearestFilter : THREE.LinearFilter; this._threePlaneTexture.minFilter = filter; @@ -393,7 +396,7 @@ namespace gdjs { */ updatePosition(): void { const angle = -gdjs.toRad(this._layer.getCameraRotation()); - const zoomFactor = this._layer.getCameraRenderingZoom(); + const zoomFactor = this._layer.getCameraZoom(); this._pixiContainer.rotation = angle; this._pixiContainer.scale.x = zoomFactor; this._pixiContainer.scale.y = zoomFactor; diff --git a/GDJS/Runtime/pixi-renderers/runtimescene-pixi-renderer.ts b/GDJS/Runtime/pixi-renderers/runtimescene-pixi-renderer.ts index e914f3a8e3c9..603c1bb3436e 100644 --- a/GDJS/Runtime/pixi-renderers/runtimescene-pixi-renderer.ts +++ b/GDJS/Runtime/pixi-renderers/runtimescene-pixi-renderer.ts @@ -47,9 +47,9 @@ namespace gdjs { // TODO (3D): should this be done for each individual layer? // Especially if we remove _pixiContainer entirely. this._pixiContainer.scale.x = - pixiRenderer.width / runtimeGame.getRenderingResolutionWidth(); + pixiRenderer.width / runtimeGame.getGameResolutionWidth(); this._pixiContainer.scale.y = - pixiRenderer.height / runtimeGame.getRenderingResolutionHeight(); + pixiRenderer.height / runtimeGame.getGameResolutionHeight(); for (const runtimeLayer of this._runtimeScene._orderedLayers) { runtimeLayer.getRenderer().onGameResolutionResized(); diff --git a/GDJS/Runtime/runtimegame.ts b/GDJS/Runtime/runtimegame.ts index 014a2855fe89..2ae7d75b1947 100644 --- a/GDJS/Runtime/runtimegame.ts +++ b/GDJS/Runtime/runtimegame.ts @@ -638,10 +638,14 @@ namespace gdjs { } } } - if (this._scaleMode === 'magnified') { + if ( + this._scaleMode === 'magnified' && + this._gameResolutionWidth > 0 && + this._gameResolutionHeight > 0 + ) { const pixelSize = Math.max( 1, - Math.floor( + Math.ceil( this._zoomFactor * Math.min( windowInnerWidth / this._gameResolutionWidth, @@ -655,7 +659,7 @@ namespace gdjs { this._gameResolutionHeight = Math.round( (this._gameResolutionHeight * pixelSize) / this._zoomFactor ); - if (this._zoomFactor !== pixelSize) { + if (this._zoomFactor !== pixelSize && pixelSize >= 1) { this._zoomFactor = pixelSize; this._notifyScenesForGameZoomFactorChange = true; } diff --git a/GDJS/Runtime/runtimescene.ts b/GDJS/Runtime/runtimescene.ts index 15b6922727f7..d0f0f3510d11 100644 --- a/GDJS/Runtime/runtimescene.ts +++ b/GDJS/Runtime/runtimescene.ts @@ -70,10 +70,10 @@ namespace gdjs { this._onceTriggers = new gdjs.OnceTriggers(); this._requestedChange = SceneChangeRequest.CONTINUE; this._cachedGameResolutionWidth = runtimeGame - ? runtimeGame.getRenderingResolutionWidth() + ? runtimeGame.getGameResolutionWidth() : 0; this._cachedGameResolutionHeight = runtimeGame - ? runtimeGame.getRenderingResolutionHeight() + ? runtimeGame.getGameResolutionHeight() : 0; this._cachedGameZoomFactor = runtimeGame ? runtimeGame.getZoomFactor() @@ -103,15 +103,13 @@ namespace gdjs { * See gdjs.RuntimeGame.startGameLoop in particular. */ onGameResolutionResized() { - const oldGameResolutionOriginX = - this.getViewportOriginX() / this._cachedGameZoomFactor; - const oldGameResolutionOriginY = - this.getViewportOriginY() / this._cachedGameZoomFactor; + const oldGameResolutionOriginX = this.getViewportOriginX(); + const oldGameResolutionOriginY = this.getViewportOriginY(); this._cachedGameResolutionWidth = this._runtimeGame - ? this._runtimeGame.getRenderingResolutionWidth() + ? this._runtimeGame.getGameResolutionWidth() : 0; this._cachedGameResolutionHeight = this._runtimeGame - ? this._runtimeGame.getRenderingResolutionHeight() + ? this._runtimeGame.getGameResolutionHeight() : 0; this._cachedGameZoomFactor = this._runtimeGame ? this._runtimeGame.getZoomFactor() From 1156a8ad75b8192c97f1ecf9bb62a147ecc2619f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Davy=20H=C3=A9lard?= Date: Tue, 25 Mar 2025 17:08:31 +0100 Subject: [PATCH 4/8] Remove useless code. --- GDJS/Runtime/runtimescene.ts | 7 ------- 1 file changed, 7 deletions(-) diff --git a/GDJS/Runtime/runtimescene.ts b/GDJS/Runtime/runtimescene.ts index d0f0f3510d11..c32de70f2599 100644 --- a/GDJS/Runtime/runtimescene.ts +++ b/GDJS/Runtime/runtimescene.ts @@ -45,7 +45,6 @@ namespace gdjs { _cachedGameResolutionWidth: integer; _cachedGameResolutionHeight: integer; - _cachedGameZoomFactor: float; /** * A network ID associated to the scene to be used @@ -75,9 +74,6 @@ namespace gdjs { this._cachedGameResolutionHeight = runtimeGame ? runtimeGame.getGameResolutionHeight() : 0; - this._cachedGameZoomFactor = runtimeGame - ? runtimeGame.getZoomFactor() - : 0; this._renderer = new gdjs.RuntimeSceneRenderer( this, @@ -111,9 +107,6 @@ namespace gdjs { this._cachedGameResolutionHeight = this._runtimeGame ? this._runtimeGame.getGameResolutionHeight() : 0; - this._cachedGameZoomFactor = this._runtimeGame - ? this._runtimeGame.getZoomFactor() - : 0; for (const name in this._layers.items) { if (this._layers.items.hasOwnProperty(name)) { const theLayer: gdjs.RuntimeLayer = this._layers.items[name]; From ecbf73823b81da2640c30b66abb4b42c83c32fc6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Davy=20H=C3=A9lard?= Date: Tue, 25 Mar 2025 17:41:54 +0100 Subject: [PATCH 5/8] Avoid bad setup to slow down games. --- GDJS/Runtime/runtimegame.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/GDJS/Runtime/runtimegame.ts b/GDJS/Runtime/runtimegame.ts index 2ae7d75b1947..a37dab670935 100644 --- a/GDJS/Runtime/runtimegame.ts +++ b/GDJS/Runtime/runtimegame.ts @@ -641,7 +641,10 @@ namespace gdjs { if ( this._scaleMode === 'magnified' && this._gameResolutionWidth > 0 && - this._gameResolutionHeight > 0 + this._gameResolutionHeight > 0 && + // Fall back on linear if magnified is used on a high resolution game. + this._originalWidth <= 960 && + this._originalHeight <= 540 ) { const pixelSize = Math.max( 1, From 30c750d498beae6af209f2b4dda5ff93eadc27d5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Davy=20H=C3=A9lard?= Date: Tue, 25 Mar 2025 18:27:25 +0100 Subject: [PATCH 6/8] Make the graphical debugger more readable. --- .../pixi-renderers/DebuggerPixiRenderer.ts | 53 +++++++++++++++---- .../runtimescene-pixi-renderer.ts | 6 +++ 2 files changed, 49 insertions(+), 10 deletions(-) diff --git a/GDJS/Runtime/pixi-renderers/DebuggerPixiRenderer.ts b/GDJS/Runtime/pixi-renderers/DebuggerPixiRenderer.ts index 1957181a9ff4..2a7ce95692e8 100644 --- a/GDJS/Runtime/pixi-renderers/DebuggerPixiRenderer.ts +++ b/GDJS/Runtime/pixi-renderers/DebuggerPixiRenderer.ts @@ -47,7 +47,7 @@ namespace gdjs { // Add on top of all layers: this._debugDrawContainer.addChild(this._debugDraw); if (pixiContainer) { - pixiContainer.addChild(this._debugDrawContainer); + //pixiContainer.addChild(this._debugDrawContainer); } } const debugDraw = this._debugDraw; @@ -112,7 +112,8 @@ namespace gdjs { const polygon: float[] = []; polygon.push.apply( polygon, - layer.applyLayerTransformation( + this.applyLayerTransformation( + layer, aabb.min[0], aabb.min[1], 0, @@ -121,7 +122,8 @@ namespace gdjs { ); polygon.push.apply( polygon, - layer.applyLayerTransformation( + this.applyLayerTransformation( + layer, aabb.max[0], aabb.min[1], 0, @@ -130,7 +132,8 @@ namespace gdjs { ); polygon.push.apply( polygon, - layer.applyLayerTransformation( + this.applyLayerTransformation( + layer, aabb.max[0], aabb.max[1], 0, @@ -139,7 +142,8 @@ namespace gdjs { ); polygon.push.apply( polygon, - layer.applyLayerTransformation( + this.applyLayerTransformation( + layer, aabb.min[0], aabb.max[1], 0, @@ -185,7 +189,8 @@ namespace gdjs { // as this is for debug draw. const polygon: float[] = []; hitboxes[j].vertices.forEach((point) => { - point = layer.applyLayerTransformation( + point = this.applyLayerTransformation( + layer, point[0], point[1], 0, @@ -205,7 +210,8 @@ namespace gdjs { debugDraw.fill.alpha = 0.3; // Draw Center point - const centerPoint = layer.applyLayerTransformation( + const centerPoint = this.applyLayerTransformation( + layer, object.getCenterXInScene(), object.getCenterYInScene(), 0, @@ -221,7 +227,8 @@ namespace gdjs { ); // Draw position point - const positionPoint = layer.applyLayerTransformation( + const positionPoint = this.applyLayerTransformation( + layer, object.getX(), object.getY(), 0, @@ -245,7 +252,8 @@ namespace gdjs { Math.abs(originPoint[0] - positionPoint[0]) >= 1 || Math.abs(originPoint[1] - positionPoint[1]) >= 1 ) { - originPoint = layer.applyLayerTransformation( + originPoint = this.applyLayerTransformation( + layer, originPoint[0], originPoint[1], 0, @@ -270,7 +278,8 @@ namespace gdjs { for (const customPointName in animationFrame.points.items) { let customPoint = object.getPointPosition(customPointName); - customPoint = layer.applyLayerTransformation( + customPoint = this.applyLayerTransformation( + layer, customPoint[0], customPoint[1], 0, @@ -303,6 +312,30 @@ namespace gdjs { debugDraw.endFill(); } + private applyLayerTransformation( + layer: gdjs.RuntimeLayer, + x: float, + y: float, + cameraId: integer, + result: FloatPoint + ): FloatPoint { + layer.applyLayerTransformation( + x, + y, + cameraId, + result + ); + const gamePixiContainer = this._instanceContainer + .getRenderer() + .getRendererObject(); + if (!gamePixiContainer) { + return result; + } + result[0] *= gamePixiContainer.scale.x; + result[1] *= gamePixiContainer.scale.y; + return result; + } + clearDebugDraw(): void { if (this._debugDraw) { this._debugDraw.clear(); diff --git a/GDJS/Runtime/pixi-renderers/runtimescene-pixi-renderer.ts b/GDJS/Runtime/pixi-renderers/runtimescene-pixi-renderer.ts index 603c1bb3436e..89ced94d85d0 100644 --- a/GDJS/Runtime/pixi-renderers/runtimescene-pixi-renderer.ts +++ b/GDJS/Runtime/pixi-renderers/runtimescene-pixi-renderer.ts @@ -275,6 +275,12 @@ namespace gdjs { pixiRenderer.render(this._pixiContainer, { clear: this._runtimeScene.getClearCanvas(), }); + const debugContainer = this._runtimeScene + .getDebuggerRenderer() + .getRendererObject(); + if (debugContainer) { + pixiRenderer.render(debugContainer, { clear: false }); + } this._layerRenderingMetrics.rendered2DLayersCount++; } From 7eef5fb722dce63df5015e41ab39fe28932f7dc6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Davy=20H=C3=A9lard?= Date: Fri, 28 Mar 2025 17:34:40 +0100 Subject: [PATCH 7/8] Format --- GDJS/Runtime/pixi-renderers/DebuggerPixiRenderer.ts | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/GDJS/Runtime/pixi-renderers/DebuggerPixiRenderer.ts b/GDJS/Runtime/pixi-renderers/DebuggerPixiRenderer.ts index 2a7ce95692e8..344bbf05efd5 100644 --- a/GDJS/Runtime/pixi-renderers/DebuggerPixiRenderer.ts +++ b/GDJS/Runtime/pixi-renderers/DebuggerPixiRenderer.ts @@ -319,15 +319,10 @@ namespace gdjs { cameraId: integer, result: FloatPoint ): FloatPoint { - layer.applyLayerTransformation( - x, - y, - cameraId, - result - ); + layer.applyLayerTransformation(x, y, cameraId, result); const gamePixiContainer = this._instanceContainer - .getRenderer() - .getRendererObject(); + .getRenderer() + .getRendererObject(); if (!gamePixiContainer) { return result; } From 7ceaf0741887cce6aa5984431f9bd3d9f3104d66 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Davy=20H=C3=A9lard?= Date: Fri, 28 Mar 2025 17:54:28 +0100 Subject: [PATCH 8/8] Add more comments --- .../TextObject/textruntimeobject-pixi-renderer.ts | 9 +++++---- GDJS/Runtime/pixi-renderers/DebuggerPixiRenderer.ts | 10 ++-------- GDJS/Runtime/runtimegame.ts | 4 ++++ .../app/src/ProjectManager/ProjectPropertiesDialog.js | 4 ++-- 4 files changed, 13 insertions(+), 14 deletions(-) diff --git a/Extensions/TextObject/textruntimeobject-pixi-renderer.ts b/Extensions/TextObject/textruntimeobject-pixi-renderer.ts index c6f99c777f27..1cf12ceb3deb 100644 --- a/Extensions/TextObject/textruntimeobject-pixi-renderer.ts +++ b/Extensions/TextObject/textruntimeobject-pixi-renderer.ts @@ -4,7 +4,7 @@ namespace gdjs { _fontManager: any; _text: PIXI.Text; _justCreated: boolean = true; - _upscaleRatio: float = 1; + _upscaleRatio: integer = 1; constructor( runtimeObject: gdjs.TextRuntimeObject, @@ -185,10 +185,11 @@ namespace gdjs { } /** - * Set the text object scale. - * @param upscaleRatio The new scale for the text object. + * Set the text object upscale ratio. + * @param upscaleRatio The new upscale ratio for the text object. + * @see gdjs.RuntimeGame.getZoomFactor */ - setUpscaleRatio(upscaleRatio: float): void { + setUpscaleRatio(upscaleRatio: integer): void { this._upscaleRatio = upscaleRatio; this._text.scale.x = this._object.getScaleX() / this._upscaleRatio; this._text.scale.y = this._object.getScaleY() / this._upscaleRatio; diff --git a/GDJS/Runtime/pixi-renderers/DebuggerPixiRenderer.ts b/GDJS/Runtime/pixi-renderers/DebuggerPixiRenderer.ts index 344bbf05efd5..2df63a192a28 100644 --- a/GDJS/Runtime/pixi-renderers/DebuggerPixiRenderer.ts +++ b/GDJS/Runtime/pixi-renderers/DebuggerPixiRenderer.ts @@ -37,18 +37,10 @@ namespace gdjs { showPointsNames: boolean, showCustomPoints: boolean ) { - const pixiContainer = this._instanceContainer - .getRenderer() - .getRendererObject(); if (!this._debugDraw || !this._debugDrawContainer) { this._debugDrawContainer = new PIXI.Container(); this._debugDraw = new PIXI.Graphics(); - - // Add on top of all layers: this._debugDrawContainer.addChild(this._debugDraw); - if (pixiContainer) { - //pixiContainer.addChild(this._debugDrawContainer); - } } const debugDraw = this._debugDraw; @@ -326,6 +318,8 @@ namespace gdjs { if (!gamePixiContainer) { return result; } + // The scale is usually near 1 unless the 'magnified' scale mode is used. + // See gdjs.RuntimeGame.getZoomFactor result[0] *= gamePixiContainer.scale.x; result[1] *= gamePixiContainer.scale.y; return result; diff --git a/GDJS/Runtime/runtimegame.ts b/GDJS/Runtime/runtimegame.ts index a37dab670935..abbb3484e3c2 100644 --- a/GDJS/Runtime/runtimegame.ts +++ b/GDJS/Runtime/runtimegame.ts @@ -587,6 +587,10 @@ namespace gdjs { return this._gameResolutionHeight; } + /** + * The scale is usually near 1 unless the 'magnified' scale mode is used. + * @returns the factor between game resolution size and rendering resolution size. + */ getZoomFactor() { return this._zoomFactor; } diff --git a/newIDE/app/src/ProjectManager/ProjectPropertiesDialog.js b/newIDE/app/src/ProjectManager/ProjectPropertiesDialog.js index 66779df8495a..1ebd11b3bb8e 100644 --- a/newIDE/app/src/ProjectManager/ProjectPropertiesDialog.js +++ b/newIDE/app/src/ProjectManager/ProjectPropertiesDialog.js @@ -729,7 +729,7 @@ const ProjectPropertiesDialog = (props: Props) => { > { />