Last active
November 18, 2021 14:01
-
-
Save dwaard/b413e8b3ef764156e4d7b1ac05dbc64b to your computer and use it in GitHub Desktop.
A basic Game Loop
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import Game from './Game.js'; | |
/** | |
* Represents a basic Game Loop based on `requestAnimationFrame()`. | |
* | |
* The implementation of this class depends on another class: `Game`. This | |
* means that, if you use this class, you need to either have a `Game` class | |
* that exactly implements the three methods `processInput()`, `update(elapsed)` | |
* and `render()` or change the code in the `step()` method of this class so it | |
* represents your own game methods. | |
* | |
* @see https://gameprogrammingpatterns.com/game-loop.html | |
* @author BugSlayer | |
*/ | |
export default class GameLoop { | |
public static readonly STATE_IDLE = 0; | |
public static readonly STATE_STARTING = 1; | |
public static readonly STATE_RUNNING = 2; | |
public static readonly STATE_STOPPING = 3; | |
public static readonly NORMAL_MODE = 0; | |
public static readonly PLAY_CATCH_UP = 1; | |
/** | |
* The current mode of the gameloop | |
*/ | |
private mode: number; | |
/** | |
* The current state of this gameloop | |
*/ | |
private state: number; | |
/** | |
* The game to animate | |
*/ | |
private game: Game; | |
private previousElapsed: number; | |
/** | |
* Holds the start time of the game | |
*/ | |
private gameStart: number; | |
/** | |
* Holds the time where the last animation step method ended. | |
*/ | |
private frameEnd: number; | |
/** | |
* The total time in milliseconds that is elapsed since the start of the | |
* game | |
*/ | |
public gameTime: number; | |
/** | |
* The amount of frames that are processed since the start of the game | |
*/ | |
public frameCount: number; | |
/** | |
* An indication of the current crames per second of this gameloop | |
*/ | |
public fps: number; | |
/** | |
* An indication of the load of this gameloop. The load is the ratio between | |
* the time needed to update the game and the time the computer waits to | |
* render the next frame. | |
*/ | |
public load: number; | |
/** | |
* Construct a new instance of this class. | |
* | |
* @param game the game to animate | |
* @param mode OPTIONAL, the mode of the gameloop. It defaults to | |
* GameLoop.NORMAL_MODE, which is fine for simple games | |
*/ | |
constructor(game: Game, mode: number = GameLoop.NORMAL_MODE) { | |
this.state = GameLoop.STATE_IDLE; | |
this.mode = mode; | |
this.game = game; | |
} | |
/** | |
* Start the game loop. | |
*/ | |
public start(): void { | |
if (this.state === GameLoop.STATE_IDLE) { | |
this.state = GameLoop.STATE_STARTING; | |
this.gameStart = performance.now(); | |
this.frameEnd = this.gameStart; | |
this.previousElapsed = this.gameStart; | |
this.gameTime = 0; | |
this.frameCount = 0; | |
requestAnimationFrame(this.step); | |
} | |
} | |
/** | |
* Requests to gracefully stop the gameloop. | |
*/ | |
public stop(): void { | |
this.state = GameLoop.STATE_STOPPING; | |
} | |
/** | |
* Returns `true` if the given state exactly matches the current state of | |
* this object | |
* | |
* @param state the state to check | |
* @returns `true` if the given state exactly matches the current state of | |
* this object | |
*/ | |
public isInState(state: number): boolean { | |
return this.state === state; | |
} | |
/** | |
* This MUST be an arrow method in order to keep the `this` variable working | |
* correctly. It will be overwritten by another object otherwise caused by | |
* javascript scoping behaviour. | |
* | |
* @param timestamp a `DOMHighResTimeStamp` similar to the one returned by | |
* `performance.now()`, indicating the point in time when `requestAnimationFrame()` | |
* starts to execute callback functions | |
*/ | |
private step = (timestamp: number) => { | |
// Handle first animation frame | |
if (this.isInState(GameLoop.STATE_STARTING)) { | |
this.state = GameLoop.STATE_RUNNING; | |
} | |
this.game.processInput(); | |
// Let the game update itself | |
let shouldStop = false; | |
if (this.mode === GameLoop.PLAY_CATCH_UP) { | |
const step = 1; | |
while (this.previousElapsed < timestamp && !shouldStop) { | |
shouldStop = this.game.update(step); | |
this.previousElapsed += step; | |
} | |
} else { | |
const elapsed = timestamp - this.previousElapsed; | |
shouldStop = this.game.update(elapsed); | |
this.previousElapsed = timestamp; | |
} | |
// Let the game render itself | |
this.game.render(); | |
// Check if a next animation frame needs to be requested | |
if (!shouldStop || this.isInState(GameLoop.STATE_STOPPING)) { | |
requestAnimationFrame(this.step); | |
} else { | |
this.state = GameLoop.STATE_IDLE; | |
} | |
// Handle time measurement and analysis | |
const now = performance.now(); | |
const stepTime = timestamp - now; | |
const frameTime = now - this.frameEnd; | |
this.fps = Math.round(1000 / frameTime); | |
this.load = stepTime / frameTime; | |
this.frameEnd = now; | |
this.gameTime = now - this.gameStart; | |
this.frameCount += 1; | |
}; | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import GameLoop from './GameLoop.js'; | |
/** | |
* Example of a game class that can collaborate with GameLoop. Mind that | |
* only attributes, methods and other code that is relevant for this example | |
* are shown here | |
*/ | |
export default class Game { | |
private gameloop: GameLoop; | |
/** | |
* Construct a new instance of this class | |
*/ | |
public constructor() { | |
this.gameloop = new GameLoop(this); | |
} | |
/** | |
* Start the game. | |
*/ | |
public start(): void { | |
this.gameloop.start(); | |
} | |
/** | |
* Handles any user input that has happened since the last call | |
*/ | |
public processInput(): void { | |
} | |
/** | |
* Advances the game simulation one step. It may run AI and physics (usually | |
* in that order) | |
* | |
* @param elapsed the time in ms that has been elapsed since the previous | |
* call | |
* @returns `true` if the game should stop animation | |
*/ | |
public update(elapsed: number): boolean { | |
} | |
/** | |
* Draw the game so the player can see what happened | |
*/ | |
public render(): void { | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment