littlejsengine 1.18.25 → 1.18.27
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/littlejs.d.ts +22 -0
- package/dist/littlejs.esm.js +688 -618
- package/dist/littlejs.esm.min.js +1 -1
- package/dist/littlejs.js +684 -618
- package/dist/littlejs.min.js +1 -1
- package/dist/littlejs.release.js +684 -618
- package/package.json +59 -58
- package/plugins/threejs.js +3 -0
- package/src/engine.js +612 -612
- package/src/engineDraw.js +24 -3
- package/src/engineExport.js +4 -0
- package/src/engineInput.js +34 -5
- package/src/engineSettings.js +13 -0
package/dist/littlejs.release.js
CHANGED
|
@@ -3,616 +3,616 @@
|
|
|
3
3
|
|
|
4
4
|
'use strict';
|
|
5
5
|
|
|
6
|
-
/**
|
|
7
|
-
* LittleJS - The Tiny Fast JavaScript Game Engine
|
|
8
|
-
* MIT License - Copyright 2021 Frank Force
|
|
9
|
-
*
|
|
10
|
-
* Engine Features
|
|
11
|
-
* - Object oriented system with EngineObject base class
|
|
12
|
-
* - Automatic object lifecycle (update, physics, collision, rendering)
|
|
13
|
-
* - Engine helper classes: Vector2, Color, Timer, RandomGenerator
|
|
14
|
-
* - Hybrid rendering with WebGL batching and Canvas2D fallback
|
|
15
|
-
* - Audio system with wave, mp3, or ZzFX sound effects
|
|
16
|
-
* - Input system with keyboard, mouse, gamepad, and touch support
|
|
17
|
-
* - Tile layer rendering and collision detection
|
|
18
|
-
* - Particle effect system with emitters
|
|
19
|
-
* - Medal/achievement system with local storage
|
|
20
|
-
* - Comprehensive debug tools and visualizations
|
|
21
|
-
* - Fixed 60 FPS timestep with configurable time scale
|
|
22
|
-
* - Raycast and spatial query utilities
|
|
23
|
-
* - Plugin system for extending engine functionality
|
|
24
|
-
* - Start with engineInit() and provide your game callbacks
|
|
25
|
-
* @namespace Engine
|
|
26
|
-
*/
|
|
27
|
-
|
|
28
|
-
/** Name of engine
|
|
29
|
-
* @type {string}
|
|
30
|
-
* @default
|
|
31
|
-
* @memberof Engine */
|
|
32
|
-
const engineName = 'LittleJS';
|
|
33
|
-
|
|
34
|
-
/** Version of engine
|
|
35
|
-
* @type {string}
|
|
36
|
-
* @default
|
|
37
|
-
* @memberof Engine */
|
|
38
|
-
const engineVersion = '1.18.
|
|
39
|
-
|
|
40
|
-
/** Frames per second to update
|
|
41
|
-
* @type {number}
|
|
42
|
-
* @default
|
|
43
|
-
* @memberof Engine */
|
|
44
|
-
const frameRate = 60;
|
|
45
|
-
|
|
46
|
-
/** How many seconds each frame lasts, engine uses a fixed time step
|
|
47
|
-
* @type {number}
|
|
48
|
-
* @default 1/60
|
|
49
|
-
* @memberof Engine */
|
|
50
|
-
const timeDelta = 1/frameRate;
|
|
51
|
-
|
|
52
|
-
/** Array containing all engine objects
|
|
53
|
-
* @type {Array<EngineObject>}
|
|
54
|
-
* @memberof Engine */
|
|
55
|
-
let engineObjects = [];
|
|
56
|
-
|
|
57
|
-
/** Array with only objects set to collide with other objects this frame (for optimization)
|
|
58
|
-
* @type {Array<EngineObject>}
|
|
59
|
-
* @memberof Engine */
|
|
60
|
-
let engineObjectsCollide = [];
|
|
61
|
-
|
|
62
|
-
/** Current update frame, used to calculate time
|
|
63
|
-
* @type {number}
|
|
64
|
-
* @memberof Engine */
|
|
65
|
-
let frame = 0;
|
|
66
|
-
|
|
67
|
-
/** Current engine time since start in seconds
|
|
68
|
-
* @type {number}
|
|
69
|
-
* @memberof Engine */
|
|
70
|
-
let time = 0;
|
|
71
|
-
|
|
72
|
-
/** Actual clock time since start in seconds (not affected by pause, timescale, or frame rate clamping)
|
|
73
|
-
* @type {number}
|
|
74
|
-
* @memberof Engine */
|
|
75
|
-
let timeReal = 0;
|
|
76
|
-
|
|
77
|
-
/** Is the game paused? Causes time and objects to not be updated
|
|
78
|
-
* @type {boolean}
|
|
79
|
-
* @default false
|
|
80
|
-
* @memberof Engine */
|
|
81
|
-
let paused = false;
|
|
82
|
-
|
|
83
|
-
/** Get if game is paused
|
|
84
|
-
* @return {boolean}
|
|
85
|
-
* @memberof Engine */
|
|
86
|
-
function getPaused() { return paused; }
|
|
87
|
-
|
|
88
|
-
/** Set if game is paused
|
|
89
|
-
* @param {boolean} [isPaused]
|
|
90
|
-
* @memberof Engine */
|
|
91
|
-
function setPaused(isPaused=true) { paused = isPaused; }
|
|
92
|
-
|
|
93
|
-
// Engine internal variables
|
|
94
|
-
let frameTimeLastMS = 0, frameTimeBufferMS = 0, averageFPS = 0;
|
|
95
|
-
let engineUpdateInternal; // assigned by engineInit so engineStep can drive it
|
|
96
|
-
let showEngineVersion = true;
|
|
97
|
-
|
|
98
|
-
///////////////////////////////////////////////////////////////////////////////
|
|
99
|
-
// plugin hooks
|
|
100
|
-
|
|
101
|
-
const pluginList = [];
|
|
102
|
-
class EnginePlugin
|
|
103
|
-
{
|
|
104
|
-
constructor(update, render, glContextLost, glContextRestored)
|
|
105
|
-
{
|
|
106
|
-
this.update = update;
|
|
107
|
-
this.render = render;
|
|
108
|
-
this.glContextLost = glContextLost;
|
|
109
|
-
this.glContextRestored = glContextRestored;
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
/**
|
|
114
|
-
* @callback PluginCallback - Update or render function for a plugin
|
|
115
|
-
* @memberof Engine
|
|
116
|
-
*/
|
|
117
|
-
|
|
118
|
-
/** Add a new update function for a plugin
|
|
119
|
-
* @param {PluginCallback} [update]
|
|
120
|
-
* @param {PluginCallback} [render]
|
|
121
|
-
* @param {PluginCallback} [glContextLost]
|
|
122
|
-
* @param {PluginCallback} [glContextRestored]
|
|
123
|
-
* @memberof Engine */
|
|
124
|
-
function engineAddPlugin(update, render, glContextLost, glContextRestored)
|
|
125
|
-
{
|
|
126
|
-
// make sure plugin functions are unique
|
|
127
|
-
ASSERT(!pluginList.find(p=>
|
|
128
|
-
p.update === update && p.render === render &&
|
|
129
|
-
p.glContextLost === glContextLost &&
|
|
130
|
-
p.glContextRestored === glContextRestored));
|
|
131
|
-
|
|
132
|
-
const plugin = new EnginePlugin(update, render, glContextLost, glContextRestored);
|
|
133
|
-
pluginList.push(plugin);
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
///////////////////////////////////////////////////////////////////////////////
|
|
137
|
-
// Main Engine Functions
|
|
138
|
-
|
|
139
|
-
/**
|
|
140
|
-
* @callback GameInitCallback - Called after the engine starts, can be async
|
|
141
|
-
* @return {void|Promise<void>}
|
|
142
|
-
* @memberof Engine
|
|
143
|
-
*/
|
|
144
|
-
/**
|
|
145
|
-
* @callback GameCallback - Update or render function for the game
|
|
146
|
-
* @memberof Engine
|
|
147
|
-
*/
|
|
148
|
-
|
|
149
|
-
/** Startup LittleJS engine with your callback functions
|
|
150
|
-
* @param {GameInitCallback} gameInit - Called once after the engine starts up, can be async for loading
|
|
151
|
-
* @param {GameCallback} gameUpdate - Called every frame before objects are updated (60fps), use for game logic
|
|
152
|
-
* @param {GameCallback} gameUpdatePost - Called after physics and objects are updated, even when paused, use for UI updates
|
|
153
|
-
* @param {GameCallback} gameRender - Called before objects are rendered, use for drawing backgrounds/world elements
|
|
154
|
-
* @param {GameCallback} gameRenderPost - Called after objects are rendered, use for drawing UI/overlays
|
|
155
|
-
* @param {Array<string>} [imageSources=[]] - List of image file paths to preload (e.g., ['player.png', 'tiles.png'])
|
|
156
|
-
* @param {HTMLElement} [rootElement] - Root DOM element to attach canvas to, defaults to document.body
|
|
157
|
-
* @example
|
|
158
|
-
* // Basic engine startup
|
|
159
|
-
* engineInit(
|
|
160
|
-
* ()=> { LOG('Game initialized!'); }, // gameInit
|
|
161
|
-
* ()=> { updateGameLogic(); }, // gameUpdate
|
|
162
|
-
* ()=> { updateUI(); }, // gameUpdatePost
|
|
163
|
-
* ()=> { drawBackground(); }, // gameRender
|
|
164
|
-
* ()=> { drawHUD(); }, // gameRenderPost
|
|
165
|
-
* ['tiles.png', 'tilesLevel.png'] // images to load
|
|
166
|
-
* );
|
|
167
|
-
* @memberof Engine */
|
|
168
|
-
async function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRenderPost, imageSources=[], rootElement)
|
|
169
|
-
{
|
|
170
|
-
showEngineVersion && console.log(`${engineName} Engine v${engineVersion}`);
|
|
171
|
-
ASSERT(!mainContext, 'engine already initialized');
|
|
172
|
-
// runtime guard so release builds (where the assert is stripped) don't
|
|
173
|
-
// double-register listeners / double-add canvases on a second call
|
|
174
|
-
if (mainContext) return;
|
|
175
|
-
ASSERT(isArray(imageSources), 'pass in images as array');
|
|
176
|
-
|
|
177
|
-
// ensure body exists for minimal HTML where the script runs before <body> is parsed
|
|
178
|
-
if (!document.body)
|
|
179
|
-
document.documentElement.appendChild(document.createElement('body'));
|
|
180
|
-
rootElement ||= document.body;
|
|
181
|
-
|
|
182
|
-
// allow passing in empty functions
|
|
183
|
-
gameInit ||= ()=>{};
|
|
184
|
-
gameUpdate ||= ()=>{};
|
|
185
|
-
gameUpdatePost ||= ()=>{};
|
|
186
|
-
gameRender ||= ()=>{};
|
|
187
|
-
gameRenderPost ||= ()=>{};
|
|
188
|
-
|
|
189
|
-
// Called automatically by engine to setup render system
|
|
190
|
-
function enginePreRender()
|
|
191
|
-
{
|
|
192
|
-
// save canvas size
|
|
193
|
-
mainCanvasSize = vec2(mainCanvas.width, mainCanvas.height);
|
|
194
|
-
|
|
195
|
-
// disable smoothing for pixel art
|
|
196
|
-
mainContext.imageSmoothingEnabled = !tilesPixelated;
|
|
197
|
-
|
|
198
|
-
// setup gl rendering if enabled
|
|
199
|
-
glPreRender();
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
// internal update loop for engine
|
|
203
|
-
function engineUpdate(frameTimeMS=0)
|
|
204
|
-
{
|
|
205
|
-
// update time keeping
|
|
206
|
-
let frameTimeDeltaMS = frameTimeMS - frameTimeLastMS;
|
|
207
|
-
// skip delta on the very first frame so timeReal doesn't jump
|
|
208
|
-
// by ~page-load-time when RAF starts handing real timestamps
|
|
209
|
-
if (!frameTimeLastMS) frameTimeDeltaMS = 0;
|
|
210
|
-
frameTimeLastMS = frameTimeMS;
|
|
211
|
-
if (debug || debugWatermark)
|
|
212
|
-
averageFPS = lerp(averageFPS, 1e3/(frameTimeDeltaMS||1), .05);
|
|
213
|
-
const debugSpeedUp = debug && keyIsDown('Equal'); // +
|
|
214
|
-
const debugSpeedDown = debug && keyIsDown('Minus'); // -
|
|
215
|
-
const debugScale = debugSpeedUp ? 10 : debugSpeedDown ? .1 : 1;
|
|
216
|
-
|
|
217
|
-
// apply time deltas
|
|
218
|
-
timeReal += frameTimeDeltaMS * debugScale / 1e3;
|
|
219
|
-
const combinedScale = timeScale * debugScale;
|
|
220
|
-
frameTimeDeltaMS *= combinedScale;
|
|
221
|
-
frameTimeBufferMS += paused ? 0 : frameTimeDeltaMS;
|
|
222
|
-
if (combinedScale <= 1)
|
|
223
|
-
frameTimeBufferMS = min(frameTimeBufferMS, 50); // clamp min framerate
|
|
224
|
-
|
|
225
|
-
let wasUpdated = false;
|
|
226
|
-
if (paused)
|
|
227
|
-
{
|
|
228
|
-
// update everything except the game and objects
|
|
229
|
-
wasUpdated = true;
|
|
230
|
-
updateCanvas();
|
|
231
|
-
inputUpdate();
|
|
232
|
-
pluginList.forEach(plugin=>plugin.update?.());
|
|
233
|
-
|
|
234
|
-
// update object transforms even when paused
|
|
235
|
-
for (const o of engineObjects)
|
|
236
|
-
o.parent || o.updateTransforms();
|
|
237
|
-
|
|
238
|
-
// do post update
|
|
239
|
-
debugUpdate();
|
|
240
|
-
gameUpdatePost();
|
|
241
|
-
inputUpdatePost();
|
|
242
|
-
if (debugVideoCaptureIsActive())
|
|
243
|
-
renderFrame();
|
|
244
|
-
}
|
|
245
|
-
else
|
|
246
|
-
{
|
|
247
|
-
// apply time delta smoothing, improves smoothness of framerate in some browsers
|
|
248
|
-
let deltaSmooth = 0;
|
|
249
|
-
if (frameTimeBufferMS < 0 && frameTimeBufferMS > -9)
|
|
250
|
-
{
|
|
251
|
-
// force at least one update each frame since it is waiting for refresh
|
|
252
|
-
deltaSmooth = frameTimeBufferMS;
|
|
253
|
-
frameTimeBufferMS = 0;
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
// update multiple frames if necessary in case of slow framerate
|
|
257
|
-
for (; frameTimeBufferMS >= 0; frameTimeBufferMS -= 1e3 / frameRate)
|
|
258
|
-
{
|
|
259
|
-
// increment frame and update time
|
|
260
|
-
time = frame++ / frameRate;
|
|
261
|
-
|
|
262
|
-
// update game and objects
|
|
263
|
-
wasUpdated = true;
|
|
264
|
-
updateCanvas();
|
|
265
|
-
inputUpdate();
|
|
266
|
-
gameUpdate();
|
|
267
|
-
pluginList.forEach(plugin=>plugin.update?.());
|
|
268
|
-
engineObjectsUpdate();
|
|
269
|
-
|
|
270
|
-
// do post update
|
|
271
|
-
debugUpdate();
|
|
272
|
-
gameUpdatePost();
|
|
273
|
-
inputUpdatePost();
|
|
274
|
-
if (debugVideoCaptureIsActive())
|
|
275
|
-
renderFrame();
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
// add the time smoothing back in
|
|
279
|
-
frameTimeBufferMS += deltaSmooth;
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
if (!debugVideoCaptureIsActive())
|
|
283
|
-
renderFrame();
|
|
284
|
-
if (!engineManualStep)
|
|
285
|
-
requestAnimationFrame(engineUpdate);
|
|
286
|
-
|
|
287
|
-
function renderFrame()
|
|
288
|
-
{
|
|
289
|
-
if (headlessMode) return;
|
|
290
|
-
|
|
291
|
-
// canvas must be updated before rendering
|
|
292
|
-
if (!wasUpdated)
|
|
293
|
-
updateCanvas();
|
|
294
|
-
|
|
295
|
-
// render the game and objects
|
|
296
|
-
enginePreRender();
|
|
297
|
-
gameRender();
|
|
298
|
-
engineObjects.sort((a,b)=> a.renderOrder - b.renderOrder);
|
|
299
|
-
for (const o of engineObjects)
|
|
300
|
-
o.destroyed || o.render();
|
|
301
|
-
|
|
302
|
-
// post rendering
|
|
303
|
-
gameRenderPost();
|
|
304
|
-
pluginList.forEach(plugin=>plugin.render?.());
|
|
305
|
-
inputRender();
|
|
306
|
-
debugRender();
|
|
307
|
-
glFlush();
|
|
308
|
-
debugRenderPost();
|
|
309
|
-
drawCount = 0;
|
|
310
|
-
primitiveCount = 0;
|
|
311
|
-
}
|
|
312
|
-
}
|
|
313
|
-
engineUpdateInternal = engineUpdate;
|
|
314
|
-
|
|
315
|
-
function updateCanvas()
|
|
316
|
-
{
|
|
317
|
-
if (headlessMode) return;
|
|
318
|
-
|
|
319
|
-
if (canvasFixedSize.x)
|
|
320
|
-
{
|
|
321
|
-
// set canvas fixed size
|
|
322
|
-
mainCanvasSize = canvasFixedSize.copy();
|
|
323
|
-
|
|
324
|
-
// fit to window using css width and height
|
|
325
|
-
const innerAspect = innerWidth / innerHeight;
|
|
326
|
-
const fixedAspect = canvasFixedSize.x / canvasFixedSize.y;
|
|
327
|
-
const w = innerAspect < fixedAspect ? '100%' : '';
|
|
328
|
-
const h = innerAspect < fixedAspect ? '' : '100%';
|
|
329
|
-
mainCanvas.style.width = w;
|
|
330
|
-
mainCanvas.style.height = h;
|
|
331
|
-
if (glCanvas)
|
|
332
|
-
{
|
|
333
|
-
glCanvas.style.width = w;
|
|
334
|
-
glCanvas.style.height = h;
|
|
335
|
-
}
|
|
336
|
-
}
|
|
337
|
-
else
|
|
338
|
-
{
|
|
339
|
-
// apply device pixel ratio for crisp rendering
|
|
340
|
-
const dpr = canvasPixelRatio ?? (devicePixelRatio || 1);
|
|
341
|
-
const viewWidth = innerWidth * dpr | 0;
|
|
342
|
-
const viewHeight = innerHeight * dpr | 0;
|
|
343
|
-
|
|
344
|
-
// get main canvas size based on window size
|
|
345
|
-
mainCanvasSize.x = min(viewWidth, canvasMaxSize.x);
|
|
346
|
-
mainCanvasSize.y = min(viewHeight, canvasMaxSize.y);
|
|
347
|
-
|
|
348
|
-
// responsive aspect ratio with native resolution
|
|
349
|
-
const innerAspect = viewWidth / viewHeight;
|
|
350
|
-
ASSERT(canvasMinAspect <= canvasMaxAspect);
|
|
351
|
-
if (canvasMaxAspect && innerAspect > canvasMaxAspect)
|
|
352
|
-
{
|
|
353
|
-
// full height
|
|
354
|
-
const w = mainCanvasSize.y * canvasMaxAspect | 0;
|
|
355
|
-
mainCanvasSize.x = min(w, canvasMaxSize.x);
|
|
356
|
-
}
|
|
357
|
-
else if (innerAspect < canvasMinAspect)
|
|
358
|
-
{
|
|
359
|
-
// full width
|
|
360
|
-
const h = mainCanvasSize.x / canvasMinAspect | 0;
|
|
361
|
-
mainCanvasSize.y = min(h, canvasMaxSize.y);
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
// set CSS display size so backing store renders at viewport size
|
|
365
|
-
const cssW = (mainCanvasSize.x / dpr | 0) + 'px';
|
|
366
|
-
const cssH = (mainCanvasSize.y / dpr | 0) + 'px';
|
|
367
|
-
mainCanvas.style.width = cssW;
|
|
368
|
-
mainCanvas.style.height = cssH;
|
|
369
|
-
if (glCanvas)
|
|
370
|
-
{
|
|
371
|
-
glCanvas.style.width = cssW;
|
|
372
|
-
glCanvas.style.height = cssH;
|
|
373
|
-
}
|
|
374
|
-
}
|
|
375
|
-
|
|
376
|
-
// clear main canvas and set size
|
|
377
|
-
mainCanvas.width = mainCanvasSize.x;
|
|
378
|
-
mainCanvas.height = mainCanvasSize.y;
|
|
379
|
-
|
|
380
|
-
// apply the clear color to main canvas
|
|
381
|
-
if (canvasClearColor.a > 0 && !glEnable)
|
|
382
|
-
{
|
|
383
|
-
mainContext.fillStyle = canvasClearColor.toString();
|
|
384
|
-
mainContext.fillRect(0, 0, mainCanvasSize.x, mainCanvasSize.y);
|
|
385
|
-
mainContext.fillStyle = BLACK.toString();
|
|
386
|
-
}
|
|
387
|
-
|
|
388
|
-
// set default line join and cap
|
|
389
|
-
mainContext.lineJoin = 'round';
|
|
390
|
-
mainContext.lineCap = 'round';
|
|
391
|
-
}
|
|
392
|
-
|
|
393
|
-
// skip setup if headless
|
|
394
|
-
if (headlessMode) return startEngine();
|
|
395
|
-
|
|
396
|
-
// setup webgl
|
|
397
|
-
glInit(rootElement);
|
|
398
|
-
|
|
399
|
-
// setup html
|
|
400
|
-
const styleRoot =
|
|
401
|
-
'margin:0;' + // fill the window
|
|
402
|
-
'overflow:hidden;' + // no scroll bars
|
|
403
|
-
'background:#000;' + // set background color
|
|
404
|
-
'user-select:none;' + // prevent hold to select
|
|
405
|
-
'-webkit-user-select:none;' + // compatibility for ios
|
|
406
|
-
'touch-action:none;' + // prevent mobile pinch to resize
|
|
407
|
-
'-webkit-touch-callout:none'; // compatibility for ios
|
|
408
|
-
rootElement.style.cssText = styleRoot;
|
|
409
|
-
mainCanvas = rootElement.appendChild(document.createElement('canvas'));
|
|
410
|
-
drawContext = mainContext = mainCanvas.getContext('2d');
|
|
411
|
-
|
|
412
|
-
// init stuff and start engine
|
|
413
|
-
inputInit();
|
|
414
|
-
audioInit();
|
|
415
|
-
debugInit();
|
|
416
|
-
|
|
417
|
-
// setup canvases
|
|
418
|
-
// transform way is still more reliable than flexbox or grid
|
|
419
|
-
const styleCanvas = 'position:absolute;'+ // allow canvases to overlap
|
|
420
|
-
'top:50%;left:50%;transform:translate(-50%,-50%)'; // center on screen
|
|
421
|
-
mainCanvas.style.cssText = styleCanvas;
|
|
422
|
-
if (glCanvas)
|
|
423
|
-
glCanvas.style.cssText = styleCanvas;
|
|
424
|
-
setCanvasPixelated(canvasPixelated);
|
|
425
|
-
updateCanvas();
|
|
426
|
-
glPreRender();
|
|
427
|
-
|
|
428
|
-
// create offscreen canvases for image processing
|
|
429
|
-
workCanvas = new OffscreenCanvas(64, 64);
|
|
430
|
-
workContext = workCanvas.getContext('2d');
|
|
431
|
-
workReadCanvas = new OffscreenCanvas(64, 64);
|
|
432
|
-
workReadContext = workReadCanvas.getContext('2d', { willReadFrequently: true });
|
|
433
|
-
|
|
434
|
-
// create promises for loading images
|
|
435
|
-
const promises = imageSources.map((src, i)=> loadTexture(i, src));
|
|
436
|
-
|
|
437
|
-
// no images to load
|
|
438
|
-
if (!imageSources.length)
|
|
439
|
-
promises.push(loadTexture(0));
|
|
440
|
-
|
|
441
|
-
// load engine font image
|
|
442
|
-
promises.push(imageFontInit());
|
|
443
|
-
|
|
444
|
-
if (showSplashScreen)
|
|
445
|
-
{
|
|
446
|
-
// draw splash screen
|
|
447
|
-
promises.push(new Promise(resolve =>
|
|
448
|
-
{
|
|
449
|
-
let t = 0;
|
|
450
|
-
updateSplash();
|
|
451
|
-
function updateSplash()
|
|
452
|
-
{
|
|
453
|
-
inputClear();
|
|
454
|
-
drawEngineLogo(t+=.01);
|
|
455
|
-
t>1 ? resolve() : setTimeout(updateSplash, 16);
|
|
456
|
-
}
|
|
457
|
-
}));
|
|
458
|
-
}
|
|
459
|
-
|
|
460
|
-
// wait for all the promises to finish
|
|
461
|
-
await Promise.all(promises);
|
|
462
|
-
return startEngine();
|
|
463
|
-
|
|
464
|
-
async function startEngine()
|
|
465
|
-
{
|
|
466
|
-
// wait for gameInit to load
|
|
467
|
-
await gameInit();
|
|
468
|
-
engineManualStep || engineUpdate();
|
|
469
|
-
}
|
|
470
|
-
}
|
|
471
|
-
|
|
472
|
-
// max frames engineStep can advance in one call, 10 minutes at 60fps
|
|
473
|
-
// large counts block until they finish, so this catches runaway values
|
|
474
|
-
const engineStepMaxFrames = 36000;
|
|
475
|
-
|
|
476
|
-
/** Advance the engine by a number of frames
|
|
477
|
-
* Requires setEngineManualStep(true) before engineInit
|
|
478
|
-
* Respects paused exactly as the normal update loop does
|
|
479
|
-
* @param {number} [frames] - number of engine update ticks, max 36000, each running one fixed update at timeScale 1
|
|
480
|
-
* @example
|
|
481
|
-
* setHeadlessMode(true);
|
|
482
|
-
* setEngineManualStep(true);
|
|
483
|
-
* await engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRenderPost);
|
|
484
|
-
* engineStep(600); // advance 10 seconds of game time
|
|
485
|
-
* @memberof Engine */
|
|
486
|
-
function engineStep(frames=1)
|
|
487
|
-
{
|
|
488
|
-
ASSERT(engineManualStep,
|
|
489
|
-
'engineStep requires setEngineManualStep(true) before engineInit');
|
|
490
|
-
ASSERT(engineUpdateInternal, 'engineStep requires engineInit to complete');
|
|
491
|
-
// runtime guard so release builds (where the asserts are stripped) can't
|
|
492
|
-
// start a second requestAnimationFrame chain or call an undefined update
|
|
493
|
-
if (!engineManualStep || !engineUpdateInternal) return;
|
|
494
|
-
ASSERT(Number.isInteger(frames) && frames >= 0 && frames <= engineStepMaxFrames,
|
|
495
|
-
'engineStep requires a whole frame count from 0 to ' + engineStepMaxFrames);
|
|
496
|
-
frames = min(frames, engineStepMaxFrames); // release has no asserts, don't freeze
|
|
497
|
-
for (let i = frames; i > 0; --i)
|
|
498
|
-
engineUpdateInternal(frameTimeLastMS + 1e3 / frameRate);
|
|
499
|
-
}
|
|
500
|
-
|
|
501
|
-
/** Update each engine object, remove destroyed objects, and update time
|
|
502
|
-
* can be called manually if objects need to be updated outside of main loop
|
|
503
|
-
* @memberof Engine */
|
|
504
|
-
function engineObjectsUpdate()
|
|
505
|
-
{
|
|
506
|
-
// get list of solid objects for physics optimization
|
|
507
|
-
engineObjectsCollide = engineObjects.filter(o=>o.collideSolidObjects);
|
|
508
|
-
|
|
509
|
-
// update physics before object update
|
|
510
|
-
for (const o of engineObjects)
|
|
511
|
-
if (!o.parent && !o.destroyed)
|
|
512
|
-
o.updatePhysics();
|
|
513
|
-
|
|
514
|
-
// recursive object update
|
|
515
|
-
function updateChildObject(o)
|
|
516
|
-
{
|
|
517
|
-
if (o.destroyed) return;
|
|
518
|
-
|
|
519
|
-
o.update();
|
|
520
|
-
for (const child of o.children)
|
|
521
|
-
updateChildObject(child);
|
|
522
|
-
}
|
|
523
|
-
for (const o of engineObjects)
|
|
524
|
-
{
|
|
525
|
-
if (o.parent || o.destroyed) continue;
|
|
526
|
-
|
|
527
|
-
// update top level objects
|
|
528
|
-
o.update();
|
|
529
|
-
for (const child of o.children)
|
|
530
|
-
updateChildObject(child);
|
|
531
|
-
o.updateTransforms();
|
|
532
|
-
}
|
|
533
|
-
|
|
534
|
-
// remove destroyed objects
|
|
535
|
-
engineObjects = engineObjects.filter(o=>!o.destroyed);
|
|
536
|
-
}
|
|
537
|
-
|
|
538
|
-
/** Destroy and remove all objects
|
|
539
|
-
* - This can be used to clear out all objects when restarting a level
|
|
540
|
-
* - Objects can override their destroy function to do cleanup or stick around
|
|
541
|
-
* @param {boolean} [immediate] - should attached effects be allowed to die off?
|
|
542
|
-
* @memberof Engine */
|
|
543
|
-
function engineObjectsDestroy(immediate=true)
|
|
544
|
-
{
|
|
545
|
-
for (const o of engineObjects)
|
|
546
|
-
o.parent || o.destroy(immediate);
|
|
547
|
-
engineObjects = engineObjects.filter(o=>!o.destroyed);
|
|
548
|
-
}
|
|
549
|
-
|
|
550
|
-
/** Collects all object within a given area
|
|
551
|
-
* @param {Vector2} [pos] - Center of test area, or undefined for all objects
|
|
552
|
-
* @param {Vector2|number} [size] - Radius of circle if float, rectangle size if Vector2
|
|
553
|
-
* @param {Array<EngineObject>} [objects=engineObjects] - List of objects to check
|
|
554
|
-
* @return {Array<EngineObject>} - List of collected objects
|
|
555
|
-
* @memberof Engine */
|
|
556
|
-
function engineObjectsCollect(pos, size, objects=engineObjects)
|
|
557
|
-
{
|
|
558
|
-
const collectedObjects = [];
|
|
559
|
-
if (!pos)
|
|
560
|
-
{
|
|
561
|
-
// all objects
|
|
562
|
-
for (const o of objects)
|
|
563
|
-
collectedObjects.push(o);
|
|
564
|
-
}
|
|
565
|
-
else if (size instanceof Vector2)
|
|
566
|
-
{
|
|
567
|
-
// bounding box test
|
|
568
|
-
for (const o of objects)
|
|
569
|
-
o.isOverlapping(pos, size) && collectedObjects.push(o);
|
|
570
|
-
}
|
|
571
|
-
else
|
|
572
|
-
{
|
|
573
|
-
// circle test
|
|
574
|
-
const sizeSquared = size*size;
|
|
575
|
-
for (const o of objects)
|
|
576
|
-
pos.distanceSquared(o.pos) < sizeSquared && collectedObjects.push(o);
|
|
577
|
-
}
|
|
578
|
-
return collectedObjects;
|
|
579
|
-
}
|
|
580
|
-
|
|
581
|
-
/**
|
|
582
|
-
* @callback ObjectCallbackFunction - Function that processes an object
|
|
583
|
-
* @param {EngineObject} object
|
|
584
|
-
* @memberof Engine
|
|
585
|
-
*/
|
|
586
|
-
|
|
587
|
-
/** Triggers a callback for each object within a given area
|
|
588
|
-
* @param {Vector2} [pos] - Center of test area, or undefined for all objects
|
|
589
|
-
* @param {Vector2|number} [size] - Radius of circle if float, rectangle size if Vector2
|
|
590
|
-
* @param {ObjectCallbackFunction} [callbackFunction] - Calls this function on every object that passes the test
|
|
591
|
-
* @param {Array<EngineObject>} [objects=engineObjects] - List of objects to check
|
|
592
|
-
* @memberof Engine */
|
|
593
|
-
function engineObjectsCallback(pos, size, callbackFunction, objects=engineObjects)
|
|
594
|
-
{ engineObjectsCollect(pos, size, objects).forEach(o => callbackFunction(o)); }
|
|
595
|
-
|
|
596
|
-
/** Return a list of objects intersecting a ray
|
|
597
|
-
* @param {Vector2} start
|
|
598
|
-
* @param {Vector2} end
|
|
599
|
-
* @param {Array<EngineObject>} [objects=engineObjects] - List of objects to check
|
|
600
|
-
* @return {Array<EngineObject>} - List of objects hit
|
|
601
|
-
* @memberof Engine */
|
|
602
|
-
function engineObjectsRaycast(start, end, objects=engineObjects)
|
|
603
|
-
{
|
|
604
|
-
const hitObjects = [];
|
|
605
|
-
for (const o of objects)
|
|
606
|
-
{
|
|
607
|
-
if (o.collideRaycast && isIntersecting(start, end, o.pos, o.size))
|
|
608
|
-
{
|
|
609
|
-
debugRaycast && debugRect(o.pos, o.size, '#f00');
|
|
610
|
-
hitObjects.push(o);
|
|
611
|
-
}
|
|
612
|
-
}
|
|
613
|
-
|
|
614
|
-
debugRaycast && debugLine(start, end, hitObjects.length ? '#f00' : '#00f', .02);
|
|
615
|
-
return hitObjects;
|
|
6
|
+
/**
|
|
7
|
+
* LittleJS - The Tiny Fast JavaScript Game Engine
|
|
8
|
+
* MIT License - Copyright 2021 Frank Force
|
|
9
|
+
*
|
|
10
|
+
* Engine Features
|
|
11
|
+
* - Object oriented system with EngineObject base class
|
|
12
|
+
* - Automatic object lifecycle (update, physics, collision, rendering)
|
|
13
|
+
* - Engine helper classes: Vector2, Color, Timer, RandomGenerator
|
|
14
|
+
* - Hybrid rendering with WebGL batching and Canvas2D fallback
|
|
15
|
+
* - Audio system with wave, mp3, or ZzFX sound effects
|
|
16
|
+
* - Input system with keyboard, mouse, gamepad, and touch support
|
|
17
|
+
* - Tile layer rendering and collision detection
|
|
18
|
+
* - Particle effect system with emitters
|
|
19
|
+
* - Medal/achievement system with local storage
|
|
20
|
+
* - Comprehensive debug tools and visualizations
|
|
21
|
+
* - Fixed 60 FPS timestep with configurable time scale
|
|
22
|
+
* - Raycast and spatial query utilities
|
|
23
|
+
* - Plugin system for extending engine functionality
|
|
24
|
+
* - Start with engineInit() and provide your game callbacks
|
|
25
|
+
* @namespace Engine
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
/** Name of engine
|
|
29
|
+
* @type {string}
|
|
30
|
+
* @default
|
|
31
|
+
* @memberof Engine */
|
|
32
|
+
const engineName = 'LittleJS';
|
|
33
|
+
|
|
34
|
+
/** Version of engine
|
|
35
|
+
* @type {string}
|
|
36
|
+
* @default
|
|
37
|
+
* @memberof Engine */
|
|
38
|
+
const engineVersion = '1.18.27';
|
|
39
|
+
|
|
40
|
+
/** Frames per second to update
|
|
41
|
+
* @type {number}
|
|
42
|
+
* @default
|
|
43
|
+
* @memberof Engine */
|
|
44
|
+
const frameRate = 60;
|
|
45
|
+
|
|
46
|
+
/** How many seconds each frame lasts, engine uses a fixed time step
|
|
47
|
+
* @type {number}
|
|
48
|
+
* @default 1/60
|
|
49
|
+
* @memberof Engine */
|
|
50
|
+
const timeDelta = 1/frameRate;
|
|
51
|
+
|
|
52
|
+
/** Array containing all engine objects
|
|
53
|
+
* @type {Array<EngineObject>}
|
|
54
|
+
* @memberof Engine */
|
|
55
|
+
let engineObjects = [];
|
|
56
|
+
|
|
57
|
+
/** Array with only objects set to collide with other objects this frame (for optimization)
|
|
58
|
+
* @type {Array<EngineObject>}
|
|
59
|
+
* @memberof Engine */
|
|
60
|
+
let engineObjectsCollide = [];
|
|
61
|
+
|
|
62
|
+
/** Current update frame, used to calculate time
|
|
63
|
+
* @type {number}
|
|
64
|
+
* @memberof Engine */
|
|
65
|
+
let frame = 0;
|
|
66
|
+
|
|
67
|
+
/** Current engine time since start in seconds
|
|
68
|
+
* @type {number}
|
|
69
|
+
* @memberof Engine */
|
|
70
|
+
let time = 0;
|
|
71
|
+
|
|
72
|
+
/** Actual clock time since start in seconds (not affected by pause, timescale, or frame rate clamping)
|
|
73
|
+
* @type {number}
|
|
74
|
+
* @memberof Engine */
|
|
75
|
+
let timeReal = 0;
|
|
76
|
+
|
|
77
|
+
/** Is the game paused? Causes time and objects to not be updated
|
|
78
|
+
* @type {boolean}
|
|
79
|
+
* @default false
|
|
80
|
+
* @memberof Engine */
|
|
81
|
+
let paused = false;
|
|
82
|
+
|
|
83
|
+
/** Get if game is paused
|
|
84
|
+
* @return {boolean}
|
|
85
|
+
* @memberof Engine */
|
|
86
|
+
function getPaused() { return paused; }
|
|
87
|
+
|
|
88
|
+
/** Set if game is paused
|
|
89
|
+
* @param {boolean} [isPaused]
|
|
90
|
+
* @memberof Engine */
|
|
91
|
+
function setPaused(isPaused=true) { paused = isPaused; }
|
|
92
|
+
|
|
93
|
+
// Engine internal variables
|
|
94
|
+
let frameTimeLastMS = 0, frameTimeBufferMS = 0, averageFPS = 0;
|
|
95
|
+
let engineUpdateInternal; // assigned by engineInit so engineStep can drive it
|
|
96
|
+
let showEngineVersion = true;
|
|
97
|
+
|
|
98
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
99
|
+
// plugin hooks
|
|
100
|
+
|
|
101
|
+
const pluginList = [];
|
|
102
|
+
class EnginePlugin
|
|
103
|
+
{
|
|
104
|
+
constructor(update, render, glContextLost, glContextRestored)
|
|
105
|
+
{
|
|
106
|
+
this.update = update;
|
|
107
|
+
this.render = render;
|
|
108
|
+
this.glContextLost = glContextLost;
|
|
109
|
+
this.glContextRestored = glContextRestored;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* @callback PluginCallback - Update or render function for a plugin
|
|
115
|
+
* @memberof Engine
|
|
116
|
+
*/
|
|
117
|
+
|
|
118
|
+
/** Add a new update function for a plugin
|
|
119
|
+
* @param {PluginCallback} [update]
|
|
120
|
+
* @param {PluginCallback} [render]
|
|
121
|
+
* @param {PluginCallback} [glContextLost]
|
|
122
|
+
* @param {PluginCallback} [glContextRestored]
|
|
123
|
+
* @memberof Engine */
|
|
124
|
+
function engineAddPlugin(update, render, glContextLost, glContextRestored)
|
|
125
|
+
{
|
|
126
|
+
// make sure plugin functions are unique
|
|
127
|
+
ASSERT(!pluginList.find(p=>
|
|
128
|
+
p.update === update && p.render === render &&
|
|
129
|
+
p.glContextLost === glContextLost &&
|
|
130
|
+
p.glContextRestored === glContextRestored));
|
|
131
|
+
|
|
132
|
+
const plugin = new EnginePlugin(update, render, glContextLost, glContextRestored);
|
|
133
|
+
pluginList.push(plugin);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
137
|
+
// Main Engine Functions
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* @callback GameInitCallback - Called after the engine starts, can be async
|
|
141
|
+
* @return {void|Promise<void>}
|
|
142
|
+
* @memberof Engine
|
|
143
|
+
*/
|
|
144
|
+
/**
|
|
145
|
+
* @callback GameCallback - Update or render function for the game
|
|
146
|
+
* @memberof Engine
|
|
147
|
+
*/
|
|
148
|
+
|
|
149
|
+
/** Startup LittleJS engine with your callback functions
|
|
150
|
+
* @param {GameInitCallback} gameInit - Called once after the engine starts up, can be async for loading
|
|
151
|
+
* @param {GameCallback} gameUpdate - Called every frame before objects are updated (60fps), use for game logic
|
|
152
|
+
* @param {GameCallback} gameUpdatePost - Called after physics and objects are updated, even when paused, use for UI updates
|
|
153
|
+
* @param {GameCallback} gameRender - Called before objects are rendered, use for drawing backgrounds/world elements
|
|
154
|
+
* @param {GameCallback} gameRenderPost - Called after objects are rendered, use for drawing UI/overlays
|
|
155
|
+
* @param {Array<string>} [imageSources=[]] - List of image file paths to preload (e.g., ['player.png', 'tiles.png'])
|
|
156
|
+
* @param {HTMLElement} [rootElement] - Root DOM element to attach canvas to, defaults to document.body
|
|
157
|
+
* @example
|
|
158
|
+
* // Basic engine startup
|
|
159
|
+
* engineInit(
|
|
160
|
+
* ()=> { LOG('Game initialized!'); }, // gameInit
|
|
161
|
+
* ()=> { updateGameLogic(); }, // gameUpdate
|
|
162
|
+
* ()=> { updateUI(); }, // gameUpdatePost
|
|
163
|
+
* ()=> { drawBackground(); }, // gameRender
|
|
164
|
+
* ()=> { drawHUD(); }, // gameRenderPost
|
|
165
|
+
* ['tiles.png', 'tilesLevel.png'] // images to load
|
|
166
|
+
* );
|
|
167
|
+
* @memberof Engine */
|
|
168
|
+
async function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRenderPost, imageSources=[], rootElement)
|
|
169
|
+
{
|
|
170
|
+
showEngineVersion && console.log(`${engineName} Engine v${engineVersion}`);
|
|
171
|
+
ASSERT(!mainContext, 'engine already initialized');
|
|
172
|
+
// runtime guard so release builds (where the assert is stripped) don't
|
|
173
|
+
// double-register listeners / double-add canvases on a second call
|
|
174
|
+
if (mainContext) return;
|
|
175
|
+
ASSERT(isArray(imageSources), 'pass in images as array');
|
|
176
|
+
|
|
177
|
+
// ensure body exists for minimal HTML where the script runs before <body> is parsed
|
|
178
|
+
if (!document.body)
|
|
179
|
+
document.documentElement.appendChild(document.createElement('body'));
|
|
180
|
+
rootElement ||= document.body;
|
|
181
|
+
|
|
182
|
+
// allow passing in empty functions
|
|
183
|
+
gameInit ||= ()=>{};
|
|
184
|
+
gameUpdate ||= ()=>{};
|
|
185
|
+
gameUpdatePost ||= ()=>{};
|
|
186
|
+
gameRender ||= ()=>{};
|
|
187
|
+
gameRenderPost ||= ()=>{};
|
|
188
|
+
|
|
189
|
+
// Called automatically by engine to setup render system
|
|
190
|
+
function enginePreRender()
|
|
191
|
+
{
|
|
192
|
+
// save canvas size
|
|
193
|
+
mainCanvasSize = vec2(mainCanvas.width, mainCanvas.height);
|
|
194
|
+
|
|
195
|
+
// disable smoothing for pixel art
|
|
196
|
+
mainContext.imageSmoothingEnabled = !tilesPixelated;
|
|
197
|
+
|
|
198
|
+
// setup gl rendering if enabled
|
|
199
|
+
glPreRender();
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// internal update loop for engine
|
|
203
|
+
function engineUpdate(frameTimeMS=0)
|
|
204
|
+
{
|
|
205
|
+
// update time keeping
|
|
206
|
+
let frameTimeDeltaMS = frameTimeMS - frameTimeLastMS;
|
|
207
|
+
// skip delta on the very first frame so timeReal doesn't jump
|
|
208
|
+
// by ~page-load-time when RAF starts handing real timestamps
|
|
209
|
+
if (!frameTimeLastMS) frameTimeDeltaMS = 0;
|
|
210
|
+
frameTimeLastMS = frameTimeMS;
|
|
211
|
+
if (debug || debugWatermark)
|
|
212
|
+
averageFPS = lerp(averageFPS, 1e3/(frameTimeDeltaMS||1), .05);
|
|
213
|
+
const debugSpeedUp = debug && keyIsDown('Equal'); // +
|
|
214
|
+
const debugSpeedDown = debug && keyIsDown('Minus'); // -
|
|
215
|
+
const debugScale = debugSpeedUp ? 10 : debugSpeedDown ? .1 : 1;
|
|
216
|
+
|
|
217
|
+
// apply time deltas
|
|
218
|
+
timeReal += frameTimeDeltaMS * debugScale / 1e3;
|
|
219
|
+
const combinedScale = timeScale * debugScale;
|
|
220
|
+
frameTimeDeltaMS *= combinedScale;
|
|
221
|
+
frameTimeBufferMS += paused ? 0 : frameTimeDeltaMS;
|
|
222
|
+
if (combinedScale <= 1)
|
|
223
|
+
frameTimeBufferMS = min(frameTimeBufferMS, 50); // clamp min framerate
|
|
224
|
+
|
|
225
|
+
let wasUpdated = false;
|
|
226
|
+
if (paused)
|
|
227
|
+
{
|
|
228
|
+
// update everything except the game and objects
|
|
229
|
+
wasUpdated = true;
|
|
230
|
+
updateCanvas();
|
|
231
|
+
inputUpdate();
|
|
232
|
+
pluginList.forEach(plugin=>plugin.update?.());
|
|
233
|
+
|
|
234
|
+
// update object transforms even when paused
|
|
235
|
+
for (const o of engineObjects)
|
|
236
|
+
o.parent || o.updateTransforms();
|
|
237
|
+
|
|
238
|
+
// do post update
|
|
239
|
+
debugUpdate();
|
|
240
|
+
gameUpdatePost();
|
|
241
|
+
inputUpdatePost();
|
|
242
|
+
if (debugVideoCaptureIsActive())
|
|
243
|
+
renderFrame();
|
|
244
|
+
}
|
|
245
|
+
else
|
|
246
|
+
{
|
|
247
|
+
// apply time delta smoothing, improves smoothness of framerate in some browsers
|
|
248
|
+
let deltaSmooth = 0;
|
|
249
|
+
if (frameTimeBufferMS < 0 && frameTimeBufferMS > -9)
|
|
250
|
+
{
|
|
251
|
+
// force at least one update each frame since it is waiting for refresh
|
|
252
|
+
deltaSmooth = frameTimeBufferMS;
|
|
253
|
+
frameTimeBufferMS = 0;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// update multiple frames if necessary in case of slow framerate
|
|
257
|
+
for (; frameTimeBufferMS >= 0; frameTimeBufferMS -= 1e3 / frameRate)
|
|
258
|
+
{
|
|
259
|
+
// increment frame and update time
|
|
260
|
+
time = frame++ / frameRate;
|
|
261
|
+
|
|
262
|
+
// update game and objects
|
|
263
|
+
wasUpdated = true;
|
|
264
|
+
updateCanvas();
|
|
265
|
+
inputUpdate();
|
|
266
|
+
gameUpdate();
|
|
267
|
+
pluginList.forEach(plugin=>plugin.update?.());
|
|
268
|
+
engineObjectsUpdate();
|
|
269
|
+
|
|
270
|
+
// do post update
|
|
271
|
+
debugUpdate();
|
|
272
|
+
gameUpdatePost();
|
|
273
|
+
inputUpdatePost();
|
|
274
|
+
if (debugVideoCaptureIsActive())
|
|
275
|
+
renderFrame();
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// add the time smoothing back in
|
|
279
|
+
frameTimeBufferMS += deltaSmooth;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
if (!debugVideoCaptureIsActive())
|
|
283
|
+
renderFrame();
|
|
284
|
+
if (!engineManualStep)
|
|
285
|
+
requestAnimationFrame(engineUpdate);
|
|
286
|
+
|
|
287
|
+
function renderFrame()
|
|
288
|
+
{
|
|
289
|
+
if (headlessMode) return;
|
|
290
|
+
|
|
291
|
+
// canvas must be updated before rendering
|
|
292
|
+
if (!wasUpdated)
|
|
293
|
+
updateCanvas();
|
|
294
|
+
|
|
295
|
+
// render the game and objects
|
|
296
|
+
enginePreRender();
|
|
297
|
+
gameRender();
|
|
298
|
+
engineObjects.sort((a,b)=> a.renderOrder - b.renderOrder);
|
|
299
|
+
for (const o of engineObjects)
|
|
300
|
+
o.destroyed || o.render();
|
|
301
|
+
|
|
302
|
+
// post rendering
|
|
303
|
+
gameRenderPost();
|
|
304
|
+
pluginList.forEach(plugin=>plugin.render?.());
|
|
305
|
+
inputRender();
|
|
306
|
+
debugRender();
|
|
307
|
+
glFlush();
|
|
308
|
+
debugRenderPost();
|
|
309
|
+
drawCount = 0;
|
|
310
|
+
primitiveCount = 0;
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
engineUpdateInternal = engineUpdate;
|
|
314
|
+
|
|
315
|
+
function updateCanvas()
|
|
316
|
+
{
|
|
317
|
+
if (headlessMode) return;
|
|
318
|
+
|
|
319
|
+
if (canvasFixedSize.x)
|
|
320
|
+
{
|
|
321
|
+
// set canvas fixed size
|
|
322
|
+
mainCanvasSize = canvasFixedSize.copy();
|
|
323
|
+
|
|
324
|
+
// fit to window using css width and height
|
|
325
|
+
const innerAspect = innerWidth / innerHeight;
|
|
326
|
+
const fixedAspect = canvasFixedSize.x / canvasFixedSize.y;
|
|
327
|
+
const w = innerAspect < fixedAspect ? '100%' : '';
|
|
328
|
+
const h = innerAspect < fixedAspect ? '' : '100%';
|
|
329
|
+
mainCanvas.style.width = w;
|
|
330
|
+
mainCanvas.style.height = h;
|
|
331
|
+
if (glCanvas)
|
|
332
|
+
{
|
|
333
|
+
glCanvas.style.width = w;
|
|
334
|
+
glCanvas.style.height = h;
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
else
|
|
338
|
+
{
|
|
339
|
+
// apply device pixel ratio for crisp rendering
|
|
340
|
+
const dpr = canvasPixelRatio ?? (devicePixelRatio || 1);
|
|
341
|
+
const viewWidth = innerWidth * dpr | 0;
|
|
342
|
+
const viewHeight = innerHeight * dpr | 0;
|
|
343
|
+
|
|
344
|
+
// get main canvas size based on window size
|
|
345
|
+
mainCanvasSize.x = min(viewWidth, canvasMaxSize.x);
|
|
346
|
+
mainCanvasSize.y = min(viewHeight, canvasMaxSize.y);
|
|
347
|
+
|
|
348
|
+
// responsive aspect ratio with native resolution
|
|
349
|
+
const innerAspect = viewWidth / viewHeight;
|
|
350
|
+
ASSERT(canvasMinAspect <= canvasMaxAspect);
|
|
351
|
+
if (canvasMaxAspect && innerAspect > canvasMaxAspect)
|
|
352
|
+
{
|
|
353
|
+
// full height
|
|
354
|
+
const w = mainCanvasSize.y * canvasMaxAspect | 0;
|
|
355
|
+
mainCanvasSize.x = min(w, canvasMaxSize.x);
|
|
356
|
+
}
|
|
357
|
+
else if (innerAspect < canvasMinAspect)
|
|
358
|
+
{
|
|
359
|
+
// full width
|
|
360
|
+
const h = mainCanvasSize.x / canvasMinAspect | 0;
|
|
361
|
+
mainCanvasSize.y = min(h, canvasMaxSize.y);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
// set CSS display size so backing store renders at viewport size
|
|
365
|
+
const cssW = (mainCanvasSize.x / dpr | 0) + 'px';
|
|
366
|
+
const cssH = (mainCanvasSize.y / dpr | 0) + 'px';
|
|
367
|
+
mainCanvas.style.width = cssW;
|
|
368
|
+
mainCanvas.style.height = cssH;
|
|
369
|
+
if (glCanvas)
|
|
370
|
+
{
|
|
371
|
+
glCanvas.style.width = cssW;
|
|
372
|
+
glCanvas.style.height = cssH;
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
// clear main canvas and set size
|
|
377
|
+
mainCanvas.width = mainCanvasSize.x;
|
|
378
|
+
mainCanvas.height = mainCanvasSize.y;
|
|
379
|
+
|
|
380
|
+
// apply the clear color to main canvas
|
|
381
|
+
if (canvasClearColor.a > 0 && !glEnable)
|
|
382
|
+
{
|
|
383
|
+
mainContext.fillStyle = canvasClearColor.toString();
|
|
384
|
+
mainContext.fillRect(0, 0, mainCanvasSize.x, mainCanvasSize.y);
|
|
385
|
+
mainContext.fillStyle = BLACK.toString();
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
// set default line join and cap
|
|
389
|
+
mainContext.lineJoin = 'round';
|
|
390
|
+
mainContext.lineCap = 'round';
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
// skip setup if headless
|
|
394
|
+
if (headlessMode) return startEngine();
|
|
395
|
+
|
|
396
|
+
// setup webgl
|
|
397
|
+
glInit(rootElement);
|
|
398
|
+
|
|
399
|
+
// setup html
|
|
400
|
+
const styleRoot =
|
|
401
|
+
'margin:0;' + // fill the window
|
|
402
|
+
'overflow:hidden;' + // no scroll bars
|
|
403
|
+
'background:#000;' + // set background color
|
|
404
|
+
'user-select:none;' + // prevent hold to select
|
|
405
|
+
'-webkit-user-select:none;' + // compatibility for ios
|
|
406
|
+
'touch-action:none;' + // prevent mobile pinch to resize
|
|
407
|
+
'-webkit-touch-callout:none'; // compatibility for ios
|
|
408
|
+
rootElement.style.cssText = styleRoot;
|
|
409
|
+
mainCanvas = rootElement.appendChild(document.createElement('canvas'));
|
|
410
|
+
drawContext = mainContext = mainCanvas.getContext('2d');
|
|
411
|
+
|
|
412
|
+
// init stuff and start engine
|
|
413
|
+
inputInit();
|
|
414
|
+
audioInit();
|
|
415
|
+
debugInit();
|
|
416
|
+
|
|
417
|
+
// setup canvases
|
|
418
|
+
// transform way is still more reliable than flexbox or grid
|
|
419
|
+
const styleCanvas = 'position:absolute;'+ // allow canvases to overlap
|
|
420
|
+
'top:50%;left:50%;transform:translate(-50%,-50%)'; // center on screen
|
|
421
|
+
mainCanvas.style.cssText = styleCanvas;
|
|
422
|
+
if (glCanvas)
|
|
423
|
+
glCanvas.style.cssText = styleCanvas;
|
|
424
|
+
setCanvasPixelated(canvasPixelated);
|
|
425
|
+
updateCanvas();
|
|
426
|
+
glPreRender();
|
|
427
|
+
|
|
428
|
+
// create offscreen canvases for image processing
|
|
429
|
+
workCanvas = new OffscreenCanvas(64, 64);
|
|
430
|
+
workContext = workCanvas.getContext('2d');
|
|
431
|
+
workReadCanvas = new OffscreenCanvas(64, 64);
|
|
432
|
+
workReadContext = workReadCanvas.getContext('2d', { willReadFrequently: true });
|
|
433
|
+
|
|
434
|
+
// create promises for loading images
|
|
435
|
+
const promises = imageSources.map((src, i)=> loadTexture(i, src));
|
|
436
|
+
|
|
437
|
+
// no images to load
|
|
438
|
+
if (!imageSources.length)
|
|
439
|
+
promises.push(loadTexture(0));
|
|
440
|
+
|
|
441
|
+
// load engine font image
|
|
442
|
+
promises.push(imageFontInit());
|
|
443
|
+
|
|
444
|
+
if (showSplashScreen)
|
|
445
|
+
{
|
|
446
|
+
// draw splash screen
|
|
447
|
+
promises.push(new Promise(resolve =>
|
|
448
|
+
{
|
|
449
|
+
let t = 0;
|
|
450
|
+
updateSplash();
|
|
451
|
+
function updateSplash()
|
|
452
|
+
{
|
|
453
|
+
inputClear();
|
|
454
|
+
drawEngineLogo(t+=.01);
|
|
455
|
+
t>1 ? resolve() : setTimeout(updateSplash, 16);
|
|
456
|
+
}
|
|
457
|
+
}));
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
// wait for all the promises to finish
|
|
461
|
+
await Promise.all(promises);
|
|
462
|
+
return startEngine();
|
|
463
|
+
|
|
464
|
+
async function startEngine()
|
|
465
|
+
{
|
|
466
|
+
// wait for gameInit to load
|
|
467
|
+
await gameInit();
|
|
468
|
+
engineManualStep || engineUpdate();
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
// max frames engineStep can advance in one call, 10 minutes at 60fps
|
|
473
|
+
// large counts block until they finish, so this catches runaway values
|
|
474
|
+
const engineStepMaxFrames = 36000;
|
|
475
|
+
|
|
476
|
+
/** Advance the engine by a number of frames
|
|
477
|
+
* Requires setEngineManualStep(true) before engineInit
|
|
478
|
+
* Respects paused exactly as the normal update loop does
|
|
479
|
+
* @param {number} [frames] - number of engine update ticks, max 36000, each running one fixed update at timeScale 1
|
|
480
|
+
* @example
|
|
481
|
+
* setHeadlessMode(true);
|
|
482
|
+
* setEngineManualStep(true);
|
|
483
|
+
* await engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRenderPost);
|
|
484
|
+
* engineStep(600); // advance 10 seconds of game time
|
|
485
|
+
* @memberof Engine */
|
|
486
|
+
function engineStep(frames=1)
|
|
487
|
+
{
|
|
488
|
+
ASSERT(engineManualStep,
|
|
489
|
+
'engineStep requires setEngineManualStep(true) before engineInit');
|
|
490
|
+
ASSERT(engineUpdateInternal, 'engineStep requires engineInit to complete');
|
|
491
|
+
// runtime guard so release builds (where the asserts are stripped) can't
|
|
492
|
+
// start a second requestAnimationFrame chain or call an undefined update
|
|
493
|
+
if (!engineManualStep || !engineUpdateInternal) return;
|
|
494
|
+
ASSERT(Number.isInteger(frames) && frames >= 0 && frames <= engineStepMaxFrames,
|
|
495
|
+
'engineStep requires a whole frame count from 0 to ' + engineStepMaxFrames);
|
|
496
|
+
frames = min(frames, engineStepMaxFrames); // release has no asserts, don't freeze
|
|
497
|
+
for (let i = frames; i > 0; --i)
|
|
498
|
+
engineUpdateInternal(frameTimeLastMS + 1e3 / frameRate);
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
/** Update each engine object, remove destroyed objects, and update time
|
|
502
|
+
* can be called manually if objects need to be updated outside of main loop
|
|
503
|
+
* @memberof Engine */
|
|
504
|
+
function engineObjectsUpdate()
|
|
505
|
+
{
|
|
506
|
+
// get list of solid objects for physics optimization
|
|
507
|
+
engineObjectsCollide = engineObjects.filter(o=>o.collideSolidObjects);
|
|
508
|
+
|
|
509
|
+
// update physics before object update
|
|
510
|
+
for (const o of engineObjects)
|
|
511
|
+
if (!o.parent && !o.destroyed)
|
|
512
|
+
o.updatePhysics();
|
|
513
|
+
|
|
514
|
+
// recursive object update
|
|
515
|
+
function updateChildObject(o)
|
|
516
|
+
{
|
|
517
|
+
if (o.destroyed) return;
|
|
518
|
+
|
|
519
|
+
o.update();
|
|
520
|
+
for (const child of o.children)
|
|
521
|
+
updateChildObject(child);
|
|
522
|
+
}
|
|
523
|
+
for (const o of engineObjects)
|
|
524
|
+
{
|
|
525
|
+
if (o.parent || o.destroyed) continue;
|
|
526
|
+
|
|
527
|
+
// update top level objects
|
|
528
|
+
o.update();
|
|
529
|
+
for (const child of o.children)
|
|
530
|
+
updateChildObject(child);
|
|
531
|
+
o.updateTransforms();
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
// remove destroyed objects
|
|
535
|
+
engineObjects = engineObjects.filter(o=>!o.destroyed);
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
/** Destroy and remove all objects
|
|
539
|
+
* - This can be used to clear out all objects when restarting a level
|
|
540
|
+
* - Objects can override their destroy function to do cleanup or stick around
|
|
541
|
+
* @param {boolean} [immediate] - should attached effects be allowed to die off?
|
|
542
|
+
* @memberof Engine */
|
|
543
|
+
function engineObjectsDestroy(immediate=true)
|
|
544
|
+
{
|
|
545
|
+
for (const o of engineObjects)
|
|
546
|
+
o.parent || o.destroy(immediate);
|
|
547
|
+
engineObjects = engineObjects.filter(o=>!o.destroyed);
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
/** Collects all object within a given area
|
|
551
|
+
* @param {Vector2} [pos] - Center of test area, or undefined for all objects
|
|
552
|
+
* @param {Vector2|number} [size] - Radius of circle if float, rectangle size if Vector2
|
|
553
|
+
* @param {Array<EngineObject>} [objects=engineObjects] - List of objects to check
|
|
554
|
+
* @return {Array<EngineObject>} - List of collected objects
|
|
555
|
+
* @memberof Engine */
|
|
556
|
+
function engineObjectsCollect(pos, size, objects=engineObjects)
|
|
557
|
+
{
|
|
558
|
+
const collectedObjects = [];
|
|
559
|
+
if (!pos)
|
|
560
|
+
{
|
|
561
|
+
// all objects
|
|
562
|
+
for (const o of objects)
|
|
563
|
+
collectedObjects.push(o);
|
|
564
|
+
}
|
|
565
|
+
else if (size instanceof Vector2)
|
|
566
|
+
{
|
|
567
|
+
// bounding box test
|
|
568
|
+
for (const o of objects)
|
|
569
|
+
o.isOverlapping(pos, size) && collectedObjects.push(o);
|
|
570
|
+
}
|
|
571
|
+
else
|
|
572
|
+
{
|
|
573
|
+
// circle test
|
|
574
|
+
const sizeSquared = size*size;
|
|
575
|
+
for (const o of objects)
|
|
576
|
+
pos.distanceSquared(o.pos) < sizeSquared && collectedObjects.push(o);
|
|
577
|
+
}
|
|
578
|
+
return collectedObjects;
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
/**
|
|
582
|
+
* @callback ObjectCallbackFunction - Function that processes an object
|
|
583
|
+
* @param {EngineObject} object
|
|
584
|
+
* @memberof Engine
|
|
585
|
+
*/
|
|
586
|
+
|
|
587
|
+
/** Triggers a callback for each object within a given area
|
|
588
|
+
* @param {Vector2} [pos] - Center of test area, or undefined for all objects
|
|
589
|
+
* @param {Vector2|number} [size] - Radius of circle if float, rectangle size if Vector2
|
|
590
|
+
* @param {ObjectCallbackFunction} [callbackFunction] - Calls this function on every object that passes the test
|
|
591
|
+
* @param {Array<EngineObject>} [objects=engineObjects] - List of objects to check
|
|
592
|
+
* @memberof Engine */
|
|
593
|
+
function engineObjectsCallback(pos, size, callbackFunction, objects=engineObjects)
|
|
594
|
+
{ engineObjectsCollect(pos, size, objects).forEach(o => callbackFunction(o)); }
|
|
595
|
+
|
|
596
|
+
/** Return a list of objects intersecting a ray
|
|
597
|
+
* @param {Vector2} start
|
|
598
|
+
* @param {Vector2} end
|
|
599
|
+
* @param {Array<EngineObject>} [objects=engineObjects] - List of objects to check
|
|
600
|
+
* @return {Array<EngineObject>} - List of objects hit
|
|
601
|
+
* @memberof Engine */
|
|
602
|
+
function engineObjectsRaycast(start, end, objects=engineObjects)
|
|
603
|
+
{
|
|
604
|
+
const hitObjects = [];
|
|
605
|
+
for (const o of objects)
|
|
606
|
+
{
|
|
607
|
+
if (o.collideRaycast && isIntersecting(start, end, o.pos, o.size))
|
|
608
|
+
{
|
|
609
|
+
debugRaycast && debugRect(o.pos, o.size, '#f00');
|
|
610
|
+
hitObjects.push(o);
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
debugRaycast && debugLine(start, end, hitObjects.length ? '#f00' : '#00f', .02);
|
|
615
|
+
return hitObjects;
|
|
616
616
|
}
|
|
617
617
|
/**
|
|
618
618
|
* LittleJS - Release Mode
|
|
@@ -2346,6 +2346,14 @@ let gamepadsEnable = true;
|
|
|
2346
2346
|
* @memberof Settings */
|
|
2347
2347
|
let gamepadDirectionEmulateStick = true;
|
|
2348
2348
|
|
|
2349
|
+
/** If true, axes that do not rest near center are ignored on gamepads without
|
|
2350
|
+
* standard mapping. Steering wheels and flight sticks report pedal and throttle
|
|
2351
|
+
* axes that rest at full deflection, which otherwise reads as a stick held down.
|
|
2352
|
+
* @type {boolean}
|
|
2353
|
+
* @default
|
|
2354
|
+
* @memberof Settings */
|
|
2355
|
+
let gamepadAxisFilterEnable = true;
|
|
2356
|
+
|
|
2349
2357
|
/** If true the WASD keys are also routed to the direction keys (for better accessibility)
|
|
2350
2358
|
* @type {boolean}
|
|
2351
2359
|
* @default
|
|
@@ -2684,6 +2692,11 @@ function setGamepadsEnable(enable) { gamepadsEnable = enable; }
|
|
|
2684
2692
|
* @memberof Settings */
|
|
2685
2693
|
function setGamepadDirectionEmulateStick(enable) { gamepadDirectionEmulateStick = enable; }
|
|
2686
2694
|
|
|
2695
|
+
/** Set if axes that do not rest near center are ignored on non-standard gamepads
|
|
2696
|
+
* @param {boolean} enable
|
|
2697
|
+
* @memberof Settings */
|
|
2698
|
+
function setGamepadAxisFilterEnable(enable) { gamepadAxisFilterEnable = enable; }
|
|
2699
|
+
|
|
2687
2700
|
/** Set if true the WASD keys are also routed to the direction keys
|
|
2688
2701
|
* @param {boolean} enable
|
|
2689
2702
|
* @memberof Settings */
|
|
@@ -3428,6 +3441,12 @@ let workReadCanvas;
|
|
|
3428
3441
|
* @memberof Draw */
|
|
3429
3442
|
let workReadContext;
|
|
3430
3443
|
|
|
3444
|
+
/** Extra canvas to composite behind the engine canvases when combining canvases
|
|
3445
|
+
* Set by plugins that render to their own canvas below the LittleJS canvases
|
|
3446
|
+
* @type {HTMLCanvasElement}
|
|
3447
|
+
* @memberof Draw */
|
|
3448
|
+
let backgroundCanvas;
|
|
3449
|
+
|
|
3431
3450
|
/** The size of the main canvas (and other secondary canvases)
|
|
3432
3451
|
* @type {Vector2}
|
|
3433
3452
|
* @memberof Draw */
|
|
@@ -4579,6 +4598,13 @@ function setAdditiveBlendMode(additive=true)
|
|
|
4579
4598
|
drawContext.globalCompositeOperation = additive ? 'lighter' : 'source-over';
|
|
4580
4599
|
}
|
|
4581
4600
|
|
|
4601
|
+
/** Set an extra canvas to composite behind the engine canvases when combining
|
|
4602
|
+
* Plugins that insert their own canvas below the LittleJS canvases should set
|
|
4603
|
+
* this so it appears in screenshots and video capture
|
|
4604
|
+
* @param {HTMLCanvasElement} [canvas]
|
|
4605
|
+
* @memberof Draw */
|
|
4606
|
+
function setBackgroundCanvas(canvas) { backgroundCanvas = canvas; }
|
|
4607
|
+
|
|
4582
4608
|
/** Combines LittleJS canvases onto the main canvas
|
|
4583
4609
|
* This is necessary for things like screenshots and video
|
|
4584
4610
|
* @memberof Draw */
|
|
@@ -4591,6 +4617,8 @@ function combineCanvases()
|
|
|
4591
4617
|
// leaving workContext.fillStyle transparent can't silently no-op this
|
|
4592
4618
|
workContext.fillStyle = '#000';
|
|
4593
4619
|
workContext.fillRect(0,0,w,h);
|
|
4620
|
+
if (backgroundCanvas)
|
|
4621
|
+
workContext.drawImage(backgroundCanvas, 0, 0, w, h);
|
|
4594
4622
|
glCopyToContext(workContext);
|
|
4595
4623
|
workContext.drawImage(mainCanvas, 0, 0);
|
|
4596
4624
|
mainContext.drawImage(workCanvas, 0, 0);
|
|
@@ -4834,9 +4862,15 @@ class ImageFont
|
|
|
4834
4862
|
tileInfo.pos.x = x*sizePaddedX + padding;
|
|
4835
4863
|
tileInfo.pos.y = y*sizePaddedY + padding;
|
|
4836
4864
|
|
|
4837
|
-
//
|
|
4838
|
-
|
|
4839
|
-
|
|
4865
|
+
// snap the glyph edges to whole pixels
|
|
4866
|
+
// tiles are drawn from their center, so snapping the center
|
|
4867
|
+
// to a whole pixel puts the edges on half pixels when the
|
|
4868
|
+
// size is even, and a row or column of the glyph then has
|
|
4869
|
+
// no pixel center inside it and is not rasterized at all
|
|
4870
|
+
// ceil picks the nearest aligned position, breaking ties
|
|
4871
|
+
// downward to match how this used to truncate
|
|
4872
|
+
drawPos.x = ceil(pos.x + i * size.x - centerOffset - size.x/2) + size.x/2 - .5;
|
|
4873
|
+
drawPos.y = ceil(pos.y + j * size.y - size.y/2) + size.y/2 - .5;
|
|
4840
4874
|
drawTile(drawPos, size, tileInfo, color, 0, false, undefined, useWebGL, true, context);
|
|
4841
4875
|
}
|
|
4842
4876
|
});
|
|
@@ -4983,6 +5017,7 @@ function inputClear()
|
|
|
4983
5017
|
touchGamepadStickPointerId.length = 0; // release floating sticks so they re-anchor
|
|
4984
5018
|
gamepadStickData.length = 0;
|
|
4985
5019
|
gamepadDpadData.length = 0;
|
|
5020
|
+
gamepadAxisCentered.length = 0;
|
|
4986
5021
|
}
|
|
4987
5022
|
|
|
4988
5023
|
///////////////////////////////////////////////////////////////////////////////
|
|
@@ -5220,6 +5255,11 @@ const inputData = [[]];
|
|
|
5220
5255
|
|
|
5221
5256
|
// gamepad internal variables
|
|
5222
5257
|
const gamepadStickData = [], gamepadDpadData = [], gamepadHadInput = [];
|
|
5258
|
+
// per gamepad, how many consecutive frames each axis has rested inside the
|
|
5259
|
+
// dead zone, used to tell stick axes from axes that rest at full deflection
|
|
5260
|
+
const gamepadAxisCentered = [];
|
|
5261
|
+
// how long an axis must rest inside the dead zone before it counts as a stick
|
|
5262
|
+
const gamepadAxisCenteredFrames = 15;
|
|
5223
5263
|
|
|
5224
5264
|
// touch gamepad internal variables
|
|
5225
5265
|
const touchGamepadTimer = new Timer, touchGamepadButtons = [], touchGamepadSticks = [];
|
|
@@ -5468,7 +5508,7 @@ function inputUpdate()
|
|
|
5468
5508
|
// gamepad: any button held or stick moved
|
|
5469
5509
|
let gamepadActive = false;
|
|
5470
5510
|
for (let s = gamepadStickCount(); s-- && !gamepadActive;)
|
|
5471
|
-
gamepadActive = gamepadStick(s).lengthSquared() > .
|
|
5511
|
+
gamepadActive = gamepadStick(s).lengthSquared() > .2;
|
|
5472
5512
|
for (let b = 17; b-- && !gamepadActive;)
|
|
5473
5513
|
gamepadActive = gamepadIsDown(b);
|
|
5474
5514
|
|
|
@@ -5496,12 +5536,12 @@ function inputUpdate()
|
|
|
5496
5536
|
// gamepads are updated by engine every frame automatically
|
|
5497
5537
|
function gamepadsUpdate()
|
|
5498
5538
|
{
|
|
5539
|
+
const deadZoneMin=.3, deadZoneMax=.8;
|
|
5499
5540
|
const applyDeadZones = (v)=>
|
|
5500
5541
|
{
|
|
5501
|
-
const min=.3, max=.8;
|
|
5502
5542
|
const deadZone = (v)=>
|
|
5503
|
-
v >
|
|
5504
|
-
v < -
|
|
5543
|
+
v > deadZoneMin ? percent(v, deadZoneMin, deadZoneMax) :
|
|
5544
|
+
v < -deadZoneMin ? -percent(-v, deadZoneMin, deadZoneMax) : 0;
|
|
5505
5545
|
return vec2(deadZone(v.x), deadZone(-v.y)).clampLength();
|
|
5506
5546
|
};
|
|
5507
5547
|
|
|
@@ -5585,6 +5625,7 @@ function inputUpdate()
|
|
|
5585
5625
|
gamepadStickData[i] = undefined;
|
|
5586
5626
|
gamepadDpadData[i] = undefined;
|
|
5587
5627
|
gamepadHadInput[i] = undefined;
|
|
5628
|
+
gamepadAxisCentered[i] = undefined;
|
|
5588
5629
|
continue;
|
|
5589
5630
|
}
|
|
5590
5631
|
|
|
@@ -5593,8 +5634,30 @@ function inputUpdate()
|
|
|
5593
5634
|
const dpad = gamepadDpadData[i] ?? (gamepadDpadData[i] = vec2());
|
|
5594
5635
|
|
|
5595
5636
|
// read analog sticks
|
|
5637
|
+
// gamepads without standard mapping (steering wheels, flight sticks)
|
|
5638
|
+
// can report axes that rest at full deflection instead of center,
|
|
5639
|
+
// which would otherwise read as a stick held down forever, so only
|
|
5640
|
+
// trust an axis once it has rested inside the dead zone for a moment
|
|
5641
|
+
const isStandard = gamepad.mapping === 'standard';
|
|
5642
|
+
const centered = gamepadAxisCentered[i] ?? (gamepadAxisCentered[i] = []);
|
|
5643
|
+
const readAxis = (j)=>
|
|
5644
|
+
{
|
|
5645
|
+
const v = gamepad.axes[j];
|
|
5646
|
+
if (isStandard && j < 4)
|
|
5647
|
+
return v; // spec guarantees axes 0-3 are the two sticks
|
|
5648
|
+
if (!gamepadAxisFilterEnable)
|
|
5649
|
+
return v;
|
|
5650
|
+
|
|
5651
|
+
// once an axis has proven it rests at center it stays trusted,
|
|
5652
|
+
// otherwise moving it would immediately disqualify it again
|
|
5653
|
+
const frames = centered[j] | 0;
|
|
5654
|
+
if (frames > gamepadAxisCenteredFrames)
|
|
5655
|
+
return v;
|
|
5656
|
+
centered[j] = abs(v) < deadZoneMin ? frames + 1 : 0;
|
|
5657
|
+
return 0;
|
|
5658
|
+
};
|
|
5596
5659
|
for (let j = 0; j < gamepad.axes.length-1; j+=2)
|
|
5597
|
-
sticks[j>>1] = applyDeadZones(vec2(
|
|
5660
|
+
sticks[j>>1] = applyDeadZones(vec2(readAxis(j), readAxis(j+1)));
|
|
5598
5661
|
|
|
5599
5662
|
// read buttons
|
|
5600
5663
|
let hadInput = false;
|
|
@@ -16111,6 +16174,9 @@ class ThreeJSPlugin
|
|
|
16111
16174
|
rootElement.insertBefore(threeCanvas, rootElement.firstChild);
|
|
16112
16175
|
threeCanvas.style.cssText = mainCanvas.style.cssText;
|
|
16113
16176
|
|
|
16177
|
+
// composite the 3D canvas into screenshots and video capture
|
|
16178
|
+
setBackgroundCanvas(threeCanvas);
|
|
16179
|
+
|
|
16114
16180
|
// render automatically each frame after the engine renders
|
|
16115
16181
|
engineAddPlugin(undefined, ()=> this.render());
|
|
16116
16182
|
}
|