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.esm.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 Debug System
|
|
@@ -3037,6 +3037,14 @@ let gamepadsEnable = true;
|
|
|
3037
3037
|
* @memberof Settings */
|
|
3038
3038
|
let gamepadDirectionEmulateStick = true;
|
|
3039
3039
|
|
|
3040
|
+
/** If true, axes that do not rest near center are ignored on gamepads without
|
|
3041
|
+
* standard mapping. Steering wheels and flight sticks report pedal and throttle
|
|
3042
|
+
* axes that rest at full deflection, which otherwise reads as a stick held down.
|
|
3043
|
+
* @type {boolean}
|
|
3044
|
+
* @default
|
|
3045
|
+
* @memberof Settings */
|
|
3046
|
+
let gamepadAxisFilterEnable = true;
|
|
3047
|
+
|
|
3040
3048
|
/** If true the WASD keys are also routed to the direction keys (for better accessibility)
|
|
3041
3049
|
* @type {boolean}
|
|
3042
3050
|
* @default
|
|
@@ -3375,6 +3383,11 @@ function setGamepadsEnable(enable) { gamepadsEnable = enable; }
|
|
|
3375
3383
|
* @memberof Settings */
|
|
3376
3384
|
function setGamepadDirectionEmulateStick(enable) { gamepadDirectionEmulateStick = enable; }
|
|
3377
3385
|
|
|
3386
|
+
/** Set if axes that do not rest near center are ignored on non-standard gamepads
|
|
3387
|
+
* @param {boolean} enable
|
|
3388
|
+
* @memberof Settings */
|
|
3389
|
+
function setGamepadAxisFilterEnable(enable) { gamepadAxisFilterEnable = enable; }
|
|
3390
|
+
|
|
3378
3391
|
/** Set if true the WASD keys are also routed to the direction keys
|
|
3379
3392
|
* @param {boolean} enable
|
|
3380
3393
|
* @memberof Settings */
|
|
@@ -4119,6 +4132,12 @@ let workReadCanvas;
|
|
|
4119
4132
|
* @memberof Draw */
|
|
4120
4133
|
let workReadContext;
|
|
4121
4134
|
|
|
4135
|
+
/** Extra canvas to composite behind the engine canvases when combining canvases
|
|
4136
|
+
* Set by plugins that render to their own canvas below the LittleJS canvases
|
|
4137
|
+
* @type {HTMLCanvasElement}
|
|
4138
|
+
* @memberof Draw */
|
|
4139
|
+
let backgroundCanvas;
|
|
4140
|
+
|
|
4122
4141
|
/** The size of the main canvas (and other secondary canvases)
|
|
4123
4142
|
* @type {Vector2}
|
|
4124
4143
|
* @memberof Draw */
|
|
@@ -5270,6 +5289,13 @@ function setAdditiveBlendMode(additive=true)
|
|
|
5270
5289
|
drawContext.globalCompositeOperation = additive ? 'lighter' : 'source-over';
|
|
5271
5290
|
}
|
|
5272
5291
|
|
|
5292
|
+
/** Set an extra canvas to composite behind the engine canvases when combining
|
|
5293
|
+
* Plugins that insert their own canvas below the LittleJS canvases should set
|
|
5294
|
+
* this so it appears in screenshots and video capture
|
|
5295
|
+
* @param {HTMLCanvasElement} [canvas]
|
|
5296
|
+
* @memberof Draw */
|
|
5297
|
+
function setBackgroundCanvas(canvas) { backgroundCanvas = canvas; }
|
|
5298
|
+
|
|
5273
5299
|
/** Combines LittleJS canvases onto the main canvas
|
|
5274
5300
|
* This is necessary for things like screenshots and video
|
|
5275
5301
|
* @memberof Draw */
|
|
@@ -5282,6 +5308,8 @@ function combineCanvases()
|
|
|
5282
5308
|
// leaving workContext.fillStyle transparent can't silently no-op this
|
|
5283
5309
|
workContext.fillStyle = '#000';
|
|
5284
5310
|
workContext.fillRect(0,0,w,h);
|
|
5311
|
+
if (backgroundCanvas)
|
|
5312
|
+
workContext.drawImage(backgroundCanvas, 0, 0, w, h);
|
|
5285
5313
|
glCopyToContext(workContext);
|
|
5286
5314
|
workContext.drawImage(mainCanvas, 0, 0);
|
|
5287
5315
|
mainContext.drawImage(workCanvas, 0, 0);
|
|
@@ -5525,9 +5553,15 @@ class ImageFont
|
|
|
5525
5553
|
tileInfo.pos.x = x*sizePaddedX + padding;
|
|
5526
5554
|
tileInfo.pos.y = y*sizePaddedY + padding;
|
|
5527
5555
|
|
|
5528
|
-
//
|
|
5529
|
-
|
|
5530
|
-
|
|
5556
|
+
// snap the glyph edges to whole pixels
|
|
5557
|
+
// tiles are drawn from their center, so snapping the center
|
|
5558
|
+
// to a whole pixel puts the edges on half pixels when the
|
|
5559
|
+
// size is even, and a row or column of the glyph then has
|
|
5560
|
+
// no pixel center inside it and is not rasterized at all
|
|
5561
|
+
// ceil picks the nearest aligned position, breaking ties
|
|
5562
|
+
// downward to match how this used to truncate
|
|
5563
|
+
drawPos.x = ceil(pos.x + i * size.x - centerOffset - size.x/2) + size.x/2 - .5;
|
|
5564
|
+
drawPos.y = ceil(pos.y + j * size.y - size.y/2) + size.y/2 - .5;
|
|
5531
5565
|
drawTile(drawPos, size, tileInfo, color, 0, false, undefined, useWebGL, true, context);
|
|
5532
5566
|
}
|
|
5533
5567
|
});
|
|
@@ -5674,6 +5708,7 @@ function inputClear()
|
|
|
5674
5708
|
touchGamepadStickPointerId.length = 0; // release floating sticks so they re-anchor
|
|
5675
5709
|
gamepadStickData.length = 0;
|
|
5676
5710
|
gamepadDpadData.length = 0;
|
|
5711
|
+
gamepadAxisCentered.length = 0;
|
|
5677
5712
|
}
|
|
5678
5713
|
|
|
5679
5714
|
///////////////////////////////////////////////////////////////////////////////
|
|
@@ -5911,6 +5946,11 @@ const inputData = [[]];
|
|
|
5911
5946
|
|
|
5912
5947
|
// gamepad internal variables
|
|
5913
5948
|
const gamepadStickData = [], gamepadDpadData = [], gamepadHadInput = [];
|
|
5949
|
+
// per gamepad, how many consecutive frames each axis has rested inside the
|
|
5950
|
+
// dead zone, used to tell stick axes from axes that rest at full deflection
|
|
5951
|
+
const gamepadAxisCentered = [];
|
|
5952
|
+
// how long an axis must rest inside the dead zone before it counts as a stick
|
|
5953
|
+
const gamepadAxisCenteredFrames = 15;
|
|
5914
5954
|
|
|
5915
5955
|
// touch gamepad internal variables
|
|
5916
5956
|
const touchGamepadTimer = new Timer, touchGamepadButtons = [], touchGamepadSticks = [];
|
|
@@ -6159,7 +6199,7 @@ function inputUpdate()
|
|
|
6159
6199
|
// gamepad: any button held or stick moved
|
|
6160
6200
|
let gamepadActive = false;
|
|
6161
6201
|
for (let s = gamepadStickCount(); s-- && !gamepadActive;)
|
|
6162
|
-
gamepadActive = gamepadStick(s).lengthSquared() > .
|
|
6202
|
+
gamepadActive = gamepadStick(s).lengthSquared() > .2;
|
|
6163
6203
|
for (let b = 17; b-- && !gamepadActive;)
|
|
6164
6204
|
gamepadActive = gamepadIsDown(b);
|
|
6165
6205
|
|
|
@@ -6187,12 +6227,12 @@ function inputUpdate()
|
|
|
6187
6227
|
// gamepads are updated by engine every frame automatically
|
|
6188
6228
|
function gamepadsUpdate()
|
|
6189
6229
|
{
|
|
6230
|
+
const deadZoneMin=.3, deadZoneMax=.8;
|
|
6190
6231
|
const applyDeadZones = (v)=>
|
|
6191
6232
|
{
|
|
6192
|
-
const min=.3, max=.8;
|
|
6193
6233
|
const deadZone = (v)=>
|
|
6194
|
-
v >
|
|
6195
|
-
v < -
|
|
6234
|
+
v > deadZoneMin ? percent(v, deadZoneMin, deadZoneMax) :
|
|
6235
|
+
v < -deadZoneMin ? -percent(-v, deadZoneMin, deadZoneMax) : 0;
|
|
6196
6236
|
return vec2(deadZone(v.x), deadZone(-v.y)).clampLength();
|
|
6197
6237
|
};
|
|
6198
6238
|
|
|
@@ -6276,6 +6316,7 @@ function inputUpdate()
|
|
|
6276
6316
|
gamepadStickData[i] = undefined;
|
|
6277
6317
|
gamepadDpadData[i] = undefined;
|
|
6278
6318
|
gamepadHadInput[i] = undefined;
|
|
6319
|
+
gamepadAxisCentered[i] = undefined;
|
|
6279
6320
|
continue;
|
|
6280
6321
|
}
|
|
6281
6322
|
|
|
@@ -6284,8 +6325,30 @@ function inputUpdate()
|
|
|
6284
6325
|
const dpad = gamepadDpadData[i] ?? (gamepadDpadData[i] = vec2());
|
|
6285
6326
|
|
|
6286
6327
|
// read analog sticks
|
|
6328
|
+
// gamepads without standard mapping (steering wheels, flight sticks)
|
|
6329
|
+
// can report axes that rest at full deflection instead of center,
|
|
6330
|
+
// which would otherwise read as a stick held down forever, so only
|
|
6331
|
+
// trust an axis once it has rested inside the dead zone for a moment
|
|
6332
|
+
const isStandard = gamepad.mapping === 'standard';
|
|
6333
|
+
const centered = gamepadAxisCentered[i] ?? (gamepadAxisCentered[i] = []);
|
|
6334
|
+
const readAxis = (j)=>
|
|
6335
|
+
{
|
|
6336
|
+
const v = gamepad.axes[j];
|
|
6337
|
+
if (isStandard && j < 4)
|
|
6338
|
+
return v; // spec guarantees axes 0-3 are the two sticks
|
|
6339
|
+
if (!gamepadAxisFilterEnable)
|
|
6340
|
+
return v;
|
|
6341
|
+
|
|
6342
|
+
// once an axis has proven it rests at center it stays trusted,
|
|
6343
|
+
// otherwise moving it would immediately disqualify it again
|
|
6344
|
+
const frames = centered[j] | 0;
|
|
6345
|
+
if (frames > gamepadAxisCenteredFrames)
|
|
6346
|
+
return v;
|
|
6347
|
+
centered[j] = abs(v) < deadZoneMin ? frames + 1 : 0;
|
|
6348
|
+
return 0;
|
|
6349
|
+
};
|
|
6287
6350
|
for (let j = 0; j < gamepad.axes.length-1; j+=2)
|
|
6288
|
-
sticks[j>>1] = applyDeadZones(vec2(
|
|
6351
|
+
sticks[j>>1] = applyDeadZones(vec2(readAxis(j), readAxis(j+1)));
|
|
6289
6352
|
|
|
6290
6353
|
// read buttons
|
|
6291
6354
|
let hadInput = false;
|
|
@@ -16802,6 +16865,9 @@ class ThreeJSPlugin
|
|
|
16802
16865
|
rootElement.insertBefore(threeCanvas, rootElement.firstChild);
|
|
16803
16866
|
threeCanvas.style.cssText = mainCanvas.style.cssText;
|
|
16804
16867
|
|
|
16868
|
+
// composite the 3D canvas into screenshots and video capture
|
|
16869
|
+
setBackgroundCanvas(threeCanvas);
|
|
16870
|
+
|
|
16805
16871
|
// render automatically each frame after the engine renders
|
|
16806
16872
|
engineAddPlugin(undefined, ()=> this.render());
|
|
16807
16873
|
}
|
|
@@ -16986,6 +17052,7 @@ export
|
|
|
16986
17052
|
glCircleSides,
|
|
16987
17053
|
gamepadsEnable,
|
|
16988
17054
|
gamepadDirectionEmulateStick,
|
|
17055
|
+
gamepadAxisFilterEnable,
|
|
16989
17056
|
inputWASDEmulateDirection,
|
|
16990
17057
|
touchInputEnable,
|
|
16991
17058
|
touchGamepadEnable,
|
|
@@ -17042,6 +17109,7 @@ export
|
|
|
17042
17109
|
setTouchInputEnable,
|
|
17043
17110
|
setGamepadsEnable,
|
|
17044
17111
|
setGamepadDirectionEmulateStick,
|
|
17112
|
+
setGamepadAxisFilterEnable,
|
|
17045
17113
|
setInputWASDEmulateDirection,
|
|
17046
17114
|
setTouchGamepadEnable,
|
|
17047
17115
|
setTouchGamepadPassthrough,
|
|
@@ -17157,6 +17225,7 @@ export
|
|
|
17157
17225
|
workContext,
|
|
17158
17226
|
workReadCanvas,
|
|
17159
17227
|
workReadContext,
|
|
17228
|
+
backgroundCanvas,
|
|
17160
17229
|
mainCanvasSize,
|
|
17161
17230
|
textureInfos,
|
|
17162
17231
|
drawCount,
|
|
@@ -17182,6 +17251,7 @@ export
|
|
|
17182
17251
|
drawText,
|
|
17183
17252
|
drawTextScreen,
|
|
17184
17253
|
setAdditiveBlendMode,
|
|
17254
|
+
setBackgroundCanvas,
|
|
17185
17255
|
combineCanvases,
|
|
17186
17256
|
engineImageFont,
|
|
17187
17257
|
ImageFont,
|