From e2dae6f5dfc5c9c650de573d7c37d69ea8ac5df2 Mon Sep 17 00:00:00 2001 From: Jesse Himmelstein Date: Wed, 10 Apr 2024 07:29:40 +0200 Subject: [PATCH] Added visibility handling. Bumped to v3.2.0 --- package.json | 2 +- src/running.ts | 100 ++++++++++++++++++++++++++++-------------- tests/running.test.ts | 10 ++--- 3 files changed, 73 insertions(+), 39 deletions(-) diff --git a/package.json b/package.json index 5f08a86..73d45f2 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "booyah", - "version": "3.1.1", + "version": "3.2.0", "description": "HTML5 game engine", "scripts": { "build": "tsc", diff --git a/src/running.ts b/src/running.ts index 502a539..f8395d4 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._isRunning = true; + this._runningStatus = "running"; + this._rootChip.resume(this._makeTickInfo()); + + requestAnimationFrame(() => this._onTick()); + } + }; + document.addEventListener( + "visibilitychange", + this._visibilityChangeHandler + ); + + this._runningStatus = "running"; this._lastTimeStamp = 0; this._rootContext = chip.processChipContext(this._options.rootContext, {}); @@ -56,7 +79,7 @@ export class Runner { ? this._rootChipResolvable(this._rootContext, chip.makeSignal()) : this._rootChipResolvable; - this._rootChip.once("terminated", () => (this._isRunning = false)); + this._rootChip.once("terminated", () => (this._runningStatus = "stopped")); const tickInfo: chip.TickInfo = { timeSinceLastTick: 0, @@ -64,7 +87,7 @@ export class Runner { this._rootChip.activate( tickInfo, this._rootContext, - this._options.inputSignal, + this._options.inputSignal ); requestAnimationFrame(() => this._onTick()); @@ -73,37 +96,28 @@ export class Runner { } stop() { - if (!this._isRunning) throw new Error("Already stopped"); + if (this._runningStatus === "stopped") throw new Error("Already stopped"); - this._isRunning = false; + this._runningStatus = "stopped"; this._rootChip.terminate(chip.makeSignal("stop")); + + 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 (this._runningStatus !== "running") return; - // If no time elapsed, don't update - if (timeSinceLastTick <= 0) return; + const tickInfo = this._makeTickInfo(); - // Optionally clamp time since last frame - if (this._options.minFps >= 0) { - timeSinceLastTick = Math.min( - timeSinceLastTick, - 1000 / this._options.minFps, - ); + // If no time elapsed, don't call tick() + if (tickInfo.timeSinceLastTick > 0) { + this._rootChip.tick(tickInfo); } - const tickInfo: chip.TickInfo = { - timeSinceLastTick, - }; - - this._rootChip.tick(tickInfo); - requestAnimationFrame(() => this._onTick()); } @@ -135,12 +149,32 @@ 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, + }; } } diff --git a/tests/running.test.ts b/tests/running.test.ts index e21eb82..0ceba19 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.state === "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.state === "inactive"); - expect(runner.isRunning).toBe(false); + expect(runner.runningStatus).toBe("stopped"); }); });