littlejsengine 1.19.3 → 1.20.0
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/FAQ.md +841 -820
- package/README.md +29 -25
- package/dist/littlejs.d.ts +2455 -935
- package/dist/littlejs.esm.js +30344 -21110
- package/dist/littlejs.esm.min.js +4 -3
- package/dist/littlejs.js +30259 -21052
- package/dist/littlejs.min.js +2 -2
- package/dist/littlejs.release.js +26567 -20455
- package/package.json +10 -5
- package/plugins/audioEffects.js +28 -24
- package/plugins/box2d.js +2516 -2073
- package/plugins/drawUtilities.js +206 -31
- package/plugins/gltf.js +740 -0
- package/plugins/jsconfig.json +19 -0
- package/plugins/lightSystem.js +719 -332
- package/plugins/math3d.js +58 -17
- package/plugins/medalSystem.js +326 -280
- package/plugins/newgrounds.js +400 -199
- package/plugins/pathFinder.js +208 -175
- package/plugins/pluginExport.js +15 -0
- package/plugins/postProcess.js +56 -13
- package/plugins/render3d.js +1643 -1577
- package/plugins/render3dDebug.js +109 -0
- package/plugins/render3dExtras.js +1754 -0
- package/plugins/render3dVoxels.js +502 -0
- package/plugins/textureSheet.js +496 -458
- package/plugins/threejs.js +183 -163
- package/plugins/tweakables.js +411 -0
- package/plugins/tweenSystem.js +263 -145
- package/plugins/uiSystem.js +2024 -1687
- package/plugins/zzfxm.js +159 -159
- package/src/engine.js +191 -75
- package/src/engineAudio.js +229 -125
- package/src/engineBuild.mjs +289 -256
- package/src/engineDebug.js +912 -742
- package/src/engineDraw.js +295 -193
- package/src/engineEditor.js +2431 -0
- package/src/engineExport.js +12 -0
- package/src/engineInput.js +1462 -1293
- package/src/engineMath.js +1274 -1252
- package/src/engineObject.js +194 -80
- package/src/engineParticles.js +583 -532
- package/src/engineRelease.js +67 -45
- package/src/engineSettings.js +77 -29
- package/src/engineTileLayer.js +995 -741
- package/src/engineUtilities.js +19 -13
- package/src/engineWebGL.js +239 -82
- package/src/jsconfig.json +17 -13
- package/plugins/box2d.wasm.js +0 -630
- package/plugins/box2d.wasm.wasm +0 -0
package/src/engine.js
CHANGED
|
@@ -32,7 +32,7 @@ const engineName = 'LittleJS';
|
|
|
32
32
|
* @type {string}
|
|
33
33
|
* @default
|
|
34
34
|
* @memberof Engine */
|
|
35
|
-
const engineVersion = '1.
|
|
35
|
+
const engineVersion = '1.20.0';
|
|
36
36
|
|
|
37
37
|
/** Frames per second to update
|
|
38
38
|
* @type {number}
|
|
@@ -56,6 +56,9 @@ let engineObjects = [];
|
|
|
56
56
|
* @memberof Engine */
|
|
57
57
|
let engineObjectsCollide = [];
|
|
58
58
|
|
|
59
|
+
// the same objects with the static ones last, the order 2D physics checks them in
|
|
60
|
+
let engineObjectsCollideStaticLast = [];
|
|
61
|
+
|
|
59
62
|
/** Current update frame, used to calculate time
|
|
60
63
|
* @type {number}
|
|
61
64
|
* @memberof Engine */
|
|
@@ -66,7 +69,7 @@ let frame = 0;
|
|
|
66
69
|
* @memberof Engine */
|
|
67
70
|
let time = 0;
|
|
68
71
|
|
|
69
|
-
/** Actual clock time since start in seconds (not affected by pause, timescale, or frame rate clamping)
|
|
72
|
+
/** Actual clock time since start in seconds (not affected by pause, timescale, or frame rate clamping; the debug speed keys scale it in debug builds)
|
|
70
73
|
* @type {number}
|
|
71
74
|
* @memberof Engine */
|
|
72
75
|
let timeReal = 0;
|
|
@@ -91,6 +94,23 @@ function setPaused(isPaused=true) { paused = isPaused; }
|
|
|
91
94
|
let frameTimeLastMS = 0, frameTimeBufferMS = 0, averageFPS = 0;
|
|
92
95
|
let windowWidthLast = 0, windowHeightLast = 0, windowPixelRatioLast = 0;
|
|
93
96
|
let engineUpdateInternal; // assigned by engineInit so engineStep can drive it
|
|
97
|
+
let engineFrameScheduled = false; // a frame of the loop is asked for and has not run yet
|
|
98
|
+
|
|
99
|
+
// the pairs of objects asked about a collision this update, so the other's own physics does not ask again: each
|
|
100
|
+
// asker's others, with true for a pair both said to resolve, and false for one left overlapping, ignored or only
|
|
101
|
+
// nudged apart; a map for each asker so the lookup stays quick when many objects pile up on one spot
|
|
102
|
+
const engineObjectsCollidePairs = new Map;
|
|
103
|
+
function engineObjectsCollidePairAnswer(asker, other)
|
|
104
|
+
{ return engineObjectsCollidePairs.get(asker)?.get(other); }
|
|
105
|
+
function engineObjectsCollidePairAdd(asker, other, resolve=false)
|
|
106
|
+
{
|
|
107
|
+
let others = engineObjectsCollidePairs.get(asker);
|
|
108
|
+
others || engineObjectsCollidePairs.set(asker, others = new Map);
|
|
109
|
+
others.set(other, resolve);
|
|
110
|
+
}
|
|
111
|
+
let engineInitialized = false; // engineInit ran, with or without a canvas
|
|
112
|
+
let engineObjectsUpdateCount = 0; // passes of engineObjectsUpdate so far, how a child knows it moved this pass
|
|
113
|
+
const engineChildStack = []; // the children being updated, taken off the live lists so one leaving does not skip the next
|
|
94
114
|
let showEngineVersion = true;
|
|
95
115
|
|
|
96
116
|
///////////////////////////////////////////////////////////////////////////////
|
|
@@ -115,6 +135,7 @@ class EnginePlugin
|
|
|
115
135
|
*/
|
|
116
136
|
|
|
117
137
|
/** Add a new update function for a plugin
|
|
138
|
+
* - update runs on every fixed tick, paused and timeScale 0 included; a plugin that simulates should skip those
|
|
118
139
|
* @param {PluginCallback} [update]
|
|
119
140
|
* @param {PluginCallback} [render]
|
|
120
141
|
* @param {PluginCallback} [glContextLost]
|
|
@@ -148,13 +169,15 @@ function engineAddPlugin(update, render, glContextLost, glContextRestored, preRe
|
|
|
148
169
|
*/
|
|
149
170
|
|
|
150
171
|
/** Startup LittleJS engine with your callback functions
|
|
151
|
-
* @param {GameInitCallback} gameInit - Called once after the engine starts up, can be async for loading
|
|
152
|
-
* @param {GameCallback} gameUpdate - Called every frame before objects are updated (60fps), use for game logic
|
|
153
|
-
* @param {GameCallback} gameUpdatePost - Called after physics and objects are updated, even when paused, use for UI updates
|
|
154
|
-
* @param {GameCallback} gameRender - Called before objects are rendered, use for drawing backgrounds/world elements
|
|
155
|
-
* @param {GameCallback} gameRenderPost - Called after objects are rendered, use for drawing UI/overlays
|
|
172
|
+
* @param {GameInitCallback} [gameInit] - Called once after the engine starts up, can be async for loading
|
|
173
|
+
* @param {GameCallback} [gameUpdate] - Called every frame before objects are updated (60fps), use for game logic
|
|
174
|
+
* @param {GameCallback} [gameUpdatePost] - Called after physics and objects are updated, even when paused, use for UI updates
|
|
175
|
+
* @param {GameCallback} [gameRender] - Called before objects are rendered, use for drawing backgrounds/world elements
|
|
176
|
+
* @param {GameCallback} [gameRenderPost] - Called after objects are rendered, use for drawing UI/overlays
|
|
156
177
|
* @param {Array<string>} [imageSources=[]] - List of image file paths to preload (e.g., ['player.png', 'tiles.png'])
|
|
157
178
|
* @param {HTMLElement} [rootElement] - Root DOM element to attach canvas to, defaults to document.body
|
|
179
|
+
* It keeps its own inline styles and the canvas centers inside it, but the canvas is still sized from the window,
|
|
180
|
+
* so set canvasFixedSize or canvasMaxSize to fit a smaller element
|
|
158
181
|
* @example
|
|
159
182
|
* // Basic engine startup
|
|
160
183
|
* engineInit(
|
|
@@ -169,17 +192,13 @@ function engineAddPlugin(update, render, glContextLost, glContextRestored, preRe
|
|
|
169
192
|
async function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRenderPost, imageSources=[], rootElement)
|
|
170
193
|
{
|
|
171
194
|
showEngineVersion && console.log(`${engineName} Engine v${engineVersion}`);
|
|
172
|
-
ASSERT(!
|
|
195
|
+
ASSERT(!engineInitialized, 'engine already initialized');
|
|
173
196
|
// runtime guard so release builds (where the assert is stripped) don't
|
|
174
197
|
// double-register listeners / double-add canvases on a second call
|
|
175
|
-
if (
|
|
198
|
+
if (engineInitialized) return;
|
|
199
|
+
engineInitialized = true;
|
|
176
200
|
ASSERT(isArray(imageSources), 'pass in images as array');
|
|
177
201
|
|
|
178
|
-
// ensure body exists for minimal HTML where the script runs before <body> is parsed
|
|
179
|
-
if (!document.body)
|
|
180
|
-
document.documentElement.appendChild(document.createElement('body'));
|
|
181
|
-
rootElement ||= document.body;
|
|
182
|
-
|
|
183
202
|
// allow passing in empty functions
|
|
184
203
|
gameInit ||= ()=>{};
|
|
185
204
|
gameUpdate ||= ()=>{};
|
|
@@ -190,14 +209,15 @@ async function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, game
|
|
|
190
209
|
// Called automatically by engine to setup render system
|
|
191
210
|
function enginePreRender()
|
|
192
211
|
{
|
|
193
|
-
//
|
|
194
|
-
|
|
212
|
+
// the level editor's own view while it is open, in debug builds, before the camera goes to WebGL
|
|
213
|
+
editorPreRender();
|
|
195
214
|
|
|
196
215
|
// disable smoothing for pixel art
|
|
197
216
|
mainContext.imageSmoothingEnabled = !tilesPixelated;
|
|
198
217
|
|
|
199
218
|
// setup gl rendering if enabled
|
|
200
219
|
glPreRender();
|
|
220
|
+
setShader(); // a shader left set last frame does not carry into this one
|
|
201
221
|
|
|
202
222
|
// plugins that draw underneath the 2D layer
|
|
203
223
|
pluginList.forEach(plugin=>plugin.preRender?.());
|
|
@@ -206,6 +226,8 @@ async function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, game
|
|
|
206
226
|
// internal update loop for engine
|
|
207
227
|
function engineUpdate(frameTimeMS=0)
|
|
208
228
|
{
|
|
229
|
+
const manualStepAtStart = engineManualStep;
|
|
230
|
+
|
|
209
231
|
// update time keeping
|
|
210
232
|
let frameTimeDeltaMS = frameTimeMS - frameTimeLastMS;
|
|
211
233
|
// skip delta on the very first frame so timeReal doesn't jump
|
|
@@ -214,8 +236,11 @@ async function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, game
|
|
|
214
236
|
frameTimeLastMS = frameTimeMS;
|
|
215
237
|
if (debug || debugWatermark)
|
|
216
238
|
averageFPS = lerp(averageFPS, 1e3/(frameTimeDeltaMS||1), .05);
|
|
217
|
-
|
|
218
|
-
|
|
239
|
+
audioUpdateVolume();
|
|
240
|
+
// the time keys work while the debug overlay is open, or always when debugKeysAlways is set
|
|
241
|
+
const debugKeys = debug && (debugOverlay || debugKeysAlways);
|
|
242
|
+
const debugSpeedUp = debugKeys && keyIsDown('Equal'); // +
|
|
243
|
+
const debugSpeedDown = debugKeys && keyIsDown('Minus'); // -
|
|
219
244
|
const debugScale = debugSpeedUp ? 10 : debugSpeedDown ? .1 : 1;
|
|
220
245
|
|
|
221
246
|
// apply time deltas
|
|
@@ -223,11 +248,11 @@ async function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, game
|
|
|
223
248
|
timeReal += frameTimeDeltaMS * debugScale / 1e3;
|
|
224
249
|
const combinedScale = timeScale * debugScale;
|
|
225
250
|
frameTimeDeltaMS *= combinedScale;
|
|
226
|
-
//
|
|
227
|
-
//
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
251
|
+
// paused or a time scale of 0 is frozen: it ticks on unscaled time, so the update rate stays fixed instead of
|
|
252
|
+
// following however fast the display refreshes, and gameUpdatePost and input still run to leave it
|
|
253
|
+
const frozen = paused || !combinedScale;
|
|
254
|
+
frameTimeBufferMS += frozen ? frameTimeDeltaUnscaledMS : frameTimeDeltaMS;
|
|
255
|
+
frameTimeBufferMS = min(frameTimeBufferMS, 50 * (frozen ? 1 : max(1, combinedScale))); // clamp min framerate
|
|
231
256
|
|
|
232
257
|
// apply time delta smoothing, improves smoothness of framerate in some browsers
|
|
233
258
|
let wasUpdated = false, deltaSmooth = 0;
|
|
@@ -241,22 +266,28 @@ async function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, game
|
|
|
241
266
|
// update multiple frames if necessary in case of slow framerate
|
|
242
267
|
for (; frameTimeBufferMS >= 0; frameTimeBufferMS -= 1e3 / frameRate)
|
|
243
268
|
{
|
|
244
|
-
//
|
|
245
|
-
|
|
269
|
+
// read again each tick, so a pause set by the game stops the rest of this frame's catch-up ticks
|
|
270
|
+
const frozenTick = paused || !(timeScale * debugScale);
|
|
271
|
+
|
|
272
|
+
// increment frame and update time, frozen does not advance time
|
|
273
|
+
if (!frozenTick)
|
|
246
274
|
time = frame++ / frameRate;
|
|
247
275
|
|
|
248
|
-
// update game and objects, when
|
|
276
|
+
// update game and objects, when frozen update everything except them
|
|
249
277
|
wasUpdated = true;
|
|
250
278
|
engineUpdateCanvas();
|
|
251
279
|
inputUpdate();
|
|
252
|
-
if (!
|
|
280
|
+
if (!frozenTick)
|
|
253
281
|
gameUpdate();
|
|
254
282
|
pluginList.forEach(plugin=>plugin.update?.());
|
|
255
|
-
if (
|
|
283
|
+
if (frozenTick)
|
|
256
284
|
{
|
|
257
285
|
// update object transforms even when paused
|
|
258
286
|
for (const o of engineObjects)
|
|
259
287
|
o.parent || o.updateTransforms();
|
|
288
|
+
|
|
289
|
+
// objects made and destroyed while paused, like a menu's effects, still leave the list
|
|
290
|
+
engineObjects = engineObjects.filter(o=>!o.destroyed);
|
|
260
291
|
}
|
|
261
292
|
else
|
|
262
293
|
engineObjectsUpdate();
|
|
@@ -272,6 +303,10 @@ async function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, game
|
|
|
272
303
|
// add the time smoothing back in
|
|
273
304
|
frameTimeBufferMS += deltaSmooth;
|
|
274
305
|
|
|
306
|
+
// manual step turned on by this frame's updates, set the buffer the loop and smoothing just moved again
|
|
307
|
+
if (engineManualStep && !manualStepAtStart)
|
|
308
|
+
frameTimeBufferMS = -.5e3 / frameRate;
|
|
309
|
+
|
|
275
310
|
// check if the window changed so a resize is picked up even when
|
|
276
311
|
// the game is not updating, for example when timeScale is 0
|
|
277
312
|
let windowChanged = false;
|
|
@@ -289,8 +324,7 @@ async function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, game
|
|
|
289
324
|
// than the fixed update rate would otherwise redraw identical frames
|
|
290
325
|
if (!debugVideoCaptureIsActive() && (wasUpdated || windowChanged))
|
|
291
326
|
renderFrame();
|
|
292
|
-
|
|
293
|
-
requestAnimationFrame(engineUpdate);
|
|
327
|
+
engineManualStep || engineScheduleFrame();
|
|
294
328
|
|
|
295
329
|
function renderFrame()
|
|
296
330
|
{
|
|
@@ -314,6 +348,8 @@ async function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, game
|
|
|
314
348
|
|
|
315
349
|
// post rendering
|
|
316
350
|
gameRenderPost();
|
|
351
|
+
setShader(); // plugin, input and debug draws start from the engine's state
|
|
352
|
+
setAdditiveBlendMode(false);
|
|
317
353
|
pluginList.forEach(plugin=>plugin.render?.());
|
|
318
354
|
inputRender();
|
|
319
355
|
debugRender();
|
|
@@ -323,24 +359,32 @@ async function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, game
|
|
|
323
359
|
primitiveCount = 0;
|
|
324
360
|
}
|
|
325
361
|
}
|
|
326
|
-
engineUpdateInternal = engineUpdate;
|
|
327
362
|
|
|
328
363
|
// skip setup if headless
|
|
329
364
|
if (headlessMode) return startEngine();
|
|
330
365
|
|
|
366
|
+
// ensure body exists for minimal HTML where the script runs before <body> is parsed
|
|
367
|
+
if (!document.body)
|
|
368
|
+
document.documentElement.appendChild(document.createElement('body'));
|
|
369
|
+
rootElement ||= document.body;
|
|
370
|
+
|
|
331
371
|
// setup webgl
|
|
332
372
|
glInit(rootElement);
|
|
333
373
|
|
|
334
374
|
// setup html
|
|
335
|
-
|
|
375
|
+
let styleRoot =
|
|
336
376
|
'margin:0;' + // fill the window
|
|
337
377
|
'overflow:hidden;' + // no scroll bars
|
|
338
378
|
'background:#000;' + // set background color
|
|
339
379
|
'user-select:none;' + // prevent hold to select
|
|
340
380
|
'-webkit-user-select:none;' + // compatibility for ios
|
|
341
381
|
'touch-action:none;' + // prevent mobile pinch to resize
|
|
342
|
-
'-webkit-touch-callout:none'; // compatibility for ios
|
|
343
|
-
|
|
382
|
+
'-webkit-touch-callout:none;'; // compatibility for ios
|
|
383
|
+
// the canvases center on a root element with a height of its own, not the page; one sized only by its
|
|
384
|
+
// children has none, since the canvases are placed apart from it, and would clip them all away
|
|
385
|
+
if (rootElement !== document.body && rootElement.clientHeight && getComputedStyle(rootElement).position === 'static')
|
|
386
|
+
styleRoot += 'position:relative;';
|
|
387
|
+
rootElement.style.cssText = styleRoot + rootElement.style.cssText; // its own inline styles come after and win
|
|
344
388
|
mainCanvas = rootElement.appendChild(document.createElement('canvas'));
|
|
345
389
|
drawContext = mainContext = mainCanvas.getContext('2d');
|
|
346
390
|
|
|
@@ -367,6 +411,7 @@ async function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, game
|
|
|
367
411
|
workReadCanvas = workReadContext.canvas;
|
|
368
412
|
|
|
369
413
|
// create promises for loading images
|
|
414
|
+
/** @type {Array<Promise<any>>} */
|
|
370
415
|
const promises = imageSources.map((src, i)=> loadTexture(i, src));
|
|
371
416
|
|
|
372
417
|
// no images to load
|
|
@@ -379,7 +424,8 @@ async function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, game
|
|
|
379
424
|
if (showSplashScreen)
|
|
380
425
|
{
|
|
381
426
|
// draw splash screen
|
|
382
|
-
|
|
427
|
+
/** @type {Promise<void>} */
|
|
428
|
+
const splash = new Promise(resolve =>
|
|
383
429
|
{
|
|
384
430
|
let t = 0;
|
|
385
431
|
updateSplash();
|
|
@@ -389,7 +435,8 @@ async function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, game
|
|
|
389
435
|
drawEngineLogo(t+=.01);
|
|
390
436
|
t>1 ? resolve() : setTimeout(updateSplash, 16);
|
|
391
437
|
}
|
|
392
|
-
})
|
|
438
|
+
});
|
|
439
|
+
promises.push(splash);
|
|
393
440
|
}
|
|
394
441
|
|
|
395
442
|
// wait for all the promises to finish
|
|
@@ -400,6 +447,7 @@ async function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, game
|
|
|
400
447
|
{
|
|
401
448
|
// wait for gameInit to load
|
|
402
449
|
await gameInit();
|
|
450
|
+
engineUpdateInternal = engineUpdate; // engineStep only runs once the game is set up
|
|
403
451
|
engineManualStep || engineUpdate();
|
|
404
452
|
}
|
|
405
453
|
}
|
|
@@ -440,9 +488,9 @@ function engineUpdateCanvas()
|
|
|
440
488
|
mainCanvasSize.x = min(innerWidth, canvasMaxSize.x) | 0;
|
|
441
489
|
mainCanvasSize.y = min(innerHeight, canvasMaxSize.y) | 0;
|
|
442
490
|
|
|
443
|
-
// responsive aspect ratio
|
|
444
|
-
const innerAspect =
|
|
445
|
-
ASSERT(canvasMinAspect <= canvasMaxAspect);
|
|
491
|
+
// responsive aspect ratio, of the size after canvasMaxSize, which can change its shape
|
|
492
|
+
const innerAspect = mainCanvasSize.x / mainCanvasSize.y;
|
|
493
|
+
ASSERT(!canvasMaxAspect || canvasMinAspect <= canvasMaxAspect);
|
|
446
494
|
if (canvasMaxAspect && innerAspect > canvasMaxAspect)
|
|
447
495
|
{
|
|
448
496
|
// full height
|
|
@@ -495,19 +543,38 @@ function engineUpdateCanvas()
|
|
|
495
543
|
mainContext.fillStyle = BLACK.toString();
|
|
496
544
|
}
|
|
497
545
|
|
|
498
|
-
// set default line join and cap
|
|
546
|
+
// set default line join and cap, round on purpose: it looks better, suits text and keeps sharp corners from
|
|
547
|
+
// spiking far out; WebGL outlines are square and mitered for speed, the two are not meant to match
|
|
499
548
|
mainContext.lineJoin = 'round';
|
|
500
549
|
mainContext.lineCap = 'round';
|
|
501
550
|
}
|
|
502
551
|
|
|
552
|
+
// ask for the next frame of the loop, once however often it is called before that frame, and skip it if manual
|
|
553
|
+
// step was turned on since, so turning it off and on again within a frame can not start a second loop
|
|
554
|
+
function engineScheduleFrame()
|
|
555
|
+
{
|
|
556
|
+
if (engineFrameScheduled) return;
|
|
557
|
+
engineFrameScheduled = true;
|
|
558
|
+
const next = (frameTimeMS)=>
|
|
559
|
+
{
|
|
560
|
+
engineFrameScheduled = false;
|
|
561
|
+
engineManualStep || engineUpdateInternal(frameTimeMS);
|
|
562
|
+
};
|
|
563
|
+
if (typeof requestAnimationFrame === 'function')
|
|
564
|
+
requestAnimationFrame(next);
|
|
565
|
+
else // a headless server in Node has no display to wait for, a timer keeps the pace
|
|
566
|
+
setTimeout(()=> next(performance.now()), 1e3 / frameRate);
|
|
567
|
+
}
|
|
568
|
+
|
|
503
569
|
// max frames engineStep can advance in one call, 10 minutes at 60fps
|
|
504
570
|
// large counts block until they finish, so this catches runaway values
|
|
505
571
|
const engineStepMaxFrames = 36000;
|
|
506
572
|
|
|
507
573
|
/** Advance the engine by a number of frames
|
|
508
|
-
* Requires setEngineManualStep(true) before engineInit
|
|
574
|
+
* Requires setEngineManualStep(true), before engineInit or while running; it stops early if an update turns it off
|
|
509
575
|
* Respects paused exactly as the normal update loop does
|
|
510
|
-
* @param {number} [frames] -
|
|
576
|
+
* @param {number} [frames] - frames of 1/60 of a second to advance, max 36000; timeScale sets how many fixed
|
|
577
|
+
* updates they run, as in the normal loop, one each at timeScale 1
|
|
511
578
|
* @example
|
|
512
579
|
* setHeadlessMode(true);
|
|
513
580
|
* setEngineManualStep(true);
|
|
@@ -517,7 +584,7 @@ const engineStepMaxFrames = 36000;
|
|
|
517
584
|
function engineStep(frames=1)
|
|
518
585
|
{
|
|
519
586
|
ASSERT(engineManualStep,
|
|
520
|
-
'engineStep requires setEngineManualStep(true)
|
|
587
|
+
'engineStep requires setEngineManualStep(true)');
|
|
521
588
|
ASSERT(engineUpdateInternal, 'engineStep requires engineInit to complete');
|
|
522
589
|
// runtime guard so release builds (where the asserts are stripped) can't
|
|
523
590
|
// start a second requestAnimationFrame chain or call an undefined update
|
|
@@ -525,42 +592,73 @@ function engineStep(frames=1)
|
|
|
525
592
|
ASSERT(Number.isInteger(frames) && frames >= 0 && frames <= engineStepMaxFrames,
|
|
526
593
|
'engineStep requires a whole frame count from 0 to ' + engineStepMaxFrames);
|
|
527
594
|
frames = min(frames, engineStepMaxFrames); // release has no asserts, don't freeze
|
|
528
|
-
for (let i = frames; i > 0; --i)
|
|
595
|
+
for (let i = frames; i > 0 && engineManualStep; --i) // an update that turns manual step off hands back the loop
|
|
529
596
|
engineUpdateInternal(frameTimeLastMS + 1e3 / frameRate);
|
|
530
597
|
}
|
|
531
598
|
|
|
532
|
-
/** Update each engine object
|
|
533
|
-
*
|
|
599
|
+
/** Update each engine object and remove destroyed objects; time and frame are advanced by the engine loop, not here
|
|
600
|
+
* - Can be called manually if objects need to be updated outside of main loop
|
|
534
601
|
* @memberof Engine */
|
|
535
602
|
function engineObjectsUpdate()
|
|
536
603
|
{
|
|
537
|
-
|
|
604
|
+
++engineObjectsUpdateCount;
|
|
605
|
+
engineObjectsCollidePairs.clear();
|
|
606
|
+
// objects update in render order, which rendering keeps them in, so a headless run or a frame that rendered
|
|
607
|
+
// nothing updates them the same way; the sort is stable, and nearly free on a list that is already sorted
|
|
608
|
+
engineObjects.sort((a,b)=> a.renderOrder - b.renderOrder);
|
|
609
|
+
// get list of solid objects for physics optimization, in update order, which 3D collision pairs by;
|
|
610
|
+
// 2D checks the static ones last, so a contact with a moving object can not leave something back inside a static
|
|
611
|
+
// solid it was already pushed out of
|
|
538
612
|
engineObjectsCollide = engineObjects.filter(o=>o.collideSolidObjects);
|
|
613
|
+
engineObjectsCollideStaticLast = engineObjectsCollide.filter(o=>o.mass)
|
|
614
|
+
.concat(engineObjectsCollide.filter(o=>!o.mass));
|
|
539
615
|
|
|
540
616
|
// update physics before object update
|
|
541
617
|
for (const o of engineObjects)
|
|
542
618
|
if (!o.parent && !o.destroyed)
|
|
543
619
|
o.updatePhysics();
|
|
544
620
|
|
|
545
|
-
// recursive object update
|
|
621
|
+
// recursive object update: the children are walked from a copy on a shared stack, since a child that
|
|
622
|
+
// destroys itself leaves its parent's list on the spot and the next child would slide past the loop
|
|
623
|
+
function updateChildObjects(children)
|
|
624
|
+
{
|
|
625
|
+
if (!children.length) return; // most objects have none, and this runs for every one
|
|
626
|
+
const start = engineChildStack.length;
|
|
627
|
+
for (const child of children)
|
|
628
|
+
engineChildStack.push(child);
|
|
629
|
+
for (let i = start; i < engineChildStack.length; ++i)
|
|
630
|
+
updateChildObject(engineChildStack[i]);
|
|
631
|
+
engineChildStack.length = start;
|
|
632
|
+
}
|
|
633
|
+
const pass = engineObjectsUpdateCount;
|
|
546
634
|
function updateChildObject(o)
|
|
547
635
|
{
|
|
548
|
-
if (o.destroyed) return;
|
|
636
|
+
if (o.destroyed || o.updatePass === pass) return;
|
|
549
637
|
|
|
638
|
+
// its parent is up to date, so it updates from where it is now, and its children from where it is after
|
|
639
|
+
o.updatePass = pass;
|
|
640
|
+
o.updateTransforms(false);
|
|
550
641
|
o.update();
|
|
551
|
-
|
|
552
|
-
|
|
642
|
+
o.children.length && o.updateTransforms(false);
|
|
643
|
+
updateChildObjects(o.children);
|
|
553
644
|
}
|
|
554
|
-
|
|
645
|
+
function updateTopObject(o)
|
|
555
646
|
{
|
|
556
|
-
if (o.parent || o.destroyed)
|
|
647
|
+
if (o.parent || o.destroyed || o.updatePass === pass) return; // a child that let go is not updated twice
|
|
557
648
|
|
|
558
|
-
// update top level objects
|
|
649
|
+
// update top level objects, each child places itself before it updates so it sees this frame's position,
|
|
650
|
+
// then the whole tree is placed again so what the children changed in their localPos lands before render
|
|
651
|
+
o.updatePass = pass;
|
|
559
652
|
o.update();
|
|
560
|
-
|
|
561
|
-
updateChildObject(child);
|
|
653
|
+
updateChildObjects(o.children);
|
|
562
654
|
o.updateTransforms();
|
|
563
655
|
}
|
|
656
|
+
for (const o of engineObjects)
|
|
657
|
+
updateTopObject(o);
|
|
658
|
+
|
|
659
|
+
// a child let go during the update from a place in the list already walked is on its own now, updated here
|
|
660
|
+
for (const o of engineObjects)
|
|
661
|
+
updateTopObject(o);
|
|
564
662
|
|
|
565
663
|
// remove destroyed objects
|
|
566
664
|
engineObjects = engineObjects.filter(o=>!o.destroyed);
|
|
@@ -570,7 +668,7 @@ function engineObjectsUpdate()
|
|
|
570
668
|
* - This can be used to clear out all objects when restarting a level
|
|
571
669
|
* - Objects with the persistent flag set are left alone, for things that outlive a level
|
|
572
670
|
* - Objects can override their destroy function to do cleanup or stick around
|
|
573
|
-
* @param {boolean} [immediate] -
|
|
671
|
+
* @param {boolean} [immediate] - true removes attached effects like particle emitters at once, false lets them finish first
|
|
574
672
|
* @memberof Engine */
|
|
575
673
|
function engineObjectsDestroy(immediate=true)
|
|
576
674
|
{
|
|
@@ -580,32 +678,43 @@ function engineObjectsDestroy(immediate=true)
|
|
|
580
678
|
}
|
|
581
679
|
|
|
582
680
|
/** Collects all object within a given area
|
|
681
|
+
* - An object is collected when its box overlaps the area, or with testCenters when its center is inside it
|
|
682
|
+
* - Objects destroyed this frame are left out, they are only in the list until the frame ends
|
|
583
683
|
* @param {Vector2} [pos] - Center of test area, or undefined for all objects
|
|
584
|
-
* @param {Vector2|number} [size] -
|
|
684
|
+
* @param {Vector2|number} [size] - Diameter of a circle if a number, full size of a rectangle if a Vector2,
|
|
685
|
+
* left out or 0 the objects that overlap the point at pos
|
|
585
686
|
* @param {Array<EngineObject>} [objects=engineObjects] - List of objects to check
|
|
687
|
+
* @param {boolean} [testCenters] - Test only each object's center, a little faster, and ignores object sizes
|
|
586
688
|
* @return {Array<EngineObject>} - List of collected objects
|
|
587
689
|
* @memberof Engine */
|
|
588
|
-
function engineObjectsCollect(pos, size, objects=engineObjects)
|
|
690
|
+
function engineObjectsCollect(pos, size, objects=engineObjects, testCenters=false)
|
|
589
691
|
{
|
|
590
692
|
const collectedObjects = [];
|
|
591
693
|
if (!pos)
|
|
592
694
|
{
|
|
593
695
|
// all objects
|
|
594
696
|
for (const o of objects)
|
|
595
|
-
collectedObjects.push(o);
|
|
697
|
+
o.destroyed || collectedObjects.push(o);
|
|
596
698
|
}
|
|
597
|
-
else if (size instanceof Vector2)
|
|
699
|
+
else if (!size || size instanceof Vector2)
|
|
598
700
|
{
|
|
599
|
-
// bounding box test
|
|
701
|
+
// bounding box test, a point when there is no size or a size of 0
|
|
702
|
+
const boxSize = size instanceof Vector2 ? size : vec2();
|
|
600
703
|
for (const o of objects)
|
|
601
|
-
o.isOverlapping(pos,
|
|
704
|
+
o.destroyed || (testCenters ? isOverlapping(pos, boxSize, o.pos) : o.isOverlapping(pos, boxSize))
|
|
705
|
+
&& collectedObjects.push(o);
|
|
602
706
|
}
|
|
603
707
|
else
|
|
604
708
|
{
|
|
605
|
-
// circle test
|
|
606
|
-
const
|
|
709
|
+
// circle test, a diameter like every other size, against the nearest point of each box
|
|
710
|
+
const radiusSquared = (size/2)**2;
|
|
607
711
|
for (const o of objects)
|
|
608
|
-
|
|
712
|
+
{
|
|
713
|
+
if (o.destroyed) continue;
|
|
714
|
+
const dx = testCenters ? pos.x - o.pos.x : max(abs(pos.x - o.pos.x) - o.size.x/2, 0);
|
|
715
|
+
const dy = testCenters ? pos.y - o.pos.y : max(abs(pos.y - o.pos.y) - o.size.y/2, 0);
|
|
716
|
+
dx*dx + dy*dy < radiusSquared && collectedObjects.push(o);
|
|
717
|
+
}
|
|
609
718
|
}
|
|
610
719
|
return collectedObjects;
|
|
611
720
|
}
|
|
@@ -616,16 +725,23 @@ function engineObjectsCollect(pos, size, objects=engineObjects)
|
|
|
616
725
|
* @memberof Engine
|
|
617
726
|
*/
|
|
618
727
|
|
|
619
|
-
/** Triggers a callback for each object within a given area
|
|
728
|
+
/** Triggers a callback for each object within a given area, objects destroyed this frame left out
|
|
620
729
|
* @param {Vector2} [pos] - Center of test area, or undefined for all objects
|
|
621
|
-
* @param {Vector2|number} [size] -
|
|
622
|
-
* @param {ObjectCallbackFunction} [callbackFunction] - Calls this function on every object that passes the test
|
|
730
|
+
* @param {Vector2|number} [size] - Diameter of a circle if a number, full size of a rectangle if a Vector2
|
|
731
|
+
* @param {ObjectCallbackFunction} [callbackFunction] - Calls this function on every object that passes the test, needed
|
|
732
|
+
* (marked optional only because the area before it is)
|
|
623
733
|
* @param {Array<EngineObject>} [objects=engineObjects] - List of objects to check
|
|
734
|
+
* @param {boolean} [testCenters] - Test only each object's center, see engineObjectsCollect
|
|
624
735
|
* @memberof Engine */
|
|
625
|
-
function engineObjectsCallback(pos, size, callbackFunction, objects=engineObjects)
|
|
626
|
-
{
|
|
736
|
+
function engineObjectsCallback(pos, size, callbackFunction, objects=engineObjects, testCenters=false)
|
|
737
|
+
{
|
|
738
|
+
// an object an earlier callback destroyed is skipped
|
|
739
|
+
for (const o of engineObjectsCollect(pos, size, objects, testCenters))
|
|
740
|
+
o.destroyed || callbackFunction(o);
|
|
741
|
+
}
|
|
627
742
|
|
|
628
|
-
/** Return a list of objects intersecting a ray
|
|
743
|
+
/** Return a list of objects intersecting a ray, objects destroyed this frame left out
|
|
744
|
+
* - Only objects with collideRaycast set are hit, which setCollision turns on
|
|
629
745
|
* @param {Vector2} start
|
|
630
746
|
* @param {Vector2} end
|
|
631
747
|
* @param {Array<EngineObject>} [objects=engineObjects] - List of objects to check
|
|
@@ -636,13 +752,13 @@ function engineObjectsRaycast(start, end, objects=engineObjects)
|
|
|
636
752
|
const hitObjects = [];
|
|
637
753
|
for (const o of objects)
|
|
638
754
|
{
|
|
639
|
-
if (o.collideRaycast && isIntersecting(start, end, o.pos, o.size))
|
|
755
|
+
if (o.collideRaycast && !o.destroyed && isIntersecting(start, end, o.pos, o.size))
|
|
640
756
|
{
|
|
641
|
-
debugRaycast && debugRect(o.pos, o.size, '#f00');
|
|
757
|
+
debugRaycast && debugRect(o.pos, o.size, '#f00', 0, 0, false, false);
|
|
642
758
|
hitObjects.push(o);
|
|
643
759
|
}
|
|
644
760
|
}
|
|
645
761
|
|
|
646
|
-
debugRaycast && debugLine(start, end, hitObjects.length ? '#f00' : '#00f', .02);
|
|
762
|
+
debugRaycast && debugLine(start, end, hitObjects.length ? '#f00' : '#00f', .02, 0, false);
|
|
647
763
|
return hitObjects;
|
|
648
764
|
}
|