@dryanovski/gamefoo 0.2.3 → 0.2.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/core/animate.js +147 -0
- package/dist/core/asset.js +74 -0
- package/dist/core/behaviour.js +88 -0
- package/dist/core/behaviours/collidable.js +186 -0
- package/dist/core/behaviours/control.js +75 -0
- package/dist/core/behaviours/healtkit.js +153 -0
- package/dist/core/behaviours/sprite_render.js +193 -0
- package/dist/core/camera.js +134 -0
- package/dist/core/engine.d.ts +1 -1
- package/dist/core/engine.d.ts.map +1 -1
- package/dist/core/engine.js +527 -0
- package/dist/core/fonts/font_bitmap.js +205 -0
- package/dist/core/fonts/font_bitmap_prebuild.js +137 -0
- package/dist/core/fonts/internal/font_3x5.js +169 -0
- package/dist/core/fonts/internal/font_4x6.js +171 -0
- package/dist/core/fonts/internal/font_5x5.js +129 -0
- package/dist/core/fonts/internal/font_6x8.js +171 -0
- package/dist/core/fonts/internal/font_8x13.js +171 -0
- package/dist/core/fonts/internal/font_8x8.js +171 -0
- package/dist/core/game_object_register.js +134 -0
- package/dist/core/input.js +170 -0
- package/dist/core/sprite.js +222 -0
- package/dist/core/utils/perlin_noise.js +183 -0
- package/dist/core/world.js +304 -0
- package/dist/debug/monitor.js +47 -0
- package/dist/decorators/index.js +1 -0
- package/dist/decorators/log.js +42 -0
- package/dist/entities/dynamic_entity.js +99 -0
- package/dist/entities/entity.js +283 -0
- package/dist/entities/player.js +93 -0
- package/dist/entities/text.js +62 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +47 -29
- package/dist/subsystems/camera_system.d.ts +2 -2
- package/dist/subsystems/camera_system.d.ts.map +1 -1
- package/dist/subsystems/camera_system.js +41 -0
- package/dist/subsystems/collision_system.js +17 -0
- package/dist/subsystems/monitor_system.js +20 -0
- package/dist/subsystems/object_system.d.ts +1 -1
- package/dist/subsystems/object_system.d.ts.map +1 -1
- package/dist/subsystems/object_system.js +29 -0
- package/dist/subsystems/types.d.ts +1 -1
- package/dist/subsystems/types.d.ts.map +1 -1
- package/dist/subsystems/types.js +0 -0
- package/dist/types.js +10 -0
- package/package.json +17 -6
- package/src/core/animate.ts +159 -0
- package/src/core/asset.ts +76 -0
- package/src/core/behaviour.ts +145 -0
- package/src/core/behaviours/collidable.ts +296 -0
- package/src/core/behaviours/control.ts +80 -0
- package/src/core/behaviours/healtkit.ts +166 -0
- package/src/core/behaviours/sprite_render.ts +216 -0
- package/src/core/camera.ts +145 -0
- package/src/core/engine.ts +607 -0
- package/src/core/fonts/font_bitmap.ts +232 -0
- package/src/core/fonts/font_bitmap_prebuild.ts +141 -0
- package/src/core/fonts/internal/font_3x5.ts +178 -0
- package/src/core/fonts/internal/font_4x6.ts +180 -0
- package/src/core/fonts/internal/font_5x5.ts +137 -0
- package/src/core/fonts/internal/font_6x8.ts +180 -0
- package/src/core/fonts/internal/font_8x13.ts +180 -0
- package/src/core/fonts/internal/font_8x8.ts +180 -0
- package/src/core/game_object_register.ts +146 -0
- package/src/core/input.ts +182 -0
- package/src/core/sprite.ts +339 -0
- package/src/core/utils/perlin_noise.ts +196 -0
- package/src/core/world.ts +331 -0
- package/src/debug/monitor.ts +60 -0
- package/src/decorators/index.ts +1 -0
- package/src/decorators/log.ts +45 -0
- package/src/entities/dynamic_entity.ts +106 -0
- package/src/entities/entity.ts +322 -0
- package/src/entities/player.ts +99 -0
- package/src/entities/text.ts +72 -0
- package/src/index.ts +51 -0
- package/src/subsystems/camera_system.ts +52 -0
- package/src/subsystems/collision_system.ts +21 -0
- package/src/subsystems/monitor_system.ts +26 -0
- package/src/subsystems/object_system.ts +37 -0
- package/src/subsystems/types.ts +46 -0
- package/src/types.ts +178 -0
- package/dist/index.js.map +0 -9
|
@@ -0,0 +1,607 @@
|
|
|
1
|
+
import type { SubSystem } from "../subsystems/types";
|
|
2
|
+
|
|
3
|
+
const DEFAULT_GAME_SCALE = 1;
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Configuration options for the {@link Engine}.
|
|
7
|
+
*
|
|
8
|
+
* Passed to the `Engine` constructor to customise visual defaults.
|
|
9
|
+
* Every property is optional; sensible defaults are applied internally.
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* ```ts
|
|
13
|
+
* const config: EngineConfig = {
|
|
14
|
+
* backgroundColor: "#1a1a2e",
|
|
15
|
+
* };
|
|
16
|
+
*
|
|
17
|
+
* const engine = new Engine("game", 800, 600, config);
|
|
18
|
+
* ```
|
|
19
|
+
*/
|
|
20
|
+
interface EngineConfig {
|
|
21
|
+
/**
|
|
22
|
+
* The CSS colour string used to clear the canvas each frame.
|
|
23
|
+
*
|
|
24
|
+
* Accepts any value valid for {@link CanvasRenderingContext2D.fillStyle}
|
|
25
|
+
* (hex, `rgb()`, `hsl()`, named colours, etc.).
|
|
26
|
+
*
|
|
27
|
+
* @defaultValue `"#000000"`
|
|
28
|
+
*/
|
|
29
|
+
backgroundColor?: string;
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Global scale factor applied to the canvas via CSS transforms.
|
|
33
|
+
*
|
|
34
|
+
* @defaultValue `1` (no scaling)
|
|
35
|
+
*/
|
|
36
|
+
gameScale?: number;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Core game engine responsible for the game loop, rendering pipeline,
|
|
41
|
+
* entity management, collision detection, and camera tracking.
|
|
42
|
+
*
|
|
43
|
+
* `Engine` owns a single HTML `<canvas>` element and drives a
|
|
44
|
+
* `requestAnimationFrame`-based loop that, on every tick:
|
|
45
|
+
*
|
|
46
|
+
* 1. Computes **deltaTime** (seconds since the previous frame).
|
|
47
|
+
* 2. Calls {@link Engine.update | update} — advances all entities and runs
|
|
48
|
+
* collision detection via the internal {@link World}.
|
|
49
|
+
* 3. Calls {@link Engine.render | render} — clears the canvas and draws every
|
|
50
|
+
* registered entity.
|
|
51
|
+
*
|
|
52
|
+
* ---
|
|
53
|
+
*
|
|
54
|
+
* ### Lifecycle
|
|
55
|
+
*
|
|
56
|
+
* ```text
|
|
57
|
+
* new Engine() — creates canvas context, camera, object register, world
|
|
58
|
+
* │
|
|
59
|
+
* ▼
|
|
60
|
+
* setup(fn) — runs the user-supplied initialiser, then starts the loop
|
|
61
|
+
* │
|
|
62
|
+
* ▼
|
|
63
|
+
* ┌─► loop(timestamp) — called every frame via requestAnimationFrame
|
|
64
|
+
* │ ├─ update(dt)
|
|
65
|
+
* │ └─ render()
|
|
66
|
+
* │ │
|
|
67
|
+
* └─────────┘
|
|
68
|
+
* │
|
|
69
|
+
* pause() / clear() / destroy() — stops or tears down the engine
|
|
70
|
+
* ```
|
|
71
|
+
*
|
|
72
|
+
* ---
|
|
73
|
+
*
|
|
74
|
+
* ### Minimal example
|
|
75
|
+
*
|
|
76
|
+
* ```ts
|
|
77
|
+
* import { Engine, Player } from "gamefoo";
|
|
78
|
+
*
|
|
79
|
+
* const engine = new Engine("game", 800, 600, {
|
|
80
|
+
* backgroundColor: "#1a1a2e",
|
|
81
|
+
* });
|
|
82
|
+
*
|
|
83
|
+
* const player = new Player("hero", 400, 300, 50, 50);
|
|
84
|
+
* engine.player = player;
|
|
85
|
+
*
|
|
86
|
+
* engine.setup(() => {
|
|
87
|
+
* console.log("Game started!");
|
|
88
|
+
* });
|
|
89
|
+
* ```
|
|
90
|
+
*
|
|
91
|
+
* ### Adding game objects
|
|
92
|
+
*
|
|
93
|
+
* ```ts
|
|
94
|
+
* import { Engine, DynamicEntity } from "gamefoo";
|
|
95
|
+
*
|
|
96
|
+
* const engine = new Engine("game", 800, 600, {});
|
|
97
|
+
*
|
|
98
|
+
* class Crate extends DynamicEntity {
|
|
99
|
+
* constructor(x: number, y: number) {
|
|
100
|
+
* super("crate", x, y, 32, 32);
|
|
101
|
+
* }
|
|
102
|
+
* override update(_dt: number) {}
|
|
103
|
+
* override render(ctx: CanvasRenderingContext2D) {
|
|
104
|
+
* ctx.fillStyle = "#8B4513";
|
|
105
|
+
* ctx.fillRect(this.x, this.y, 32, 32);
|
|
106
|
+
* }
|
|
107
|
+
* }
|
|
108
|
+
*
|
|
109
|
+
* engine.attachObjects(new Crate(200, 150));
|
|
110
|
+
*
|
|
111
|
+
* engine.setup(() => {
|
|
112
|
+
* console.log("Crate placed!");
|
|
113
|
+
* });
|
|
114
|
+
* ```
|
|
115
|
+
*
|
|
116
|
+
* @see {@link Camera} — viewport tracking
|
|
117
|
+
* @see {@link GameObjectRegister} — entity storage
|
|
118
|
+
* @see {@link World} — collision detection
|
|
119
|
+
*/
|
|
120
|
+
export default class Engine {
|
|
121
|
+
/**
|
|
122
|
+
* The underlying `<canvas>` DOM element retrieved by its `id` during
|
|
123
|
+
* construction.
|
|
124
|
+
*/
|
|
125
|
+
private canvas: HTMLCanvasElement;
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* The 2-D rendering context obtained from {@link Engine.canvas}.
|
|
129
|
+
* Used by {@link Engine.render} and exposed indirectly to game objects.
|
|
130
|
+
*/
|
|
131
|
+
private ctx: CanvasRenderingContext2D;
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Timestamp (in milliseconds) of the previous animation frame.
|
|
135
|
+
* Used internally to calculate `deltaTime` between frames.
|
|
136
|
+
*
|
|
137
|
+
* Reset to `0` when the engine is set up via {@link Engine.setup}.
|
|
138
|
+
*/
|
|
139
|
+
private lastTime: number = 0;
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* The logical width of the game world (and the canvas), in pixels.
|
|
143
|
+
*
|
|
144
|
+
* Set once during construction and also used by the {@link Camera}.
|
|
145
|
+
*/
|
|
146
|
+
private width: number;
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* The logical height of the game world (and the canvas), in pixels.
|
|
150
|
+
*
|
|
151
|
+
* Set once during construction and also used by the {@link Camera}.
|
|
152
|
+
*/
|
|
153
|
+
private height: number;
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Guards against calling {@link Engine.setup} more than once.
|
|
157
|
+
* Flipped to `true` after the first successful setup invocation.
|
|
158
|
+
*/
|
|
159
|
+
private _initialized: boolean = false;
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Whether the game loop is actively requesting new frames.
|
|
163
|
+
*
|
|
164
|
+
* - Set to `true` inside {@link Engine.setup}.
|
|
165
|
+
* - Set to `false` by {@link Engine.pause} or {@link Engine.clear}.
|
|
166
|
+
*/
|
|
167
|
+
private running: boolean = false;
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Merged engine configuration. Combines user-supplied values with internal
|
|
171
|
+
* defaults (`backgroundColor: "#000000"`).
|
|
172
|
+
*/
|
|
173
|
+
private cnf: EngineConfig = {
|
|
174
|
+
backgroundColor: "#000000",
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Global scale factor applied to the canvas via CSS transforms.
|
|
178
|
+
*/
|
|
179
|
+
gameScale: 1,
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Global scale factor applied to the canvas via CSS transforms.
|
|
184
|
+
*
|
|
185
|
+
* This does not affect the internal resolution (`width` / `height`) or the
|
|
186
|
+
* camera viewport, but simply scales the rendered output for display.
|
|
187
|
+
*
|
|
188
|
+
*/
|
|
189
|
+
public gameScale: number = DEFAULT_GAME_SCALE;
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* The main subsystems of the engine, responsible for core functionalities
|
|
193
|
+
* like rendering, object management, collision detection, and monitoring.
|
|
194
|
+
*
|
|
195
|
+
* Subsystems are executed in a specific order determined by their `order` property
|
|
196
|
+
*/
|
|
197
|
+
private subsystems: SubSystem[] = [];
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Creates a new `Engine` instance, binds it to a `<canvas>` element,
|
|
201
|
+
* and initialises the camera, object register, and collision world.
|
|
202
|
+
*
|
|
203
|
+
* @param canvasId - The DOM `id` attribute of the `<canvas>` element to
|
|
204
|
+
* render into. Must already exist in the document.
|
|
205
|
+
* @param width - Logical width of the game area in pixels. Sets both
|
|
206
|
+
* `canvas.width` and the camera viewport width.
|
|
207
|
+
* @param height - Logical height of the game area in pixels. Sets both
|
|
208
|
+
* `canvas.height` and the camera viewport height.
|
|
209
|
+
* @param config - Optional overrides for engine-level settings.
|
|
210
|
+
* See {@link EngineConfig}.
|
|
211
|
+
*
|
|
212
|
+
* @throws {Error} If no 2-D rendering context can be obtained from the
|
|
213
|
+
* canvas (e.g. the browser does not support Canvas 2D).
|
|
214
|
+
*
|
|
215
|
+
* @example
|
|
216
|
+
* ```ts
|
|
217
|
+
* const engine = new Engine("game", 800, 600, {
|
|
218
|
+
* backgroundColor: "#1a1a2e",
|
|
219
|
+
* });
|
|
220
|
+
* ```
|
|
221
|
+
*/
|
|
222
|
+
constructor(canvasId: string, width: number, height: number, config: EngineConfig) {
|
|
223
|
+
this.canvas = document.getElementById(canvasId) as HTMLCanvasElement;
|
|
224
|
+
this.height = height;
|
|
225
|
+
this.width = width;
|
|
226
|
+
this.canvas.width = width;
|
|
227
|
+
this.canvas.height = height;
|
|
228
|
+
const context = this.canvas.getContext("2d");
|
|
229
|
+
if (!context) {
|
|
230
|
+
throw new Error("Failed to get 2D context");
|
|
231
|
+
}
|
|
232
|
+
this.ctx = context;
|
|
233
|
+
|
|
234
|
+
this.cnf = { ...this.cnf, ...config };
|
|
235
|
+
|
|
236
|
+
this.gameScale = this.cnf.gameScale || DEFAULT_GAME_SCALE;
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Make sure to always pixelate the canvas to preserve crisp edges for pixel art.
|
|
240
|
+
*/
|
|
241
|
+
this.canvas.style.imageRendering = "pixelated";
|
|
242
|
+
this.canvas.style.imageRendering = "-moz-crisp-edges";
|
|
243
|
+
this.canvas.style.imageRendering = "crisp-edges";
|
|
244
|
+
|
|
245
|
+
this.canvas.style.width = `${this.width * this.gameScale}px`;
|
|
246
|
+
this.canvas.style.height = `${this.height * this.gameScale}px`;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Attaches a subsystem to the engine, making it part of the update and render cycles.
|
|
251
|
+
*
|
|
252
|
+
* Subsystems are executed in a specific order determined by their `order` property (lower values run first).
|
|
253
|
+
*
|
|
254
|
+
* @since 0.2.0
|
|
255
|
+
*
|
|
256
|
+
* @param subsystem - The subsystem instance to attach. Must implement the {@link SubSystem} interface.
|
|
257
|
+
*
|
|
258
|
+
* @returns The engine instance, allowing for method chaining.
|
|
259
|
+
*
|
|
260
|
+
* @example
|
|
261
|
+
* ```ts
|
|
262
|
+
* engine.use(new ObjectSystem());
|
|
263
|
+
* engine.use(new CollisionSystem());
|
|
264
|
+
* ```
|
|
265
|
+
*/
|
|
266
|
+
use(subsystem: SubSystem): this {
|
|
267
|
+
this.subsystems.push(subsystem);
|
|
268
|
+
/**
|
|
269
|
+
* Sort subsystems by their `order` property (defaulting to `100` if not specified) to ensure they run in the correct sequence.
|
|
270
|
+
*/
|
|
271
|
+
this.subsystems.sort((a, b) => (a.order || 100) - (b.order || 100));
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Call the `init` method of the subsystem if it exists, passing the engine instance.
|
|
275
|
+
* This allows subsystems to perform any necessary setup that depends on the engine
|
|
276
|
+
* being available.
|
|
277
|
+
*/
|
|
278
|
+
subsystem.init?.(this);
|
|
279
|
+
return this;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* Runs a specific lifecycle hook on all enabled subsystems that implement it.
|
|
284
|
+
*
|
|
285
|
+
* This method is used internally by the game loop to delegate update and render calls to subsystems.
|
|
286
|
+
*
|
|
287
|
+
* @param hook - The name of the lifecycle method to invoke (e.g. "update", "render").
|
|
288
|
+
* @param args - Arguments to pass to the subsystem methods (e.g. `deltaTime` for updates, `ctx` for rendering).
|
|
289
|
+
*
|
|
290
|
+
* @remarks
|
|
291
|
+
* The method iterates over all subsystems, checks if they are enabled, and if they implement the specified hook. If so, it calls the hook method with the provided arguments.
|
|
292
|
+
* This allows for a flexible and modular architecture where subsystems can independently implement the lifecycle methods they need without being tightly coupled to the engine's core logic.
|
|
293
|
+
*
|
|
294
|
+
* @example
|
|
295
|
+
* ```ts
|
|
296
|
+
* // During the update phase of the game loop:
|
|
297
|
+
* this.run("update", deltaTime);
|
|
298
|
+
*
|
|
299
|
+
* // During the render phase of the game loop:
|
|
300
|
+
* this.run("render", ctx);
|
|
301
|
+
* ```
|
|
302
|
+
*/
|
|
303
|
+
private run<K extends keyof SubSystem>(hook: K, ...args: any[]): void {
|
|
304
|
+
for (const subsystem of this.subsystems) {
|
|
305
|
+
/**
|
|
306
|
+
* Check if the subsystem is enabled before attempting to call the hook method. If `enabled` is explicitly set to `false`, skip this subsystem.
|
|
307
|
+
*/
|
|
308
|
+
if (subsystem.enabled === false) continue;
|
|
309
|
+
const fun = subsystem[hook];
|
|
310
|
+
|
|
311
|
+
if (typeof fun === "function") {
|
|
312
|
+
// @ts-expect-error
|
|
313
|
+
fun.apply(subsystem, args);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* Resizes the canvas via CSS transforms so it fits within its parent
|
|
320
|
+
* container while preserving the original aspect ratio.
|
|
321
|
+
*
|
|
322
|
+
* Call this in a `window` `resize` event listener to keep the game
|
|
323
|
+
* centred and properly scaled in responsive layouts.
|
|
324
|
+
*
|
|
325
|
+
* @remarks
|
|
326
|
+
* The method calculates the uniform scale factor from the container
|
|
327
|
+
* dimensions and centres the canvas with a CSS `translate + scale`
|
|
328
|
+
* transform. The internal resolution (`width` / `height`) remains
|
|
329
|
+
* unchanged.
|
|
330
|
+
*
|
|
331
|
+
* @example
|
|
332
|
+
* ```ts
|
|
333
|
+
* window.addEventListener("resize", () => engine.handleResize());
|
|
334
|
+
* ```
|
|
335
|
+
*/
|
|
336
|
+
handleResize() {
|
|
337
|
+
if (!this.canvas) return;
|
|
338
|
+
|
|
339
|
+
const container = this.canvas.parentElement;
|
|
340
|
+
if (!container) return;
|
|
341
|
+
|
|
342
|
+
const containerWidth = container.clientWidth;
|
|
343
|
+
const containerHeight = container.clientHeight;
|
|
344
|
+
|
|
345
|
+
const scaleX = containerWidth / this.width;
|
|
346
|
+
const scaleY = containerHeight / this.height;
|
|
347
|
+
const scale = Math.min(scaleX, scaleY);
|
|
348
|
+
const offsetX = (containerWidth - this.width * scale) / 2;
|
|
349
|
+
const offsetY = (containerHeight - this.height * scale) / 2;
|
|
350
|
+
|
|
351
|
+
this.canvas.style.transform = `translate(${offsetX}px, ${offsetY}px) scale(${scale})`;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/**
|
|
355
|
+
* Directly sets the canvas pixel dimensions.
|
|
356
|
+
*
|
|
357
|
+
* Unlike {@link Engine.handleResize}, this changes the **actual**
|
|
358
|
+
* resolution of the canvas (clearing its contents in the process).
|
|
359
|
+
*
|
|
360
|
+
* @param width - New canvas width in pixels.
|
|
361
|
+
* @param height - New canvas height in pixels.
|
|
362
|
+
*
|
|
363
|
+
* @example
|
|
364
|
+
* ```ts
|
|
365
|
+
* engine.resize(1024, 768);
|
|
366
|
+
* ```
|
|
367
|
+
*/
|
|
368
|
+
resize(width: number, height: number): void {
|
|
369
|
+
this.canvas.width = width;
|
|
370
|
+
this.canvas.height = height;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
/**
|
|
374
|
+
* Binds the `loop` method to the current `this` context so it can be passed directly to
|
|
375
|
+
* `requestAnimationFrame` without losing the correct reference to the engine instance.
|
|
376
|
+
*
|
|
377
|
+
* Also prevent the need of GC to create a new function on each frame, which could lead to performance issues over time.
|
|
378
|
+
*/
|
|
379
|
+
private boundLoop = this.loop.bind(this);
|
|
380
|
+
|
|
381
|
+
/**
|
|
382
|
+
* The core animation loop driven by `requestAnimationFrame`.
|
|
383
|
+
*
|
|
384
|
+
* Each iteration:
|
|
385
|
+
* 1. Computes **deltaTime** (seconds since the last frame).
|
|
386
|
+
* 2. Delegates to {@link Engine.update} and {@link Engine.render}.
|
|
387
|
+
* 3. Schedules itself for the next frame (unless `running` is `false`).
|
|
388
|
+
*
|
|
389
|
+
* @param timestamp - The high-resolution timestamp provided by
|
|
390
|
+
* `requestAnimationFrame`, in milliseconds.
|
|
391
|
+
*
|
|
392
|
+
* @internal
|
|
393
|
+
*/
|
|
394
|
+
private loop(timestamp: number) {
|
|
395
|
+
/**
|
|
396
|
+
* Prevent the loop from running if the engine is paused or cleared. The loop
|
|
397
|
+
* is only restarted by calling {@link Engine.setup} on a new engine instance.
|
|
398
|
+
*/
|
|
399
|
+
if (!this.running) {
|
|
400
|
+
return;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
/**
|
|
404
|
+
* Calculate deltaTime (in seconds) since the last frame. On the first frame,
|
|
405
|
+
* `lastTime` is `0`, so we set it to the current timestamp to avoid a large
|
|
406
|
+
* delta on the first update.
|
|
407
|
+
*/
|
|
408
|
+
if (this.lastTime === 0) {
|
|
409
|
+
this.lastTime = timestamp;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
/**
|
|
413
|
+
* @remarks
|
|
414
|
+
* - `timestamp` is provided by `requestAnimationFrame` and is in milliseconds. Dividing by `1000`
|
|
415
|
+
* converts it to seconds, which is a more common unit for game logic.
|
|
416
|
+
* - The first frame is handled specially to avoid a large deltaTime value that would occur if we calculated it as `(timestamp - 0) / 1000`.
|
|
417
|
+
* - This allows the game loop to adapt to varying frame rates, ensuring that game logic runs at a consistent pace regardless of how fast or slow the frames are rendered.
|
|
418
|
+
* - In summary, this block of code is crucial for maintaining smooth and consistent game updates by accurately tracking the time elapsed between frames and providing that information to the update logic.
|
|
419
|
+
*/
|
|
420
|
+
const deltaTime = (timestamp - this.lastTime) / 1000;
|
|
421
|
+
this.lastTime = timestamp;
|
|
422
|
+
|
|
423
|
+
this.run("preUpdate", deltaTime);
|
|
424
|
+
this.run("update", deltaTime);
|
|
425
|
+
this.run("postUpdate", deltaTime);
|
|
426
|
+
|
|
427
|
+
/**
|
|
428
|
+
* Call the main `update` method of the engine, which can be overridden by subclasses to
|
|
429
|
+
* implement custom update logic that runs every frame.
|
|
430
|
+
* This is where you would put any global game logic that needs to run independently of
|
|
431
|
+
* individual game objects.
|
|
432
|
+
*/
|
|
433
|
+
this.update(deltaTime);
|
|
434
|
+
|
|
435
|
+
/**
|
|
436
|
+
* Clear the canvas at the start of each frame to prepare for fresh rendering.
|
|
437
|
+
*/
|
|
438
|
+
this.clearScrean();
|
|
439
|
+
|
|
440
|
+
/**
|
|
441
|
+
* Call the main `render` method of the engine, which can be overridden by subclasses to
|
|
442
|
+
* implement custom rendering logic that runs every frame.
|
|
443
|
+
*/
|
|
444
|
+
this.render(this.ctx);
|
|
445
|
+
|
|
446
|
+
this.run("preRender", this.ctx);
|
|
447
|
+
this.run("render", this.ctx);
|
|
448
|
+
this.run("postRender", this.ctx);
|
|
449
|
+
|
|
450
|
+
/**
|
|
451
|
+
* Schedule the next frame by calling `requestAnimationFrame` with the `loop` method as the callback.
|
|
452
|
+
* This creates a continuous animation loop that keeps the game running until `running` is set to `false`.
|
|
453
|
+
*/
|
|
454
|
+
requestAnimationFrame(this.boundLoop);
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
/**
|
|
458
|
+
* Initialises the engine and starts the game loop.
|
|
459
|
+
*
|
|
460
|
+
* The supplied `setupFn` callback is invoked **synchronously** before the
|
|
461
|
+
* first frame. Use it to perform any last-minute setup that depends on
|
|
462
|
+
* the engine being ready (e.g. spawning initial entities, binding UI).
|
|
463
|
+
*
|
|
464
|
+
* Calling `setup` more than once is a no-op — a warning is logged to the
|
|
465
|
+
* console and the method returns immediately.
|
|
466
|
+
*
|
|
467
|
+
* @param setupFn - (optional) A synchronous callback executed once before the loop
|
|
468
|
+
* begins. Typically used for scene initialisation.
|
|
469
|
+
*
|
|
470
|
+
* @example
|
|
471
|
+
* ```ts
|
|
472
|
+
* engine.setup(() => {
|
|
473
|
+
* console.log("Engine initialised — first frame incoming!");
|
|
474
|
+
* });
|
|
475
|
+
* ```
|
|
476
|
+
*
|
|
477
|
+
* @example
|
|
478
|
+
* ```ts
|
|
479
|
+
* // Attempting a second setup is safely ignored:
|
|
480
|
+
* engine.setup(() => {}); // warns: "Engine is already initialized."
|
|
481
|
+
* ```
|
|
482
|
+
*/
|
|
483
|
+
public async setup(setupFn?: () => void) {
|
|
484
|
+
if (this._initialized) {
|
|
485
|
+
console.warn("Engine is already initialized.");
|
|
486
|
+
return;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
/**
|
|
490
|
+
* Handle automatically the resize of the viewport
|
|
491
|
+
*/
|
|
492
|
+
if (window && typeof window.addEventListener === "function") {
|
|
493
|
+
window.addEventListener("resize", () => this.handleResize());
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
this.lastTime = 0;
|
|
497
|
+
this.clearScrean();
|
|
498
|
+
|
|
499
|
+
/**
|
|
500
|
+
* Setup function is optional and can be used
|
|
501
|
+
* to perform any last-minute initialisation that depends on the
|
|
502
|
+
* engine being ready (e.g. spawning initial entities, binding UI).
|
|
503
|
+
*/
|
|
504
|
+
if (typeof setupFn === "function") {
|
|
505
|
+
setupFn();
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
this._initialized = true;
|
|
509
|
+
this.running = true;
|
|
510
|
+
requestAnimationFrame(this.boundLoop);
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
/**
|
|
514
|
+
* Advances the game state by one tick.
|
|
515
|
+
*
|
|
516
|
+
* Called automatically by the game loop, but is `public` so it can be
|
|
517
|
+
* invoked manually for deterministic / test-driven updates.
|
|
518
|
+
*
|
|
519
|
+
*
|
|
520
|
+
* @param deltaTime - Time elapsed since the last frame, **in seconds**.
|
|
521
|
+
*
|
|
522
|
+
* @example
|
|
523
|
+
* ```ts
|
|
524
|
+
* // Manual update (useful for unit testing):
|
|
525
|
+
* engine.update(1 / 60); // simulate a single 60 FPS tick
|
|
526
|
+
* ```
|
|
527
|
+
*/
|
|
528
|
+
public update(_deltaTime: number) {}
|
|
529
|
+
|
|
530
|
+
/**
|
|
531
|
+
* Draws one complete frame to the canvas.
|
|
532
|
+
*
|
|
533
|
+
* Called automatically after {@link Engine.update} in the game loop, but
|
|
534
|
+
* is `public` for manual / debug rendering.
|
|
535
|
+
*
|
|
536
|
+
* @example
|
|
537
|
+
* ```ts
|
|
538
|
+
* // Force a single frame repaint:
|
|
539
|
+
* engine.render((ctx) => {
|
|
540
|
+
* ctx.fillStyle = "red";
|
|
541
|
+
* });
|
|
542
|
+
* ```
|
|
543
|
+
*/
|
|
544
|
+
public render(_ctx: CanvasRenderingContext2D) {}
|
|
545
|
+
|
|
546
|
+
/**
|
|
547
|
+
* Pauses the game loop.
|
|
548
|
+
*
|
|
549
|
+
* The current frame finishes, but no further frames are scheduled.
|
|
550
|
+
* The canvas retains its last rendered state.
|
|
551
|
+
*
|
|
552
|
+
* Resume by calling {@link Engine.setup} on a **new** engine instance
|
|
553
|
+
* (the current instance cannot be restarted after pausing because
|
|
554
|
+
* `_initialized` remains `true`).
|
|
555
|
+
*
|
|
556
|
+
* @example
|
|
557
|
+
* ```ts
|
|
558
|
+
* document.addEventListener("visibilitychange", () => {
|
|
559
|
+
* if (document.hidden) engine.pause();
|
|
560
|
+
* });
|
|
561
|
+
* ```
|
|
562
|
+
*/
|
|
563
|
+
public pause() {
|
|
564
|
+
this.running = false;
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
/**
|
|
568
|
+
* Clear the screen and set default background colour.
|
|
569
|
+
*
|
|
570
|
+
* @example
|
|
571
|
+
* ```ts
|
|
572
|
+
* engine.clearScrean();
|
|
573
|
+
* // Canvas is now blank; loop is stopped.
|
|
574
|
+
* ```
|
|
575
|
+
*/
|
|
576
|
+
public clearScrean() {
|
|
577
|
+
this.ctx.clearRect(0, 0, this.width, this.height);
|
|
578
|
+
this.ctx.fillStyle = this.cnf.backgroundColor || "#000000";
|
|
579
|
+
this.ctx.fillRect(0, 0, this.width, this.height);
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
/**
|
|
583
|
+
* Tears down the engine and releases resources.
|
|
584
|
+
*
|
|
585
|
+
* Use this to perform any cleanup such as removing event listeners or
|
|
586
|
+
* releasing external references when the engine is no longer needed.
|
|
587
|
+
*
|
|
588
|
+
* @remarks
|
|
589
|
+
* Currently a no-op placeholder. Extend this method when the engine
|
|
590
|
+
* acquires resources that need explicit cleanup (e.g. `ResizeObserver`,
|
|
591
|
+
* `WebSocket`, audio contexts).
|
|
592
|
+
*
|
|
593
|
+
* @example
|
|
594
|
+
* ```ts
|
|
595
|
+
* // When leaving the game screen:
|
|
596
|
+
* engine.destroy();
|
|
597
|
+
* ```
|
|
598
|
+
*/
|
|
599
|
+
public destroy() {
|
|
600
|
+
// Clean up resources, event listeners, etc. if needed.
|
|
601
|
+
this.pause();
|
|
602
|
+
|
|
603
|
+
for (let i = this.subsystems.length - 1; i >= 0; i--) {
|
|
604
|
+
this.subsystems[i]?.destroy?.();
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
}
|