@byloth/core
Version:
An unopinionated collection of useful functions and classes that I use widely in all my projects. 🔧
157 lines (140 loc) • 4.59 kB
text/typescript
import { TimeUnit } from "../../utils/date.js";
import Publisher from "../callbacks/publisher.js";
import { FatalErrorException, RangeException, RuntimeException } from "../exceptions/index.js";
import GameLoop from "./game-loop.js";
interface ClockEventsMap
{
start: () => void;
stop: () => void;
tick: (elapsedTime: number) => void;
}
/**
* A class representing a clock.
*
* It can be started, stopped and, when running, it ticks at a specific frame rate.
* It's possible to subscribe to these events to receive notifications when they occur.
*
* ---
*
* @example
* ```ts
* const clock = new Clock();
*
* clock.onStart(() => console.log("The clock has started."));
* clock.onTick((elapsedTime) => console.log(`The clock has ticked at ${elapsedTime}ms.`));
* clock.onStop(() => console.log("The clock has stopped."));
*
* clock.start();
* ```
*/
export default class Clock extends GameLoop
{
/**
* The {@link Publisher} object that will be used to publish the events of the clock.
*/
declare protected readonly _publisher: Publisher<ClockEventsMap>;
/**
* Initializes a new instance of the {@link Clock} class.
*
* ---
*
* @example
* ```ts
* const clock = new Clock();
* ```
*
* ---
*
* @param msIfNotBrowser
* The interval in milliseconds at which the clock will tick if the environment is not a browser.
* `TimeUnit.Second` by default.
*/
public constructor(msIfNotBrowser: number = TimeUnit.Second)
{
super((elapsedTime) => this._publisher.publish("tick", elapsedTime), msIfNotBrowser);
}
/**
* Starts the execution of the clock.
*
* If the clock is already running, a {@link RuntimeException} will be thrown.
*
* ---
*
* @example
* ```ts
* clock.onStart(() => { [...] }); // This callback will be executed.
* clock.start();
* ```
*
* ---
*
* @param elapsedTime The elapsed time to set as default when the clock starts. Default is `0`.
*/
public override start(elapsedTime = 0): void
{
if (this._isRunning) { throw new RuntimeException("The clock has already been started."); }
this._startTime = performance.now() - elapsedTime;
this._start();
this._isRunning = true;
this._publisher.publish("start");
}
/**
* Stops the execution of the clock.
*
* If the clock hasn't yet started, a {@link RuntimeException} will be thrown.
*
* ---
*
* @example
* ```ts
* clock.onStop(() => { [...] }); // This callback will be executed.
* clock.stop();
* ```
*/
public override stop(): void
{
if (!(this._isRunning)) { throw new RuntimeException("The clock had already stopped or hadn't yet started."); }
if (!(this._handle)) { throw new FatalErrorException(); }
this._stop();
this._handle = undefined;
this._isRunning = false;
this._publisher.publish("stop");
}
/**
* Subscribes to the `tick` event of the clock.
*
* ---
*
* @example
* ```ts
* clock.onTick((elapsedTime) => { [...] }); // This callback will be executed.
* clock.start();
* ```
*
* ---
*
* @param callback The callback that will be executed when the clock ticks.
* @param tickStep
* The minimum time in milliseconds that must pass from the previous execution of the callback to the next one.
*
* - If it's a positive number, the callback will be executed only if the
* time passed from the previous execution is greater than this number.
* - If it's `0`, the callback will be executed every tick without even checking for the time.
* - If it's a negative number, a {@link RangeException} will be thrown.
*
* @returns A function that can be used to unsubscribe from the event.
*/
public onTick(callback: (elapsedTime: number) => void, tickStep = 0): () => void
{
if (tickStep < 0) { throw new RangeException("The tick step must be a non-negative number."); }
if (tickStep === 0) { return this._publisher.subscribe("tick", callback); }
let lastTick = 0;
return this._publisher.subscribe("tick", (elapsedTime: number) =>
{
if ((elapsedTime - lastTick) < tickStep) { return; }
callback(elapsedTime);
lastTick = elapsedTime;
});
}
public override readonly [Symbol.toStringTag]: string = "Clock";
}