From 15dd74f45e2a078532931f4cf9fa453f6cbef110 Mon Sep 17 00:00:00 2001 From: Joe Weaver Date: Wed, 21 Oct 2020 06:38:03 -0400 Subject: [PATCH] fixed fps tracking in game loop --- src/BoidDemo.ts | 2 +- src/DataTypes/RegionQuadTree.ts | 40 +++++++----- src/Loop/GameLoop.ts | 107 +++++++++++++++++++++----------- src/MainScene.ts | 2 +- src/SecondScene.ts | 2 +- 5 files changed, 97 insertions(+), 56 deletions(-) diff --git a/src/BoidDemo.ts b/src/BoidDemo.ts index c968853..57e116a 100644 --- a/src/BoidDemo.ts +++ b/src/BoidDemo.ts @@ -32,7 +32,7 @@ export default class BoidDemo extends Scene { this.viewport.enableZoom(); // Create a bunch of boids - for(let i = 0; i < 100; i++){ + for(let i = 0; i < 200; i++){ let boid = this.add.graphic(Boid, layer, new Vec2(this.worldSize.x*Math.random(), this.worldSize.y*Math.random())); boid.fb = new FlockBehavior(this, boid, this.boids, 75, 50); boid.setSize(5, 5); diff --git a/src/DataTypes/RegionQuadTree.ts b/src/DataTypes/RegionQuadTree.ts index 205317a..711bcf8 100644 --- a/src/DataTypes/RegionQuadTree.ts +++ b/src/DataTypes/RegionQuadTree.ts @@ -45,12 +45,25 @@ export default class QuadTree implements Collection { this.capacity = capacity ? capacity : 10; // If we're at the bottom of the tree, don't set a max size - if(this.maxDepth === 0){ + if(this.maxDepth === 1){ this.capacity = Infinity; } this.divided = false; this.items = new Array(); + + // Create all of the children for this quadtree if there are any + if(this.maxDepth > 1){ + let x = this.boundary.x; + let y = this.boundary.y; + let hw = this.boundary.hw; + let hh = this.boundary.hh; + + this.nw = new QuadTree(new Vec2(x-hw/2, y-hh/2), new Vec2(hw/2, hh/2), this.maxDepth - 1, this.capacity); + this.ne = new QuadTree(new Vec2(x+hw/2, y-hh/2), new Vec2(hw/2, hh/2), this.maxDepth - 1, this.capacity); + this.sw = new QuadTree(new Vec2(x-hw/2, y+hh/2), new Vec2(hw/2, hh/2), this.maxDepth - 1, this.capacity); + this.se = new QuadTree(new Vec2(x+hw/2, y+hh/2), new Vec2(hw/2, hh/2), this.maxDepth - 1, this.capacity); + } } /** @@ -71,7 +84,6 @@ export default class QuadTree implements Collection { // We aren't divided, but are at capacity - divide this.subdivide(); this.deferInsert(item); - this.divided = true; } } } @@ -173,16 +185,7 @@ export default class QuadTree implements Collection { * Divides this quadtree up into 4 smaller ones - called through insert. */ protected subdivide(): void { - let x = this.boundary.x; - let y = this.boundary.y; - let hw = this.boundary.hw; - let hh = this.boundary.hh; - - this.nw = new QuadTree(new Vec2(x-hw/2, y-hh/2), new Vec2(hw/2, hh/2), this.maxDepth - 1, this.capacity); - this.ne = new QuadTree(new Vec2(x+hw/2, y-hh/2), new Vec2(hw/2, hh/2), this.maxDepth - 1, this.capacity); - this.sw = new QuadTree(new Vec2(x-hw/2, y+hh/2), new Vec2(hw/2, hh/2), this.maxDepth - 1, this.capacity); - this.se = new QuadTree(new Vec2(x+hw/2, y+hh/2), new Vec2(hw/2, hh/2), this.maxDepth - 1, this.capacity); - + this.divided = true; this.distributeItems(); } @@ -239,11 +242,16 @@ export default class QuadTree implements Collection { } } + /** + * Clear the items in this quadtree + */ clear(): void { - delete this.nw; - delete this.ne; - delete this.sw; - delete this.se; + if(this.nw){ + this.nw.clear(); + this.ne.clear(); + this.sw.clear(); + this.se.clear(); + } for(let item in this.items){ delete this.items[item]; diff --git a/src/Loop/GameLoop.ts b/src/Loop/GameLoop.ts index 8a3922d..06c415e 100644 --- a/src/Loop/GameLoop.ts +++ b/src/Loop/GameLoop.ts @@ -9,26 +9,46 @@ import SceneManager from "../Scene/SceneManager"; import AudioManager from "../Sound/AudioManager"; export default class GameLoop { - // The amount of time to spend on a physics step - private maxFPS: number; + /** The max allowed update fps.*/ + private maxUpdateFPS: number; + + /** The timestep for each update. This is the deltaT passed to update calls. */ private simulationTimestep: number; - // The time when the last frame was drawn - private lastFrameTime: number; + /** The amount of time we are yet to simulate. */ + private frameDelta: number; - // The current frame of the game + /** The time when the last frame was drawn. */ + private lastFrameTime: number; + + /** The minimum time we want to wait between game frames. */ + private minFrameDelay: number; + + /** The current frame of the game. */ private frame: number; - // Keeping track of the fps - private runningFrameSum: number; - private numFramesInSum: number; - private maxFramesInSum: number; - private fps: number; + /** The actual fps of the game. */ + private fps: number; + + /** The time between fps measurement updates. */ + private fpsUpdateInterval: number; - private started: boolean; - private running: boolean; - private frameDelta: number; + /** The time of the last fps update. */ + private lastFpsUpdate: number; + + /** The number of frames since the last fps update was done. */ + private framesSinceLastFpsUpdate: number; + + /** The status of whether or not the game loop has started. */ + private started: boolean; + + /** The status of whether or not the game loop is currently running. */ + private running: boolean; + + /** The panic state of the game. True if we have too many update frames in a single render. */ private panic: boolean; + + /** The number of update steps this iteration of the game loop. */ private numUpdateSteps: number; // Game canvas and its width and height @@ -51,16 +71,23 @@ export default class GameLoop { // Typecast the config object to a GameConfig object let gameConfig = config ? config : new GameConfig(); - this.maxFPS = 60; - this.simulationTimestep = Math.floor(1000/this.maxFPS); + this.maxUpdateFPS = 60; + this.simulationTimestep = Math.floor(1000/this.maxUpdateFPS); + this.frameDelta = 0; + this.lastFrameTime = 0; + this.minFrameDelay = 0; this.frame = 0; - this.runningFrameSum = 0; - this.numFramesInSum = 0; - this.maxFramesInSum = 30; - this.fps = this.maxFPS; - + this.fps = this.maxUpdateFPS; // Initialize the fps to the max allowed fps + this.fpsUpdateInterval = 1000; + this.lastFpsUpdate = 0; + this.framesSinceLastFpsUpdate = 0; this.started = false; this.running = false; + this.panic = false; + this.numUpdateSteps = 0; + + // Set the max fps to 60fps + // this.setMaxFPS(60); // Get the game canvas and give it a background color this.GAME_CANVAS = document.getElementById("game-canvas"); @@ -103,9 +130,13 @@ export default class GameLoop { * Changes the maximum allowed physics framerate of the game * @param initMax */ - setMaxFPS(initMax: number): void { - this.maxFPS = initMax; - this.simulationTimestep = Math.floor(1000/this.maxFPS); + setMaxUpdateFPS(initMax: number): void { + this.maxUpdateFPS = initMax; + this.simulationTimestep = Math.floor(1000/this.maxUpdateFPS); + } + + setMaxFPS(maxFPS: number): void { + this.minFrameDelay = 1000/maxFPS; } getSceneManager(): SceneManager { @@ -116,15 +147,10 @@ export default class GameLoop { * Updates the frame count and sum of time for the framerate of the game * @param timestep */ - private updateFrameCount(timestep: number): void { - this.frame += 1; - this.numFramesInSum += 1; - this.runningFrameSum += timestep; - if(this.numFramesInSum >= this.maxFramesInSum){ - this.fps = 1000 * this.numFramesInSum / this.runningFrameSum; - this.numFramesInSum = 0; - this.runningFrameSum = 0; - } + private updateFPS(timestamp: number): void { + this.fps = 0.9 * this.framesSinceLastFpsUpdate * 1000 / (timestamp - this.lastFpsUpdate) +(1 - 0.9) * this.fps; + this.lastFpsUpdate = timestamp; + this.framesSinceLastFpsUpdate = 0; Debug.log("fps", "FPS: " + this.fps.toFixed(1)); } @@ -150,6 +176,8 @@ export default class GameLoop { this.render(); this.lastFrameTime = timestamp; + this.lastFpsUpdate = timestamp; + this.framesSinceLastFpsUpdate = 0; window.requestAnimationFrame(this.doFrame); } @@ -163,14 +191,22 @@ export default class GameLoop { window.requestAnimationFrame(this.doFrame); // If we are trying to update too soon, return and do nothing - if(timestamp < this.lastFrameTime + this.simulationTimestep){ + if(timestamp < this.lastFrameTime + this.minFrameDelay){ return } // Currently, update and draw are synced - eventually it would probably be good to desync these - this.frameDelta = timestamp - this.lastFrameTime; + this.frameDelta += timestamp - this.lastFrameTime; this.lastFrameTime = timestamp; + // Update the estimate of the framerate + if(timestamp > this.lastFpsUpdate + this.fpsUpdateInterval){ + this.updateFPS(timestamp); + } + + this.frame++; + this.framesSinceLastFpsUpdate++; + // Update while we can (This will present problems if we leave the window) this.numUpdateSteps = 0; while(this.frameDelta >= this.simulationTimestep){ @@ -181,9 +217,6 @@ export default class GameLoop { if(this.numUpdateSteps > 100){ this.panic = true; } - - // Update the frame of the game - this.updateFrameCount(this.simulationTimestep); } // Updates are done, draw diff --git a/src/MainScene.ts b/src/MainScene.ts index 15acf0c..88d68d5 100644 --- a/src/MainScene.ts +++ b/src/MainScene.ts @@ -95,7 +95,7 @@ export default class MainScene extends Scene { let i = 0; let fps = [15, 30, 60]; cycleFramerateButton.onClick = () => { - this.game.setMaxFPS(fps[i]); + this.game.setMaxUpdateFPS(fps[i]); i = (i + 1) % 3; } diff --git a/src/SecondScene.ts b/src/SecondScene.ts index 20f7439..601e09f 100644 --- a/src/SecondScene.ts +++ b/src/SecondScene.ts @@ -72,7 +72,7 @@ export default class SecondScene extends Scene { let i = 0; let fps = [15, 30, 60]; cycleFramerateButton.onClick = () => { - this.game.setMaxFPS(fps[i]); + this.game.setMaxUpdateFPS(fps[i]); i = (i + 1) % 3; }