Skip to content

Instantly share code, notes, and snippets.

@dwaard
Last active November 18, 2021 14:01
Show Gist options
  • Save dwaard/b413e8b3ef764156e4d7b1ac05dbc64b to your computer and use it in GitHub Desktop.
Save dwaard/b413e8b3ef764156e4d7b1ac05dbc64b to your computer and use it in GitHub Desktop.
A basic Game Loop
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;
};
}
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