diff --git a/README.md b/README.md index 7058860..3e0d5d9 100644 --- a/README.md +++ b/README.md @@ -34,6 +34,7 @@ In addition to TypeScript, we also rely heavily on the following libraries: Of course, you'll probably want to use libraries for rendering and audio, among other things. To keep Booyah independent of a particular game tools, those integrations are provided in separate libraries: - [booyah-pixi](https://github.com/play-curious/booyah-pixi) integrates into [PixiJS](https://pixijs.com/) and [PixiJS Sound](https://github.com/pixijs/sound). +- [booyah-maquette](https://github.com/play-curious/booyah-maquette) integrates into the [Maquette](https://maquettejs.org/) virtual DOM library. - More to come... ## Development diff --git a/package.json b/package.json index a5fb519..f503ee6 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "booyah", - "version": "4.0.1", + "version": "4.0.2", "description": "HTML5 game engine", "scripts": { "build": "tsc", diff --git a/src/chip.ts b/src/chip.ts index 7f09b1c..c6af42b 100644 --- a/src/chip.ts +++ b/src/chip.ts @@ -191,7 +191,9 @@ export function isChip(e: any): e is Chip { ); } -export function isChipResolvable(e: any): e is ChipResolvable { +export function isChipResolvable( + e: ChipResolvable | ActivateChildChipOptions, +): e is ChipResolvable { return typeof e === "function" || isChip(e); } @@ -722,11 +724,13 @@ export abstract class Composite extends ChipBase { // Unpack arguments let chipResolvable: ChipResolvable; - if (typeof chipOrOptions === "function" || isChip(chipOrOptions)) { - chipResolvable = chipOrOptions; - } else { - chipResolvable = chipOrOptions.chip; - options = chipOrOptions; + if (typeof chipOrOptions !== "undefined") { + if (typeof chipOrOptions === "function" || isChip(chipOrOptions)) { + chipResolvable = chipOrOptions; + } else { + chipResolvable = chipOrOptions.chip; + options = chipOrOptions; + } } options = fillInOptions(options, new ActivateChildChipOptions()); diff --git a/src/running.ts b/src/running.ts index 30fd81a..df0c434 100644 --- a/src/running.ts +++ b/src/running.ts @@ -26,10 +26,11 @@ export class RunnerOptions { */ export class Runner { private _options: RunnerOptions; - private _isRunning = false; + private _runningStatus: "stopped" | "running" | "paused" = "stopped"; private _lastTimeStamp: number; private _rootContext: chip.ChipContext; private _rootChip: chip.Chip; + private _visibilityChangeHandler: () => void; /** * @@ -38,17 +39,39 @@ export class Runner { */ constructor( private readonly _rootChipResolvable: chip.ChipResolvable, - options?: Partial, + options?: Partial ) { this._options = chip.fillInOptions(options, new RunnerOptions()); - - this._isRunning = false; } start() { - if (this._isRunning) throw new Error("Already started"); + if (this._runningStatus !== "stopped") throw new Error("Already started"); + + this._visibilityChangeHandler = () => { + if (document.visibilityState === "hidden") { + if (this._runningStatus !== "running") return; + + console.log("Runner: pausing"); + this._runningStatus = "paused"; + + this._rootChip.pause(this._makeTickInfo()); + } else { + if (this._runningStatus !== "paused") return; + + console.log("Runner: resuming"); + + this._runningStatus = "running"; + this._rootChip.resume(this._makeTickInfo()); + + requestAnimationFrame(() => this._onTick()); + } + }; + document.addEventListener( + "visibilitychange", + this._visibilityChangeHandler + ); - this._isRunning = true; + this._runningStatus = "running"; this._lastTimeStamp = 0; this._rootContext = chip.processChipContext(this._options.rootContext, {}); @@ -62,7 +85,7 @@ export class Runner { this._rootChip.activate( tickInfo, this._rootContext, - this._options.inputSignal, + this._options.inputSignal ); requestAnimationFrame(() => this._onTick()); @@ -71,13 +94,11 @@ export class Runner { } stop() { - if (!this._isRunning) throw new Error("Already stopped"); - - this._isRunning = false; + if (this._runningStatus === "stopped") throw new Error("Already stopped"); const timeStamp = performance.now(); const timeSinceLastTick = this._clampTimeSinceLastTick( - timeStamp - this._lastTimeStamp, + timeStamp - this._lastTimeStamp ); const tickInfo: chip.TickInfo = { @@ -85,29 +106,28 @@ export class Runner { }; this._rootChip.terminate(tickInfo, chip.makeSignal("stop")); + this._runningStatus = "stopped"; + + document.removeEventListener( + "visibilitychange", + this._visibilityChangeHandler + ); + delete this._visibilityChangeHandler; } private _onTick() { - if (!this._isRunning) return; - - const timeStamp = performance.now(); - - let timeSinceLastTick = timeStamp - this._lastTimeStamp; - this._lastTimeStamp = timeStamp; - - // If no time elapsed, don't update - if (timeSinceLastTick <= 0) return; + if (this._runningStatus !== "running") return; - timeSinceLastTick = this._clampTimeSinceLastTick(timeSinceLastTick); - - const tickInfo: chip.TickInfo = { - timeSinceLastTick, - }; + // If no time elapsed, stop early + const tickInfo = this._makeTickInfo(); + if (tickInfo.timeSinceLastTick === 0) return; if (this._rootChip.chipState === "requestedTermination") { + // If the chip is done, stop the runner too this._rootChip.terminate(tickInfo); - this._isRunning = false; + this._runningStatus = "stopped"; } else { + // Call `tick()` and start the update loop again this._rootChip.tick(tickInfo); requestAnimationFrame(() => this._onTick()); } @@ -141,13 +161,33 @@ export class Runner { tickInfo, this._rootContext, chip.makeSignal("afterReload"), - reloadMemento, + reloadMemento ); }); } - get isRunning(): boolean { - return this._isRunning; + get runningStatus() { + return this._runningStatus; + } + + private _makeTickInfo(): chip.TickInfo { + const timeStamp = performance.now(); + + // Force time to be >= 0 + let timeSinceLastTick = Math.max(0, timeStamp - this._lastTimeStamp); + this._lastTimeStamp = timeStamp; + + // Optionally clamp time since last frame + if (this._options.minFps >= 0) { + timeSinceLastTick = Math.min( + timeSinceLastTick, + 1000 / this._options.minFps + ); + } + + return { + timeSinceLastTick, + }; } private _clampTimeSinceLastTick(timeSinceLastTick: number) { diff --git a/tests/chip.test.ts b/tests/chip.test.ts index fe54a72..42adcd5 100644 --- a/tests/chip.test.ts +++ b/tests/chip.test.ts @@ -916,7 +916,6 @@ describe("Alternative", () => { // Terminate second child children[1].requestTermination(); - debugger; alternative.tick(makeFrameInfo()); // Alternative should request termination as well, with an output signal of the index of the child diff --git a/tests/running.test.ts b/tests/running.test.ts index 81525bd..bc638e0 100644 --- a/tests/running.test.ts +++ b/tests/running.test.ts @@ -20,7 +20,7 @@ describe("Running", () => { const rootChip = new chip.Lambda(() => ranCount++); const runner = new running.Runner(rootChip); - expect(runner.isRunning).toBe(false); + expect(runner.runningStatus).toBe("stopped"); runner.start(); @@ -28,7 +28,7 @@ describe("Running", () => { await wait(frameTime * 5); expect(ranCount).toBe(1); - expect(runner.isRunning).toBe(false); + expect(runner.runningStatus).toBe("stopped"); }); test("runs a chip multiple times", async () => { @@ -47,7 +47,7 @@ describe("Running", () => { expect(ranCount).toBe(3); expect(rootChip.chipState === "inactive"); - expect(runner.isRunning).toBe(false); + expect(runner.runningStatus).toBe("stopped"); }); test("stops on demand", async () => { @@ -63,12 +63,12 @@ describe("Running", () => { // Short wait await wait(frameTime * 5); - expect(runner.isRunning).toBe(true); + expect(runner.runningStatus).toBe("running"); runner.stop(); expect(ranCount).toBeGreaterThan(0); expect(rootChip.chipState === "inactive"); - expect(runner.isRunning).toBe(false); + expect(runner.runningStatus).toBe("stopped"); }); });