Separates game loop from doodler draw loop. Each state is in charge of registering and unregistering layers
This commit is contained in:
parent
43a5268ed5
commit
3befb69f51
63
GameLoop.ts
Normal file
63
GameLoop.ts
Normal file
@ -0,0 +1,63 @@
|
||||
import { Doodler } from "@bearmetal/doodler";
|
||||
import { StateMachine } from "./state/machine.ts";
|
||||
import { getContextItem } from "./lib/context.ts";
|
||||
|
||||
export class GameLoop<T> {
|
||||
lastTime: number;
|
||||
running: boolean;
|
||||
targetFps: number;
|
||||
|
||||
constructor(targetFps: number = 60) {
|
||||
this.lastTime = performance.now();
|
||||
this.running = false;
|
||||
this.targetFps = targetFps;
|
||||
}
|
||||
|
||||
async start(state: StateMachine<T>) {
|
||||
if (this.running) return;
|
||||
this.running = true;
|
||||
this.lastTime = performance.now();
|
||||
|
||||
while (this.running) {
|
||||
const currentTime = performance.now();
|
||||
const deltaTime = (currentTime - this.lastTime) / 1000; // Convert to seconds
|
||||
this.lastTime = currentTime;
|
||||
|
||||
try {
|
||||
// Wait for state update to complete before continuing
|
||||
await state.update(deltaTime);
|
||||
} catch (error) {
|
||||
console.error("Error in game loop:", error);
|
||||
this.stop();
|
||||
break;
|
||||
}
|
||||
|
||||
// Use setTimeout to prevent immediate loop continuation
|
||||
// and allow other tasks to run
|
||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||
}
|
||||
}
|
||||
|
||||
stop() {
|
||||
this.running = false;
|
||||
}
|
||||
}
|
||||
|
||||
// // Usage example:
|
||||
// const gameState = {
|
||||
// update: async (deltaTime) => {
|
||||
// console.log(`Updating with delta time: ${deltaTime.toFixed(3)}s`);
|
||||
// // Simulate some async work
|
||||
// await new Promise(resolve => setTimeout(resolve, 16)); // ~60fps
|
||||
// }
|
||||
// };
|
||||
|
||||
// // Create and start the loop
|
||||
// const loop = new GameLoop();
|
||||
// loop.start(gameState);
|
||||
|
||||
// // Stop the loop after 5 seconds (example)
|
||||
// setTimeout(() => {
|
||||
// loop.stop();
|
||||
// console.log('Loop stopped');
|
||||
// }, 5000);
|
234
bundle.js
234
bundle.js
@ -78,12 +78,12 @@
|
||||
}, 2);
|
||||
}
|
||||
|
||||
// https://jsr.io/@bearmetal/doodler/0.0.5-a/geometry/constants.ts
|
||||
// https://jsr.io/@bearmetal/doodler/0.0.5-b/geometry/constants.ts
|
||||
var Constants = {
|
||||
TWO_PI: Math.PI * 2
|
||||
};
|
||||
|
||||
// https://jsr.io/@bearmetal/doodler/0.0.5-a/geometry/vector.ts
|
||||
// https://jsr.io/@bearmetal/doodler/0.0.5-b/geometry/vector.ts
|
||||
var Vector = class _Vector {
|
||||
x;
|
||||
y;
|
||||
@ -368,7 +368,7 @@
|
||||
}
|
||||
};
|
||||
|
||||
// https://jsr.io/@bearmetal/doodler/0.0.5-a/FPSCounter.ts
|
||||
// https://jsr.io/@bearmetal/doodler/0.0.5-b/FPSCounter.ts
|
||||
var FPSCounter = class {
|
||||
frameTimes = [];
|
||||
maxSamples;
|
||||
@ -396,7 +396,7 @@
|
||||
}
|
||||
};
|
||||
|
||||
// https://jsr.io/@bearmetal/doodler/0.0.5-a/canvas.ts
|
||||
// https://jsr.io/@bearmetal/doodler/0.0.5-b/canvas.ts
|
||||
var Doodler = class {
|
||||
ctx;
|
||||
_canvas;
|
||||
@ -488,9 +488,14 @@
|
||||
// Layer management
|
||||
createLayer(layer) {
|
||||
this.layers.push(layer);
|
||||
return this.layers.length - 1;
|
||||
}
|
||||
deleteLayer(layer) {
|
||||
this.layers = this.layers.filter((l) => l !== layer);
|
||||
if (typeof layer === "number") {
|
||||
this.layers = this.layers.filter((_, i) => i !== layer);
|
||||
} else {
|
||||
this.layers = this.layers.filter((l) => l !== layer);
|
||||
}
|
||||
}
|
||||
moveLayer(layer, index) {
|
||||
let temp = this.layers.filter((l) => l !== layer);
|
||||
@ -759,13 +764,13 @@
|
||||
}
|
||||
};
|
||||
|
||||
// https://jsr.io/@bearmetal/doodler/0.0.5-a/timing/EaseInOut.ts
|
||||
// https://jsr.io/@bearmetal/doodler/0.0.5-b/timing/EaseInOut.ts
|
||||
var easeInOut = (x) => x < 0.5 ? 4 * x * x * x : 1 - Math.pow(-2 * x + 2, 3) / 2;
|
||||
|
||||
// https://jsr.io/@bearmetal/doodler/0.0.5-a/timing/Map.ts
|
||||
// https://jsr.io/@bearmetal/doodler/0.0.5-b/timing/Map.ts
|
||||
var map = (value, x1, y1, x2, y2) => (value - x1) * (y2 - x2) / (y1 - x1) + x2;
|
||||
|
||||
// https://jsr.io/@bearmetal/doodler/0.0.5-a/zoomableCanvas.ts
|
||||
// https://jsr.io/@bearmetal/doodler/0.0.5-b/zoomableCanvas.ts
|
||||
var ZoomableDoodler = class extends Doodler {
|
||||
scale = 1;
|
||||
dragging = false;
|
||||
@ -1167,6 +1172,12 @@
|
||||
update(dt, ctx2) {
|
||||
this.currentState?.update(dt, ctx2);
|
||||
}
|
||||
optimizePerformance(percent) {
|
||||
const ctx2 = getContext();
|
||||
if (percent < 0.5) {
|
||||
ctx2.track.optimize(percent);
|
||||
}
|
||||
}
|
||||
get current() {
|
||||
return this.currentState;
|
||||
}
|
||||
@ -1420,6 +1431,12 @@
|
||||
get lastSegment() {
|
||||
return this.segments.values().toArray().pop();
|
||||
}
|
||||
optimize(percent) {
|
||||
console.log("Optimizing track", percent * 100 / 4);
|
||||
for (const segment of this.segments.values()) {
|
||||
segment.recalculateRailPoints(Math.round(percent * 100 / 4));
|
||||
}
|
||||
}
|
||||
registerSegment(segment) {
|
||||
segment.setTrack(this);
|
||||
this.segments.set(segment.id, segment);
|
||||
@ -1508,9 +1525,14 @@
|
||||
}
|
||||
generatePath() {
|
||||
if (!this.firstSegment) throw new Error("No first segment");
|
||||
const flags = { looping: true };
|
||||
const rightOnlyPath = [
|
||||
this.firstSegment.copy(),
|
||||
...this.findRightPath(this.firstSegment, /* @__PURE__ */ new Set([this.firstSegment.id]))
|
||||
...this.findRightPath(
|
||||
this.firstSegment,
|
||||
/* @__PURE__ */ new Set([this.firstSegment.id]),
|
||||
flags
|
||||
)
|
||||
];
|
||||
rightOnlyPath.forEach((s, i, arr) => {
|
||||
if (i === 0) return;
|
||||
@ -1519,9 +1541,17 @@
|
||||
s.prev = prev;
|
||||
prev.next = s;
|
||||
});
|
||||
if (flags.looping) {
|
||||
const first = rightOnlyPath[0];
|
||||
const last = rightOnlyPath[rightOnlyPath.length - 1];
|
||||
first.points[0] = last.points[3];
|
||||
last.points[3] = first.points[0];
|
||||
first.prev = last;
|
||||
last.next = first;
|
||||
}
|
||||
return new Spline(rightOnlyPath);
|
||||
}
|
||||
*findRightPath(start, seen) {
|
||||
*findRightPath(start, seen, flags) {
|
||||
if (start.frontNeighbours.length === 0) {
|
||||
return;
|
||||
}
|
||||
@ -1542,12 +1572,17 @@
|
||||
rightMost = segment;
|
||||
}
|
||||
}
|
||||
if (seen.has(rightMost.id)) return;
|
||||
if (seen.has(rightMost.id)) {
|
||||
if (seen.values().next().value === rightMost.id) {
|
||||
flags.looping = true;
|
||||
}
|
||||
return;
|
||||
}
|
||||
seen.add(rightMost.id);
|
||||
yield rightMost.copy();
|
||||
yield* this.findRightPath(rightMost, seen);
|
||||
yield* this.findRightPath(rightMost, seen, flags);
|
||||
}
|
||||
*findLeftPath(start, seen) {
|
||||
*findLeftPath(start, seen, flags) {
|
||||
if (start.frontNeighbours.length === 0) {
|
||||
return;
|
||||
}
|
||||
@ -1568,10 +1603,15 @@
|
||||
leftMost = segment;
|
||||
}
|
||||
}
|
||||
if (seen.has(leftMost.id)) return;
|
||||
if (seen.has(leftMost.id)) {
|
||||
if (seen.values().next().value === leftMost.id) {
|
||||
flags.looping = true;
|
||||
}
|
||||
return;
|
||||
}
|
||||
seen.add(leftMost.id);
|
||||
yield leftMost.copy();
|
||||
yield* this.findLeftPath(leftMost, seen);
|
||||
yield* this.findLeftPath(leftMost, seen, flags);
|
||||
}
|
||||
};
|
||||
var TrackSegment = class _TrackSegment extends PathSegment {
|
||||
@ -1579,10 +1619,25 @@
|
||||
backNeighbours = [];
|
||||
track;
|
||||
doodler;
|
||||
normalPoints = [];
|
||||
antiNormalPoints = [];
|
||||
constructor(p, id) {
|
||||
super(p);
|
||||
this.doodler = getContextItem("doodler");
|
||||
this.id = id ?? crypto.randomUUID();
|
||||
this.recalculateRailPoints();
|
||||
}
|
||||
recalculateRailPoints(resolution = 100) {
|
||||
this.normalPoints = [];
|
||||
this.antiNormalPoints = [];
|
||||
for (let i = 0; i <= resolution; i++) {
|
||||
const t = i / resolution;
|
||||
const normal = this.tangent(t).rotate(Math.PI / 2);
|
||||
normal.setMag(6);
|
||||
const p = this.getPointAtT(t);
|
||||
this.normalPoints.push(p.copy().add(normal));
|
||||
this.antiNormalPoints.push(p.copy().add(normal.rotate(Math.PI)));
|
||||
}
|
||||
}
|
||||
setTrack(t) {
|
||||
this.track = t;
|
||||
@ -1619,21 +1674,16 @@
|
||||
});
|
||||
});
|
||||
}
|
||||
const lineResolution = 100;
|
||||
const normalPoints = [];
|
||||
const antiNormalPoints = [];
|
||||
for (let i = 0; i <= lineResolution; i++) {
|
||||
const t = i / lineResolution;
|
||||
const normal = this.tangent(t).rotate(Math.PI / 2);
|
||||
normal.setMag(6);
|
||||
const p = this.getPointAtT(t);
|
||||
normalPoints.push(p.copy().add(normal));
|
||||
antiNormalPoints.push(p.copy().add(normal.rotate(Math.PI)));
|
||||
}
|
||||
this.doodler.deferDrawing(
|
||||
() => {
|
||||
this.doodler.drawLine(normalPoints, { color: "grey" });
|
||||
this.doodler.drawLine(antiNormalPoints, { color: "grey" });
|
||||
this.doodler.drawLine(this.normalPoints, {
|
||||
color: "grey",
|
||||
weight: 1.5
|
||||
});
|
||||
this.doodler.drawLine(this.antiNormalPoints, {
|
||||
color: "grey",
|
||||
weight: 1.5
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
||||
@ -1737,21 +1787,12 @@
|
||||
looped = false;
|
||||
constructor(segs) {
|
||||
this.segments = segs;
|
||||
if (this.segments.at(-1)?.next === this.segments[0]) {
|
||||
this.looped = true;
|
||||
}
|
||||
this.pointSpacing = 1;
|
||||
this.evenPoints = this.calculateEvenlySpacedPoints(1);
|
||||
this.nodes = [];
|
||||
for (let i = 0; i < this.points.length; i += 3) {
|
||||
const node = {
|
||||
anchor: this.points[i],
|
||||
controls: [
|
||||
this.points.at(i - 1),
|
||||
this.points[(i + 1) % this.points.length]
|
||||
],
|
||||
mirrored: false,
|
||||
tangent: true
|
||||
};
|
||||
this.nodes.push(node);
|
||||
}
|
||||
}
|
||||
// setContext(ctx: CanvasRenderingContext2D) {
|
||||
// this.ctx = ctx;
|
||||
@ -1761,7 +1802,10 @@
|
||||
// }
|
||||
draw() {
|
||||
for (const segment of this.segments) {
|
||||
segment.draw();
|
||||
const doodler2 = getContextItem("doodler");
|
||||
doodler2.drawWithAlpha(0.5, () => {
|
||||
doodler2.drawBezier(...segment.points, { color: "red" });
|
||||
});
|
||||
}
|
||||
}
|
||||
calculateEvenlySpacedPoints(spacing, resolution = 1) {
|
||||
@ -1934,6 +1978,7 @@
|
||||
ghostSegment;
|
||||
ghostRotated = false;
|
||||
closestEnd;
|
||||
layers = [];
|
||||
update(dt) {
|
||||
const inputManager2 = getContextItem("inputManager");
|
||||
const track = getContextItem("track");
|
||||
@ -2015,19 +2060,6 @@
|
||||
this.ghostSegment = void 0;
|
||||
this.ghostRotated = false;
|
||||
}
|
||||
this.selectedSegment?.draw();
|
||||
if (this.ghostSegment) {
|
||||
doodler2.drawWithAlpha(0.5, () => {
|
||||
if (!this.ghostSegment) return;
|
||||
this.ghostSegment.draw();
|
||||
if (getContextItemOrDefault("debug", false)) {
|
||||
const colors2 = getContextItem("colors");
|
||||
for (const [i, point] of this.ghostSegment.points.entries() ?? []) {
|
||||
doodler2.fillCircle(point, 4, { color: colors2[i + 3] });
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
const translation = new Vector(0, 0);
|
||||
if (inputManager2.getKeyState("ArrowUp")) {
|
||||
@ -2045,9 +2077,27 @@
|
||||
if (translation.x !== 0 || translation.y !== 0) {
|
||||
track.translate(translation);
|
||||
}
|
||||
track.draw(true);
|
||||
}
|
||||
start() {
|
||||
const doodler2 = getContextItem("doodler");
|
||||
this.layers.push(
|
||||
doodler2.createLayer(() => {
|
||||
this.selectedSegment?.draw();
|
||||
if (this.ghostSegment) {
|
||||
doodler2.drawWithAlpha(0.5, () => {
|
||||
if (!this.ghostSegment) return;
|
||||
this.ghostSegment.draw();
|
||||
if (getContextItemOrDefault("debug", false)) {
|
||||
const colors2 = getContextItem("colors");
|
||||
for (const [i, point] of this.ghostSegment.points.entries() ?? []) {
|
||||
doodler2.fillCircle(point, 4, { color: colors2[i + 3] });
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
track.draw(true);
|
||||
})
|
||||
);
|
||||
setContextItem("trackSegments", [
|
||||
void 0,
|
||||
new StraightTrack(),
|
||||
@ -2129,6 +2179,9 @@
|
||||
}
|
||||
redoBuffer = [];
|
||||
stop() {
|
||||
for (const layer of this.layers) {
|
||||
getContextItem("doodler").deleteLayer(layer);
|
||||
}
|
||||
const inputManager2 = getContextItem("inputManager");
|
||||
inputManager2.offKey("e");
|
||||
inputManager2.offKey("w");
|
||||
@ -2577,16 +2630,27 @@
|
||||
2 /* PAUSED */,
|
||||
3 /* EDIT_TRACK */
|
||||
]);
|
||||
layers = [];
|
||||
update(dt) {
|
||||
const ctx2 = getContext();
|
||||
const input = getContextItem("inputManager");
|
||||
ctx2.track.draw();
|
||||
for (const train of ctx2.trains) {
|
||||
train.move(dt);
|
||||
train.draw();
|
||||
}
|
||||
}
|
||||
start() {
|
||||
const doodler2 = getContextItem("doodler");
|
||||
this.layers.push(
|
||||
doodler2.createLayer(() => {
|
||||
const track2 = getContextItem("track");
|
||||
track2.draw();
|
||||
}),
|
||||
doodler2.createLayer(() => {
|
||||
const trains = getContextItem("trains");
|
||||
for (const train of trains) {
|
||||
train.draw();
|
||||
}
|
||||
})
|
||||
);
|
||||
const inputManager2 = getContextItem("inputManager");
|
||||
const track = getContextItem("track");
|
||||
const ctx2 = getContext();
|
||||
@ -2608,6 +2672,9 @@
|
||||
});
|
||||
}
|
||||
stop() {
|
||||
for (const layer of this.layers) {
|
||||
getContextItem("doodler").deleteLayer(layer);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@ -2631,6 +2698,7 @@
|
||||
validTransitions = /* @__PURE__ */ new Set([
|
||||
1 /* RUNNING */
|
||||
]);
|
||||
layers = [];
|
||||
update() {
|
||||
}
|
||||
start() {
|
||||
@ -2648,6 +2716,13 @@
|
||||
resources2.ready().then(() => {
|
||||
this.stateMachine.transitionTo(1 /* RUNNING */);
|
||||
});
|
||||
const doodler2 = getContextItem("doodler");
|
||||
this.layers.push(doodler2.createLayer((_, __, dTime) => {
|
||||
doodler2.clearRect(new Vector(0, 0), doodler2.width, doodler2.height);
|
||||
doodler2.fillRect(new Vector(0, 0), doodler2.width, doodler2.height, {
|
||||
color: "#302040"
|
||||
});
|
||||
}));
|
||||
}
|
||||
stop() {
|
||||
}
|
||||
@ -2675,6 +2750,39 @@
|
||||
return stateMachine;
|
||||
}
|
||||
|
||||
// GameLoop.ts
|
||||
var GameLoop = class {
|
||||
lastTime;
|
||||
running;
|
||||
targetFps;
|
||||
constructor(targetFps = 60) {
|
||||
this.lastTime = performance.now();
|
||||
this.running = false;
|
||||
this.targetFps = targetFps;
|
||||
}
|
||||
async start(state2) {
|
||||
if (this.running) return;
|
||||
this.running = true;
|
||||
this.lastTime = performance.now();
|
||||
while (this.running) {
|
||||
const currentTime = performance.now();
|
||||
const deltaTime = (currentTime - this.lastTime) / 1e3;
|
||||
this.lastTime = currentTime;
|
||||
try {
|
||||
await state2.update(deltaTime);
|
||||
} catch (error) {
|
||||
console.error("Error in game loop:", error);
|
||||
this.stop();
|
||||
break;
|
||||
}
|
||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||
}
|
||||
}
|
||||
stop() {
|
||||
this.running = false;
|
||||
}
|
||||
};
|
||||
|
||||
// main.ts
|
||||
var inputManager = new InputManager();
|
||||
var resources = new ResourceManager();
|
||||
@ -2704,9 +2812,6 @@
|
||||
var state = bootstrapGameStateMachine();
|
||||
setContextItem("state", state);
|
||||
doodler.init();
|
||||
doodler.createLayer((_, __, dTime) => {
|
||||
state.update(dTime);
|
||||
});
|
||||
document.addEventListener("keydown", (e) => {
|
||||
if ((e.ctrlKey || e.metaKey) && e.key === "s") {
|
||||
e.preventDefault();
|
||||
@ -2718,12 +2823,19 @@
|
||||
setInterval(() => {
|
||||
const doodler2 = getContextItem("doodler");
|
||||
const frameRate = doodler2.fps;
|
||||
if (frameRate < 0.5) return;
|
||||
let fpsEl = document.getElementById("fps");
|
||||
if (!fpsEl) {
|
||||
fpsEl = document.createElement("div");
|
||||
fpsEl.id = "fps";
|
||||
document.body.appendChild(fpsEl);
|
||||
}
|
||||
const fPerc = frameRate / 60;
|
||||
if (fPerc < 0.6) {
|
||||
state.optimizePerformance(fPerc);
|
||||
}
|
||||
fpsEl.textContent = frameRate.toFixed(1) + " fps";
|
||||
}, 1e3);
|
||||
var gameLoop = new GameLoop();
|
||||
gameLoop.start(state);
|
||||
})();
|
||||
|
@ -13,6 +13,6 @@
|
||||
"dev": "deno run -RWEN --allow-run dev.ts dev"
|
||||
},
|
||||
"imports": {
|
||||
"@bearmetal/doodler": "jsr:@bearmetal/doodler@0.0.5-a"
|
||||
"@bearmetal/doodler": "jsr:@bearmetal/doodler@0.0.5-b"
|
||||
}
|
||||
}
|
8
deno.lock
generated
8
deno.lock
generated
@ -1,7 +1,7 @@
|
||||
{
|
||||
"version": "4",
|
||||
"specifiers": {
|
||||
"jsr:@bearmetal/doodler@0.0.5-a": "0.0.5-a",
|
||||
"jsr:@bearmetal/doodler@0.0.5-b": "0.0.5-b",
|
||||
"jsr:@luca/esbuild-deno-loader@*": "0.11.0",
|
||||
"jsr:@luca/esbuild-deno-loader@0.11.1": "0.11.1",
|
||||
"jsr:@std/assert@*": "1.0.10",
|
||||
@ -25,8 +25,8 @@
|
||||
"@bearmetal/doodler@0.0.4": {
|
||||
"integrity": "b631083cff84994c513f70d1f09e6a9256edabcb224112c93a9ca6a87c88a389"
|
||||
},
|
||||
"@bearmetal/doodler@0.0.5-a": {
|
||||
"integrity": "c59d63f071623ad4c7588e24b464874786759e56a6b12782689251a5cf3a1c08"
|
||||
"@bearmetal/doodler@0.0.5-b": {
|
||||
"integrity": "94f265ea21162f943291526800de7f3f6560634a4fe762a38cd73892685b6742"
|
||||
},
|
||||
"@luca/esbuild-deno-loader@0.11.0": {
|
||||
"integrity": "c05a989aa7c4ee6992a27be5f15cfc5be12834cab7ff84cabb47313737c51a2c",
|
||||
@ -232,7 +232,7 @@
|
||||
},
|
||||
"workspace": {
|
||||
"dependencies": [
|
||||
"jsr:@bearmetal/doodler@0.0.5-a"
|
||||
"jsr:@bearmetal/doodler@0.0.5-b"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
17
main.ts
17
main.ts
@ -11,8 +11,9 @@ import { ResourceManager } from "./lib/resources.ts";
|
||||
import { addButton } from "./ui/button.ts";
|
||||
import { TrackSystem } from "./track/system.ts";
|
||||
import { StraightTrack } from "./track/shapes.ts";
|
||||
import { StateMachine } from "./state/machine.ts";
|
||||
import { State, StateMachine } from "./state/machine.ts";
|
||||
import { bootstrapGameStateMachine } from "./state/states/index.ts";
|
||||
import { GameLoop } from "./GameLoop.ts";
|
||||
|
||||
const inputManager = new InputManager();
|
||||
const resources = new ResourceManager();
|
||||
@ -50,9 +51,9 @@ setContextItem("state", state);
|
||||
|
||||
doodler.init();
|
||||
|
||||
doodler.createLayer((_, __, dTime) => {
|
||||
state.update(dTime);
|
||||
});
|
||||
// doodler.createLayer((_, __, dTime) => {
|
||||
// state.update(dTime);
|
||||
// });
|
||||
|
||||
document.addEventListener("keydown", (e) => {
|
||||
if ((e.ctrlKey || e.metaKey) && e.key === "s") {
|
||||
@ -66,11 +67,19 @@ document.addEventListener("keydown", (e) => {
|
||||
setInterval(() => {
|
||||
const doodler = getContextItem<Doodler>("doodler");
|
||||
const frameRate = doodler.fps;
|
||||
if (frameRate < 0.5) return;
|
||||
let fpsEl = document.getElementById("fps");
|
||||
if (!fpsEl) {
|
||||
fpsEl = document.createElement("div");
|
||||
fpsEl.id = "fps";
|
||||
document.body.appendChild(fpsEl);
|
||||
}
|
||||
const fPerc = frameRate / 60;
|
||||
if (fPerc < 0.6) {
|
||||
state.optimizePerformance(fPerc);
|
||||
}
|
||||
fpsEl.textContent = frameRate.toFixed(1) + " fps";
|
||||
}, 1000);
|
||||
|
||||
const gameLoop = new GameLoop();
|
||||
gameLoop.start(state);
|
||||
|
@ -1,3 +1,7 @@
|
||||
import { getContext } from "../lib/context.ts";
|
||||
import { TrackSystem } from "../track/system.ts";
|
||||
import { Train } from "../train.old.ts";
|
||||
|
||||
export class StateMachine<T> {
|
||||
private _states: Map<T, State<T>> = new Map();
|
||||
private currentState?: State<T>;
|
||||
@ -6,6 +10,13 @@ export class StateMachine<T> {
|
||||
this.currentState?.update(dt, ctx);
|
||||
}
|
||||
|
||||
optimizePerformance(percent: number) {
|
||||
const ctx = getContext() as { trains: Train[]; track: TrackSystem };
|
||||
if (percent < 0.5) {
|
||||
ctx.track.optimize(percent);
|
||||
}
|
||||
}
|
||||
|
||||
get current() {
|
||||
return this.currentState;
|
||||
}
|
||||
|
@ -33,6 +33,8 @@ export class EditTrackState extends State<States> {
|
||||
private ghostRotated = false;
|
||||
private closestEnd?: End;
|
||||
|
||||
layers: number[] = [];
|
||||
|
||||
override update(dt: number): void {
|
||||
const inputManager = getContextItem<InputManager>("inputManager");
|
||||
const track = getContextItem<TrackSystem>("track");
|
||||
@ -138,22 +140,6 @@ export class EditTrackState extends State<States> {
|
||||
this.ghostSegment = undefined;
|
||||
this.ghostRotated = false;
|
||||
}
|
||||
|
||||
this.selectedSegment?.draw();
|
||||
if (this.ghostSegment) {
|
||||
doodler.drawWithAlpha(0.5, () => {
|
||||
if (!this.ghostSegment) return;
|
||||
this.ghostSegment.draw();
|
||||
if (getContextItemOrDefault("debug", false)) {
|
||||
const colors = getContextItem<string[]>("colors");
|
||||
for (
|
||||
const [i, point] of this.ghostSegment.points.entries() ?? []
|
||||
) {
|
||||
doodler.fillCircle(point, 4, { color: colors[i + 3] });
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// manipulate only end of segment while maintaining length
|
||||
@ -263,13 +249,34 @@ export class EditTrackState extends State<States> {
|
||||
track.translate(translation);
|
||||
}
|
||||
|
||||
track.draw(true);
|
||||
// TODO
|
||||
// Draw ui
|
||||
// Draw track points
|
||||
// Draw track tangents
|
||||
}
|
||||
override start(): void {
|
||||
const doodler = getContextItem<Doodler>("doodler");
|
||||
this.layers.push(
|
||||
doodler.createLayer(() => {
|
||||
this.selectedSegment?.draw();
|
||||
if (this.ghostSegment) {
|
||||
doodler.drawWithAlpha(0.5, () => {
|
||||
if (!this.ghostSegment) return;
|
||||
this.ghostSegment.draw();
|
||||
if (getContextItemOrDefault("debug", false)) {
|
||||
const colors = getContextItem<string[]>("colors");
|
||||
for (
|
||||
const [i, point] of this.ghostSegment.points.entries() ?? []
|
||||
) {
|
||||
doodler.fillCircle(point, 4, { color: colors[i + 3] });
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
track.draw(true);
|
||||
}),
|
||||
);
|
||||
|
||||
setContextItem("trackSegments", [
|
||||
undefined,
|
||||
new StraightTrack(),
|
||||
@ -376,9 +383,19 @@ export class EditTrackState extends State<States> {
|
||||
|
||||
// TODO
|
||||
// Cache trains and save
|
||||
|
||||
// const trackCount = 2000;
|
||||
// for (let i = 0; i < trackCount; i++) {
|
||||
// const seg = new StraightTrack();
|
||||
// track.registerSegment(seg);
|
||||
// }
|
||||
}
|
||||
redoBuffer: TrackSegment[] = [];
|
||||
override stop(): void {
|
||||
for (const layer of this.layers) {
|
||||
getContextItem<Doodler>("doodler").deleteLayer(layer);
|
||||
}
|
||||
|
||||
const inputManager = getContextItem<InputManager>("inputManager");
|
||||
inputManager.offKey("e");
|
||||
inputManager.offKey("w");
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { Doodler, Vector } from "@bearmetal/doodler";
|
||||
import { bootstrapInputs } from "../../inputs.ts";
|
||||
import { setContextItem } from "../../lib/context.ts";
|
||||
import { getContextItem, setContextItem } from "../../lib/context.ts";
|
||||
import { InputManager } from "../../lib/input.ts";
|
||||
import { ResourceManager } from "../../lib/resources.ts";
|
||||
import { StraightTrack } from "../../track/shapes.ts";
|
||||
@ -13,6 +14,8 @@ export class LoadState extends State<States> {
|
||||
States.RUNNING,
|
||||
]);
|
||||
|
||||
layers: number[] = [];
|
||||
|
||||
override update(): void {
|
||||
// noop
|
||||
}
|
||||
@ -37,6 +40,14 @@ export class LoadState extends State<States> {
|
||||
resources.ready().then(() => {
|
||||
this.stateMachine.transitionTo(States.RUNNING);
|
||||
});
|
||||
|
||||
const doodler = getContextItem<Doodler>("doodler");
|
||||
this.layers.push(doodler.createLayer((_, __, dTime) => {
|
||||
doodler.clearRect(new Vector(0, 0), doodler.width, doodler.height);
|
||||
doodler.fillRect(new Vector(0, 0), doodler.width, doodler.height, {
|
||||
color: "#302040",
|
||||
});
|
||||
}));
|
||||
}
|
||||
override stop(): void {
|
||||
// noop
|
||||
|
@ -1,3 +1,4 @@
|
||||
import { Doodler } from "@bearmetal/doodler";
|
||||
import { getContext, getContextItem } from "../../lib/context.ts";
|
||||
import { InputManager } from "../../lib/input.ts";
|
||||
import { TrackSystem } from "../../track/system.ts";
|
||||
@ -14,26 +15,40 @@ export class RunningState extends State<States> {
|
||||
States.PAUSED,
|
||||
States.EDIT_TRACK,
|
||||
]);
|
||||
|
||||
layers: number[] = [];
|
||||
|
||||
override update(dt: number): void {
|
||||
const ctx = getContext() as { trains: Train[]; track: TrackSystem };
|
||||
// const ctx = getContext() as { trains: DotFollower[]; track: TrackSystem };
|
||||
const input = getContextItem<InputManager>("inputManager");
|
||||
// TODO
|
||||
// Update trains
|
||||
// Update world
|
||||
// Handle input
|
||||
// Draw (maybe via a layer system that syncs with doodler)
|
||||
ctx.track.draw();
|
||||
for (const train of ctx.trains) {
|
||||
// if (input.getKeyState("ArrowUp")) {
|
||||
// train.acceleration.x += 10;
|
||||
// }
|
||||
train.move(dt);
|
||||
train.draw();
|
||||
}
|
||||
// Monitor world events
|
||||
for (const train of ctx.trains) {
|
||||
train.move(dt);
|
||||
}
|
||||
}
|
||||
override start(): void {
|
||||
const doodler = getContextItem<Doodler>("doodler");
|
||||
this.layers.push(
|
||||
doodler.createLayer(() => {
|
||||
const track = getContextItem<TrackSystem>("track");
|
||||
track.draw();
|
||||
}),
|
||||
doodler.createLayer(() => {
|
||||
const trains = getContextItem<Train[]>("trains");
|
||||
for (const train of trains) {
|
||||
// if (input.getKeyState("ArrowUp")) {
|
||||
// train.acceleration.x += 10;
|
||||
// }
|
||||
train.draw();
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
// noop
|
||||
const inputManager = getContextItem<InputManager>("inputManager");
|
||||
const track = getContextItem<TrackSystem>("track");
|
||||
@ -46,7 +61,7 @@ export class RunningState extends State<States> {
|
||||
const train = new Train(track.path, [new RedEngine(), new Tender()]);
|
||||
ctx.trains.push(train);
|
||||
});
|
||||
// const trainCount = 1000;
|
||||
// const trainCount = 2000;
|
||||
// for (let i = 0; i < trainCount; i++) {
|
||||
// const train = new Train(track.path, [new RedEngine(), new Tender()]);
|
||||
// ctx.trains.push(train);
|
||||
@ -66,6 +81,8 @@ export class RunningState extends State<States> {
|
||||
});
|
||||
}
|
||||
override stop(): void {
|
||||
// noop
|
||||
for (const layer of this.layers) {
|
||||
getContextItem<Doodler>("doodler").deleteLayer(layer);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
113
track/system.ts
113
track/system.ts
@ -22,6 +22,13 @@ export class TrackSystem {
|
||||
return this.segments.values().toArray().pop();
|
||||
}
|
||||
|
||||
optimize(percent: number) {
|
||||
console.log("Optimizing track", percent * 100 / 4);
|
||||
for (const segment of this.segments.values()) {
|
||||
segment.recalculateRailPoints(Math.round(percent * 100 / 4));
|
||||
}
|
||||
}
|
||||
|
||||
registerSegment(segment: TrackSegment) {
|
||||
segment.setTrack(this);
|
||||
this.segments.set(segment.id, segment);
|
||||
@ -147,9 +154,14 @@ export class TrackSystem {
|
||||
|
||||
generatePath() {
|
||||
if (!this.firstSegment) throw new Error("No first segment");
|
||||
const flags = { looping: true };
|
||||
const rightOnlyPath = [
|
||||
this.firstSegment.copy(),
|
||||
...this.findRightPath(this.firstSegment, new Set([this.firstSegment.id])),
|
||||
...this.findRightPath(
|
||||
this.firstSegment,
|
||||
new Set([this.firstSegment.id]),
|
||||
flags,
|
||||
),
|
||||
];
|
||||
|
||||
rightOnlyPath.forEach((s, i, arr) => {
|
||||
@ -159,6 +171,14 @@ export class TrackSystem {
|
||||
s.prev = prev;
|
||||
prev.next = s;
|
||||
});
|
||||
if (flags.looping) {
|
||||
const first = rightOnlyPath[0];
|
||||
const last = rightOnlyPath[rightOnlyPath.length - 1];
|
||||
first.points[0] = last.points[3];
|
||||
last.points[3] = first.points[0];
|
||||
first.prev = last;
|
||||
last.next = first;
|
||||
}
|
||||
|
||||
return new Spline<TrackSegment>(rightOnlyPath);
|
||||
}
|
||||
@ -166,6 +186,7 @@ export class TrackSystem {
|
||||
*findRightPath(
|
||||
start: TrackSegment,
|
||||
seen: Set<string>,
|
||||
flags: { looping: boolean },
|
||||
): Generator<TrackSegment> {
|
||||
if (start.frontNeighbours.length === 0) {
|
||||
return;
|
||||
@ -187,14 +208,20 @@ export class TrackSystem {
|
||||
rightMost = segment;
|
||||
}
|
||||
}
|
||||
if (seen.has(rightMost.id)) return;
|
||||
if (seen.has(rightMost.id)) {
|
||||
if (seen.values().next().value === rightMost.id) {
|
||||
flags.looping = true;
|
||||
}
|
||||
return;
|
||||
}
|
||||
seen.add(rightMost.id);
|
||||
yield rightMost.copy();
|
||||
yield* this.findRightPath(rightMost, seen);
|
||||
yield* this.findRightPath(rightMost, seen, flags);
|
||||
}
|
||||
*findLeftPath(
|
||||
start: TrackSegment,
|
||||
seen: Set<string>,
|
||||
flags: { looping: boolean },
|
||||
): Generator<TrackSegment> {
|
||||
if (start.frontNeighbours.length === 0) {
|
||||
return;
|
||||
@ -216,10 +243,15 @@ export class TrackSystem {
|
||||
leftMost = segment;
|
||||
}
|
||||
}
|
||||
if (seen.has(leftMost.id)) return;
|
||||
if (seen.has(leftMost.id)) {
|
||||
if (seen.values().next().value === leftMost.id) {
|
||||
flags.looping = true;
|
||||
}
|
||||
return;
|
||||
}
|
||||
seen.add(leftMost.id);
|
||||
yield leftMost.copy();
|
||||
yield* this.findLeftPath(leftMost, seen);
|
||||
yield* this.findLeftPath(leftMost, seen, flags);
|
||||
}
|
||||
}
|
||||
|
||||
@ -232,11 +264,27 @@ export class TrackSegment extends PathSegment {
|
||||
track?: TrackSystem;
|
||||
|
||||
doodler: Doodler;
|
||||
normalPoints: Vector[] = [];
|
||||
antiNormalPoints: Vector[] = [];
|
||||
|
||||
constructor(p: VectorSet, id?: string) {
|
||||
super(p);
|
||||
this.doodler = getContextItem<Doodler>("doodler");
|
||||
this.id = id ?? crypto.randomUUID();
|
||||
this.recalculateRailPoints();
|
||||
}
|
||||
|
||||
recalculateRailPoints(resolution = 100) {
|
||||
this.normalPoints = [];
|
||||
this.antiNormalPoints = [];
|
||||
for (let i = 0; i <= resolution; i++) {
|
||||
const t = i / resolution;
|
||||
const normal = this.tangent(t).rotate(Math.PI / 2);
|
||||
normal.setMag(6);
|
||||
const p = this.getPointAtT(t);
|
||||
this.normalPoints.push(p.copy().add(normal));
|
||||
this.antiNormalPoints.push(p.copy().add(normal.rotate(Math.PI)));
|
||||
}
|
||||
}
|
||||
|
||||
setTrack(t: TrackSystem) {
|
||||
@ -299,21 +347,17 @@ export class TrackSegment extends PathSegment {
|
||||
// });
|
||||
});
|
||||
}
|
||||
const lineResolution = 100;
|
||||
const normalPoints: Vector[] = [];
|
||||
const antiNormalPoints: Vector[] = [];
|
||||
for (let i = 0; i <= lineResolution; i++) {
|
||||
const t = i / lineResolution;
|
||||
const normal = this.tangent(t).rotate(Math.PI / 2);
|
||||
normal.setMag(6);
|
||||
const p = this.getPointAtT(t);
|
||||
normalPoints.push(p.copy().add(normal));
|
||||
antiNormalPoints.push(p.copy().add(normal.rotate(Math.PI)));
|
||||
}
|
||||
|
||||
this.doodler.deferDrawing(
|
||||
() => {
|
||||
this.doodler.drawLine(normalPoints, { color: "grey" });
|
||||
this.doodler.drawLine(antiNormalPoints, { color: "grey" });
|
||||
this.doodler.drawLine(this.normalPoints, {
|
||||
color: "grey",
|
||||
weight: 1.5,
|
||||
});
|
||||
this.doodler.drawLine(this.antiNormalPoints, {
|
||||
color: "grey",
|
||||
weight: 1.5,
|
||||
});
|
||||
},
|
||||
);
|
||||
// this.doodler.drawCircle(p, 2, {
|
||||
@ -438,21 +482,24 @@ export class Spline<T extends PathSegment = PathSegment> {
|
||||
|
||||
constructor(segs: T[]) {
|
||||
this.segments = segs;
|
||||
if (this.segments.at(-1)?.next === this.segments[0]) {
|
||||
this.looped = true;
|
||||
}
|
||||
this.pointSpacing = 1;
|
||||
this.evenPoints = this.calculateEvenlySpacedPoints(1);
|
||||
this.nodes = [];
|
||||
for (let i = 0; i < this.points.length; i += 3) {
|
||||
const node: IControlNode = {
|
||||
anchor: this.points[i],
|
||||
controls: [
|
||||
this.points.at(i - 1)!,
|
||||
this.points[(i + 1) % this.points.length],
|
||||
],
|
||||
mirrored: false,
|
||||
tangent: true,
|
||||
};
|
||||
this.nodes.push(node);
|
||||
}
|
||||
// for (let i = 0; i < this.points.length; i += 3) {
|
||||
// const node: IControlNode = {
|
||||
// anchor: this.points[i],
|
||||
// controls: [
|
||||
// this.points.at(i - 1)!,
|
||||
// this.points[(i + 1) % this.points.length],
|
||||
// ],
|
||||
// mirrored: false,
|
||||
// tangent: true,
|
||||
// };
|
||||
// this.nodes.push(node);
|
||||
// }
|
||||
}
|
||||
|
||||
// setContext(ctx: CanvasRenderingContext2D) {
|
||||
@ -464,7 +511,11 @@ export class Spline<T extends PathSegment = PathSegment> {
|
||||
|
||||
draw() {
|
||||
for (const segment of this.segments) {
|
||||
segment.draw();
|
||||
// segment.draw();
|
||||
const doodler = getContextItem<Doodler>("doodler");
|
||||
doodler.drawWithAlpha(0.5, () => {
|
||||
doodler.drawBezier(...segment.points, { color: "red" });
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user