Skip to content

Commit

Permalink
Added visibility handling. Bumped to v3.2.0
Browse files Browse the repository at this point in the history
  • Loading branch information
drpepper committed Apr 10, 2024
1 parent 14bdd88 commit e2dae6f
Show file tree
Hide file tree
Showing 3 changed files with 73 additions and 39 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "booyah",
"version": "3.1.1",
"version": "3.2.0",
"description": "HTML5 game engine",
"scripts": {
"build": "tsc",
Expand Down
100 changes: 67 additions & 33 deletions src/running.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;

/**
*
Expand All @@ -38,33 +39,55 @@ export class Runner {
*/
constructor(
private readonly _rootChipResolvable: chip.ChipResolvable,
options?: Partial<RunnerOptions>,
options?: Partial<RunnerOptions>
) {
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, {});
this._rootChip = _.isFunction(this._rootChipResolvable)
? 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,
};
this._rootChip.activate(
tickInfo,
this._rootContext,
this._options.inputSignal,
this._options.inputSignal
);

requestAnimationFrame(() => this._onTick());
Expand All @@ -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());
}

Expand Down Expand Up @@ -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,
};
}
}
10 changes: 5 additions & 5 deletions tests/running.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,15 +20,15 @@ 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();

// Short wait
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 () => {
Expand All @@ -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 () => {
Expand All @@ -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");
});
});

0 comments on commit e2dae6f

Please sign in to comment.