Skip to content

Commit

Permalink
Ported over changes from master
Browse files Browse the repository at this point in the history
  • Loading branch information
drpepper committed Aug 26, 2024
2 parents 310014b + d9cccad commit 13b6587
Show file tree
Hide file tree
Showing 6 changed files with 86 additions and 42 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
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": "4.0.1",
"version": "4.0.2",
"description": "HTML5 game engine",
"scripts": {
"build": "tsc",
Expand Down
16 changes: 10 additions & 6 deletions src/chip.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

Expand Down Expand Up @@ -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());
Expand Down
98 changes: 69 additions & 29 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,17 +39,39 @@ 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._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, {});
Expand All @@ -62,7 +85,7 @@ export class Runner {
this._rootChip.activate(
tickInfo,
this._rootContext,
this._options.inputSignal,
this._options.inputSignal
);

requestAnimationFrame(() => this._onTick());
Expand All @@ -71,43 +94,40 @@ 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 = {
timeSinceLastTick,
};

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());
}
Expand Down Expand Up @@ -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) {
Expand Down
1 change: 0 additions & 1 deletion tests/chip.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
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.chipState === "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.chipState === "inactive");
expect(runner.isRunning).toBe(false);
expect(runner.runningStatus).toBe("stopped");
});
});

0 comments on commit 13b6587

Please sign in to comment.