235 lines
7.1 KiB
TypeScript
235 lines
7.1 KiB
TypeScript
import GameLoop from "./GameLoop";
|
|
import Debug from "../Debug/Debug";
|
|
import Stats from "../Debug/Stats";
|
|
|
|
/**
|
|
* A game loop with a fixed update time and a variable render time.
|
|
* Every frame, the game updates until all time since the last frame has been processed.
|
|
* If too much time has passed, such as if the last update was too slow,
|
|
* or if the browser was put into the background, the loop will panic and discard time.
|
|
* A render happens at the end of every frame. This happens as fast as possible unless specified.
|
|
* A loop of this type allows for deterministic behavior - No matter what the frame rate is, the update should behave the same,
|
|
* as it is occuring in a fixed interval.
|
|
*/
|
|
export default class FixedUpdateGameLoop extends GameLoop {
|
|
|
|
/** The max allowed update fps.*/
|
|
private maxUpdateFPS: number;
|
|
|
|
/** The timestep for each update. This is the deltaT passed to update calls. */
|
|
private updateTimestep: number;
|
|
|
|
/** The amount of time we are yet to simulate. */
|
|
private frameDelta: number;
|
|
|
|
/** 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;
|
|
|
|
/** The actual fps of the game. */
|
|
private fps: number;
|
|
|
|
/** The time between fps measurement updates. */
|
|
private fpsUpdateInterval: 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 paused */
|
|
private paused: boolean;
|
|
|
|
/** The status of whether or not the game loop is currently running. */
|
|
private running: boolean;
|
|
|
|
/** The number of update steps this iteration of the game loop. */
|
|
private numUpdateSteps: number;
|
|
|
|
constructor() {
|
|
super();
|
|
this.maxUpdateFPS = 60;
|
|
this.updateTimestep = Math.floor(1000/this.maxUpdateFPS);
|
|
this.frameDelta = 0;
|
|
this.lastFrameTime = 0;
|
|
this.minFrameDelay = 0;
|
|
this.frame = 0;
|
|
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.paused = false;
|
|
this.running = false;
|
|
this.numUpdateSteps = 0;
|
|
}
|
|
|
|
getFPS(): number {
|
|
return 0;
|
|
}
|
|
|
|
/**
|
|
* Updates the frame count and sum of time for the framerate of the game
|
|
* @param timestep The current time in ms
|
|
*/
|
|
protected 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));
|
|
Stats.updateFPS(this.fps);
|
|
}
|
|
|
|
/**
|
|
* Changes the maximum allowed physics framerate of the game
|
|
* @param initMax The max framerate
|
|
*/
|
|
setMaxUpdateFPS(initMax: number): void {
|
|
this.maxUpdateFPS = initMax;
|
|
this.updateTimestep = Math.floor(1000/this.maxUpdateFPS);
|
|
}
|
|
|
|
/**
|
|
* Sets the maximum rendering framerate
|
|
* @param maxFPS The max framerate
|
|
*/
|
|
setMaxFPS(maxFPS: number): void {
|
|
this.minFrameDelay = 1000/maxFPS;
|
|
}
|
|
|
|
/**
|
|
* This function is called when the game loop panics, i.e. it tries to process too much time in an entire frame.
|
|
* This will reset the amount of time back to zero.
|
|
* @returns The amount of time we are discarding from processing.
|
|
*/
|
|
resetFrameDelta() : number {
|
|
let oldFrameDelta = this.frameDelta;
|
|
this.frameDelta = 0;
|
|
return oldFrameDelta;
|
|
}
|
|
|
|
/**
|
|
* Starts up the game loop and calls the first requestAnimationFrame
|
|
*/
|
|
start(): void {
|
|
if(!this.started){
|
|
this.started = true;
|
|
|
|
window.requestAnimationFrame((timestamp) => this.doFirstFrame(timestamp));
|
|
}
|
|
}
|
|
|
|
pause(): void {
|
|
this.paused = true;
|
|
}
|
|
|
|
resume(): void {
|
|
this.paused = false;
|
|
}
|
|
|
|
/**
|
|
* The first game frame - initializes the first frame time and begins the render
|
|
* @param timestamp The current time in ms
|
|
*/
|
|
protected doFirstFrame(timestamp: number): void {
|
|
this.running = true;
|
|
|
|
this._doRender();
|
|
|
|
this.lastFrameTime = timestamp;
|
|
this.lastFpsUpdate = timestamp;
|
|
this.framesSinceLastFpsUpdate = 0;
|
|
|
|
window.requestAnimationFrame((t) => this.doFrame(t));
|
|
}
|
|
|
|
/**
|
|
* Handles any processing that needs to be done at the start of the frame
|
|
* @param timestamp The time of the frame in ms
|
|
*/
|
|
protected startFrame(timestamp: number): void {
|
|
// Update the amount of time we need our update to process
|
|
this.frameDelta += timestamp - this.lastFrameTime;
|
|
|
|
// Set the new time of the last frame
|
|
this.lastFrameTime = timestamp;
|
|
|
|
// Update the estimate of the framerate
|
|
if(timestamp > this.lastFpsUpdate + this.fpsUpdateInterval){
|
|
this.updateFPS(timestamp);
|
|
}
|
|
|
|
// Increment the number of frames
|
|
this.frame++;
|
|
this.framesSinceLastFpsUpdate++;
|
|
}
|
|
|
|
/**
|
|
* The main loop of the game. Updates until the current time is reached. Renders once
|
|
* @param timestamp The current time in ms
|
|
*/
|
|
protected doFrame = (timestamp: number): void => {
|
|
// If a pause was executed, stop doing the loop.
|
|
if(this.paused){
|
|
return;
|
|
}
|
|
|
|
// Request animation frame to prepare for another update or render
|
|
window.requestAnimationFrame((t) => this.doFrame(t));
|
|
|
|
// If we are trying to render too soon, do nothing.
|
|
if(timestamp < this.lastFrameTime + this.minFrameDelay){
|
|
return;
|
|
}
|
|
|
|
// A frame is actually happening
|
|
this.startFrame(timestamp);
|
|
|
|
// Update while there is still time to make up. If we do too many update steps, panic and exit the loop.
|
|
this.numUpdateSteps = 0;
|
|
let panic = false;
|
|
|
|
while(this.frameDelta >= this.updateTimestep){
|
|
// Do an update
|
|
this._doUpdate(this.updateTimestep/1000);
|
|
|
|
// Remove the update step time from the time we have to process
|
|
this.frameDelta -= this.updateTimestep;
|
|
|
|
// Increment steps and check if we've done too many
|
|
this.numUpdateSteps++;
|
|
if(this.numUpdateSteps > 100){
|
|
panic = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Updates are done, render
|
|
this._doRender();
|
|
|
|
// Wrap up the frame
|
|
this.finishFrame(panic);
|
|
}
|
|
|
|
/**
|
|
* Wraps up the frame and handles the panic state if there is one
|
|
* @param panic Whether or not the loop panicked
|
|
*/
|
|
protected finishFrame(panic: boolean): void {
|
|
if(panic) {
|
|
var discardedTime = Math.round(this.resetFrameDelta());
|
|
console.warn('Main loop panicked, probably because the browser tab was put in the background. Discarding ' + discardedTime + 'ms');
|
|
}
|
|
}
|
|
|
|
} |