From 0c5e5ef578e816b0cafc3a52265427bd6e565e1a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20N=C3=A9grier?= Date: Fri, 23 Apr 2021 13:29:23 +0200 Subject: [PATCH] Skip "render" if nothing changed on screen For each requested animation frame (RAF) in Phaser, Phaser calls the "update" method, then the "render" method of each scenes. The "render" method takes some time (and energy) to perform the rendering. The fact is we probably don't need to call "render" if nothing changed on the screen (which happens most of the frames in a typical WorkAdventure game). This commit is therefore overloading the "Game" class of Phaser to add a "dirty" flag. Scenes can now add a "isDirty()" method. If all displayed scenes are pristine (not dirty), Phaser will skip rendering the frame altogether. This saves "a lot" of energy, resulting in laptops that are not overheating when using WorkAdventure \o/ --- front/src/Phaser/Game/Game.ts | 85 ++++++++++++++++++++++++++++++ front/src/Phaser/Game/GameScene.ts | 25 +++++++++ front/src/Phaser/Menu/MenuScene.ts | 4 ++ front/src/Phaser/Player/Player.ts | 5 ++ front/src/index.ts | 4 +- 5 files changed, 122 insertions(+), 1 deletion(-) create mode 100644 front/src/Phaser/Game/Game.ts diff --git a/front/src/Phaser/Game/Game.ts b/front/src/Phaser/Game/Game.ts new file mode 100644 index 00000000..6a48572a --- /dev/null +++ b/front/src/Phaser/Game/Game.ts @@ -0,0 +1,85 @@ +const Events = Phaser.Core.Events; + +/** + * A specialization of the main Phaser Game scene. + * It comes with an optimization to skip rendering. + * + * Beware, the "step" function might vary in future versions of Phaser. + */ +export class Game extends Phaser.Game { + public step(time: number, delta: number) + { + // @ts-ignore + if (this.pendingDestroy) + { + // @ts-ignore + return this.runDestroy(); + } + + const eventEmitter = this.events; + + // Global Managers like Input and Sound update in the prestep + + eventEmitter.emit(Events.PRE_STEP, time, delta); + + // This is mostly meant for user-land code and plugins + + eventEmitter.emit(Events.STEP, time, delta); + + // Update the Scene Manager and all active Scenes + + this.scene.update(time, delta); + + // Our final event before rendering starts + + eventEmitter.emit(Events.POST_STEP, time, delta); + + // This "if" is the changed introduced by the new "Game" class to avoid rendering unnecessarily. + if (this.isDirty()) { + const renderer = this.renderer; + + // Run the Pre-render (clearing the canvas, setting background colors, etc) + + renderer.preRender(); + + eventEmitter.emit(Events.PRE_RENDER, renderer, time, delta); + + // The main render loop. Iterates all Scenes and all Cameras in those scenes, rendering to the renderer instance. + + this.scene.render(renderer); + + // The Post-Render call. Tidies up loose end, takes snapshots, etc. + + renderer.postRender(); + + // The final event before the step repeats. Your last chance to do anything to the canvas before it all starts again. + + eventEmitter.emit(Events.POST_RENDER, renderer, time, delta); + } + } + + private isDirty(): boolean { + // Loop through the scenes in forward order + for (let i = 0; i < this.scene.scenes.length; i++) + { + const scene = this.scene.scenes[i]; + const sys = scene.sys; + + if (sys.settings.visible && sys.settings.status >= Phaser.Scenes.LOADING && sys.settings.status < Phaser.Scenes.SLEEPING) + { + // @ts-ignore + if(typeof scene.isDirty === 'function') { + // @ts-ignore + const isDirty = scene.isDirty() || scene.tweens.getAllTweens().length > 0; + if (isDirty) { + return true; + } + } else { + return true; + } + } + } + + return false; + } +} diff --git a/front/src/Phaser/Game/GameScene.ts b/front/src/Phaser/Game/GameScene.ts index 7e709cc6..672de5e6 100644 --- a/front/src/Phaser/Game/GameScene.ts +++ b/front/src/Phaser/Game/GameScene.ts @@ -1061,6 +1061,7 @@ ${escapedMessage} } createCollisionWithPlayer() { + this.physics.disableUpdate(); //add collision layer this.Layers.forEach((Layer: Phaser.Tilemaps.TilemapLayer) => { this.physics.add.collider(this.CurrentPlayer, Layer, (object1: GameObject, object2: GameObject) => { @@ -1186,17 +1187,36 @@ ${escapedMessage} }); } + private dirty:boolean = true; + /** * @param time * @param delta The delta time in ms since the last frame. This is a smoothed and capped value based on the FPS rate. */ update(time: number, delta: number) : void { + // TODO: add Events.ADDED_TO_SCENE to the scene to track new objects. + // When an object is added, add ANIMATION_UPDATE event on this object (and remove the listener on Events.REMOVE_FROM_SCENE) + // This way, we can set the dirty flag only when an animation is added!!! + + + this.dirty = false; mediaManager.setLastUpdateScene(); this.currentTick = time; + if (this.CurrentPlayer.isMoving() === true) { + this.dirty = true; + } this.CurrentPlayer.moveUser(delta); + if (this.CurrentPlayer.isMoving() === true) { + this.dirty = true; + this.physics.enableUpdate(); + } else { + this.physics.disableUpdate(); + } + // Let's handle all events while (this.pendingEvents.length !== 0) { + this.dirty = true; const event = this.pendingEvents.dequeue(); switch (event.type) { case "InitUserPositionEvent": @@ -1222,6 +1242,7 @@ ${escapedMessage} // Let's move all users const updatedPlayersPositions = this.playersPositionInterpolator.getUpdatedPositions(time); updatedPlayersPositions.forEach((moveEvent: HasMovedEvent, userId: number) => { + this.dirty = true; const player: RemotePlayer | undefined = this.MapPlayersByKey.get(userId); if (player === undefined) { throw new Error('Cannot find player with ID "' + userId + '"'); @@ -1491,4 +1512,8 @@ ${escapedMessage} message: 'If you want more information, you may contact us at: workadventure@thecodingmachine.com' }); } + + public isDirty(): boolean { + return this.dirty; + } } diff --git a/front/src/Phaser/Menu/MenuScene.ts b/front/src/Phaser/Menu/MenuScene.ts index 187f98c1..c5611139 100644 --- a/front/src/Phaser/Menu/MenuScene.ts +++ b/front/src/Phaser/Menu/MenuScene.ts @@ -349,4 +349,8 @@ export class MenuScene extends Phaser.Scene { } } } + + public isDirty(): boolean { + return false; + } } diff --git a/front/src/Phaser/Player/Player.ts b/front/src/Phaser/Player/Player.ts index 438f1228..3db7f051 100644 --- a/front/src/Phaser/Player/Player.ts +++ b/front/src/Phaser/Player/Player.ts @@ -7,6 +7,7 @@ export const hasMovedEventName = "hasMoved"; export interface CurrentGamerInterface extends Character{ moveUser(delta: number) : void; say(text : string) : void; + isMoving(): boolean; } export class Player extends Character implements CurrentGamerInterface { @@ -83,4 +84,8 @@ export class Player extends Character implements CurrentGamerInterface { } this.wasMoving = moving; } + + public isMoving(): boolean { + return this.wasMoving; + } } diff --git a/front/src/index.ts b/front/src/index.ts index 89582cd4..de6aeb1a 100644 --- a/front/src/index.ts +++ b/front/src/index.ts @@ -18,6 +18,7 @@ import {localUserStore} from "./Connexion/LocalUserStore"; import {ErrorScene} from "./Phaser/Reconnecting/ErrorScene"; import {iframeListener} from "./Api/IframeListener"; import {discussionManager} from "./WebRtc/DiscussionManager"; +import {Game} from "./Phaser/Game/Game"; const {width, height} = coWebsiteManager.getGameSize(); @@ -103,7 +104,8 @@ const config: GameConfig = { } }; -const game = new Phaser.Game(config); +//const game = new Phaser.Game(config); +const game = new Game(config); window.addEventListener('resize', function (event) { coWebsiteManager.resetStyle();