littlejsengine 1.19.3 → 1.21.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 +2857 -955
- package/dist/littlejs.esm.js +33428 -20995
- package/dist/littlejs.esm.min.js +4 -3
- package/dist/littlejs.js +33386 -20996
- package/dist/littlejs.min.js +2 -2
- package/dist/littlejs.release.js +27841 -20422
- 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 +786 -0
- package/plugins/jsconfig.json +19 -0
- package/plugins/lightSystem.js +719 -332
- package/plugins/math3d.js +148 -27
- package/plugins/medalSystem.js +326 -280
- package/plugins/newgrounds.js +400 -199
- package/plugins/pathFinder.js +208 -175
- package/plugins/pluginExport.js +25 -0
- package/plugins/postProcess.js +96 -24
- package/plugins/render3d.js +2067 -1632
- package/plugins/render3dDebug.js +110 -0
- package/plugins/render3dEditor.js +1812 -0
- package/plugins/render3dExtras.js +1948 -0
- package/plugins/render3dLevel.js +189 -0
- package/plugins/render3dVoxels.js +514 -0
- package/plugins/sceneSystem.js +58 -0
- package/plugins/textureSheet.js +496 -458
- package/plugins/threejs.js +183 -163
- package/plugins/tweakables.js +411 -0
- package/plugins/tweenSystem.js +265 -145
- package/plugins/uiSystem.js +2024 -1687
- package/plugins/zzfxm.js +159 -159
- package/src/engine.js +378 -97
- package/src/engineAudio.js +231 -127
- package/src/engineBuild.mjs +292 -256
- package/src/engineDebug.js +967 -743
- package/src/engineDraw.js +391 -198
- package/src/engineEditor.js +2452 -0
- package/src/engineExport.js +18 -0
- package/src/engineInput.js +1466 -1293
- package/src/engineLogo.js +19 -0
- package/src/engineMath.js +1274 -1252
- package/src/engineObject.js +197 -82
- package/src/engineParticles.js +583 -532
- package/src/engineRelease.js +79 -45
- package/src/engineSettings.js +115 -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.21.0';
|
|
36
36
|
|
|
37
37
|
/** Frames per second to update
|
|
38
38
|
* @type {number}
|
|
@@ -40,11 +40,12 @@ const engineVersion = '1.19.3';
|
|
|
40
40
|
* @memberof Engine */
|
|
41
41
|
const frameRate = 60;
|
|
42
42
|
|
|
43
|
-
/** How many seconds
|
|
43
|
+
/** How many seconds the update covers: 1/60 with the fixed time step, or with engineVariableStep the time of the
|
|
44
|
+
* display frame it runs on, times timeScale; while paused it keeps the last update's
|
|
44
45
|
* @type {number}
|
|
45
46
|
* @default 1/60
|
|
46
47
|
* @memberof Engine */
|
|
47
|
-
|
|
48
|
+
let timeDelta = 1/frameRate;
|
|
48
49
|
|
|
49
50
|
/** Array containing all engine objects
|
|
50
51
|
* @type {Array<EngineObject>}
|
|
@@ -56,6 +57,9 @@ let engineObjects = [];
|
|
|
56
57
|
* @memberof Engine */
|
|
57
58
|
let engineObjectsCollide = [];
|
|
58
59
|
|
|
60
|
+
// the same objects with the static ones last, the order 2D physics checks them in
|
|
61
|
+
let engineObjectsCollideStaticLast = [];
|
|
62
|
+
|
|
59
63
|
/** Current update frame, used to calculate time
|
|
60
64
|
* @type {number}
|
|
61
65
|
* @memberof Engine */
|
|
@@ -66,7 +70,7 @@ let frame = 0;
|
|
|
66
70
|
* @memberof Engine */
|
|
67
71
|
let time = 0;
|
|
68
72
|
|
|
69
|
-
/** Actual clock time since start in seconds (not affected by pause, timescale, or frame rate clamping)
|
|
73
|
+
/** 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
74
|
* @type {number}
|
|
71
75
|
* @memberof Engine */
|
|
72
76
|
let timeReal = 0;
|
|
@@ -89,8 +93,97 @@ function setPaused(isPaused=true) { paused = isPaused; }
|
|
|
89
93
|
|
|
90
94
|
// Engine internal variables
|
|
91
95
|
let frameTimeLastMS = 0, frameTimeBufferMS = 0, averageFPS = 0;
|
|
96
|
+
|
|
97
|
+
// delta smoothing, after Time Delta Smoothing by Frank Force (2013): a frame is on screen for whole display frames,
|
|
98
|
+
// so each delta is rounded to them and the rest is carried to the next, keeping the total real time; the frame
|
|
99
|
+
// length is estimated from recent frames and kept internal, since the browser does not say what it is
|
|
100
|
+
const frameDeltaHistory = [];
|
|
101
|
+
let frameIntervalMS = 1e3 / 60, frameDeltaCarryMS = 0, frameDeltaCarryAverageMS = 0, frameDeltaSmoothing = true;
|
|
102
|
+
function engineSmoothDelta(deltaMS)
|
|
103
|
+
{
|
|
104
|
+
if (!(deltaMS > 0)) return 0;
|
|
105
|
+
if (deltaMS < 250) // a gap from a hidden tab is not a display frame
|
|
106
|
+
{
|
|
107
|
+
frameDeltaHistory.push(deltaMS);
|
|
108
|
+
frameDeltaHistory.length > 64 && frameDeltaHistory.shift();
|
|
109
|
+
|
|
110
|
+
// the frames each delta held are counted in the last estimate or in the lower quartile of the last 16,
|
|
111
|
+
// whichever the deltas fit better: the quartile is one frame even when a busy game misses most of them, and
|
|
112
|
+
// it takes over when the window moves to a display with another refresh rate, or the estimate went wrong
|
|
113
|
+
const recent = frameDeltaHistory.slice(-16).sort((a, b)=> a - b);
|
|
114
|
+
const [lastMS, lastOff] = engineFrameFit(frameIntervalMS);
|
|
115
|
+
const [quartileMS, quartileOff] = engineFrameFit(recent[recent.length >> 2]);
|
|
116
|
+
frameIntervalMS = lastOff <= quartileOff ? lastMS : quartileMS;
|
|
117
|
+
|
|
118
|
+
// deltas far from whole frames mean the display has no fixed refresh, with a margin so it does not flip
|
|
119
|
+
frameDeltaSmoothing = min(lastOff, quartileOff) < (frameDeltaSmoothing ? .2 : .12);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (!frameDeltaSmoothing)
|
|
123
|
+
{
|
|
124
|
+
// deltas that are not whole frames of one length, like a variable refresh display, are the frame times
|
|
125
|
+
// as they are, with any carry paid, and smoothing starts over once they fit again
|
|
126
|
+
const rawMS = deltaMS + frameDeltaCarryMS;
|
|
127
|
+
frameDeltaCarryMS = min(rawMS, 0);
|
|
128
|
+
frameDeltaCarryAverageMS = 0;
|
|
129
|
+
return max(rawMS, 0);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// whole frames, 0 for one that came early or 2 after one was missed, the carry stays within half a frame; a
|
|
133
|
+
// little of its slow average is paid each frame, so jitter can not hold it at half a frame and flip every frame
|
|
134
|
+
const pullMS = frameDeltaCarryAverageMS / 32;
|
|
135
|
+
frameDeltaCarryMS += deltaMS;
|
|
136
|
+
const frames = round((frameDeltaCarryMS - pullMS) / frameIntervalMS);
|
|
137
|
+
if (frames < 1)
|
|
138
|
+
return 0; // the carry keeps it
|
|
139
|
+
const smoothMS = frames * frameIntervalMS + pullMS;
|
|
140
|
+
frameDeltaCarryMS -= smoothMS;
|
|
141
|
+
frameDeltaCarryAverageMS += (frameDeltaCarryMS - frameDeltaCarryAverageMS) / 32;
|
|
142
|
+
return smoothMS;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// fit the delta history as whole frames of about unitMS: the frame length is the least squares slope of time over
|
|
146
|
+
// frames held, so jitter and whole ms timestamps average out, with how far the deltas are from whole frames
|
|
147
|
+
function engineFrameFit(unitMS)
|
|
148
|
+
{
|
|
149
|
+
let timeMS = 0, frames = 0, offMS = 0, sumF = 0, sumT = 0, sumFF = 0, sumFT = 0;
|
|
150
|
+
const count = frameDeltaHistory.length;
|
|
151
|
+
for (const d of frameDeltaHistory)
|
|
152
|
+
{
|
|
153
|
+
const held = max(1, round(d / unitMS));
|
|
154
|
+
timeMS += d;
|
|
155
|
+
frames += held;
|
|
156
|
+
offMS += abs(d - held * unitMS);
|
|
157
|
+
sumF += frames, sumT += timeMS, sumFF += frames * frames, sumFT += frames * timeMS;
|
|
158
|
+
}
|
|
159
|
+
const spread = count * sumFF - sumF * sumF;
|
|
160
|
+
return [spread ? (count * sumFT - sumF * sumT) / spread : unitMS, offMS / count / unitMS];
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// where the fixed step's time counts from, moved when the variable step hands back so time goes on from there
|
|
164
|
+
let timeFixedStart = 0, frameFixedStart = 0;
|
|
92
165
|
let windowWidthLast = 0, windowHeightLast = 0, windowPixelRatioLast = 0;
|
|
93
166
|
let engineUpdateInternal; // assigned by engineInit so engineStep can drive it
|
|
167
|
+
let engineFrameScheduled = false; // a frame of the loop is asked for and has not run yet
|
|
168
|
+
|
|
169
|
+
// the pairs of objects asked about a collision this update, so the other's own physics does not ask again: each
|
|
170
|
+
// asker's others, with true for a pair both said to resolve, and false for one left overlapping, ignored or only
|
|
171
|
+
// nudged apart; a map for each asker so the lookup stays quick when many objects pile up on one spot
|
|
172
|
+
const engineObjectsCollidePairs = new Map;
|
|
173
|
+
function engineObjectsCollidePairAnswer(asker, other)
|
|
174
|
+
{ return engineObjectsCollidePairs.get(asker)?.get(other); }
|
|
175
|
+
function engineObjectsCollidePairAdd(asker, other, resolve=false)
|
|
176
|
+
{
|
|
177
|
+
let others = engineObjectsCollidePairs.get(asker);
|
|
178
|
+
others || engineObjectsCollidePairs.set(asker, others = new Map);
|
|
179
|
+
others.set(other, resolve);
|
|
180
|
+
}
|
|
181
|
+
let engineInitialized = false; // engineInit ran, with or without a canvas
|
|
182
|
+
// the loads startup waits for, each counted for the loading screen, and how many are done; undefined once the game
|
|
183
|
+
// loop starts
|
|
184
|
+
let engineLoads, engineLoadsDone = 0;
|
|
185
|
+
let engineObjectsUpdateCount = 0; // passes of engineObjectsUpdate so far, how a child knows it moved this pass
|
|
186
|
+
const engineChildStack = []; // the children being updated, taken off the live lists so one leaving does not skip the next
|
|
94
187
|
let showEngineVersion = true;
|
|
95
188
|
|
|
96
189
|
///////////////////////////////////////////////////////////////////////////////
|
|
@@ -115,6 +208,7 @@ class EnginePlugin
|
|
|
115
208
|
*/
|
|
116
209
|
|
|
117
210
|
/** Add a new update function for a plugin
|
|
211
|
+
* - update runs on every fixed tick, paused and timeScale 0 included; a plugin that simulates should skip those
|
|
118
212
|
* @param {PluginCallback} [update]
|
|
119
213
|
* @param {PluginCallback} [render]
|
|
120
214
|
* @param {PluginCallback} [glContextLost]
|
|
@@ -137,24 +231,83 @@ function engineAddPlugin(update, render, glContextLost, glContextRestored, preRe
|
|
|
137
231
|
///////////////////////////////////////////////////////////////////////////////
|
|
138
232
|
// Main Engine Functions
|
|
139
233
|
|
|
234
|
+
/** Add something the game loads to what startup waits for: while engineInit and gameInit run, the game loop starts
|
|
235
|
+
* once it is done, and the loading screen counts it; images from loadTexture and sounds from files are added on
|
|
236
|
+
* their own, and a load that fails counts as done; after startup it does nothing
|
|
237
|
+
* @param {Promise<any>} promise
|
|
238
|
+
* @return {Promise<any>} - The same promise
|
|
239
|
+
* @example
|
|
240
|
+
* async function gameInit() { level = await engineAddLoad(fetchJSON('level.json')); }
|
|
241
|
+
* @memberof Engine */
|
|
242
|
+
function engineAddLoad(promise)
|
|
243
|
+
{
|
|
244
|
+
if (engineLoads)
|
|
245
|
+
{
|
|
246
|
+
engineLoads.push(promise);
|
|
247
|
+
const done = ()=> { ++engineLoadsDone; };
|
|
248
|
+
promise.then(done, done);
|
|
249
|
+
}
|
|
250
|
+
return promise;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// wait for every load, the ones added while waiting too, drawing the loading screen each frame meanwhile
|
|
254
|
+
async function engineWaitForLoads()
|
|
255
|
+
{
|
|
256
|
+
let waiting = true;
|
|
257
|
+
const start = performance.now();
|
|
258
|
+
const drawFrame = ()=>
|
|
259
|
+
{
|
|
260
|
+
if (!waiting) return;
|
|
261
|
+
engineLoadingScreenDraw((performance.now() - start) / 1e3);
|
|
262
|
+
setTimeout(drawFrame, 16);
|
|
263
|
+
};
|
|
264
|
+
headlessMode || drawFrame();
|
|
265
|
+
try
|
|
266
|
+
{
|
|
267
|
+
for (let count; count !== engineLoads.length;)
|
|
268
|
+
{
|
|
269
|
+
count = engineLoads.length;
|
|
270
|
+
await Promise.allSettled(engineLoads);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
finally { waiting = false; }
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// one frame of the loading screen, once loading has taken half a second, so a fast load never shows it; input
|
|
277
|
+
// while it shows is dropped, as it is under the splash
|
|
278
|
+
function engineLoadingScreenDraw(elapsed)
|
|
279
|
+
{
|
|
280
|
+
if (headlessMode || !loadingScreen || elapsed < .5) return;
|
|
281
|
+
inputClear();
|
|
282
|
+
engineUpdateCanvas();
|
|
283
|
+
loadingScreen(engineLoadsDone / engineLoads.length);
|
|
284
|
+
}
|
|
285
|
+
|
|
140
286
|
/**
|
|
141
287
|
* @callback GameInitCallback - Called after the engine starts, can be async
|
|
142
288
|
* @return {void|Promise<void>}
|
|
143
289
|
* @memberof Engine
|
|
144
290
|
*/
|
|
291
|
+
/**
|
|
292
|
+
* @callback LoadingScreenCallback - Draws the loading screen on mainContext, each frame while the game loads
|
|
293
|
+
* @param {number} progress - The part of the loads done, 0 to 1
|
|
294
|
+
* @memberof Engine
|
|
295
|
+
*/
|
|
145
296
|
/**
|
|
146
297
|
* @callback GameCallback - Update or render function for the game
|
|
147
298
|
* @memberof Engine
|
|
148
299
|
*/
|
|
149
300
|
|
|
150
301
|
/** 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
|
|
302
|
+
* @param {GameInitCallback} [gameInit] - Called once after the engine starts up, can be async for loading
|
|
303
|
+
* @param {GameCallback} [gameUpdate] - Called every frame before objects are updated (60fps), use for game logic
|
|
304
|
+
* @param {GameCallback} [gameUpdatePost] - Called after physics and objects are updated, even when paused, use for UI updates
|
|
305
|
+
* @param {GameCallback} [gameRender] - Called before objects are rendered, use for drawing backgrounds/world elements
|
|
306
|
+
* @param {GameCallback} [gameRenderPost] - Called after objects are rendered, use for drawing UI/overlays
|
|
156
307
|
* @param {Array<string>} [imageSources=[]] - List of image file paths to preload (e.g., ['player.png', 'tiles.png'])
|
|
157
308
|
* @param {HTMLElement} [rootElement] - Root DOM element to attach canvas to, defaults to document.body
|
|
309
|
+
* It keeps its own inline styles and the canvas centers inside it, but the canvas is still sized from the window,
|
|
310
|
+
* so set canvasFixedSize or canvasMaxSize to fit a smaller element
|
|
158
311
|
* @example
|
|
159
312
|
* // Basic engine startup
|
|
160
313
|
* engineInit(
|
|
@@ -169,17 +322,14 @@ function engineAddPlugin(update, render, glContextLost, glContextRestored, preRe
|
|
|
169
322
|
async function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRenderPost, imageSources=[], rootElement)
|
|
170
323
|
{
|
|
171
324
|
showEngineVersion && console.log(`${engineName} Engine v${engineVersion}`);
|
|
172
|
-
ASSERT(!
|
|
325
|
+
ASSERT(!engineInitialized, 'engine already initialized');
|
|
173
326
|
// runtime guard so release builds (where the assert is stripped) don't
|
|
174
327
|
// double-register listeners / double-add canvases on a second call
|
|
175
|
-
if (
|
|
328
|
+
if (engineInitialized) return;
|
|
329
|
+
engineInitialized = true;
|
|
330
|
+
engineLoads = [], engineLoadsDone = 0;
|
|
176
331
|
ASSERT(isArray(imageSources), 'pass in images as array');
|
|
177
332
|
|
|
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
333
|
// allow passing in empty functions
|
|
184
334
|
gameInit ||= ()=>{};
|
|
185
335
|
gameUpdate ||= ()=>{};
|
|
@@ -190,14 +340,15 @@ async function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, game
|
|
|
190
340
|
// Called automatically by engine to setup render system
|
|
191
341
|
function enginePreRender()
|
|
192
342
|
{
|
|
193
|
-
//
|
|
194
|
-
|
|
343
|
+
// the level editor's own view while it is open, in debug builds, before the camera goes to WebGL
|
|
344
|
+
editorPreRender();
|
|
195
345
|
|
|
196
346
|
// disable smoothing for pixel art
|
|
197
347
|
mainContext.imageSmoothingEnabled = !tilesPixelated;
|
|
198
348
|
|
|
199
349
|
// setup gl rendering if enabled
|
|
200
350
|
glPreRender();
|
|
351
|
+
setShader(); // a shader left set last frame does not carry into this one
|
|
201
352
|
|
|
202
353
|
// plugins that draw underneath the 2D layer
|
|
203
354
|
pluginList.forEach(plugin=>plugin.preRender?.());
|
|
@@ -206,6 +357,8 @@ async function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, game
|
|
|
206
357
|
// internal update loop for engine
|
|
207
358
|
function engineUpdate(frameTimeMS=0)
|
|
208
359
|
{
|
|
360
|
+
const manualStepAtStart = engineManualStep;
|
|
361
|
+
|
|
209
362
|
// update time keeping
|
|
210
363
|
let frameTimeDeltaMS = frameTimeMS - frameTimeLastMS;
|
|
211
364
|
// skip delta on the very first frame so timeReal doesn't jump
|
|
@@ -214,8 +367,14 @@ async function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, game
|
|
|
214
367
|
frameTimeLastMS = frameTimeMS;
|
|
215
368
|
if (debug || debugWatermark)
|
|
216
369
|
averageFPS = lerp(averageFPS, 1e3/(frameTimeDeltaMS||1), .05);
|
|
217
|
-
|
|
218
|
-
|
|
370
|
+
// the time the frame will be on screen, in whole display frames; engineStep's steps are exact already
|
|
371
|
+
if (!manualStepAtStart)
|
|
372
|
+
frameTimeDeltaMS = engineSmoothDelta(frameTimeDeltaMS);
|
|
373
|
+
audioUpdateVolume();
|
|
374
|
+
// the time keys work while the debug overlay is open, or always when debugKeysAlways is set
|
|
375
|
+
const debugKeys = debug && (debugOverlay || debugKeysAlways);
|
|
376
|
+
const debugSpeedUp = debugKeys && keyIsDown('Equal'); // +
|
|
377
|
+
const debugSpeedDown = debugKeys && keyIsDown('Minus'); // -
|
|
219
378
|
const debugScale = debugSpeedUp ? 10 : debugSpeedDown ? .1 : 1;
|
|
220
379
|
|
|
221
380
|
// apply time deltas
|
|
@@ -223,40 +382,75 @@ async function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, game
|
|
|
223
382
|
timeReal += frameTimeDeltaMS * debugScale / 1e3;
|
|
224
383
|
const combinedScale = timeScale * debugScale;
|
|
225
384
|
frameTimeDeltaMS *= combinedScale;
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
frameTimeBufferMS += paused ? frameTimeDeltaUnscaledMS : frameTimeDeltaMS;
|
|
229
|
-
if (paused || combinedScale <= 1)
|
|
230
|
-
frameTimeBufferMS = min(frameTimeBufferMS, 50); // clamp min framerate
|
|
231
|
-
|
|
232
|
-
// apply time delta smoothing, improves smoothness of framerate in some browsers
|
|
233
|
-
let wasUpdated = false, deltaSmooth = 0;
|
|
234
|
-
if (frameTimeBufferMS < 0 && frameTimeBufferMS > -9)
|
|
385
|
+
let wasUpdated = false;
|
|
386
|
+
if (engineVariableStep)
|
|
235
387
|
{
|
|
236
|
-
//
|
|
237
|
-
|
|
238
|
-
|
|
388
|
+
// one update for the frame with timeDelta the time it covers, per-frame values are the game's to scale;
|
|
389
|
+
// engineStep's frames are exactly 1/60, its first too, which has no frame before it to take a delta from
|
|
390
|
+
if (frameTimeDeltaUnscaledMS > 0 || manualStepAtStart)
|
|
391
|
+
{
|
|
392
|
+
// frozen stands still as in the fixed step, and timeDelta keeps the last update's
|
|
393
|
+
const frozenTick = paused || !combinedScale;
|
|
394
|
+
if (!frozenTick)
|
|
395
|
+
{
|
|
396
|
+
timeDelta = manualStepAtStart ? combinedScale / frameRate :
|
|
397
|
+
min(frameTimeDeltaUnscaledMS, 50) * combinedScale / 1e3; // clamp min framerate
|
|
398
|
+
time += timeDelta;
|
|
399
|
+
++frame;
|
|
400
|
+
}
|
|
401
|
+
engineTick(frozenTick);
|
|
402
|
+
}
|
|
239
403
|
}
|
|
240
|
-
|
|
241
|
-
// update multiple frames if necessary in case of slow framerate
|
|
242
|
-
for (; frameTimeBufferMS >= 0; frameTimeBufferMS -= 1e3 / frameRate)
|
|
404
|
+
else
|
|
243
405
|
{
|
|
244
|
-
//
|
|
245
|
-
|
|
246
|
-
|
|
406
|
+
// paused or a time scale of 0 is frozen: it ticks on unscaled time, so the update rate stays fixed instead
|
|
407
|
+
// of following however fast the display refreshes, and gameUpdatePost and input still run to leave it
|
|
408
|
+
const frozen = paused || !combinedScale;
|
|
409
|
+
frameTimeBufferMS += frozen ? frameTimeDeltaUnscaledMS : frameTimeDeltaMS;
|
|
410
|
+
frameTimeBufferMS = min(frameTimeBufferMS, 50 * (frozen ? 1 : max(1, combinedScale))); // clamp min framerate
|
|
411
|
+
|
|
412
|
+
// apply time delta smoothing, improves smoothness of framerate in some browsers
|
|
413
|
+
let deltaSmooth = 0;
|
|
414
|
+
if (frameTimeBufferMS < 0 && frameTimeBufferMS > -9)
|
|
415
|
+
{
|
|
416
|
+
// force at least one update each frame since it is waiting for refresh
|
|
417
|
+
deltaSmooth = frameTimeBufferMS;
|
|
418
|
+
frameTimeBufferMS = 0;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
// update multiple frames if necessary in case of slow framerate
|
|
422
|
+
for (; frameTimeBufferMS >= 0; frameTimeBufferMS -= 1e3 / frameRate)
|
|
423
|
+
{
|
|
424
|
+
// read again each tick, so a pause set by the game stops the rest of this frame's catch-up ticks
|
|
425
|
+
const frozenTick = paused || !(timeScale * debugScale);
|
|
426
|
+
|
|
427
|
+
// increment frame and update time, frozen does not advance time
|
|
428
|
+
if (!frozenTick)
|
|
429
|
+
time = timeFixedStart + (frame++ - frameFixedStart) / frameRate;
|
|
430
|
+
engineTick(frozenTick);
|
|
431
|
+
}
|
|
247
432
|
|
|
248
|
-
//
|
|
433
|
+
// add the time smoothing back in
|
|
434
|
+
frameTimeBufferMS += deltaSmooth;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
// one tick of the loop: update game and objects, when frozen update everything except them
|
|
438
|
+
function engineTick(frozenTick)
|
|
439
|
+
{
|
|
249
440
|
wasUpdated = true;
|
|
250
441
|
engineUpdateCanvas();
|
|
251
442
|
inputUpdate();
|
|
252
|
-
if (!
|
|
443
|
+
if (!frozenTick)
|
|
253
444
|
gameUpdate();
|
|
254
445
|
pluginList.forEach(plugin=>plugin.update?.());
|
|
255
|
-
if (
|
|
446
|
+
if (frozenTick)
|
|
256
447
|
{
|
|
257
448
|
// update object transforms even when paused
|
|
258
449
|
for (const o of engineObjects)
|
|
259
450
|
o.parent || o.updateTransforms();
|
|
451
|
+
|
|
452
|
+
// objects made and destroyed while paused, like a menu's effects, still leave the list
|
|
453
|
+
engineObjects = engineObjects.filter(o=>!o.destroyed);
|
|
260
454
|
}
|
|
261
455
|
else
|
|
262
456
|
engineObjectsUpdate();
|
|
@@ -269,8 +463,9 @@ async function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, game
|
|
|
269
463
|
renderFrame();
|
|
270
464
|
}
|
|
271
465
|
|
|
272
|
-
//
|
|
273
|
-
|
|
466
|
+
// manual step turned on by this frame's updates, set the buffer the loop and smoothing just moved again
|
|
467
|
+
if (engineManualStep && !manualStepAtStart)
|
|
468
|
+
frameTimeBufferMS = -.5e3 / frameRate;
|
|
274
469
|
|
|
275
470
|
// check if the window changed so a resize is picked up even when
|
|
276
471
|
// the game is not updating, for example when timeScale is 0
|
|
@@ -289,8 +484,7 @@ async function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, game
|
|
|
289
484
|
// than the fixed update rate would otherwise redraw identical frames
|
|
290
485
|
if (!debugVideoCaptureIsActive() && (wasUpdated || windowChanged))
|
|
291
486
|
renderFrame();
|
|
292
|
-
|
|
293
|
-
requestAnimationFrame(engineUpdate);
|
|
487
|
+
engineManualStep || engineScheduleFrame();
|
|
294
488
|
|
|
295
489
|
function renderFrame()
|
|
296
490
|
{
|
|
@@ -314,6 +508,8 @@ async function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, game
|
|
|
314
508
|
|
|
315
509
|
// post rendering
|
|
316
510
|
gameRenderPost();
|
|
511
|
+
setShader(); // plugin, input and debug draws start from the engine's state
|
|
512
|
+
setAdditiveBlendMode(false);
|
|
317
513
|
pluginList.forEach(plugin=>plugin.render?.());
|
|
318
514
|
inputRender();
|
|
319
515
|
debugRender();
|
|
@@ -323,24 +519,32 @@ async function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, game
|
|
|
323
519
|
primitiveCount = 0;
|
|
324
520
|
}
|
|
325
521
|
}
|
|
326
|
-
engineUpdateInternal = engineUpdate;
|
|
327
522
|
|
|
328
523
|
// skip setup if headless
|
|
329
|
-
if (headlessMode) return startEngine();
|
|
524
|
+
if (headlessMode) return startEngine([]);
|
|
525
|
+
|
|
526
|
+
// ensure body exists for minimal HTML where the script runs before <body> is parsed
|
|
527
|
+
if (!document.body)
|
|
528
|
+
document.documentElement.appendChild(document.createElement('body'));
|
|
529
|
+
rootElement ||= document.body;
|
|
330
530
|
|
|
331
531
|
// setup webgl
|
|
332
532
|
glInit(rootElement);
|
|
333
533
|
|
|
334
534
|
// setup html
|
|
335
|
-
|
|
535
|
+
let styleRoot =
|
|
336
536
|
'margin:0;' + // fill the window
|
|
337
537
|
'overflow:hidden;' + // no scroll bars
|
|
338
538
|
'background:#000;' + // set background color
|
|
339
539
|
'user-select:none;' + // prevent hold to select
|
|
340
540
|
'-webkit-user-select:none;' + // compatibility for ios
|
|
341
541
|
'touch-action:none;' + // prevent mobile pinch to resize
|
|
342
|
-
'-webkit-touch-callout:none'; // compatibility for ios
|
|
343
|
-
|
|
542
|
+
'-webkit-touch-callout:none;'; // compatibility for ios
|
|
543
|
+
// the canvases center on a root element with a height of its own, not the page; one sized only by its
|
|
544
|
+
// children has none, since the canvases are placed apart from it, and would clip them all away
|
|
545
|
+
if (rootElement !== document.body && rootElement.clientHeight && getComputedStyle(rootElement).position === 'static')
|
|
546
|
+
styleRoot += 'position:relative;';
|
|
547
|
+
rootElement.style.cssText = styleRoot + rootElement.style.cssText; // its own inline styles come after and win
|
|
344
548
|
mainCanvas = rootElement.appendChild(document.createElement('canvas'));
|
|
345
549
|
drawContext = mainContext = mainCanvas.getContext('2d');
|
|
346
550
|
|
|
@@ -367,6 +571,7 @@ async function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, game
|
|
|
367
571
|
workReadCanvas = workReadContext.canvas;
|
|
368
572
|
|
|
369
573
|
// create promises for loading images
|
|
574
|
+
/** @type {Array<Promise<any>>} */
|
|
370
575
|
const promises = imageSources.map((src, i)=> loadTexture(i, src));
|
|
371
576
|
|
|
372
577
|
// no images to load
|
|
@@ -379,7 +584,8 @@ async function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, game
|
|
|
379
584
|
if (showSplashScreen)
|
|
380
585
|
{
|
|
381
586
|
// draw splash screen
|
|
382
|
-
|
|
587
|
+
/** @type {Promise<void>} */
|
|
588
|
+
const splash = new Promise(resolve =>
|
|
383
589
|
{
|
|
384
590
|
let t = 0;
|
|
385
591
|
updateSplash();
|
|
@@ -389,17 +595,24 @@ async function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, game
|
|
|
389
595
|
drawEngineLogo(t+=.01);
|
|
390
596
|
t>1 ? resolve() : setTimeout(updateSplash, 16);
|
|
391
597
|
}
|
|
392
|
-
})
|
|
598
|
+
});
|
|
599
|
+
promises.push(splash);
|
|
393
600
|
}
|
|
394
601
|
|
|
395
|
-
//
|
|
396
|
-
await
|
|
397
|
-
return startEngine();
|
|
602
|
+
// the splash first, the images load under it, then the loading screen for the rest
|
|
603
|
+
showSplashScreen && await promises.at(-1);
|
|
604
|
+
return startEngine(promises);
|
|
398
605
|
|
|
399
|
-
|
|
606
|
+
// gameInit runs once the images are in, and the game loop starts once it and everything loaded while it ran are
|
|
607
|
+
// done, the loading screen showing in the meantime; an error in gameInit reaches the caller
|
|
608
|
+
async function startEngine(images)
|
|
400
609
|
{
|
|
401
|
-
|
|
402
|
-
|
|
610
|
+
const init = (async ()=> { await Promise.all(images); await gameInit(); })();
|
|
611
|
+
engineAddLoad(init);
|
|
612
|
+
await engineWaitForLoads();
|
|
613
|
+
engineLoads = undefined;
|
|
614
|
+
await init;
|
|
615
|
+
engineUpdateInternal = engineUpdate; // engineStep only runs once the game is set up
|
|
403
616
|
engineManualStep || engineUpdate();
|
|
404
617
|
}
|
|
405
618
|
}
|
|
@@ -440,9 +653,9 @@ function engineUpdateCanvas()
|
|
|
440
653
|
mainCanvasSize.x = min(innerWidth, canvasMaxSize.x) | 0;
|
|
441
654
|
mainCanvasSize.y = min(innerHeight, canvasMaxSize.y) | 0;
|
|
442
655
|
|
|
443
|
-
// responsive aspect ratio
|
|
444
|
-
const innerAspect =
|
|
445
|
-
ASSERT(canvasMinAspect <= canvasMaxAspect);
|
|
656
|
+
// responsive aspect ratio, of the size after canvasMaxSize, which can change its shape
|
|
657
|
+
const innerAspect = mainCanvasSize.x / mainCanvasSize.y;
|
|
658
|
+
ASSERT(!canvasMaxAspect || canvasMinAspect <= canvasMaxAspect);
|
|
446
659
|
if (canvasMaxAspect && innerAspect > canvasMaxAspect)
|
|
447
660
|
{
|
|
448
661
|
// full height
|
|
@@ -495,19 +708,38 @@ function engineUpdateCanvas()
|
|
|
495
708
|
mainContext.fillStyle = BLACK.toString();
|
|
496
709
|
}
|
|
497
710
|
|
|
498
|
-
// set default line join and cap
|
|
711
|
+
// set default line join and cap, round on purpose: it looks better, suits text and keeps sharp corners from
|
|
712
|
+
// spiking far out; WebGL outlines are square and mitered for speed, the two are not meant to match
|
|
499
713
|
mainContext.lineJoin = 'round';
|
|
500
714
|
mainContext.lineCap = 'round';
|
|
501
715
|
}
|
|
502
716
|
|
|
717
|
+
// ask for the next frame of the loop, once however often it is called before that frame, and skip it if manual
|
|
718
|
+
// step was turned on since, so turning it off and on again within a frame can not start a second loop
|
|
719
|
+
function engineScheduleFrame()
|
|
720
|
+
{
|
|
721
|
+
if (engineFrameScheduled) return;
|
|
722
|
+
engineFrameScheduled = true;
|
|
723
|
+
const next = (frameTimeMS)=>
|
|
724
|
+
{
|
|
725
|
+
engineFrameScheduled = false;
|
|
726
|
+
engineManualStep || engineUpdateInternal(frameTimeMS);
|
|
727
|
+
};
|
|
728
|
+
if (typeof requestAnimationFrame === 'function')
|
|
729
|
+
requestAnimationFrame(next);
|
|
730
|
+
else // a headless server in Node has no display to wait for, a timer keeps the pace
|
|
731
|
+
setTimeout(()=> next(performance.now()), 1e3 / frameRate);
|
|
732
|
+
}
|
|
733
|
+
|
|
503
734
|
// max frames engineStep can advance in one call, 10 minutes at 60fps
|
|
504
735
|
// large counts block until they finish, so this catches runaway values
|
|
505
736
|
const engineStepMaxFrames = 36000;
|
|
506
737
|
|
|
507
738
|
/** Advance the engine by a number of frames
|
|
508
|
-
* Requires setEngineManualStep(true) before engineInit
|
|
739
|
+
* Requires setEngineManualStep(true), before engineInit or while running; it stops early if an update turns it off
|
|
509
740
|
* Respects paused exactly as the normal update loop does
|
|
510
|
-
* @param {number} [frames] -
|
|
741
|
+
* @param {number} [frames] - frames of 1/60 of a second to advance, max 36000; timeScale sets how many fixed
|
|
742
|
+
* updates they run, as in the normal loop, one each at timeScale 1
|
|
511
743
|
* @example
|
|
512
744
|
* setHeadlessMode(true);
|
|
513
745
|
* setEngineManualStep(true);
|
|
@@ -517,7 +749,7 @@ const engineStepMaxFrames = 36000;
|
|
|
517
749
|
function engineStep(frames=1)
|
|
518
750
|
{
|
|
519
751
|
ASSERT(engineManualStep,
|
|
520
|
-
'engineStep requires setEngineManualStep(true)
|
|
752
|
+
'engineStep requires setEngineManualStep(true)');
|
|
521
753
|
ASSERT(engineUpdateInternal, 'engineStep requires engineInit to complete');
|
|
522
754
|
// runtime guard so release builds (where the asserts are stripped) can't
|
|
523
755
|
// start a second requestAnimationFrame chain or call an undefined update
|
|
@@ -525,42 +757,73 @@ function engineStep(frames=1)
|
|
|
525
757
|
ASSERT(Number.isInteger(frames) && frames >= 0 && frames <= engineStepMaxFrames,
|
|
526
758
|
'engineStep requires a whole frame count from 0 to ' + engineStepMaxFrames);
|
|
527
759
|
frames = min(frames, engineStepMaxFrames); // release has no asserts, don't freeze
|
|
528
|
-
for (let i = frames; i > 0; --i)
|
|
760
|
+
for (let i = frames; i > 0 && engineManualStep; --i) // an update that turns manual step off hands back the loop
|
|
529
761
|
engineUpdateInternal(frameTimeLastMS + 1e3 / frameRate);
|
|
530
762
|
}
|
|
531
763
|
|
|
532
|
-
/** Update each engine object
|
|
533
|
-
*
|
|
764
|
+
/** Update each engine object and remove destroyed objects; time and frame are advanced by the engine loop, not here
|
|
765
|
+
* - Can be called manually if objects need to be updated outside of main loop
|
|
534
766
|
* @memberof Engine */
|
|
535
767
|
function engineObjectsUpdate()
|
|
536
768
|
{
|
|
537
|
-
|
|
769
|
+
++engineObjectsUpdateCount;
|
|
770
|
+
engineObjectsCollidePairs.clear();
|
|
771
|
+
// objects update in render order, which rendering keeps them in, so a headless run or a frame that rendered
|
|
772
|
+
// nothing updates them the same way; the sort is stable, and nearly free on a list that is already sorted
|
|
773
|
+
engineObjects.sort((a,b)=> a.renderOrder - b.renderOrder);
|
|
774
|
+
// get list of solid objects for physics optimization, in update order, which 3D collision pairs by;
|
|
775
|
+
// 2D checks the static ones last, so a contact with a moving object can not leave something back inside a static
|
|
776
|
+
// solid it was already pushed out of
|
|
538
777
|
engineObjectsCollide = engineObjects.filter(o=>o.collideSolidObjects);
|
|
778
|
+
engineObjectsCollideStaticLast = engineObjectsCollide.filter(o=>o.mass)
|
|
779
|
+
.concat(engineObjectsCollide.filter(o=>!o.mass));
|
|
539
780
|
|
|
540
781
|
// update physics before object update
|
|
541
782
|
for (const o of engineObjects)
|
|
542
783
|
if (!o.parent && !o.destroyed)
|
|
543
784
|
o.updatePhysics();
|
|
544
785
|
|
|
545
|
-
// recursive object update
|
|
786
|
+
// recursive object update: the children are walked from a copy on a shared stack, since a child that
|
|
787
|
+
// destroys itself leaves its parent's list on the spot and the next child would slide past the loop
|
|
788
|
+
function updateChildObjects(children)
|
|
789
|
+
{
|
|
790
|
+
if (!children.length) return; // most objects have none, and this runs for every one
|
|
791
|
+
const start = engineChildStack.length;
|
|
792
|
+
for (const child of children)
|
|
793
|
+
engineChildStack.push(child);
|
|
794
|
+
for (let i = start; i < engineChildStack.length; ++i)
|
|
795
|
+
updateChildObject(engineChildStack[i]);
|
|
796
|
+
engineChildStack.length = start;
|
|
797
|
+
}
|
|
798
|
+
const pass = engineObjectsUpdateCount;
|
|
546
799
|
function updateChildObject(o)
|
|
547
800
|
{
|
|
548
|
-
if (o.destroyed) return;
|
|
801
|
+
if (o.destroyed || o.updatePass === pass) return;
|
|
549
802
|
|
|
803
|
+
// its parent is up to date, so it updates from where it is now, and its children from where it is after
|
|
804
|
+
o.updatePass = pass;
|
|
805
|
+
o.updateTransforms(false);
|
|
550
806
|
o.update();
|
|
551
|
-
|
|
552
|
-
|
|
807
|
+
o.children.length && o.updateTransforms(false);
|
|
808
|
+
updateChildObjects(o.children);
|
|
553
809
|
}
|
|
554
|
-
|
|
810
|
+
function updateTopObject(o)
|
|
555
811
|
{
|
|
556
|
-
if (o.parent || o.destroyed)
|
|
812
|
+
if (o.parent || o.destroyed || o.updatePass === pass) return; // a child that let go is not updated twice
|
|
557
813
|
|
|
558
|
-
// update top level objects
|
|
814
|
+
// update top level objects, each child places itself before it updates so it sees this frame's position,
|
|
815
|
+
// then the whole tree is placed again so what the children changed in their localPos lands before render
|
|
816
|
+
o.updatePass = pass;
|
|
559
817
|
o.update();
|
|
560
|
-
|
|
561
|
-
updateChildObject(child);
|
|
818
|
+
updateChildObjects(o.children);
|
|
562
819
|
o.updateTransforms();
|
|
563
820
|
}
|
|
821
|
+
for (const o of engineObjects)
|
|
822
|
+
updateTopObject(o);
|
|
823
|
+
|
|
824
|
+
// a child let go during the update from a place in the list already walked is on its own now, updated here
|
|
825
|
+
for (const o of engineObjects)
|
|
826
|
+
updateTopObject(o);
|
|
564
827
|
|
|
565
828
|
// remove destroyed objects
|
|
566
829
|
engineObjects = engineObjects.filter(o=>!o.destroyed);
|
|
@@ -570,7 +833,7 @@ function engineObjectsUpdate()
|
|
|
570
833
|
* - This can be used to clear out all objects when restarting a level
|
|
571
834
|
* - Objects with the persistent flag set are left alone, for things that outlive a level
|
|
572
835
|
* - Objects can override their destroy function to do cleanup or stick around
|
|
573
|
-
* @param {boolean} [immediate] -
|
|
836
|
+
* @param {boolean} [immediate] - true removes attached effects like particle emitters at once, false lets them finish first
|
|
574
837
|
* @memberof Engine */
|
|
575
838
|
function engineObjectsDestroy(immediate=true)
|
|
576
839
|
{
|
|
@@ -580,32 +843,43 @@ function engineObjectsDestroy(immediate=true)
|
|
|
580
843
|
}
|
|
581
844
|
|
|
582
845
|
/** Collects all object within a given area
|
|
846
|
+
* - An object is collected when its box overlaps the area, or with testCenters when its center is inside it
|
|
847
|
+
* - Objects destroyed this frame are left out, they are only in the list until the frame ends
|
|
583
848
|
* @param {Vector2} [pos] - Center of test area, or undefined for all objects
|
|
584
|
-
* @param {Vector2|number} [size] -
|
|
849
|
+
* @param {Vector2|number} [size] - Diameter of a circle if a number, full size of a rectangle if a Vector2,
|
|
850
|
+
* left out or 0 the objects that overlap the point at pos
|
|
585
851
|
* @param {Array<EngineObject>} [objects=engineObjects] - List of objects to check
|
|
852
|
+
* @param {boolean} [testCenters] - Test only each object's center, a little faster, and ignores object sizes
|
|
586
853
|
* @return {Array<EngineObject>} - List of collected objects
|
|
587
854
|
* @memberof Engine */
|
|
588
|
-
function engineObjectsCollect(pos, size, objects=engineObjects)
|
|
855
|
+
function engineObjectsCollect(pos, size, objects=engineObjects, testCenters=false)
|
|
589
856
|
{
|
|
590
857
|
const collectedObjects = [];
|
|
591
858
|
if (!pos)
|
|
592
859
|
{
|
|
593
860
|
// all objects
|
|
594
861
|
for (const o of objects)
|
|
595
|
-
collectedObjects.push(o);
|
|
862
|
+
o.destroyed || collectedObjects.push(o);
|
|
596
863
|
}
|
|
597
|
-
else if (size instanceof Vector2)
|
|
864
|
+
else if (!size || size instanceof Vector2)
|
|
598
865
|
{
|
|
599
|
-
// bounding box test
|
|
866
|
+
// bounding box test, a point when there is no size or a size of 0
|
|
867
|
+
const boxSize = size instanceof Vector2 ? size : vec2();
|
|
600
868
|
for (const o of objects)
|
|
601
|
-
o.isOverlapping(pos,
|
|
869
|
+
o.destroyed || (testCenters ? isOverlapping(pos, boxSize, o.pos) : o.isOverlapping(pos, boxSize))
|
|
870
|
+
&& collectedObjects.push(o);
|
|
602
871
|
}
|
|
603
872
|
else
|
|
604
873
|
{
|
|
605
|
-
// circle test
|
|
606
|
-
const
|
|
874
|
+
// circle test, a diameter like every other size, against the nearest point of each box
|
|
875
|
+
const radiusSquared = (size/2)**2;
|
|
607
876
|
for (const o of objects)
|
|
608
|
-
|
|
877
|
+
{
|
|
878
|
+
if (o.destroyed) continue;
|
|
879
|
+
const dx = testCenters ? pos.x - o.pos.x : max(abs(pos.x - o.pos.x) - o.size.x/2, 0);
|
|
880
|
+
const dy = testCenters ? pos.y - o.pos.y : max(abs(pos.y - o.pos.y) - o.size.y/2, 0);
|
|
881
|
+
dx*dx + dy*dy < radiusSquared && collectedObjects.push(o);
|
|
882
|
+
}
|
|
609
883
|
}
|
|
610
884
|
return collectedObjects;
|
|
611
885
|
}
|
|
@@ -616,16 +890,23 @@ function engineObjectsCollect(pos, size, objects=engineObjects)
|
|
|
616
890
|
* @memberof Engine
|
|
617
891
|
*/
|
|
618
892
|
|
|
619
|
-
/** Triggers a callback for each object within a given area
|
|
893
|
+
/** Triggers a callback for each object within a given area, objects destroyed this frame left out
|
|
620
894
|
* @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
|
|
895
|
+
* @param {Vector2|number} [size] - Diameter of a circle if a number, full size of a rectangle if a Vector2
|
|
896
|
+
* @param {ObjectCallbackFunction} [callbackFunction] - Calls this function on every object that passes the test, needed
|
|
897
|
+
* (marked optional only because the area before it is)
|
|
623
898
|
* @param {Array<EngineObject>} [objects=engineObjects] - List of objects to check
|
|
899
|
+
* @param {boolean} [testCenters] - Test only each object's center, see engineObjectsCollect
|
|
624
900
|
* @memberof Engine */
|
|
625
|
-
function engineObjectsCallback(pos, size, callbackFunction, objects=engineObjects)
|
|
626
|
-
{
|
|
901
|
+
function engineObjectsCallback(pos, size, callbackFunction, objects=engineObjects, testCenters=false)
|
|
902
|
+
{
|
|
903
|
+
// an object an earlier callback destroyed is skipped
|
|
904
|
+
for (const o of engineObjectsCollect(pos, size, objects, testCenters))
|
|
905
|
+
o.destroyed || callbackFunction(o);
|
|
906
|
+
}
|
|
627
907
|
|
|
628
|
-
/** Return a list of objects intersecting a ray
|
|
908
|
+
/** Return a list of objects intersecting a ray, objects destroyed this frame left out
|
|
909
|
+
* - Only objects with collideRaycast set are hit, which setCollision turns on
|
|
629
910
|
* @param {Vector2} start
|
|
630
911
|
* @param {Vector2} end
|
|
631
912
|
* @param {Array<EngineObject>} [objects=engineObjects] - List of objects to check
|
|
@@ -636,13 +917,13 @@ function engineObjectsRaycast(start, end, objects=engineObjects)
|
|
|
636
917
|
const hitObjects = [];
|
|
637
918
|
for (const o of objects)
|
|
638
919
|
{
|
|
639
|
-
if (o.collideRaycast && isIntersecting(start, end, o.pos, o.size))
|
|
920
|
+
if (o.collideRaycast && !o.destroyed && isIntersecting(start, end, o.pos, o.size))
|
|
640
921
|
{
|
|
641
|
-
debugRaycast && debugRect(o.pos, o.size, '#f00');
|
|
922
|
+
debugRaycast && debugRect(o.pos, o.size, '#f00', 0, 0, false, false);
|
|
642
923
|
hitObjects.push(o);
|
|
643
924
|
}
|
|
644
925
|
}
|
|
645
926
|
|
|
646
|
-
debugRaycast && debugLine(start, end, hitObjects.length ? '#f00' : '#00f', .02);
|
|
927
|
+
debugRaycast && debugLine(start, end, hitObjects.length ? '#f00' : '#00f', .02, 0, false);
|
|
647
928
|
return hitObjects;
|
|
648
929
|
}
|