littlejsengine 1.18.29 → 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 -815
- package/README.md +211 -199
- package/dist/littlejs.d.ts +4383 -812
- package/dist/littlejs.esm.js +31848 -16680
- package/dist/littlejs.esm.min.js +14 -1
- package/dist/littlejs.js +31230 -16166
- package/dist/littlejs.min.js +13 -1
- package/dist/littlejs.release.js +27419 -15457
- package/package.json +10 -5
- package/plugins/audioEffects.js +375 -0
- 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 -330
- package/plugins/math3d.js +911 -0
- package/plugins/medalSystem.js +118 -72
- package/plugins/newgrounds.js +400 -199
- package/plugins/pathFinder.js +208 -175
- package/plugins/pluginExport.js +196 -115
- package/plugins/postProcess.js +284 -166
- package/plugins/render3d.js +4072 -0
- package/plugins/render3dDebug.js +109 -0
- package/plugins/render3dExtras.js +1754 -0
- package/plugins/render3dVoxels.js +502 -0
- package/plugins/textureSheet.js +86 -48
- package/plugins/threejs.js +183 -158
- package/plugins/tweakables.js +411 -0
- package/plugins/tweenSystem.js +646 -526
- package/plugins/uiSystem.js +2024 -1687
- package/plugins/zzfxm.js +8 -15
- package/src/engine.js +345 -194
- package/src/engineAudio.js +1022 -777
- package/src/engineBuild.mjs +289 -253
- package/src/engineDebug.js +912 -735
- package/src/engineDraw.js +1774 -1510
- package/src/engineEditor.js +2431 -0
- package/src/engineExport.js +419 -396
- package/src/engineInput.js +1462 -1293
- package/src/engineLogo.js +4 -3
- package/src/engineMath.js +1274 -1183
- package/src/engineObject.js +674 -551
- package/src/engineParticles.js +583 -532
- package/src/engineRelease.js +67 -45
- package/src/engineSettings.js +808 -726
- package/src/engineTileLayer.js +995 -741
- package/src/engineUtilities.js +32 -13
- package/src/engineWebGL.js +1162 -941
- 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;
|
|
@@ -89,7 +92,25 @@ function setPaused(isPaused=true) { paused = isPaused; }
|
|
|
89
92
|
|
|
90
93
|
// Engine internal variables
|
|
91
94
|
let frameTimeLastMS = 0, frameTimeBufferMS = 0, averageFPS = 0;
|
|
95
|
+
let windowWidthLast = 0, windowHeightLast = 0, windowPixelRatioLast = 0;
|
|
92
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
|
|
93
114
|
let showEngineVersion = true;
|
|
94
115
|
|
|
95
116
|
///////////////////////////////////////////////////////////////////////////////
|
|
@@ -98,12 +119,13 @@ let showEngineVersion = true;
|
|
|
98
119
|
const pluginList = [];
|
|
99
120
|
class EnginePlugin
|
|
100
121
|
{
|
|
101
|
-
constructor(update, render, glContextLost, glContextRestored)
|
|
122
|
+
constructor(update, render, glContextLost, glContextRestored, preRender)
|
|
102
123
|
{
|
|
103
124
|
this.update = update;
|
|
104
125
|
this.render = render;
|
|
105
126
|
this.glContextLost = glContextLost;
|
|
106
127
|
this.glContextRestored = glContextRestored;
|
|
128
|
+
this.preRender = preRender;
|
|
107
129
|
}
|
|
108
130
|
}
|
|
109
131
|
|
|
@@ -113,20 +135,23 @@ class EnginePlugin
|
|
|
113
135
|
*/
|
|
114
136
|
|
|
115
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
|
|
116
139
|
* @param {PluginCallback} [update]
|
|
117
140
|
* @param {PluginCallback} [render]
|
|
118
141
|
* @param {PluginCallback} [glContextLost]
|
|
119
142
|
* @param {PluginCallback} [glContextRestored]
|
|
143
|
+
* @param {PluginCallback} [preRender] - Called after the canvas is cleared and before gameRender
|
|
120
144
|
* @memberof Engine */
|
|
121
|
-
function engineAddPlugin(update, render, glContextLost, glContextRestored)
|
|
145
|
+
function engineAddPlugin(update, render, glContextLost, glContextRestored, preRender)
|
|
122
146
|
{
|
|
123
147
|
// make sure plugin functions are unique
|
|
124
148
|
ASSERT(!pluginList.find(p=>
|
|
125
149
|
p.update === update && p.render === render &&
|
|
126
150
|
p.glContextLost === glContextLost &&
|
|
127
|
-
p.glContextRestored === glContextRestored
|
|
151
|
+
p.glContextRestored === glContextRestored &&
|
|
152
|
+
p.preRender === preRender));
|
|
128
153
|
|
|
129
|
-
const plugin = new EnginePlugin(update, render, glContextLost, glContextRestored);
|
|
154
|
+
const plugin = new EnginePlugin(update, render, glContextLost, glContextRestored, preRender);
|
|
130
155
|
pluginList.push(plugin);
|
|
131
156
|
}
|
|
132
157
|
|
|
@@ -144,13 +169,15 @@ function engineAddPlugin(update, render, glContextLost, glContextRestored)
|
|
|
144
169
|
*/
|
|
145
170
|
|
|
146
171
|
/** Startup LittleJS engine with your callback functions
|
|
147
|
-
* @param {GameInitCallback} gameInit - Called once after the engine starts up, can be async for loading
|
|
148
|
-
* @param {GameCallback} gameUpdate - Called every frame before objects are updated (60fps), use for game logic
|
|
149
|
-
* @param {GameCallback} gameUpdatePost - Called after physics and objects are updated, even when paused, use for UI updates
|
|
150
|
-
* @param {GameCallback} gameRender - Called before objects are rendered, use for drawing backgrounds/world elements
|
|
151
|
-
* @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
|
|
152
177
|
* @param {Array<string>} [imageSources=[]] - List of image file paths to preload (e.g., ['player.png', 'tiles.png'])
|
|
153
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
|
|
154
181
|
* @example
|
|
155
182
|
* // Basic engine startup
|
|
156
183
|
* engineInit(
|
|
@@ -165,17 +192,13 @@ function engineAddPlugin(update, render, glContextLost, glContextRestored)
|
|
|
165
192
|
async function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRenderPost, imageSources=[], rootElement)
|
|
166
193
|
{
|
|
167
194
|
showEngineVersion && console.log(`${engineName} Engine v${engineVersion}`);
|
|
168
|
-
ASSERT(!
|
|
195
|
+
ASSERT(!engineInitialized, 'engine already initialized');
|
|
169
196
|
// runtime guard so release builds (where the assert is stripped) don't
|
|
170
197
|
// double-register listeners / double-add canvases on a second call
|
|
171
|
-
if (
|
|
198
|
+
if (engineInitialized) return;
|
|
199
|
+
engineInitialized = true;
|
|
172
200
|
ASSERT(isArray(imageSources), 'pass in images as array');
|
|
173
201
|
|
|
174
|
-
// ensure body exists for minimal HTML where the script runs before <body> is parsed
|
|
175
|
-
if (!document.body)
|
|
176
|
-
document.documentElement.appendChild(document.createElement('body'));
|
|
177
|
-
rootElement ||= document.body;
|
|
178
|
-
|
|
179
202
|
// allow passing in empty functions
|
|
180
203
|
gameInit ||= ()=>{};
|
|
181
204
|
gameUpdate ||= ()=>{};
|
|
@@ -186,19 +209,25 @@ async function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, game
|
|
|
186
209
|
// Called automatically by engine to setup render system
|
|
187
210
|
function enginePreRender()
|
|
188
211
|
{
|
|
189
|
-
//
|
|
190
|
-
|
|
212
|
+
// the level editor's own view while it is open, in debug builds, before the camera goes to WebGL
|
|
213
|
+
editorPreRender();
|
|
191
214
|
|
|
192
215
|
// disable smoothing for pixel art
|
|
193
216
|
mainContext.imageSmoothingEnabled = !tilesPixelated;
|
|
194
217
|
|
|
195
218
|
// setup gl rendering if enabled
|
|
196
219
|
glPreRender();
|
|
220
|
+
setShader(); // a shader left set last frame does not carry into this one
|
|
221
|
+
|
|
222
|
+
// plugins that draw underneath the 2D layer
|
|
223
|
+
pluginList.forEach(plugin=>plugin.preRender?.());
|
|
197
224
|
}
|
|
198
225
|
|
|
199
226
|
// internal update loop for engine
|
|
200
227
|
function engineUpdate(frameTimeMS=0)
|
|
201
228
|
{
|
|
229
|
+
const manualStepAtStart = engineManualStep;
|
|
230
|
+
|
|
202
231
|
// update time keeping
|
|
203
232
|
let frameTimeDeltaMS = frameTimeMS - frameTimeLastMS;
|
|
204
233
|
// skip delta on the very first frame so timeReal doesn't jump
|
|
@@ -207,30 +236,61 @@ async function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, game
|
|
|
207
236
|
frameTimeLastMS = frameTimeMS;
|
|
208
237
|
if (debug || debugWatermark)
|
|
209
238
|
averageFPS = lerp(averageFPS, 1e3/(frameTimeDeltaMS||1), .05);
|
|
210
|
-
|
|
211
|
-
|
|
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'); // -
|
|
212
244
|
const debugScale = debugSpeedUp ? 10 : debugSpeedDown ? .1 : 1;
|
|
213
245
|
|
|
214
246
|
// apply time deltas
|
|
247
|
+
const frameTimeDeltaUnscaledMS = frameTimeDeltaMS;
|
|
215
248
|
timeReal += frameTimeDeltaMS * debugScale / 1e3;
|
|
216
249
|
const combinedScale = timeScale * debugScale;
|
|
217
250
|
frameTimeDeltaMS *= combinedScale;
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
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
|
|
256
|
+
|
|
257
|
+
// apply time delta smoothing, improves smoothness of framerate in some browsers
|
|
258
|
+
let wasUpdated = false, deltaSmooth = 0;
|
|
259
|
+
if (frameTimeBufferMS < 0 && frameTimeBufferMS > -9)
|
|
260
|
+
{
|
|
261
|
+
// force at least one update each frame since it is waiting for refresh
|
|
262
|
+
deltaSmooth = frameTimeBufferMS;
|
|
263
|
+
frameTimeBufferMS = 0;
|
|
264
|
+
}
|
|
221
265
|
|
|
222
|
-
|
|
223
|
-
|
|
266
|
+
// update multiple frames if necessary in case of slow framerate
|
|
267
|
+
for (; frameTimeBufferMS >= 0; frameTimeBufferMS -= 1e3 / frameRate)
|
|
224
268
|
{
|
|
225
|
-
//
|
|
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)
|
|
274
|
+
time = frame++ / frameRate;
|
|
275
|
+
|
|
276
|
+
// update game and objects, when frozen update everything except them
|
|
226
277
|
wasUpdated = true;
|
|
227
|
-
|
|
278
|
+
engineUpdateCanvas();
|
|
228
279
|
inputUpdate();
|
|
280
|
+
if (!frozenTick)
|
|
281
|
+
gameUpdate();
|
|
229
282
|
pluginList.forEach(plugin=>plugin.update?.());
|
|
283
|
+
if (frozenTick)
|
|
284
|
+
{
|
|
285
|
+
// update object transforms even when paused
|
|
286
|
+
for (const o of engineObjects)
|
|
287
|
+
o.parent || o.updateTransforms();
|
|
230
288
|
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
289
|
+
// objects made and destroyed while paused, like a menu's effects, still leave the list
|
|
290
|
+
engineObjects = engineObjects.filter(o=>!o.destroyed);
|
|
291
|
+
}
|
|
292
|
+
else
|
|
293
|
+
engineObjectsUpdate();
|
|
234
294
|
|
|
235
295
|
// do post update
|
|
236
296
|
debugUpdate();
|
|
@@ -239,47 +299,32 @@ async function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, game
|
|
|
239
299
|
if (debugVideoCaptureIsActive())
|
|
240
300
|
renderFrame();
|
|
241
301
|
}
|
|
242
|
-
else
|
|
243
|
-
{
|
|
244
|
-
// apply time delta smoothing, improves smoothness of framerate in some browsers
|
|
245
|
-
let deltaSmooth = 0;
|
|
246
|
-
if (frameTimeBufferMS < 0 && frameTimeBufferMS > -9)
|
|
247
|
-
{
|
|
248
|
-
// force at least one update each frame since it is waiting for refresh
|
|
249
|
-
deltaSmooth = frameTimeBufferMS;
|
|
250
|
-
frameTimeBufferMS = 0;
|
|
251
|
-
}
|
|
252
302
|
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
{
|
|
256
|
-
// increment frame and update time
|
|
257
|
-
time = frame++ / frameRate;
|
|
303
|
+
// add the time smoothing back in
|
|
304
|
+
frameTimeBufferMS += deltaSmooth;
|
|
258
305
|
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
inputUpdate();
|
|
263
|
-
gameUpdate();
|
|
264
|
-
pluginList.forEach(plugin=>plugin.update?.());
|
|
265
|
-
engineObjectsUpdate();
|
|
266
|
-
|
|
267
|
-
// do post update
|
|
268
|
-
debugUpdate();
|
|
269
|
-
gameUpdatePost();
|
|
270
|
-
inputUpdatePost();
|
|
271
|
-
if (debugVideoCaptureIsActive())
|
|
272
|
-
renderFrame();
|
|
273
|
-
}
|
|
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;
|
|
274
309
|
|
|
275
|
-
|
|
276
|
-
|
|
310
|
+
// check if the window changed so a resize is picked up even when
|
|
311
|
+
// the game is not updating, for example when timeScale is 0
|
|
312
|
+
let windowChanged = false;
|
|
313
|
+
if (!headlessMode)
|
|
314
|
+
{
|
|
315
|
+
const dpr = devicePixelRatio;
|
|
316
|
+
windowChanged = windowWidthLast !== innerWidth ||
|
|
317
|
+
windowHeightLast !== innerHeight || windowPixelRatioLast !== dpr;
|
|
318
|
+
windowWidthLast = innerWidth;
|
|
319
|
+
windowHeightLast = innerHeight;
|
|
320
|
+
windowPixelRatioLast = dpr;
|
|
277
321
|
}
|
|
278
322
|
|
|
279
|
-
|
|
323
|
+
// render only when something changed, displays that refresh faster
|
|
324
|
+
// than the fixed update rate would otherwise redraw identical frames
|
|
325
|
+
if (!debugVideoCaptureIsActive() && (wasUpdated || windowChanged))
|
|
280
326
|
renderFrame();
|
|
281
|
-
|
|
282
|
-
requestAnimationFrame(engineUpdate);
|
|
327
|
+
engineManualStep || engineScheduleFrame();
|
|
283
328
|
|
|
284
329
|
function renderFrame()
|
|
285
330
|
{
|
|
@@ -287,17 +332,24 @@ async function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, game
|
|
|
287
332
|
|
|
288
333
|
// canvas must be updated before rendering
|
|
289
334
|
if (!wasUpdated)
|
|
290
|
-
|
|
335
|
+
engineUpdateCanvas();
|
|
291
336
|
|
|
292
337
|
// render the game and objects
|
|
293
338
|
enginePreRender();
|
|
294
339
|
gameRender();
|
|
295
340
|
engineObjects.sort((a,b)=> a.renderOrder - b.renderOrder);
|
|
296
341
|
for (const o of engineObjects)
|
|
297
|
-
|
|
342
|
+
{
|
|
343
|
+
if (o.destroyed) continue;
|
|
344
|
+
setShader(o.shader); // each object draws with its own shader, or none
|
|
345
|
+
o.render();
|
|
346
|
+
}
|
|
347
|
+
setShader(); // back to the engine's for gameRenderPost
|
|
298
348
|
|
|
299
349
|
// post rendering
|
|
300
350
|
gameRenderPost();
|
|
351
|
+
setShader(); // plugin, input and debug draws start from the engine's state
|
|
352
|
+
setAdditiveBlendMode(false);
|
|
301
353
|
pluginList.forEach(plugin=>plugin.render?.());
|
|
302
354
|
inputRender();
|
|
303
355
|
debugRender();
|
|
@@ -307,102 +359,32 @@ async function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, game
|
|
|
307
359
|
primitiveCount = 0;
|
|
308
360
|
}
|
|
309
361
|
}
|
|
310
|
-
engineUpdateInternal = engineUpdate;
|
|
311
|
-
|
|
312
|
-
function updateCanvas()
|
|
313
|
-
{
|
|
314
|
-
if (headlessMode) return;
|
|
315
|
-
|
|
316
|
-
if (canvasFixedSize.x)
|
|
317
|
-
{
|
|
318
|
-
// set canvas fixed size
|
|
319
|
-
mainCanvasSize = canvasFixedSize.copy();
|
|
320
|
-
|
|
321
|
-
// fit to window using css width and height
|
|
322
|
-
const innerAspect = innerWidth / innerHeight;
|
|
323
|
-
const fixedAspect = canvasFixedSize.x / canvasFixedSize.y;
|
|
324
|
-
const w = innerAspect < fixedAspect ? '100%' : '';
|
|
325
|
-
const h = innerAspect < fixedAspect ? '' : '100%';
|
|
326
|
-
mainCanvas.style.width = w;
|
|
327
|
-
mainCanvas.style.height = h;
|
|
328
|
-
if (glCanvas)
|
|
329
|
-
{
|
|
330
|
-
glCanvas.style.width = w;
|
|
331
|
-
glCanvas.style.height = h;
|
|
332
|
-
}
|
|
333
|
-
}
|
|
334
|
-
else
|
|
335
|
-
{
|
|
336
|
-
// apply device pixel ratio for crisp rendering
|
|
337
|
-
const dpr = canvasPixelRatio ?? (devicePixelRatio || 1);
|
|
338
|
-
const viewWidth = innerWidth * dpr | 0;
|
|
339
|
-
const viewHeight = innerHeight * dpr | 0;
|
|
340
|
-
|
|
341
|
-
// get main canvas size based on window size
|
|
342
|
-
mainCanvasSize.x = min(viewWidth, canvasMaxSize.x);
|
|
343
|
-
mainCanvasSize.y = min(viewHeight, canvasMaxSize.y);
|
|
344
|
-
|
|
345
|
-
// responsive aspect ratio with native resolution
|
|
346
|
-
const innerAspect = viewWidth / viewHeight;
|
|
347
|
-
ASSERT(canvasMinAspect <= canvasMaxAspect);
|
|
348
|
-
if (canvasMaxAspect && innerAspect > canvasMaxAspect)
|
|
349
|
-
{
|
|
350
|
-
// full height
|
|
351
|
-
const w = mainCanvasSize.y * canvasMaxAspect | 0;
|
|
352
|
-
mainCanvasSize.x = min(w, canvasMaxSize.x);
|
|
353
|
-
}
|
|
354
|
-
else if (innerAspect < canvasMinAspect)
|
|
355
|
-
{
|
|
356
|
-
// full width
|
|
357
|
-
const h = mainCanvasSize.x / canvasMinAspect | 0;
|
|
358
|
-
mainCanvasSize.y = min(h, canvasMaxSize.y);
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
// set CSS display size so backing store renders at viewport size
|
|
362
|
-
const cssW = (mainCanvasSize.x / dpr | 0) + 'px';
|
|
363
|
-
const cssH = (mainCanvasSize.y / dpr | 0) + 'px';
|
|
364
|
-
mainCanvas.style.width = cssW;
|
|
365
|
-
mainCanvas.style.height = cssH;
|
|
366
|
-
if (glCanvas)
|
|
367
|
-
{
|
|
368
|
-
glCanvas.style.width = cssW;
|
|
369
|
-
glCanvas.style.height = cssH;
|
|
370
|
-
}
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
// clear main canvas and set size
|
|
374
|
-
mainCanvas.width = mainCanvasSize.x;
|
|
375
|
-
mainCanvas.height = mainCanvasSize.y;
|
|
376
|
-
|
|
377
|
-
// apply the clear color to main canvas
|
|
378
|
-
if (canvasClearColor.a > 0 && !glEnable)
|
|
379
|
-
{
|
|
380
|
-
mainContext.fillStyle = canvasClearColor.toString();
|
|
381
|
-
mainContext.fillRect(0, 0, mainCanvasSize.x, mainCanvasSize.y);
|
|
382
|
-
mainContext.fillStyle = BLACK.toString();
|
|
383
|
-
}
|
|
384
362
|
|
|
385
|
-
// set default line join and cap
|
|
386
|
-
mainContext.lineJoin = 'round';
|
|
387
|
-
mainContext.lineCap = 'round';
|
|
388
|
-
}
|
|
389
|
-
|
|
390
363
|
// skip setup if headless
|
|
391
364
|
if (headlessMode) return startEngine();
|
|
392
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
|
+
|
|
393
371
|
// setup webgl
|
|
394
372
|
glInit(rootElement);
|
|
395
373
|
|
|
396
374
|
// setup html
|
|
397
|
-
|
|
375
|
+
let styleRoot =
|
|
398
376
|
'margin:0;' + // fill the window
|
|
399
377
|
'overflow:hidden;' + // no scroll bars
|
|
400
378
|
'background:#000;' + // set background color
|
|
401
379
|
'user-select:none;' + // prevent hold to select
|
|
402
380
|
'-webkit-user-select:none;' + // compatibility for ios
|
|
403
381
|
'touch-action:none;' + // prevent mobile pinch to resize
|
|
404
|
-
'-webkit-touch-callout:none'; // compatibility for ios
|
|
405
|
-
|
|
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
|
|
406
388
|
mainCanvas = rootElement.appendChild(document.createElement('canvas'));
|
|
407
389
|
drawContext = mainContext = mainCanvas.getContext('2d');
|
|
408
390
|
|
|
@@ -419,16 +401,17 @@ async function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, game
|
|
|
419
401
|
if (glCanvas)
|
|
420
402
|
glCanvas.style.cssText = styleCanvas;
|
|
421
403
|
setCanvasPixelated(canvasPixelated);
|
|
422
|
-
|
|
404
|
+
engineUpdateCanvas();
|
|
423
405
|
glPreRender();
|
|
424
406
|
|
|
425
407
|
// create offscreen canvases for image processing
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
408
|
+
workContext = createCanvasContext(64);
|
|
409
|
+
workCanvas = workContext.canvas;
|
|
410
|
+
workReadContext = createCanvasContext(64, 64, true);
|
|
411
|
+
workReadCanvas = workReadContext.canvas;
|
|
430
412
|
|
|
431
413
|
// create promises for loading images
|
|
414
|
+
/** @type {Array<Promise<any>>} */
|
|
432
415
|
const promises = imageSources.map((src, i)=> loadTexture(i, src));
|
|
433
416
|
|
|
434
417
|
// no images to load
|
|
@@ -441,7 +424,8 @@ async function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, game
|
|
|
441
424
|
if (showSplashScreen)
|
|
442
425
|
{
|
|
443
426
|
// draw splash screen
|
|
444
|
-
|
|
427
|
+
/** @type {Promise<void>} */
|
|
428
|
+
const splash = new Promise(resolve =>
|
|
445
429
|
{
|
|
446
430
|
let t = 0;
|
|
447
431
|
updateSplash();
|
|
@@ -451,7 +435,8 @@ async function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, game
|
|
|
451
435
|
drawEngineLogo(t+=.01);
|
|
452
436
|
t>1 ? resolve() : setTimeout(updateSplash, 16);
|
|
453
437
|
}
|
|
454
|
-
})
|
|
438
|
+
});
|
|
439
|
+
promises.push(splash);
|
|
455
440
|
}
|
|
456
441
|
|
|
457
442
|
// wait for all the promises to finish
|
|
@@ -462,18 +447,134 @@ async function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, game
|
|
|
462
447
|
{
|
|
463
448
|
// wait for gameInit to load
|
|
464
449
|
await gameInit();
|
|
450
|
+
engineUpdateInternal = engineUpdate; // engineStep only runs once the game is set up
|
|
465
451
|
engineManualStep || engineUpdate();
|
|
466
452
|
}
|
|
467
453
|
}
|
|
468
454
|
|
|
455
|
+
// Resize the canvas to fit the window and prepare it for a new frame
|
|
456
|
+
// Called automatically each frame and by the splash screen before the loop starts
|
|
457
|
+
// mainCanvasSize is css pixels and the backing store is that scaled by the
|
|
458
|
+
// pixel ratio, so the ratio only changes sharpness, never how big things look
|
|
459
|
+
function engineUpdateCanvas()
|
|
460
|
+
{
|
|
461
|
+
if (headlessMode) return;
|
|
462
|
+
|
|
463
|
+
// the backing store is scaled by this, every size below is css pixels
|
|
464
|
+
const dpr = getCanvasPixelRatio();
|
|
465
|
+
|
|
466
|
+
if (canvasFixedSize.x)
|
|
467
|
+
{
|
|
468
|
+
// set canvas fixed size
|
|
469
|
+
mainCanvasSize = canvasFixedSize.copy();
|
|
470
|
+
|
|
471
|
+
// fit to window using css width and height
|
|
472
|
+
const innerAspect = innerWidth / innerHeight;
|
|
473
|
+
const fixedAspect = canvasFixedSize.x / canvasFixedSize.y;
|
|
474
|
+
const w = innerAspect < fixedAspect ? '100%' : '';
|
|
475
|
+
const h = innerAspect < fixedAspect ? '' : '100%';
|
|
476
|
+
mainCanvas.style.width = w;
|
|
477
|
+
mainCanvas.style.height = h;
|
|
478
|
+
if (glCanvas)
|
|
479
|
+
{
|
|
480
|
+
glCanvas.style.width = w;
|
|
481
|
+
glCanvas.style.height = h;
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
else
|
|
485
|
+
{
|
|
486
|
+
// get main canvas size based on window size, in css pixels so
|
|
487
|
+
// canvasMaxSize caps how big the canvas looks, not its resolution
|
|
488
|
+
mainCanvasSize.x = min(innerWidth, canvasMaxSize.x) | 0;
|
|
489
|
+
mainCanvasSize.y = min(innerHeight, canvasMaxSize.y) | 0;
|
|
490
|
+
|
|
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);
|
|
494
|
+
if (canvasMaxAspect && innerAspect > canvasMaxAspect)
|
|
495
|
+
{
|
|
496
|
+
// full height
|
|
497
|
+
const w = mainCanvasSize.y * canvasMaxAspect | 0;
|
|
498
|
+
mainCanvasSize.x = min(w, canvasMaxSize.x);
|
|
499
|
+
}
|
|
500
|
+
else if (innerAspect < canvasMinAspect)
|
|
501
|
+
{
|
|
502
|
+
// full width
|
|
503
|
+
const h = mainCanvasSize.x / canvasMinAspect | 0;
|
|
504
|
+
mainCanvasSize.y = min(h, canvasMaxSize.y);
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
// css size is the canvas size, the backing store is scaled up below
|
|
508
|
+
mainCanvas.style.width = mainCanvasSize.x + 'px';
|
|
509
|
+
mainCanvas.style.height = mainCanvasSize.y + 'px';
|
|
510
|
+
if (glCanvas)
|
|
511
|
+
{
|
|
512
|
+
glCanvas.style.width = mainCanvasSize.x + 'px';
|
|
513
|
+
glCanvas.style.height = mainCanvasSize.y + 'px';
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
// clear main canvas and set size
|
|
518
|
+
// only set the size when it changes, setting it invalidates the canvas
|
|
519
|
+
// frame which makes the browser rebuild the display list for the page
|
|
520
|
+
const bufferSizeX = mainCanvasSize.x * dpr | 0;
|
|
521
|
+
const bufferSizeY = mainCanvasSize.y * dpr | 0;
|
|
522
|
+
if (mainCanvas.width !== bufferSizeX || mainCanvas.height !== bufferSizeY)
|
|
523
|
+
{
|
|
524
|
+
mainCanvas.width = bufferSizeX;
|
|
525
|
+
mainCanvas.height = bufferSizeY;
|
|
526
|
+
}
|
|
527
|
+
else
|
|
528
|
+
{
|
|
529
|
+
// setting the size also resets the context state, match that
|
|
530
|
+
mainContext.setTransform(1, 0, 0, 1, 0, 0);
|
|
531
|
+
mainContext.globalCompositeOperation = 'source-over';
|
|
532
|
+
mainContext.clearRect(0, 0, bufferSizeX, bufferSizeY);
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
// scale the context so 2d drawing is in css pixels
|
|
536
|
+
mainContext.setTransform(dpr, 0, 0, dpr, 0, 0);
|
|
537
|
+
|
|
538
|
+
// apply the clear color to main canvas
|
|
539
|
+
if (canvasClearColor.a > 0 && !glEnable)
|
|
540
|
+
{
|
|
541
|
+
mainContext.fillStyle = canvasClearColor.toString();
|
|
542
|
+
mainContext.fillRect(0, 0, mainCanvasSize.x, mainCanvasSize.y);
|
|
543
|
+
mainContext.fillStyle = BLACK.toString();
|
|
544
|
+
}
|
|
545
|
+
|
|
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
|
|
548
|
+
mainContext.lineJoin = 'round';
|
|
549
|
+
mainContext.lineCap = 'round';
|
|
550
|
+
}
|
|
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
|
+
|
|
469
569
|
// max frames engineStep can advance in one call, 10 minutes at 60fps
|
|
470
570
|
// large counts block until they finish, so this catches runaway values
|
|
471
571
|
const engineStepMaxFrames = 36000;
|
|
472
572
|
|
|
473
573
|
/** Advance the engine by a number of frames
|
|
474
|
-
* Requires setEngineManualStep(true) before engineInit
|
|
574
|
+
* Requires setEngineManualStep(true), before engineInit or while running; it stops early if an update turns it off
|
|
475
575
|
* Respects paused exactly as the normal update loop does
|
|
476
|
-
* @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
|
|
477
578
|
* @example
|
|
478
579
|
* setHeadlessMode(true);
|
|
479
580
|
* setEngineManualStep(true);
|
|
@@ -483,7 +584,7 @@ const engineStepMaxFrames = 36000;
|
|
|
483
584
|
function engineStep(frames=1)
|
|
484
585
|
{
|
|
485
586
|
ASSERT(engineManualStep,
|
|
486
|
-
'engineStep requires setEngineManualStep(true)
|
|
587
|
+
'engineStep requires setEngineManualStep(true)');
|
|
487
588
|
ASSERT(engineUpdateInternal, 'engineStep requires engineInit to complete');
|
|
488
589
|
// runtime guard so release builds (where the asserts are stripped) can't
|
|
489
590
|
// start a second requestAnimationFrame chain or call an undefined update
|
|
@@ -491,42 +592,73 @@ function engineStep(frames=1)
|
|
|
491
592
|
ASSERT(Number.isInteger(frames) && frames >= 0 && frames <= engineStepMaxFrames,
|
|
492
593
|
'engineStep requires a whole frame count from 0 to ' + engineStepMaxFrames);
|
|
493
594
|
frames = min(frames, engineStepMaxFrames); // release has no asserts, don't freeze
|
|
494
|
-
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
|
|
495
596
|
engineUpdateInternal(frameTimeLastMS + 1e3 / frameRate);
|
|
496
597
|
}
|
|
497
598
|
|
|
498
|
-
/** Update each engine object
|
|
499
|
-
*
|
|
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
|
|
500
601
|
* @memberof Engine */
|
|
501
602
|
function engineObjectsUpdate()
|
|
502
603
|
{
|
|
503
|
-
|
|
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
|
|
504
612
|
engineObjectsCollide = engineObjects.filter(o=>o.collideSolidObjects);
|
|
613
|
+
engineObjectsCollideStaticLast = engineObjectsCollide.filter(o=>o.mass)
|
|
614
|
+
.concat(engineObjectsCollide.filter(o=>!o.mass));
|
|
505
615
|
|
|
506
616
|
// update physics before object update
|
|
507
617
|
for (const o of engineObjects)
|
|
508
618
|
if (!o.parent && !o.destroyed)
|
|
509
619
|
o.updatePhysics();
|
|
510
620
|
|
|
511
|
-
// 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;
|
|
512
634
|
function updateChildObject(o)
|
|
513
635
|
{
|
|
514
|
-
if (o.destroyed) return;
|
|
636
|
+
if (o.destroyed || o.updatePass === pass) return;
|
|
515
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);
|
|
516
641
|
o.update();
|
|
517
|
-
|
|
518
|
-
|
|
642
|
+
o.children.length && o.updateTransforms(false);
|
|
643
|
+
updateChildObjects(o.children);
|
|
519
644
|
}
|
|
520
|
-
|
|
645
|
+
function updateTopObject(o)
|
|
521
646
|
{
|
|
522
|
-
if (o.parent || o.destroyed)
|
|
647
|
+
if (o.parent || o.destroyed || o.updatePass === pass) return; // a child that let go is not updated twice
|
|
523
648
|
|
|
524
|
-
// 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;
|
|
525
652
|
o.update();
|
|
526
|
-
|
|
527
|
-
updateChildObject(child);
|
|
653
|
+
updateChildObjects(o.children);
|
|
528
654
|
o.updateTransforms();
|
|
529
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);
|
|
530
662
|
|
|
531
663
|
// remove destroyed objects
|
|
532
664
|
engineObjects = engineObjects.filter(o=>!o.destroyed);
|
|
@@ -534,43 +666,55 @@ function engineObjectsUpdate()
|
|
|
534
666
|
|
|
535
667
|
/** Destroy and remove all objects
|
|
536
668
|
* - This can be used to clear out all objects when restarting a level
|
|
669
|
+
* - Objects with the persistent flag set are left alone, for things that outlive a level
|
|
537
670
|
* - Objects can override their destroy function to do cleanup or stick around
|
|
538
|
-
* @param {boolean} [immediate] -
|
|
671
|
+
* @param {boolean} [immediate] - true removes attached effects like particle emitters at once, false lets them finish first
|
|
539
672
|
* @memberof Engine */
|
|
540
673
|
function engineObjectsDestroy(immediate=true)
|
|
541
674
|
{
|
|
542
675
|
for (const o of engineObjects)
|
|
543
|
-
o.parent || o.destroy(immediate);
|
|
676
|
+
o.parent || o.persistent || o.destroy(immediate);
|
|
544
677
|
engineObjects = engineObjects.filter(o=>!o.destroyed);
|
|
545
678
|
}
|
|
546
679
|
|
|
547
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
|
|
548
683
|
* @param {Vector2} [pos] - Center of test area, or undefined for all objects
|
|
549
|
-
* @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
|
|
550
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
|
|
551
688
|
* @return {Array<EngineObject>} - List of collected objects
|
|
552
689
|
* @memberof Engine */
|
|
553
|
-
function engineObjectsCollect(pos, size, objects=engineObjects)
|
|
690
|
+
function engineObjectsCollect(pos, size, objects=engineObjects, testCenters=false)
|
|
554
691
|
{
|
|
555
692
|
const collectedObjects = [];
|
|
556
693
|
if (!pos)
|
|
557
694
|
{
|
|
558
695
|
// all objects
|
|
559
696
|
for (const o of objects)
|
|
560
|
-
collectedObjects.push(o);
|
|
697
|
+
o.destroyed || collectedObjects.push(o);
|
|
561
698
|
}
|
|
562
|
-
else if (size instanceof Vector2)
|
|
699
|
+
else if (!size || size instanceof Vector2)
|
|
563
700
|
{
|
|
564
|
-
// 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();
|
|
565
703
|
for (const o of objects)
|
|
566
|
-
o.isOverlapping(pos,
|
|
704
|
+
o.destroyed || (testCenters ? isOverlapping(pos, boxSize, o.pos) : o.isOverlapping(pos, boxSize))
|
|
705
|
+
&& collectedObjects.push(o);
|
|
567
706
|
}
|
|
568
707
|
else
|
|
569
708
|
{
|
|
570
|
-
// circle test
|
|
571
|
-
const
|
|
709
|
+
// circle test, a diameter like every other size, against the nearest point of each box
|
|
710
|
+
const radiusSquared = (size/2)**2;
|
|
572
711
|
for (const o of objects)
|
|
573
|
-
|
|
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
|
+
}
|
|
574
718
|
}
|
|
575
719
|
return collectedObjects;
|
|
576
720
|
}
|
|
@@ -581,16 +725,23 @@ function engineObjectsCollect(pos, size, objects=engineObjects)
|
|
|
581
725
|
* @memberof Engine
|
|
582
726
|
*/
|
|
583
727
|
|
|
584
|
-
/** 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
|
|
585
729
|
* @param {Vector2} [pos] - Center of test area, or undefined for all objects
|
|
586
|
-
* @param {Vector2|number} [size] -
|
|
587
|
-
* @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)
|
|
588
733
|
* @param {Array<EngineObject>} [objects=engineObjects] - List of objects to check
|
|
734
|
+
* @param {boolean} [testCenters] - Test only each object's center, see engineObjectsCollect
|
|
589
735
|
* @memberof Engine */
|
|
590
|
-
function engineObjectsCallback(pos, size, callbackFunction, objects=engineObjects)
|
|
591
|
-
{
|
|
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
|
+
}
|
|
592
742
|
|
|
593
|
-
/** 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
|
|
594
745
|
* @param {Vector2} start
|
|
595
746
|
* @param {Vector2} end
|
|
596
747
|
* @param {Array<EngineObject>} [objects=engineObjects] - List of objects to check
|
|
@@ -601,13 +752,13 @@ function engineObjectsRaycast(start, end, objects=engineObjects)
|
|
|
601
752
|
const hitObjects = [];
|
|
602
753
|
for (const o of objects)
|
|
603
754
|
{
|
|
604
|
-
if (o.collideRaycast && isIntersecting(start, end, o.pos, o.size))
|
|
755
|
+
if (o.collideRaycast && !o.destroyed && isIntersecting(start, end, o.pos, o.size))
|
|
605
756
|
{
|
|
606
|
-
debugRaycast && debugRect(o.pos, o.size, '#f00');
|
|
757
|
+
debugRaycast && debugRect(o.pos, o.size, '#f00', 0, 0, false, false);
|
|
607
758
|
hitObjects.push(o);
|
|
608
759
|
}
|
|
609
760
|
}
|
|
610
761
|
|
|
611
|
-
debugRaycast && debugLine(start, end, hitObjects.length ? '#f00' : '#00f', .02);
|
|
762
|
+
debugRaycast && debugLine(start, end, hitObjects.length ? '#f00' : '#00f', .02, 0, false);
|
|
612
763
|
return hitObjects;
|
|
613
764
|
}
|