Skip to content

Instantly share code, notes, and snippets.

@dwaard
Last active November 29, 2021 14:22
Show Gist options
  • Save dwaard/848252422f45a0f297633a7225c0cdd8 to your computer and use it in GitHub Desktop.
Save dwaard/848252422f45a0f297633a7225c0cdd8 to your computer and use it in GitHub Desktop.
An improved version of an implementation of a generic Game Loop. It uses Inheritance to specify a Scene class that the gameloop animates.
import Scene from './Scene.js';
/**
* Represents a basic Game Loop based on `requestAnimationFrame()`.
*
* The implementation of this class depends on another class: `Scene`. This
* means that, if you use this class, you must have a class that is a subclass
* of `Scene` that overides the three methods `processInput()`, `update(elapsed)`
* and `render()`.
*
* It is possible for a game to switch to another Scene object during the game, so
* you can create different classes for each game screen, levels, etc. To let the
* gameloop switch to another scene, you must return an instance of a new Scene
* subclass in the `update(elapsed)` method. See the documentation of that method
* in the `Scene` class for more information on this.
*
* @see Scene
* @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 currentScene: Scene;
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;
/**
* The timestamp of the exact moment where the gameloop has started
* animating the current scene
*/
public sceneStart: number;
/**
* The elapsed time between `sceneStart` and the timestamp of the current
* frame
*/
public sceneTime: number;
/**
* The amount of frames that are processed since `sceneStart`
*/
public sceneFrameCount: 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 mode OPTIONAL, the mode of the gameloop. It defaults to
* GameLoop.NORMAL_MODE, which is fine for simple games
*/
constructor(mode: number = GameLoop.NORMAL_MODE) {
this.state = GameLoop.STATE_IDLE;
this.mode = mode;
}
/**
* Start the game loop.
*
* @param scene the game to start animating
*/
public start(scene: Scene): void {
if (this.state === GameLoop.STATE_IDLE) {
this.state = GameLoop.STATE_STARTING;
this.currentScene = scene;
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;
}
/*
* Sets the next scene to animate
*
* @param next the next scene to animate
*/
private setNextScene(next: Scene) {
this.currentScene = next;
this.sceneStart = performance.now();
this.sceneTime = 0;
this.sceneFrameCount = 0;
}
/**
* 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.currentScene.processInput();
// Let the game update itself
let nextScene: Scene = null;
if (this.mode === GameLoop.PLAY_CATCH_UP) {
const step = 1;
while (this.previousElapsed < timestamp && !nextScene) {
nextScene = this.currentScene.update(step);
this.previousElapsed += step;
}
} else {
const elapsed = timestamp - this.previousElapsed;
nextScene = this.currentScene.update(elapsed);
this.previousElapsed = timestamp;
}
if (nextScene) {
this.setNextScene(nextScene);
} else {
// Let the game render itself
this.currentScene.render();
}
// Check if a next animation frame needs to be requested
if (!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.sceneTime = now - this.sceneStart;
this.frameCount += 1;
this.sceneFrameCount += 1;
};
}
/**
* A superclass for objects that must be able to be animated by a `GameLoop`.
*
* Implementing classes must override the three methods `processInput()`,
* `update(elapsed)` and `render()`.
*
* @see GameLoop
* @author BugSlayer
*/
export default abstract class Scene {
/**
* Handles any user input that has happened since the last call
*/
public abstract processInput(): void;
/**
* Advances the game simulation one step. It may run AI and physics (usually
* in that order). The return value of this method determines what the `GameLoop`
* that is animating this object will do next. If `null` is returned, the
* GameLoop will render this scene and proceeds to the next animation frame.
* If this methods returns a `Scene` (subclass) object, it will NOT render this
* scene but will start considering that object as the current scene to animate.
* In other words, by returning a Scene object, you can set the next scene to
* animate.
*
* @param elapsed the time in ms that has been elapsed since the previous
* call
* @returns a new `Scene` object if the game should start animating that scene
* on the next animation frame. If the game should just continue with the
* current scene, just return `null`
*/
public abstract update(elapsed: number): Scene;
/**
* Draw the game so the player can see what happened
*/
public abstract render(): void;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment