separated Game from Gameloop
This commit is contained in:
parent
34b9a2d71d
commit
1512fa5c8f
6
src/DataTypes/Functions/NullFunc.ts
Normal file
6
src/DataTypes/Functions/NullFunc.ts
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
/**
|
||||||
|
* A placeholder function for No Operation. Does nothing
|
||||||
|
*/
|
||||||
|
const NullFunc = () => {};
|
||||||
|
|
||||||
|
export default NullFunc;
|
218
src/Loop/FixedUpdateGameLoop.ts
Normal file
218
src/Loop/FixedUpdateGameLoop.ts
Normal file
|
@ -0,0 +1,218 @@
|
||||||
|
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 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.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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 => {
|
||||||
|
// 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');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
169
src/Loop/Game.ts
Normal file
169
src/Loop/Game.ts
Normal file
|
@ -0,0 +1,169 @@
|
||||||
|
import EventQueue from "../Events/EventQueue";
|
||||||
|
import InputReceiver from "../Input/InputReceiver";
|
||||||
|
import InputHandler from "../Input/InputHandler";
|
||||||
|
import Recorder from "../Playback/Recorder";
|
||||||
|
import Debug from "../Debug/Debug";
|
||||||
|
import ResourceManager from "../ResourceManager/ResourceManager";
|
||||||
|
import Viewport from "../SceneGraph/Viewport";
|
||||||
|
import SceneManager from "../Scene/SceneManager";
|
||||||
|
import AudioManager from "../Sound/AudioManager";
|
||||||
|
import Stats from "../Debug/Stats";
|
||||||
|
import RenderingManager from "../Rendering/RenderingManager";
|
||||||
|
import CanvasRenderer from "../Rendering/CanvasRenderer";
|
||||||
|
import Color from "../Utils/Color";
|
||||||
|
import GameOptions from "./GameOptions";
|
||||||
|
import GameLoop from "./GameLoop";
|
||||||
|
import FixedUpdateGameLoop from "./FixedUpdateGameLoop";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The main loop of the game engine.
|
||||||
|
* Handles the update order, and initializes all subsystems.
|
||||||
|
* The Game manages the update cycle, and requests animation frames to render to the browser.
|
||||||
|
*/
|
||||||
|
export default class Game {
|
||||||
|
gameOptions: GameOptions;
|
||||||
|
|
||||||
|
// The game loop
|
||||||
|
private loop: GameLoop;
|
||||||
|
|
||||||
|
// Game canvas and its width and height
|
||||||
|
readonly GAME_CANVAS: HTMLCanvasElement;
|
||||||
|
readonly DEBUG_CANVAS: HTMLCanvasElement;
|
||||||
|
readonly WIDTH: number;
|
||||||
|
readonly HEIGHT: number;
|
||||||
|
private viewport: Viewport;
|
||||||
|
private ctx: CanvasRenderingContext2D;
|
||||||
|
private clearColor: Color;
|
||||||
|
|
||||||
|
// All of the necessary subsystems that need to run here
|
||||||
|
private eventQueue: EventQueue;
|
||||||
|
private inputHandler: InputHandler;
|
||||||
|
private inputReceiver: InputReceiver;
|
||||||
|
private recorder: Recorder;
|
||||||
|
private resourceManager: ResourceManager;
|
||||||
|
private sceneManager: SceneManager;
|
||||||
|
private audioManager: AudioManager;
|
||||||
|
private renderingManager: RenderingManager;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new Game
|
||||||
|
* @param options The options for Game initialization
|
||||||
|
*/
|
||||||
|
constructor(options?: Record<string, any>){
|
||||||
|
// Typecast the config object to a GameConfig object
|
||||||
|
this.gameOptions = GameOptions.parse(options);
|
||||||
|
|
||||||
|
// Create an instance of a game loop
|
||||||
|
this.loop = new FixedUpdateGameLoop();
|
||||||
|
|
||||||
|
// Get the game canvas and give it a background color
|
||||||
|
this.GAME_CANVAS = <HTMLCanvasElement>document.getElementById("game-canvas");
|
||||||
|
this.DEBUG_CANVAS = <HTMLCanvasElement>document.getElementById("debug-canvas");
|
||||||
|
|
||||||
|
// Give the canvas a size and get the rendering context
|
||||||
|
this.WIDTH = this.gameOptions.viewportSize.x;
|
||||||
|
this.HEIGHT = this.gameOptions.viewportSize.y;
|
||||||
|
|
||||||
|
// For now, just hard code a canvas renderer. We can do this with options later
|
||||||
|
this.renderingManager = new CanvasRenderer();
|
||||||
|
this.initializeGameWindow();
|
||||||
|
this.ctx = this.renderingManager.initializeCanvas(this.GAME_CANVAS, this.WIDTH, this.HEIGHT);
|
||||||
|
this.clearColor = new Color(this.gameOptions.clearColor.r, this.gameOptions.clearColor.g, this.gameOptions.clearColor.b);
|
||||||
|
|
||||||
|
// Initialize debugging and stats
|
||||||
|
Debug.initializeDebugCanvas(this.DEBUG_CANVAS, this.WIDTH, this.HEIGHT);
|
||||||
|
Stats.initStats();
|
||||||
|
|
||||||
|
// Size the viewport to the game canvas
|
||||||
|
this.viewport = new Viewport();
|
||||||
|
this.viewport.setCanvasSize(this.WIDTH, this.HEIGHT);
|
||||||
|
this.viewport.setSize(this.WIDTH, this.HEIGHT);
|
||||||
|
|
||||||
|
// Initialize all necessary game subsystems
|
||||||
|
this.eventQueue = EventQueue.getInstance();
|
||||||
|
this.inputHandler = new InputHandler(this.GAME_CANVAS);
|
||||||
|
this.inputReceiver = InputReceiver.getInstance();
|
||||||
|
this.inputReceiver.setViewport(this.viewport);
|
||||||
|
this.recorder = new Recorder();
|
||||||
|
this.resourceManager = ResourceManager.getInstance();
|
||||||
|
this.sceneManager = new SceneManager(this.viewport, this.renderingManager);
|
||||||
|
this.audioManager = AudioManager.getInstance();
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set up the game window that holds the canvases
|
||||||
|
*/
|
||||||
|
private initializeGameWindow(): void {
|
||||||
|
const gameWindow = document.getElementById("game-window");
|
||||||
|
|
||||||
|
// Set the height of the game window
|
||||||
|
gameWindow.style.width = this.WIDTH + "px";
|
||||||
|
gameWindow.style.height = this.HEIGHT + "px";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retreives the SceneManager from the Game
|
||||||
|
* @returns The SceneManager
|
||||||
|
*/
|
||||||
|
getSceneManager(): SceneManager {
|
||||||
|
return this.sceneManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Starts the game
|
||||||
|
*/
|
||||||
|
start(): void {
|
||||||
|
// Set the update function of the loop
|
||||||
|
this.loop.doUpdate = (deltaT: number) => this.update(deltaT);
|
||||||
|
|
||||||
|
// Set the render function of the loop
|
||||||
|
this.loop.doRender = () => this.render();
|
||||||
|
|
||||||
|
// Start the loop
|
||||||
|
this.loop.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates all necessary subsystems of the game. Defers scene updates to the sceneManager
|
||||||
|
* @param deltaT The time sine the last update
|
||||||
|
*/
|
||||||
|
update(deltaT: number): void {
|
||||||
|
// Handle all events that happened since the start of the last loop
|
||||||
|
this.eventQueue.update(deltaT);
|
||||||
|
|
||||||
|
// Update the input data structures so game objects can see the input
|
||||||
|
this.inputReceiver.update(deltaT);
|
||||||
|
|
||||||
|
// Update the recording of the game
|
||||||
|
this.recorder.update(deltaT);
|
||||||
|
|
||||||
|
// Update all scenes
|
||||||
|
this.sceneManager.update(deltaT);
|
||||||
|
|
||||||
|
// Update all sounds
|
||||||
|
this.audioManager.update(deltaT);
|
||||||
|
|
||||||
|
// Load or unload any resources if needed
|
||||||
|
this.resourceManager.update(deltaT);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clears the canvas and defers scene rendering to the sceneManager. Renders the debug canvas
|
||||||
|
*/
|
||||||
|
render(): void {
|
||||||
|
// Clear the canvases
|
||||||
|
this.ctx.clearRect(0, 0, this.WIDTH, this.HEIGHT);
|
||||||
|
Debug.clearCanvas();
|
||||||
|
|
||||||
|
// Game Canvas
|
||||||
|
this.ctx.fillStyle = this.clearColor.toString();
|
||||||
|
this.ctx.fillRect(0, 0, this.WIDTH, this.HEIGHT);
|
||||||
|
this.sceneManager.render();
|
||||||
|
|
||||||
|
// Debug render
|
||||||
|
Debug.render();
|
||||||
|
Stats.render();
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,327 +1,59 @@
|
||||||
import EventQueue from "../Events/EventQueue";
|
import NullFunc from "../DataTypes/Functions/NullFunc";
|
||||||
import InputReceiver from "../Input/InputReceiver";
|
|
||||||
import InputHandler from "../Input/InputHandler";
|
|
||||||
import Recorder from "../Playback/Recorder";
|
|
||||||
import Debug from "../Debug/Debug";
|
|
||||||
import ResourceManager from "../ResourceManager/ResourceManager";
|
|
||||||
import Viewport from "../SceneGraph/Viewport";
|
|
||||||
import SceneManager from "../Scene/SceneManager";
|
|
||||||
import AudioManager from "../Sound/AudioManager";
|
|
||||||
import Stats from "../Debug/Stats";
|
|
||||||
import RenderingManager from "../Rendering/RenderingManager";
|
|
||||||
import CanvasRenderer from "../Rendering/CanvasRenderer";
|
|
||||||
import Color from "../Utils/Color";
|
|
||||||
import GameOptions from "./GameOptions";
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The main loop of the game engine.
|
* The main game loop of the game. Keeps track of fps and handles scheduling of updates and rendering.
|
||||||
* Handles the update order, and initializes all subsystems.
|
* This class is left abstract, so that a subclass can handle exactly how the loop is scheduled.
|
||||||
* The GameLoop manages the update cycle, and requests animation frames to render to the browser.
|
* For an example of different types of game loop scheduling, check out @link(Game Programming Patterns)(https://gameprogrammingpatterns.com/game-loop.html)
|
||||||
*/
|
*/
|
||||||
export default class GameLoop {
|
export default abstract class GameLoop {
|
||||||
gameOptions: GameOptions;
|
|
||||||
|
|
||||||
/** The max allowed update fps.*/
|
/** The function to call when an update occurs */
|
||||||
private maxUpdateFPS: number;
|
protected _doUpdate: Function = NullFunc;
|
||||||
|
|
||||||
/** The timestep for each update. This is the deltaT passed to update calls. */
|
set doUpdate(update: Function){
|
||||||
private simulationTimestep: number;
|
this._doUpdate = update;
|
||||||
|
}
|
||||||
|
|
||||||
/** The amount of time we are yet to simulate. */
|
/** The function to call when a render occurs */
|
||||||
private frameDelta: number;
|
protected _doRender: Function = NullFunc;
|
||||||
|
|
||||||
/** The time when the last frame was drawn. */
|
|
||||||
private lastFrameTime: number;
|
|
||||||
|
|
||||||
/** The minimum time we want to wait between game frames. */
|
set doRender(render: Function){
|
||||||
private minFrameDelay: number;
|
this._doRender = render;
|
||||||
|
|
||||||
/** 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 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
|
|
||||||
readonly GAME_CANVAS: HTMLCanvasElement;
|
|
||||||
readonly DEBUG_CANVAS: HTMLCanvasElement;
|
|
||||||
readonly WIDTH: number;
|
|
||||||
readonly HEIGHT: number;
|
|
||||||
private viewport: Viewport;
|
|
||||||
private ctx: CanvasRenderingContext2D;
|
|
||||||
private clearColor: Color;
|
|
||||||
|
|
||||||
// All of the necessary subsystems that need to run here
|
|
||||||
private eventQueue: EventQueue;
|
|
||||||
private inputHandler: InputHandler;
|
|
||||||
private inputReceiver: InputReceiver;
|
|
||||||
private recorder: Recorder;
|
|
||||||
private resourceManager: ResourceManager;
|
|
||||||
private sceneManager: SceneManager;
|
|
||||||
private audioManager: AudioManager;
|
|
||||||
private renderingManager: RenderingManager;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates a new GameLoop
|
|
||||||
* @param options The options for GameLoop initialization
|
|
||||||
*/
|
|
||||||
constructor(options?: Record<string, any>){
|
|
||||||
// Typecast the config object to a GameConfig object
|
|
||||||
this.gameOptions = GameOptions.parse(options);
|
|
||||||
|
|
||||||
this.maxUpdateFPS = 60;
|
|
||||||
this.simulationTimestep = 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.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 = <HTMLCanvasElement>document.getElementById("game-canvas");
|
|
||||||
this.DEBUG_CANVAS = <HTMLCanvasElement>document.getElementById("debug-canvas");
|
|
||||||
|
|
||||||
// Give the canvas a size and get the rendering context
|
|
||||||
this.WIDTH = this.gameOptions.viewportSize.x;
|
|
||||||
this.HEIGHT = this.gameOptions.viewportSize.y;
|
|
||||||
|
|
||||||
// For now, just hard code a canvas renderer. We can do this with options later
|
|
||||||
this.renderingManager = new CanvasRenderer();
|
|
||||||
this.initializeGameWindow();
|
|
||||||
this.ctx = this.renderingManager.initializeCanvas(this.GAME_CANVAS, this.WIDTH, this.HEIGHT);
|
|
||||||
this.clearColor = new Color(this.gameOptions.clearColor.r, this.gameOptions.clearColor.g, this.gameOptions.clearColor.b);
|
|
||||||
|
|
||||||
// Initialize debug canvas
|
|
||||||
|
|
||||||
Debug.initializeDebugCanvas(this.DEBUG_CANVAS, this.WIDTH, this.HEIGHT);
|
|
||||||
|
|
||||||
// Size the viewport to the game canvas
|
|
||||||
this.viewport = new Viewport();
|
|
||||||
this.viewport.setCanvasSize(this.WIDTH, this.HEIGHT);
|
|
||||||
this.viewport.setSize(this.WIDTH, this.HEIGHT);
|
|
||||||
|
|
||||||
// Initialize all necessary game subsystems
|
|
||||||
this.eventQueue = EventQueue.getInstance();
|
|
||||||
this.inputHandler = new InputHandler(this.GAME_CANVAS);
|
|
||||||
this.inputReceiver = InputReceiver.getInstance();
|
|
||||||
this.inputReceiver.setViewport(this.viewport);
|
|
||||||
this.recorder = new Recorder();
|
|
||||||
this.resourceManager = ResourceManager.getInstance();
|
|
||||||
this.sceneManager = new SceneManager(this.viewport, this, this.renderingManager);
|
|
||||||
this.audioManager = AudioManager.getInstance();
|
|
||||||
|
|
||||||
Stats.initStats();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set up the game window that holds the canvases
|
* Retrieves the current FPS of the game
|
||||||
*/
|
*/
|
||||||
private initializeGameWindow(): void {
|
abstract getFPS(): number;
|
||||||
const gameWindow = document.getElementById("game-window");
|
|
||||||
|
|
||||||
// Set the height of the game window
|
|
||||||
gameWindow.style.width = this.WIDTH + "px";
|
|
||||||
gameWindow.style.height = this.HEIGHT + "px";
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Changes the maximum allowed physics framerate of the game
|
* Starts up the game loop
|
||||||
* @param initMax The max framerate
|
|
||||||
*/
|
*/
|
||||||
setMaxUpdateFPS(initMax: number): void {
|
abstract start(): void;
|
||||||
this.maxUpdateFPS = initMax;
|
|
||||||
this.simulationTimestep = Math.floor(1000/this.maxUpdateFPS);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets the maximum rendering framerate
|
* Runs the first frame of the game. No update occurs here, only a render.
|
||||||
* @param maxFPS The max framerate
|
* This is needed to initialize delta time values
|
||||||
|
* @param timestamp The timestamp of the frame. This is received from the browser
|
||||||
*/
|
*/
|
||||||
setMaxFPS(maxFPS: number): void {
|
protected abstract doFirstFrame(timestamp: number): void;
|
||||||
this.minFrameDelay = 1000/maxFPS;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retreives the SceneManager from the GameLoop
|
* Run before any updates or the render of a frame.
|
||||||
* @returns The SceneManager
|
* @param timestamp The timestamp of the frame. This is received from the browser
|
||||||
*/
|
*/
|
||||||
getSceneManager(): SceneManager {
|
protected abstract startFrame(timestamp: number): void;
|
||||||
return this.sceneManager;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Updates the frame count and sum of time for the framerate of the game
|
* The core of the frame, where any necessary updates occur, and where a render happens
|
||||||
* @param timestep The current time in ms
|
* @param timestamp The timestamp of the frame. This is received from the browser
|
||||||
*/
|
*/
|
||||||
private updateFPS(timestamp: number): void {
|
protected abstract doFrame(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);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Starts up the game loop and calls the first requestAnimationFrame
|
* Wraps up the frame
|
||||||
|
* @param panic Whether or not the update cycle panicked. This happens when too many updates try to happen in a single frame
|
||||||
*/
|
*/
|
||||||
start(): void {
|
protected abstract finishFrame(panic: boolean): void;
|
||||||
if(!this.started){
|
|
||||||
this.started = true;
|
|
||||||
|
|
||||||
window.requestAnimationFrame(this.startFrame);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The first game frame - initializes the first frame time and begins the render
|
|
||||||
* @param timestamp The current time in ms
|
|
||||||
*/
|
|
||||||
startFrame(timestamp: number): void {
|
|
||||||
this.running = true;
|
|
||||||
|
|
||||||
this.render();
|
|
||||||
|
|
||||||
this.lastFrameTime = timestamp;
|
|
||||||
this.lastFpsUpdate = timestamp;
|
|
||||||
this.framesSinceLastFpsUpdate = 0;
|
|
||||||
|
|
||||||
window.requestAnimationFrame(this.doFrame);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The main loop of the game. Updates and renders every frame
|
|
||||||
* @param timestamp
|
|
||||||
*/
|
|
||||||
doFrame(timestamp: number): void {
|
|
||||||
// Request animation frame to prepare for another update or render
|
|
||||||
window.requestAnimationFrame(this.doFrame);
|
|
||||||
|
|
||||||
// If we are trying to update too soon, return and do nothing
|
|
||||||
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.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){
|
|
||||||
this.update(this.simulationTimestep/1000);
|
|
||||||
this.frameDelta -= this.simulationTimestep;
|
|
||||||
|
|
||||||
this.numUpdateSteps++;
|
|
||||||
if(this.numUpdateSteps > 100){
|
|
||||||
this.panic = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Updates are done, draw
|
|
||||||
this.render();
|
|
||||||
|
|
||||||
// End the frame
|
|
||||||
this.end();
|
|
||||||
|
|
||||||
this.panic = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Ends the game loop
|
|
||||||
*/
|
|
||||||
end(){
|
|
||||||
if(this.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');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
resetFrameDelta() : number {
|
|
||||||
var oldFrameDelta = this.frameDelta;
|
|
||||||
this.frameDelta = 0;
|
|
||||||
return oldFrameDelta;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Updates all necessary subsystems of the game. Defers scene updates to the sceneManager
|
|
||||||
* @param deltaT The time sine the last update
|
|
||||||
*/
|
|
||||||
update(deltaT: number): void {
|
|
||||||
// Handle all events that happened since the start of the last loop
|
|
||||||
this.eventQueue.update(deltaT);
|
|
||||||
|
|
||||||
// Update the input data structures so game objects can see the input
|
|
||||||
this.inputReceiver.update(deltaT);
|
|
||||||
|
|
||||||
// Update the recording of the game
|
|
||||||
this.recorder.update(deltaT);
|
|
||||||
|
|
||||||
// Update all scenes
|
|
||||||
this.sceneManager.update(deltaT);
|
|
||||||
|
|
||||||
// Update all sounds
|
|
||||||
this.audioManager.update(deltaT);
|
|
||||||
|
|
||||||
// Load or unload any resources if needed
|
|
||||||
this.resourceManager.update(deltaT);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Clears the canvas and defers scene rendering to the sceneManager. Renders the debug canvas
|
|
||||||
*/
|
|
||||||
render(): void {
|
|
||||||
// Clear the canvases
|
|
||||||
this.ctx.clearRect(0, 0, this.WIDTH, this.HEIGHT);
|
|
||||||
Debug.clearCanvas();
|
|
||||||
|
|
||||||
// Game Canvas
|
|
||||||
this.ctx.fillStyle = this.clearColor.toString();
|
|
||||||
this.ctx.fillRect(0, 0, this.WIDTH, this.HEIGHT);
|
|
||||||
this.sceneManager.render();
|
|
||||||
|
|
||||||
// Debug render
|
|
||||||
Debug.render();
|
|
||||||
Stats.render();
|
|
||||||
}
|
|
||||||
}
|
}
|
|
@ -35,7 +35,7 @@ export default class ResourceManager {
|
||||||
private loadonly_imagesToLoad: number;
|
private loadonly_imagesToLoad: number;
|
||||||
/** The queue of images we must load */
|
/** The queue of images we must load */
|
||||||
private loadonly_imageLoadingQueue: Queue<KeyPathPair>;
|
private loadonly_imageLoadingQueue: Queue<KeyPathPair>;
|
||||||
/** A map of the images that are currently loaded and (presumably) being used by the scene */
|
/** A map of the images that are currently loaded and being used by the scene. The reference to these images only exist here for easy cleanup. */
|
||||||
private images: Map<HTMLImageElement>;
|
private images: Map<HTMLImageElement>;
|
||||||
|
|
||||||
/** Number to keep track of how many tilemaps need to be loaded */
|
/** Number to keep track of how many tilemaps need to be loaded */
|
||||||
|
|
|
@ -8,7 +8,7 @@ import SceneGraphArray from "../SceneGraph/SceneGraphArray";
|
||||||
import FactoryManager from "./Factories/FactoryManager";
|
import FactoryManager from "./Factories/FactoryManager";
|
||||||
import Tilemap from "../Nodes/Tilemap";
|
import Tilemap from "../Nodes/Tilemap";
|
||||||
import ResourceManager from "../ResourceManager/ResourceManager";
|
import ResourceManager from "../ResourceManager/ResourceManager";
|
||||||
import GameLoop from "../Loop/GameLoop";
|
import Game from "../Loop/Game";
|
||||||
import SceneManager from "./SceneManager";
|
import SceneManager from "./SceneManager";
|
||||||
import Receiver from "../Events/Receiver";
|
import Receiver from "../Events/Receiver";
|
||||||
import Emitter from "../Events/Emitter";
|
import Emitter from "../Events/Emitter";
|
||||||
|
@ -40,9 +40,6 @@ export default class Scene implements Updateable {
|
||||||
/** A flag that represents whether this scene is running or not. */
|
/** A flag that represents whether this scene is running or not. */
|
||||||
protected running: boolean;
|
protected running: boolean;
|
||||||
|
|
||||||
/** The overall game loop. */
|
|
||||||
protected game: GameLoop;
|
|
||||||
|
|
||||||
/** The manager of this scene. */
|
/** The manager of this scene. */
|
||||||
protected sceneManager: SceneManager;
|
protected sceneManager: SceneManager;
|
||||||
|
|
||||||
|
@ -93,17 +90,16 @@ export default class Scene implements Updateable {
|
||||||
* @param viewport The viewport of the game
|
* @param viewport The viewport of the game
|
||||||
* @param sceneManager The SceneManager that owns this Scene
|
* @param sceneManager The SceneManager that owns this Scene
|
||||||
* @param renderingManager The RenderingManager that will handle this Scene's rendering
|
* @param renderingManager The RenderingManager that will handle this Scene's rendering
|
||||||
* @param game The instance of the GameLoop
|
* @param game The instance of the Game
|
||||||
* @param options The options for Scene initialization
|
* @param options The options for Scene initialization
|
||||||
*/
|
*/
|
||||||
constructor(viewport: Viewport, sceneManager: SceneManager, renderingManager: RenderingManager, game: GameLoop, options: Record<string, any>){
|
constructor(viewport: Viewport, sceneManager: SceneManager, renderingManager: RenderingManager, options: Record<string, any>){
|
||||||
this.sceneOptions = SceneOptions.parse(options);
|
this.sceneOptions = SceneOptions.parse(options);
|
||||||
|
|
||||||
this.worldSize = new Vec2(500, 500);
|
this.worldSize = new Vec2(500, 500);
|
||||||
this.viewport = viewport;
|
this.viewport = viewport;
|
||||||
this.viewport.setBounds(0, 0, 2560, 1280);
|
this.viewport.setBounds(0, 0, 2560, 1280);
|
||||||
this.running = false;
|
this.running = false;
|
||||||
this.game = game;
|
|
||||||
this.sceneManager = sceneManager;
|
this.sceneManager = sceneManager;
|
||||||
this.receiver = new Receiver();
|
this.receiver = new Receiver();
|
||||||
this.emitter = new Emitter();
|
this.emitter = new Emitter();
|
||||||
|
|
|
@ -1,38 +1,38 @@
|
||||||
import Scene from "./Scene";
|
import Scene from "./Scene";
|
||||||
import ResourceManager from "../ResourceManager/ResourceManager";
|
import ResourceManager from "../ResourceManager/ResourceManager";
|
||||||
import Viewport from "../SceneGraph/Viewport";
|
import Viewport from "../SceneGraph/Viewport";
|
||||||
import GameLoop from "../Loop/GameLoop";
|
import Game from "../Loop/Game";
|
||||||
import RenderingManager from "../Rendering/RenderingManager";
|
import RenderingManager from "../Rendering/RenderingManager";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The SceneManager of the game engine. There is only one of theses.
|
|
||||||
* The SceneManager acts as an interface to create Scenes, and handles the lifecycle methods of Scenes.
|
* The SceneManager acts as an interface to create Scenes, and handles the lifecycle methods of Scenes.
|
||||||
* The Scene manager keeps track of systems that are constant across scene, such as the @reference[ResourceManager]
|
* It gives Scenes access to information they need from the @reference[Game] class while keeping a layer of separation.
|
||||||
*/
|
*/
|
||||||
export default class SceneManager {
|
export default class SceneManager {
|
||||||
/** The current Scene of the game */
|
/** The current Scene of the game */
|
||||||
protected currentScene: Scene;
|
protected currentScene: Scene;
|
||||||
|
|
||||||
/** The Viewport of the game */
|
/** The Viewport of the game */
|
||||||
protected viewport: Viewport;
|
protected viewport: Viewport;
|
||||||
|
|
||||||
/** A reference to the ResourceManager */
|
/** A reference to the ResourceManager */
|
||||||
protected resourceManager: ResourceManager;
|
protected resourceManager: ResourceManager;
|
||||||
/** The GameLoop this SceneManager belongs to */
|
|
||||||
protected game: GameLoop;
|
|
||||||
/** A counter to keep track of game ids */
|
/** A counter to keep track of game ids */
|
||||||
protected idCounter: number;
|
protected idCounter: number;
|
||||||
|
|
||||||
/** The RenderingManager of the game */
|
/** The RenderingManager of the game */
|
||||||
protected renderingManager: RenderingManager;
|
protected renderingManager: RenderingManager;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a new SceneManager
|
* Creates a new SceneManager
|
||||||
* @param viewport The Viewport of the game
|
* @param viewport The Viewport of the game
|
||||||
* @param game The GameLoop instance
|
* @param game The Game instance
|
||||||
* @param renderingManager The RenderingManager of the game
|
* @param renderingManager The RenderingManager of the game
|
||||||
*/
|
*/
|
||||||
constructor(viewport: Viewport, game: GameLoop, renderingManager: RenderingManager){
|
constructor(viewport: Viewport, renderingManager: RenderingManager){
|
||||||
this.resourceManager = ResourceManager.getInstance();
|
this.resourceManager = ResourceManager.getInstance();
|
||||||
this.viewport = viewport;
|
this.viewport = viewport;
|
||||||
this.game = game;
|
|
||||||
this.renderingManager = renderingManager;
|
this.renderingManager = renderingManager;
|
||||||
this.idCounter = 0;
|
this.idCounter = 0;
|
||||||
}
|
}
|
||||||
|
@ -43,7 +43,7 @@ export default class SceneManager {
|
||||||
* @param constr The constructor of the scene to add
|
* @param constr The constructor of the scene to add
|
||||||
*/
|
*/
|
||||||
public addScene<T extends Scene>(constr: new (...args: any) => T, options: Record<string, any>): void {
|
public addScene<T extends Scene>(constr: new (...args: any) => T, options: Record<string, any>): void {
|
||||||
let scene = new constr(this.viewport, this, this.renderingManager, this.game, options);
|
let scene = new constr(this.viewport, this, this.renderingManager, options);
|
||||||
this.currentScene = scene;
|
this.currentScene = scene;
|
||||||
|
|
||||||
// Enqueue all scene asset loads
|
// Enqueue all scene asset loads
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import GameLoop from "./Loop/GameLoop";
|
import Game from "./Loop/Game";
|
||||||
import {} from "./index";
|
import {} from "./index";
|
||||||
import MainMenu from "./_DemoClasses/Mario/MainMenu";
|
import MainMenu from "./_DemoClasses/Mario/MainMenu";
|
||||||
import Level1 from "./_DemoClasses/Mario/Level1";
|
import Level1 from "./_DemoClasses/Mario/Level1";
|
||||||
|
@ -11,7 +11,7 @@ function main(){
|
||||||
clearColor: {r: 34, g: 32, b: 52}
|
clearColor: {r: 34, g: 32, b: 52}
|
||||||
}
|
}
|
||||||
|
|
||||||
let game = new GameLoop(options);
|
let game = new Game(options);
|
||||||
game.start();
|
game.start();
|
||||||
|
|
||||||
let sm = game.getSceneManager();
|
let sm = game.getSceneManager();
|
||||||
|
|
|
@ -27,7 +27,7 @@
|
||||||
"src/Input/InputHandler.ts",
|
"src/Input/InputHandler.ts",
|
||||||
"src/Input/InputReceiver.ts",
|
"src/Input/InputReceiver.ts",
|
||||||
|
|
||||||
"src/Loop/GameLoop.ts",
|
"src/Loop/Game.ts",
|
||||||
|
|
||||||
"src/Nodes/Tilemaps/OrthogonalTilemap.ts",
|
"src/Nodes/Tilemaps/OrthogonalTilemap.ts",
|
||||||
"src/Nodes/UIElements/Button.ts",
|
"src/Nodes/UIElements/Button.ts",
|
||||||
|
|
Loading…
Reference in New Issue
Block a user