diff --git a/front/src/Enum/EnvironmentVariable.ts b/front/src/Enum/EnvironmentVariable.ts index 753a9ef3..b7fc6edb 100644 --- a/front/src/Enum/EnvironmentVariable.ts +++ b/front/src/Enum/EnvironmentVariable.ts @@ -10,8 +10,8 @@ const TURN_USER: string = process.env.TURN_USER || ''; const TURN_PASSWORD: string = process.env.TURN_PASSWORD || ''; const JITSI_URL : string|undefined = (process.env.JITSI_URL === '') ? undefined : process.env.JITSI_URL; const JITSI_PRIVATE_MODE : boolean = process.env.JITSI_PRIVATE_MODE == "true"; -const RESOLUTION = 1 / window.devicePixelRatio; -const ZOOM_LEVEL = 2 * window.devicePixelRatio/*3/4*/; +const RESOLUTION = 2; +const ZOOM_LEVEL = 1; const POSITION_DELAY = 200; // Wait 200ms between sending position events const MAX_EXTRAPOLATION_TIME = 100; // Extrapolate a maximum of 250ms if no new movement is sent by the player export const MAX_USERNAME_LENGTH = parseInt(process.env.MAX_USERNAME_LENGTH || '') || 8; diff --git a/front/src/Phaser/Components/MobileJoystick.ts b/front/src/Phaser/Components/MobileJoystick.ts index 1ace529c..8c3cefdc 100644 --- a/front/src/Phaser/Components/MobileJoystick.ts +++ b/front/src/Phaser/Components/MobileJoystick.ts @@ -1,4 +1,6 @@ import VirtualJoystick from 'phaser3-rex-plugins/plugins/virtualjoystick.js'; +import ScaleManager = Phaser.Scale.ScaleManager; +import {waScaleManager} from "../Services/WaScaleManager"; const outOfScreenX = -1000; const outOfScreenY = -1000; @@ -11,7 +13,8 @@ export const joystickThumbKey = 'joystickThumb'; export const joystickThumbImg = 'resources/objects/smallHandleFilledGrey.png'; export class MobileJoystick extends VirtualJoystick { - + private resizeCallback: () => void; + constructor(scene: Phaser.Scene) { super(scene, { x: outOfScreenX, @@ -31,5 +34,18 @@ export class MobileJoystick extends VirtualJoystick { this.x = outOfScreenX; this.y = outOfScreenY; }); + this.resizeCallback = this.resize.bind(this); + this.scene.scale.on(Phaser.Scale.Events.RESIZE, this.resizeCallback); } -} \ No newline at end of file + + private resize() { + this.base.setDisplaySize(60 / waScaleManager.zoomModifier, 60 / waScaleManager.zoomModifier); + this.thumb.setDisplaySize(30 / waScaleManager.zoomModifier, 30 / waScaleManager.zoomModifier); + this.setRadius(20 / waScaleManager.zoomModifier); + } + + public destroy() { + super.destroy(); + this.scene.scale.removeListener(Phaser.Scale.Events.RESIZE, this.resizeCallback); + } +} diff --git a/front/src/Phaser/Game/GameScene.ts b/front/src/Phaser/Game/GameScene.ts index ea293c04..de4bea65 100644 --- a/front/src/Phaser/Game/GameScene.ts +++ b/front/src/Phaser/Game/GameScene.ts @@ -81,6 +81,7 @@ import { lazyLoadCompanionResource } from "../Companion/CompanionTexturesLoading import {touchScreenManager} from "../../Touch/TouchScreenManager"; import {PinchManager} from "../UserInput/PinchManager"; import {joystickBaseImg, joystickBaseKey, joystickThumbImg, joystickThumbKey} from "../Components/MobileJoystick"; +import {waScaleManager} from "../Services/WaScaleManager"; export interface GameSceneInitInterface { initPosition: PointInterface|null, @@ -176,6 +177,7 @@ export class GameScene extends ResizableScene implements CenterListener { private popUpElements : Map = new Map(); private originalMapUrl: string|undefined; private cursorKeys: any; + private pinchManager: PinchManager|undefined; constructor(private room: Room, MapUrlFile: string, customKey?: string|undefined) { super({ @@ -364,7 +366,7 @@ export class GameScene extends ResizableScene implements CenterListener { this.startLayerName = urlManager.getStartLayerNameFromUrl(); if (touchScreenManager.supportTouchScreen) { - new PinchManager(this); + this.pinchManager = new PinchManager(this); } this.messageSubscription = worldFullMessageStream.stream.subscribe((message) => this.showWorldFullError()) @@ -420,7 +422,7 @@ export class GameScene extends ResizableScene implements CenterListener { //initialise list of other player this.MapPlayers = this.physics.add.group({immovable: true}); - + //create input to move mediaManager.setUserInputManager(this.userInputManager); this.userInputManager = new UserInputManager(this); @@ -899,6 +901,8 @@ ${escapedMessage} this.simplePeer.closeAllConnections(); this.simplePeer?.unregister(); this.messageSubscription?.unsubscribe(); + this.userInputManager.destroy(); + this.pinchManager?.destroy(); for(const iframeEvents of this.iframeSubscriptionList){ iframeEvents.unsubscribe(); @@ -1185,11 +1189,17 @@ ${escapedMessage} */ update(time: number, delta: number) : void { if (this.cursorKeys.up.isDown) { - this.cameras.main.zoom *= 1.2; + //this.cameras.main.zoom *= 1.2; + //this.scale.setGameSize(this.scale.width * 1.2, this.scale.height * 1.2); + waScaleManager.zoomModifier *= 1.2; + this.updateCameraOffset(); } else if(this.cursorKeys.down.isDown) { - this.cameras.main.zoom *= 0.8; + //this.scale.setGameSize(this.scale.width * 0.8, this.scale.height * 0.8); + //this.cameras.main.zoom *= 0.8; + waScaleManager.zoomModifier /= 1.2; + this.updateCameraOffset(); } - + mediaManager.setLastUpdateScene(); this.currentTick = time; this.CurrentPlayer.moveUser(delta); @@ -1425,17 +1435,18 @@ ${escapedMessage} } /** - * Updates the offset of the character compared to the center of the screen according to the layout mananger - * (tries to put the character in the center of the reamining space if there is a discussion going on. + * Updates the offset of the character compared to the center of the screen according to the layout manager + * (tries to put the character in the center of the remaining space if there is a discussion going on. */ private updateCameraOffset(): void { const array = layoutManager.findBiggestAvailableArray(); let xCenter = (array.xEnd - array.xStart) / 2 + array.xStart; let yCenter = (array.yEnd - array.yStart) / 2 + array.yStart; + const game = HtmlUtils.querySelectorOrFail('#game canvas'); // Let's put this in Game coordinates by applying the zoom level: - this.cameras.main.setFollowOffset(xCenter - this.game.renderer.width / 2, yCenter - this.game.renderer.height / 2); + this.cameras.main.setFollowOffset((xCenter - game.offsetWidth/2) * window.devicePixelRatio / this.scale.zoom , (yCenter - game.offsetHeight/2) * window.devicePixelRatio / this.scale.zoom); } public onCenterChange(): void { diff --git a/front/src/Phaser/Services/HdpiManager.ts b/front/src/Phaser/Services/HdpiManager.ts new file mode 100644 index 00000000..6ffad8cd --- /dev/null +++ b/front/src/Phaser/Services/HdpiManager.ts @@ -0,0 +1,63 @@ +import ScaleManager = Phaser.Scale.ScaleManager; + +interface Size { + width: number; + height: number; +} + +export class HdpiManager { + private _zoomModifier: number = 1; + + public constructor(private minGamePixelsNumber: number) { + } + + /** + * Returns the optimal size in "game pixels" based on the screen size in "real pixels". + * + * Note: the function is returning the optimal size in "game pixels" in the "game" property, + * but also recommends resizing the "real" pixel screen size of the canvas. + * The proposed new real size is a few pixels bigger than the real size available (if the size is not a multiple of the pixel size) and should overflow. + * + * @param realPixelScreenSize + */ + public getOptimalGameSize(realPixelScreenSize: Size): { game: Size, real: Size } { + const realPixelNumber = realPixelScreenSize.width * realPixelScreenSize.height; + // If the screen has not a definition small enough to match the minimum number of pixels we want to display, + // let's make the canvas the size of the screen (in real pixels) + if (realPixelNumber <= this.minGamePixelsNumber) { + return { + game: realPixelScreenSize, + real: realPixelScreenSize + }; + } + + let i = 1; + + while (true) { + if (realPixelNumber <= this.minGamePixelsNumber * i * i) { + break; + } + + i++; + } + + return { + game: { + width: Math.ceil(realPixelScreenSize.width / (i - 1) / this._zoomModifier), + height: Math.ceil(realPixelScreenSize.height / (i - 1) / this._zoomModifier), + }, + real: { + width: Math.ceil(realPixelScreenSize.width / (i - 1)) * (i - 1), + height: Math.ceil(realPixelScreenSize.height / (i - 1)) * (i - 1), + } + } + } + + public get zoomModifier(): number { + return this._zoomModifier; + } + + public set zoomModifier(zoomModifier: number) { + this._zoomModifier = zoomModifier; + } +} diff --git a/front/src/Phaser/Services/WaScaleManager.ts b/front/src/Phaser/Services/WaScaleManager.ts new file mode 100644 index 00000000..05689071 --- /dev/null +++ b/front/src/Phaser/Services/WaScaleManager.ts @@ -0,0 +1,47 @@ +import {HdpiManager} from "./HdpiManager"; +import ScaleManager = Phaser.Scale.ScaleManager; +import {coWebsiteManager} from "../../WebRtc/CoWebsiteManager"; + + +class WaScaleManager { + private hdpiManager: HdpiManager; + private scaleManager!: ScaleManager; + + public constructor(private minGamePixelsNumber: number) { + this.hdpiManager = new HdpiManager(minGamePixelsNumber); + } + + public setScaleManager(scaleManager: ScaleManager) { + this.scaleManager = scaleManager; + } + + public applyNewSize() { + const {width, height} = coWebsiteManager.getGameSize(); + + let devicePixelRatio = 1; + if (window.devicePixelRatio) { + devicePixelRatio = window.devicePixelRatio; + } + + const { game: gameSize, real: realSize } = this.hdpiManager.getOptimalGameSize({width: width * devicePixelRatio, height: height * devicePixelRatio}); + + this.scaleManager.setZoom(realSize.width / gameSize.width / devicePixelRatio); + this.scaleManager.resize(gameSize.width, gameSize.height); + + // Override bug in canvas resizing in Phaser. Let's resize the canvas ourselves + const style = this.scaleManager.canvas.style; + style.width = Math.ceil(realSize.width / devicePixelRatio) + 'px'; + style.height = Math.ceil(realSize.height / devicePixelRatio) + 'px'; + }; + + public get zoomModifier(): number { + return this.hdpiManager.zoomModifier; + } + + public set zoomModifier(zoomModifier: number) { + this.hdpiManager.zoomModifier = zoomModifier; + this.applyNewSize(); + } +} + +export const waScaleManager = new WaScaleManager(640*480); diff --git a/front/src/Phaser/UserInput/PinchManager.ts b/front/src/Phaser/UserInput/PinchManager.ts index f7c445fa..c702369d 100644 --- a/front/src/Phaser/UserInput/PinchManager.ts +++ b/front/src/Phaser/UserInput/PinchManager.ts @@ -1,22 +1,20 @@ import {Pinch} from "phaser3-rex-plugins/plugins/gestures.js"; +import {waScaleManager} from "../Services/WaScaleManager"; export class PinchManager { private scene: Phaser.Scene; private pinch!: any; // eslint-disable-line - + constructor(scene: Phaser.Scene) { this.scene = scene; this.pinch = new Pinch(scene); this.pinch.on('pinch', (pinch:any) => { // eslint-disable-line - let newZoom = this.scene.cameras.main.zoom * pinch.scaleFactor; - if (newZoom < 0.25) { - newZoom = 0.25; - } else if(newZoom > 2) { - newZoom = 2; - } - this.scene.cameras.main.setZoom(newZoom); + waScaleManager.zoomModifier *= pinch.scaleFactor; }); } - -} \ No newline at end of file + + destroy() { + this.pinch.removeAllListeners(); + } +} diff --git a/front/src/Phaser/UserInput/UserInputManager.ts b/front/src/Phaser/UserInput/UserInputManager.ts index 897fb372..ccc50cce 100644 --- a/front/src/Phaser/UserInput/UserInputManager.ts +++ b/front/src/Phaser/UserInput/UserInputManager.ts @@ -59,7 +59,7 @@ export class UserInputManager { this.initVirtualJoystick(); } } - + initVirtualJoystick() { this.joystick = new MobileJoystick(this.Scene); this.joystick.on("update", () => { @@ -171,4 +171,8 @@ export class UserInputManager { removeSpaceEventListner(callback : Function){ this.Scene.input.keyboard.removeListener('keyup-SPACE', callback); } + + destroy(): void { + this.joystick.destroy(); + } } diff --git a/front/src/index.ts b/front/src/index.ts index 27f19271..0bc7310d 100644 --- a/front/src/index.ts +++ b/front/src/index.ts @@ -17,6 +17,8 @@ import {HelpCameraSettingsScene} from "./Phaser/Menu/HelpCameraSettingsScene"; import {localUserStore} from "./Connexion/LocalUserStore"; import {ErrorScene} from "./Phaser/Reconnecting/ErrorScene"; import {iframeListener} from "./Api/IframeListener"; +import {HdpiManager} from "./Phaser/Services/HdpiManager"; +import {waScaleManager} from "./Phaser/Services/WaScaleManager"; const {width, height} = coWebsiteManager.getGameSize(); @@ -67,16 +69,22 @@ switch (phaserMode) { throw new Error('phaserMode parameter must be one of "auto", "canvas" or "webgl"'); } +const hdpiManager = new HdpiManager(640*480); +const { game: gameSize, real: realSize } = hdpiManager.getOptimalGameSize({width, height}); const config: GameConfig = { type: mode, title: "WorkAdventure", - width: width / 2, - height: height / 2, - parent: "game", - zoom: 2, + scale: { + parent: "game", + width: gameSize.width, + height: gameSize.height, + zoom: realSize.width / gameSize.width, + autoRound: true, + resizeInterval: 999999999999 + }, scene: [EntryScene, LoginScene, SelectCharacterScene, SelectCompanionScene, EnableCameraScene, ReconnectingScene, ErrorScene, CustomizeScene, MenuScene, HelpCameraSettingsScene], - resolution: window.devicePixelRatio / 2, + //resolution: window.devicePixelRatio / 2, fps: fps, dom: { createContainer: true @@ -105,10 +113,13 @@ const config: GameConfig = { const game = new Phaser.Game(config); +waScaleManager.setScaleManager(game.scale); +waScaleManager.applyNewSize(); + window.addEventListener('resize', function (event) { coWebsiteManager.resetStyle(); - const {width, height} = coWebsiteManager.getGameSize(); - game.scale.resize(width / RESOLUTION, height / RESOLUTION); + + waScaleManager.applyNewSize(); // Let's trigger the onResize method of any active scene that is a ResizableScene for (const scene of game.scene.getScenes(true)) { @@ -119,8 +130,7 @@ window.addEventListener('resize', function (event) { }); coWebsiteManager.onResize.subscribe(() => { - const {width, height} = coWebsiteManager.getGameSize(); - game.scale.resize(width / RESOLUTION, height / RESOLUTION); + waScaleManager.applyNewSize(); }); iframeListener.init(); diff --git a/front/tests/Phaser/Services/HdpiManagerTest.ts b/front/tests/Phaser/Services/HdpiManagerTest.ts new file mode 100644 index 00000000..2a4d2662 --- /dev/null +++ b/front/tests/Phaser/Services/HdpiManagerTest.ts @@ -0,0 +1,32 @@ +import "jasmine"; +import {HdpiManager} from "../../../src/Phaser/Services/HdpiManager"; + +describe("Test HdpiManager", () => { + it("should match screen size if size is too small.", () => { + const hdpiManager = new HdpiManager(640*480); + + const result = hdpiManager.getOptimalGameSize({ width: 320, height: 200 }); + expect(result.game.width).toEqual(320); + expect(result.game.height).toEqual(200); + expect(result.real.width).toEqual(320); + expect(result.real.height).toEqual(200); + }); + + it("should match multiple just above.", () => { + const hdpiManager = new HdpiManager(640*480); + + let result = hdpiManager.getOptimalGameSize({ width: 960, height: 600 }); + expect(result.game.width).toEqual(960); + expect(result.game.height).toEqual(600); + + result = hdpiManager.getOptimalGameSize({ width: 640 * 2 + 50, height: 480 * 2 + 50 }); + expect(result.game.width).toEqual(Math.ceil((640 * 2 + 50) / 2)); + expect(result.game.height).toEqual((480 * 2 + 50) / 2); + + result = hdpiManager.getOptimalGameSize({ width: 640 * 3 + 50, height: 480 * 3 + 50 }); + expect(result.game.width).toEqual(Math.ceil((640 * 3 + 50) / 3)); + expect(result.game.height).toEqual(Math.ceil((480 * 3 + 50) / 3)); + expect(result.real.width).toEqual(result.game.width * 3); + expect(result.real.height).toEqual(result.game.height * 3); + }); +});