littlejsengine 1.14.19 → 1.14.28

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.
Files changed (70) hide show
  1. package/dist/littlejs.d.ts +217 -38
  2. package/dist/littlejs.esm.js +879 -324
  3. package/dist/littlejs.esm.min.js +1 -1
  4. package/dist/littlejs.js +861 -324
  5. package/dist/littlejs.min.js +1 -1
  6. package/dist/littlejs.release.js +799 -325
  7. package/examples/box2d/gameObjects.js +6 -10
  8. package/examples/box2d/scenes.js +2 -2
  9. package/examples/breakout/gameObjects.js +1 -1
  10. package/examples/electron/game.js +1 -41
  11. package/examples/electron/index.html +2 -2
  12. package/examples/htmlMenu/game.js +1 -1
  13. package/examples/index.html +19 -10
  14. package/examples/module/game.js +5 -7
  15. package/examples/particles/index.html +6 -9
  16. package/examples/platformer/gameCharacter.js +4 -3
  17. package/examples/platformer/gameEffects.js +7 -2
  18. package/examples/platformer/gameObjects.js +6 -11
  19. package/examples/shorts/animation.js +2 -2
  20. package/examples/shorts/base.html +4 -1
  21. package/examples/shorts/box2d.js +2 -2
  22. package/examples/shorts/box2dCar.js +18 -7
  23. package/examples/shorts/box2dPool.js +108 -0
  24. package/examples/shorts/cameraDrag.js +22 -0
  25. package/examples/shorts/debugDraw.js +37 -0
  26. package/examples/shorts/flappyGame.js +1 -0
  27. package/examples/shorts/fps.js +90 -0
  28. package/examples/shorts/helloWorld.js +1 -1
  29. package/examples/shorts/hillGlideGame.js +1 -3
  30. package/examples/shorts/input.js +59 -0
  31. package/examples/shorts/landerGame.js +1 -3
  32. package/examples/shorts/medals.js +15 -9
  33. package/examples/shorts/music.js +15 -17
  34. package/examples/shorts/musicPlayer.js +24 -24
  35. package/examples/shorts/platformer.js +0 -2
  36. package/examples/shorts/postProcess.js +1 -1
  37. package/examples/shorts/sequencer.js +3 -3
  38. package/examples/shorts/slidingPuzzle.js +1 -1
  39. package/examples/shorts/spaceGame.js +1 -3
  40. package/examples/shorts/spriteAtlas.js +6 -8
  41. package/examples/shorts/starfield.js +1 -1
  42. package/examples/shorts/tileRaycast.js +39 -0
  43. package/examples/shorts/tiles.png +0 -0
  44. package/examples/shorts/tiltedView.js +1 -4
  45. package/examples/shorts/timers.js +1 -2
  46. package/examples/shorts/topDown.js +0 -2
  47. package/examples/starter/game.js +5 -7
  48. package/examples/starter/index.html +2 -2
  49. package/examples/typescript/game.js +3 -2
  50. package/examples/typescript/game.ts +5 -7
  51. package/examples/uiSystem/game.js +2 -2
  52. package/package.json +1 -1
  53. package/plugins/box2d.js +170 -50
  54. package/plugins/pluginExport.js +2 -0
  55. package/plugins/uiSystem.js +55 -27
  56. package/src/engine.js +19 -16
  57. package/src/engineAudio.js +27 -15
  58. package/src/engineDebug.js +64 -0
  59. package/src/engineDraw.js +88 -32
  60. package/src/engineExport.js +16 -0
  61. package/src/engineInput.js +35 -25
  62. package/src/engineMedals.js +2 -2
  63. package/src/engineObject.js +55 -10
  64. package/src/engineParticles.js +36 -20
  65. package/src/engineRelease.js +2 -1
  66. package/src/engineSettings.js +20 -1
  67. package/src/engineTileLayer.js +64 -59
  68. package/src/engineUtilities.js +213 -59
  69. package/src/engineWebGL.js +13 -8
  70. package/examples/shorts/raycasting.js +0 -79
@@ -119,6 +119,11 @@ let mouseDeltaScreen = vec2();
119
119
  * @memberof Input */
120
120
  let mouseWheel = 0;
121
121
 
122
+ /** True if mouse was inside the document window, set to false when mouse leaves
123
+ * @type {boolean}
124
+ * @memberof Input */
125
+ let mouseInWindow = true;
126
+
122
127
  /** Returns true if user is using gamepad (has more recently pressed a gamepad button)
123
128
  * @type {boolean}
124
129
  * @memberof Input */
@@ -182,9 +187,9 @@ function inputUpdate()
182
187
  if(!(touchInputEnable && isTouchDevice) && !document.hasFocus())
183
188
  inputClear();
184
189
 
185
- // update mouse world space position
190
+ // update mouse world space position and delta
186
191
  mousePos = screenToWorld(mousePosScreen);
187
- mouseDelta = mouseDeltaScreen.multiply(vec2(1,-1)).rotate(-cameraAngle);
192
+ mouseDelta = screenToWorldDelta(mouseDeltaScreen);
188
193
 
189
194
  // update gamepads if enabled
190
195
  gamepadsUpdate();
@@ -213,10 +218,10 @@ function inputInit()
213
218
  document.addEventListener('mousedown', onMouseDown);
214
219
  document.addEventListener('mouseup', onMouseUp);
215
220
  document.addEventListener('mousemove', onMouseMove);
221
+ document.addEventListener('mouseleave', onMouseLeave);
216
222
  document.addEventListener('wheel', onMouseWheel);
217
223
  document.addEventListener('contextmenu', onContextMenu);
218
224
  document.addEventListener('blur', onBlur);
219
- document.addEventListener('mouseleave', onMouseLeave);
220
225
 
221
226
  // init touch input
222
227
  if (isTouchDevice && touchInputEnable)
@@ -238,14 +243,14 @@ function inputInit()
238
243
  if (inputWASDEmulateDirection)
239
244
  inputData[0][remapKey(e.code)] = 4;
240
245
  }
241
- function remapKey(c)
246
+ function remapKey(k)
242
247
  {
243
248
  // handle remapping wasd keys to directions
244
249
  return inputWASDEmulateDirection ?
245
- c === 'KeyW' ? 'ArrowUp' :
246
- c === 'KeyS' ? 'ArrowDown' :
247
- c === 'KeyA' ? 'ArrowLeft' :
248
- c === 'KeyD' ? 'ArrowRight' : c : c;
250
+ k === 'KeyW' ? 'ArrowUp' :
251
+ k === 'KeyS' ? 'ArrowDown' :
252
+ k === 'KeyA' ? 'ArrowLeft' :
253
+ k === 'KeyD' ? 'ArrowRight' : k : k;
249
254
  }
250
255
  function onMouseDown(e)
251
256
  {
@@ -258,7 +263,10 @@ function inputInit()
258
263
 
259
264
  isUsingGamepad = false;
260
265
  inputData[0][e.button] = 3;
266
+
267
+ const mousePosScreenLast = mousePosScreen;
261
268
  mousePosScreen = mouseEventToScreen(vec2(e.x,e.y));
269
+ mouseDeltaScreen = mouseDeltaScreen.add(mousePosScreen.subtract(mousePosScreenLast));
262
270
  inputPreventDefault && e.button && e.preventDefault();
263
271
  }
264
272
  function onMouseUp(e)
@@ -269,18 +277,20 @@ function inputInit()
269
277
  }
270
278
  function onMouseMove(e)
271
279
  {
280
+ mouseInWindow = true;
281
+ const mousePosScreenLast = mousePosScreen;
272
282
  mousePosScreen = mouseEventToScreen(vec2(e.x,e.y));
273
- mouseDeltaScreen = mouseDeltaScreen.add(vec2(e.movementX, e.movementY));
283
+
284
+ // when pointer is locked use movementX/Y for delta
285
+ const movement = pointerLockIsActive() ?
286
+ vec2(e.movementX, e.movementY) :
287
+ mousePosScreen.subtract(mousePosScreenLast);
288
+ mouseDeltaScreen = mouseDeltaScreen.add(movement);
274
289
  }
290
+ function onMouseLeave() { mouseInWindow = false; } // mouse moved off window
275
291
  function onMouseWheel(e) { mouseWheel = e.ctrlKey ? 0 : sign(e.deltaY); }
276
292
  function onContextMenu(e) { e.preventDefault(); } // prevent right click menu
277
293
  function onBlur() { inputClear(); } // reset input when focus is lost
278
- function onMouseLeave()
279
- {
280
- // set mouse position and delta when leaving canvas
281
- mousePosScreen = vec2(-1);
282
- mouseDeltaScreen = vec2(0);
283
- }
284
294
  }
285
295
 
286
296
  // convert a mouse or touch event position to screen space
@@ -324,8 +334,8 @@ function gamepadsUpdate()
324
334
  else if (touchGamepadStick.lengthSquared() > .3)
325
335
  {
326
336
  // convert to 8 way dpad
327
- sticks[0].x = Math.round(touchGamepadStick.x);
328
- sticks[0].y = -Math.round(touchGamepadStick.y);
337
+ sticks[0].x = round(touchGamepadStick.x);
338
+ sticks[0].y = -round(touchGamepadStick.y);
329
339
  sticks[0] = sticks[0].clampLength();
330
340
  }
331
341
 
@@ -416,7 +426,7 @@ let touchGamepadTimer = new Timer, touchGamepadButtons = [], touchGamepadStick =
416
426
  function touchGamepadButtonCenter()
417
427
  {
418
428
  // draw right face buttons
419
- let center = vec2(mainCanvasSize.x-touchGamepadSize, mainCanvasSize.y-touchGamepadSize);
429
+ const center = vec2(mainCanvasSize.x-touchGamepadSize, mainCanvasSize.y-touchGamepadSize);
420
430
  if (touchGamepadButtonCount <= 2)
421
431
  center.x += touchGamepadSize/2;
422
432
  return center;
@@ -426,14 +436,13 @@ function touchGamepadButtonCenter()
426
436
  function touchInputInit()
427
437
  {
428
438
  // add non passive touch event listeners
429
- let handleTouch = handleTouchDefault;
430
439
  document.addEventListener('touchstart', (e) => handleTouch(e), { passive: false });
431
440
  document.addEventListener('touchmove', (e) => handleTouch(e), { passive: false });
432
441
  document.addEventListener('touchend', (e) => handleTouch(e), { passive: false });
433
442
 
434
443
  // handle all touch events the same way
435
444
  let wasTouching;
436
- function handleTouchDefault(e)
445
+ function handleTouch(e)
437
446
  {
438
447
  if (!touchInputEnable)
439
448
  return;
@@ -453,11 +462,11 @@ function touchInputInit()
453
462
  {
454
463
  // set event pos and pass it along
455
464
  const pos = vec2(e.touches[0].clientX, e.touches[0].clientY);
456
- const lastMousePosScreen = mousePosScreen;
465
+ const mousePosScreenLast = mousePosScreen;
457
466
  mousePosScreen = mouseEventToScreen(pos);
458
467
  if (wasTouching)
459
468
  {
460
- mouseDeltaScreen = mouseDeltaScreen.add(mousePosScreen.subtract(lastMousePosScreen));
469
+ mouseDeltaScreen = mouseDeltaScreen.add(mousePosScreen.subtract(mousePosScreenLast));
461
470
  isUsingGamepad = touchGamepadEnable;
462
471
  }
463
472
  else
@@ -489,7 +498,7 @@ function touchInputInit()
489
498
  if (touching)
490
499
  {
491
500
  touchGamepadTimer.set();
492
- if (paused && !wasTouching)
501
+ if (touchGamepadCenterButton && !wasTouching && paused)
493
502
  {
494
503
  // touch anywhere to press start when paused
495
504
  touchGamepadButtons[9] = 1;
@@ -528,7 +537,8 @@ function touchInputInit()
528
537
  if (button < touchGamepadButtonCount)
529
538
  touchGamepadButtons[button] = 1;
530
539
  }
531
- else if (startCenter.distance(touchPos) < touchGamepadSize && !wasTouching)
540
+ else if (touchGamepadCenterButton && !wasTouching &&
541
+ startCenter.distance(touchPos) < touchGamepadSize)
532
542
  {
533
543
  // virtual start button in center
534
544
  touchGamepadButtons[9] = 1;
@@ -583,7 +593,7 @@ function touchGamepadRender()
583
593
  const buttonSize = touchGamepadButtonCount > 1 ? touchGamepadSize/4 : touchGamepadSize/2;
584
594
  for (let i=0; i<touchGamepadButtonCount; i++)
585
595
  {
586
- let j = mod(i-1, 4);
596
+ const j = mod(i-1, 4);
587
597
  let button = touchGamepadButtonCount > 2 ?
588
598
  j : min(j, touchGamepadButtonCount-1);
589
599
  // fix button locations (swap 2 and 3 to match gamepad layout)
@@ -167,11 +167,11 @@ class Medal
167
167
  const descriptionSize = height*.3;
168
168
  const pos = vec2(x + medalDisplayIconSize + 2*gap.x, y + gap.y*2 + nameSize/2);
169
169
  const textWidth = width - medalDisplayIconSize - 3*gap.x;
170
- drawTextScreen(this.name, pos, nameSize, BLACK, 0, undefined, 'left', undefined, textWidth);
170
+ drawTextScreen(this.name, pos, nameSize, BLACK, 0, undefined, 'left', undefined, undefined, textWidth);
171
171
 
172
172
  // draw the description
173
173
  pos.y = y + height - gap.y*2 - descriptionSize/2;
174
- drawTextScreen(this.description, pos, descriptionSize, BLACK, 0, undefined, 'left', undefined, textWidth);
174
+ drawTextScreen(this.description, pos, descriptionSize, BLACK, 0, undefined, 'left', undefined, undefined, textWidth);
175
175
  context.restore();
176
176
  }
177
177
 
@@ -135,11 +135,10 @@ class EngineObject
135
135
  }
136
136
 
137
137
  /** Update the object physics, called automatically by engine once each frame */
138
- update()
138
+ updatePhysics()
139
139
  {
140
140
  // child objects do not have physics
141
- if (this.parent)
142
- return;
141
+ ASSERT(!this.parent);
143
142
 
144
143
  if (this.clampSpeed)
145
144
  {
@@ -189,7 +188,7 @@ class EngineObject
189
188
  continue;
190
189
 
191
190
  // check collision
192
- if (!isOverlapping(this.pos, this.size, o.pos, o.size))
191
+ if (!this.isOverlappingObject(o))
193
192
  continue;
194
193
 
195
194
  // notify objects of collision and check if should be resolved
@@ -287,6 +286,27 @@ class EngineObject
287
286
  // test which side we bounced off (or both if a corner)
288
287
  const blockedLayerY = tileCollisionTest(vec2(oldPos.x, this.pos.y), this.size, this);
289
288
  const blockedLayerX = tileCollisionTest(vec2(this.pos.x, oldPos.y), this.size, this);
289
+
290
+ if (blockedLayerX)
291
+ {
292
+ // try to move up a tiny bit
293
+ const epsilon = 1e-3;
294
+ const maxMoveUp = .1;
295
+ const y = floor(oldPos.y-this.size.y/2+1) +
296
+ this.size.y/2 + epsilon;
297
+ const delta = y - this.pos.y;
298
+ if (delta < maxMoveUp)
299
+ if (!tileCollisionTest(vec2(this.pos.x, y), this.size, this))
300
+ {
301
+ this.pos.y = y;
302
+ debugPhysics && debugRect(this.pos, this.size, '#ff0');
303
+ return;
304
+ }
305
+
306
+ // move to previous position and bounce
307
+ this.pos.x = oldPos.x;
308
+ this.velocity.x *= -this.restitution;
309
+ }
290
310
  if (blockedLayerY || !blockedLayerX)
291
311
  {
292
312
  // bounce velocity
@@ -310,18 +330,15 @@ class EngineObject
310
330
  this.groundObject = undefined;
311
331
  }
312
332
  }
313
- if (blockedLayerX)
314
- {
315
- // move to previous position and bounce
316
- this.pos.x = oldPos.x;
317
- this.velocity.x *= -this.restitution;
318
- }
319
333
  debugPhysics && debugRect(this.pos, this.size, '#f00');
320
334
  }
321
335
  }
322
336
  }
323
337
  }
324
338
 
339
+ /** Update the object, called automatically by engine once each frame. Does nothing by default. */
340
+ update() {}
341
+
325
342
  /** Render the object, draws a tile by default, automatically called each frame, sorted by renderOrder */
326
343
  render()
327
344
  {
@@ -373,10 +390,24 @@ class EngineObject
373
390
  */
374
391
  collideWithObject(object) { return true; }
375
392
 
393
+ /** Get this object's up vector
394
+ * @param {number} [scale] - length of the vector
395
+ * @return {Vector2} */
396
+ getUp(scale=1) { return vec2().setAngle(this.angle, scale); }
397
+
398
+ /** Get this object's right vector
399
+ * @param {number} [scale] - length of the vector
400
+ * @return {Vector2} */
401
+ getRight(scale=1) { return vec2().setAngle(this.angle+PI/2, scale); }
402
+
376
403
  /** How long since the object was created
377
404
  * @return {number} */
378
405
  getAliveTime() { return time - this.spawnTime; }
379
406
 
407
+ /** Get the speed of this object
408
+ * @return {number} */
409
+ getSpeed() { return this.velocity.length(); }
410
+
380
411
  /** Apply acceleration to this object (adjust velocity, not affected by mass)
381
412
  * @param {Vector2} acceleration */
382
413
  applyAcceleration(acceleration)
@@ -421,6 +452,20 @@ class EngineObject
421
452
  child.parent = 0;
422
453
  }
423
454
 
455
+ /** Check if overlapping another engine object
456
+ * Collisions are resoloved to prevent overlaps
457
+ * @param {EngineObject} object
458
+ * @return {boolean} */
459
+ isOverlappingObject(object)
460
+ { return this.isOverlapping(object.pos, object.size); }
461
+
462
+ /** Check if overlapping a point or aligned bounding box
463
+ * @param {Vector2} pos - Center of box
464
+ * @param {Vector2} [size=(0,0)] - Size of box, uses a point if undefined
465
+ * @return {boolean} */
466
+ isOverlapping(pos, size=vec2())
467
+ { return isOverlapping(this.pos, this.size, pos, size); }
468
+
424
469
  /** Set how this object collides
425
470
  * @param {boolean} [collideSolidObjects] - Does it collide with solid objects?
426
471
  * @param {boolean} [isSolid] - Does it collide with and block other objects? (expensive in large numbers)
@@ -152,19 +152,34 @@ class ParticleEmitter extends EngineObject
152
152
  this.particleCreateCallback = undefined;
153
153
  /** @property {number} - Track particle emit time */
154
154
  this.emitTimeBuffer = 0;
155
+ /** @property {number} - Percentage of velocity to pass to particles (0-1) */
156
+ this.velocityInheritance = 0;
157
+
158
+ // track previous position and angle
159
+ this.previousAngle = this.angle;
160
+ this.previousPos = this.pos.copy();
155
161
  }
156
162
 
157
163
  /** Update the emitter to spawn particles, called automatically by engine once each frame */
158
164
  update()
159
165
  {
160
- // only do default update to apply parent transforms
161
- this.parent && super.update();
166
+ if (this.velocityInheritance)
167
+ {
168
+ // pass emitter velocity to particles
169
+ const p = this.velocityInheritance;
170
+ this.velocity.x = p * (this.pos.x - this.previousPos.x);
171
+ this.velocity.y = p * (this.pos.y - this.previousPos.y);
172
+ this.angleVelocity = p * (this.angle - this.previousAngle);
173
+ this.previousAngle = this.angle;
174
+ this.previousPos.x = this.pos.x;
175
+ this.previousPos.y = this.pos.y;
176
+ }
162
177
 
163
178
  // update emitter
164
179
  if (!this.emitTime || this.getAliveTime() <= this.emitTime)
165
180
  {
166
181
  // emit particles
167
- if (this.emitRate * particleEmitRateScale)
182
+ if (this.emitRate && particleEmitRateScale)
168
183
  {
169
184
  const rate = 1/this.emitRate/particleEmitRateScale;
170
185
  for (this.emitTimeBuffer += timeDelta; this.emitTimeBuffer > 0; this.emitTimeBuffer -= rate)
@@ -217,7 +232,7 @@ class ParticleEmitter extends EngineObject
217
232
  const particle = new Particle(pos, this.tileInfo, angle, colorStart, colorEnd, particleTime, sizeStart, sizeEnd, this.fadeRate, this.additive, this.trailScale, this.localSpace && this, this.particleDestroyCallback);
218
233
  particle.velocity = vec2().setAngle(velocityAngle, speed);
219
234
  particle.angleVelocity = angleSpeed;
220
- if (!this.localSpace)
235
+ if (!this.localSpace && this.velocityInheritance > 0)
221
236
  {
222
237
  // apply emitter velocity to particle
223
238
  particle.velocity.x += this.velocity.x;
@@ -302,8 +317,6 @@ class Particle extends EngineObject
302
317
  /** Update the object physics, called automatically by engine once each frame */
303
318
  update()
304
319
  {
305
- super.update();
306
-
307
320
  if (this.collideTiles || this.collideSolidObjects)
308
321
  {
309
322
  // only apply max circular speed if particle can collide
@@ -315,6 +328,16 @@ class Particle extends EngineObject
315
328
  this.velocity.y *= s;
316
329
  }
317
330
  }
331
+
332
+ if (this.lifeTime > 0 && time - this.spawnTime > this.lifeTime)
333
+ {
334
+ // destroy particle when its time runs out
335
+ const c = this.colorEnd;
336
+ this.color.set(c.r, c.g, c.b, c.a);
337
+ this.size.set(this.sizeEnd, this.sizeEnd);
338
+ this.destroyCallback && this.destroyCallback(this);
339
+ this.destroyed = 1;
340
+ }
318
341
  }
319
342
 
320
343
  /** Render the particle, automatically called each frame, sorted by renderOrder */
@@ -323,7 +346,7 @@ class Particle extends EngineObject
323
346
  // lerp color and size
324
347
  const p1 = this.lifeTime > 0 ? min((time - this.spawnTime) / this.lifeTime, 1) : 1, p2 = 1-p1;
325
348
  const radius = p2 * this.sizeStart + p1 * this.sizeEnd;
326
- this.size.x = this.size.y = radius;
349
+ const size = vec2(radius);
327
350
  this.color.r = p2 * this.colorStart.r + p1 * this.colorEnd.r;
328
351
  this.color.g = p2 * this.colorStart.g + p1 * this.colorEnd.g;
329
352
  this.color.b = p2 * this.colorStart.b + p1 * this.colorEnd.b;
@@ -343,7 +366,7 @@ class Particle extends EngineObject
343
366
  {
344
367
  // in local space of emitter
345
368
  const a = this.localSpaceEmitter.angle;
346
- const c = Math.cos(a), s = Math.sin(a);
369
+ const c = cos(a), s = sin(a);
347
370
  pos = this.localSpaceEmitter.pos.add(
348
371
  new Vector2(pos.x*c - pos.y*s, pos.x*s + pos.y*c));
349
372
  angle += this.localSpaceEmitter.angle;
@@ -359,21 +382,14 @@ class Particle extends EngineObject
359
382
  {
360
383
  // stretch in direction of motion
361
384
  const trailLength = speed * this.trailScale;
362
- this.size.y = max(this.size.x, trailLength);
363
- angle = Math.atan2(direction.x, direction.y);
364
- drawTile(pos, this.size, this.tileInfo, this.color, angle, this.mirror);
385
+ size.y = max(size.x, trailLength);
386
+ angle = atan2(direction.x, direction.y);
387
+ drawTile(pos, size, this.tileInfo, this.color, angle, this.mirror);
365
388
  }
366
389
  }
367
390
  else
368
- drawTile(pos, this.size, this.tileInfo, this.color, angle, this.mirror);
391
+ drawTile(pos, size, this.tileInfo, this.color, angle, this.mirror);
369
392
  this.additive && setBlendMode();
370
- debugParticles && debugRect(pos, this.size, '#f005', 0, angle);
371
-
372
- if (p1 === 1)
373
- {
374
- // destroy particle when its time runs out
375
- this.destroyCallback && this.destroyCallback(this);
376
- this.destroyed = 1;
377
- }
393
+ debugParticles && debugRect(pos, size, '#f005', 0, angle);
378
394
  }
379
395
  }
@@ -38,4 +38,5 @@ function debugShowErrors(){}
38
38
  function debugVideoCaptureIsActive(){ return false; }
39
39
  function debugVideoCaptureStart (){}
40
40
  function debugVideoCaptureStop (){}
41
- function debugVideoCaptureUpdate(){}
41
+ function debugVideoCaptureUpdate(){}
42
+ function debugProtectConstant(o){ return o; }
@@ -216,6 +216,14 @@ let touchInputEnable = true;
216
216
  * @memberof Settings */
217
217
  let touchGamepadEnable = false;
218
218
 
219
+ /** True if touch gamepad should have start button in the center
220
+ * - When the game is paused, any touch will press the button
221
+ * - This can function as a way to pause/unpause the game
222
+ * @type {boolean}
223
+ * @default
224
+ * @memberof Settings */
225
+ let touchGamepadCenterButton = true;
226
+
219
227
  /** True if touch gamepad should be analog stick or false to use if 8 way dpad
220
228
  * @type {boolean}
221
229
  * @default
@@ -391,9 +399,14 @@ function setHeadlessMode(headless) { headlessMode = headless; }
391
399
  * @memberof Settings */
392
400
  function setGLEnable(enable)
393
401
  {
402
+ if (enable && !glCanBeEnabled)
403
+ {
404
+ console.warn('Can not enable WebGL if it was disabled on start.');
405
+ return;
406
+ }
394
407
  glEnable = enable;
395
408
  if (glCanvas) // hide glCanvas if WebGL is disabled
396
- glCanvas.style.visibility = enable ? 'visible' : 'hidden';
409
+ glCanvas.style.display = enable ? '' : 'none';
397
410
  }
398
411
 
399
412
  /** Set how many sided polygons to use when drawing circles and ellipses with WebGL
@@ -481,6 +494,12 @@ function setTouchInputEnable(enable) { touchInputEnable = enable; }
481
494
  * @memberof Settings */
482
495
  function setTouchGamepadEnable(enable) { touchGamepadEnable = enable; }
483
496
 
497
+ /** True if touch gamepad should have start button in the center
498
+ * - This can function as a way to pause/unpause the game
499
+ * @param {boolean} enable
500
+ * @memberof Settings */
501
+ function setTouchGamepadCenterButton(enable) { touchGamepadCenterButton = enable; }
502
+
484
503
  /** Set if touch gamepad should be analog stick or 8 way dpad
485
504
  * @param {boolean} analog
486
505
  * @memberof Settings */
@@ -47,23 +47,23 @@ function tileCollisionTest(pos, size=vec2(), object, solidOnly=true)
47
47
  }
48
48
  }
49
49
 
50
- /** Return the center of first tile hit, undefined if nothing was hit.
51
- * This does not return the exact intersection, but the center of the tile hit.
50
+ /** Return the exact position of the boudnary of first tile hit, undefined if nothing was hit.
51
+ * The point will be inside the colliding tile if it hits (may have a tiny shift)
52
52
  * @param {Vector2} posStart
53
53
  * @param {Vector2} posEnd
54
54
  * @param {EngineObject} [object] - An object or undefined for generic test
55
+ * @param {Vector2} [normal] - Optional normal of the surface hit
55
56
  * @param {boolean} [solidOnly=true] - Only check solid layers if true
56
- * @return {Vector2}
57
+ * @return {Vector2|undefined} - position of the center of the tile hit or undefined if no hit
57
58
  * @memberof TileLayers */
58
- function tileCollisionRaycast(posStart, posEnd, object, solidOnly=true)
59
+ function tileCollisionRaycast(posStart, posEnd, object, normal, solidOnly=true)
59
60
  {
60
61
  for (const layer of tileCollisionLayers)
61
62
  {
62
63
  if (!solidOnly || layer.isSolid)
63
64
  {
64
- const hitPos = layer.collisionRaycast(posStart, posEnd, object)
65
- if (hitPos)
66
- return hitPos;
65
+ const hitPos = layer.collisionRaycast(posStart, posEnd, object, normal)
66
+ if (hitPos) return hitPos;
67
67
  }
68
68
  }
69
69
  }
@@ -201,6 +201,8 @@ class CanvasLayer extends EngineObject
201
201
  /** @property {TextureInfo} - Texture info to use for this object rendering */
202
202
  const useWebGL = false; // do not use webgl by default
203
203
  this.textureInfo = new TextureInfo(this.canvas, useWebGL);
204
+ /** @property {boolean} - True if WebGL texture needs to be refreshed */
205
+ this.refreshWebGL = false;
204
206
 
205
207
  // disable physics by default
206
208
  this.mass = this.gravityScale = this.friction = this.restitution = 0;
@@ -234,8 +236,15 @@ class CanvasLayer extends EngineObject
234
236
  * @memberof Draw */
235
237
  draw(pos, size, angle=0, color=WHITE, mirror=false, additiveColor, screenSpace=false, context)
236
238
  {
237
- // draw the canvas layer as a single tile that uses the whole texture
238
239
  const useWebGL = glEnable && this.textureInfo.hasWebGL();
240
+ if (useWebGL && this.refreshWebGL)
241
+ {
242
+ // update the WebGL texture
243
+ this.textureInfo.createWebGLTexture();
244
+ this.refreshWebGL = false;
245
+ }
246
+
247
+ // draw the canvas layer as a single tile that uses the whole texture
239
248
  const tileInfo = new TileInfo().setFullImage(this.textureInfo);
240
249
  drawTile(pos, size, tileInfo, color, angle, mirror, additiveColor, useWebGL, screenSpace, context);
241
250
  }
@@ -305,9 +314,18 @@ class CanvasLayer extends EngineObject
305
314
  { this.drawTile(pos, size, undefined, color, angle); }
306
315
 
307
316
  /** Create or update the WebGL texture for this layer
308
- * @param {boolean} [enable] - enable WebGL rendering and update the texture */
309
- useWebGL(enable=true)
317
+ * @param {boolean} [enable] - enable WebGL rendering and update the texture
318
+ * @param {boolean} [immediate] - shoulkd the texture be updated immediately
319
+ */
320
+ useWebGL(enable=true, immediate=false)
310
321
  {
322
+ if (!immediate && enable && this.textureInfo.hasWebGL())
323
+ {
324
+ // refresh the texture when needed
325
+ this.refreshWebGL = true;
326
+ return;
327
+ }
328
+
311
329
  if (enable)
312
330
  this.textureInfo.createWebGLTexture();
313
331
  else
@@ -338,7 +356,7 @@ class TileLayer extends CanvasLayer
338
356
  */
339
357
  constructor(position, size, tileInfo=tile(), renderOrder=0)
340
358
  {
341
- const canvasSize = size.multiply(tileInfo.size);
359
+ const canvasSize = tileInfo ? size.multiply(tileInfo.size) : size;
342
360
  super(position, size, 0, renderOrder, canvasSize);
343
361
 
344
362
  // set tile info
@@ -391,6 +409,13 @@ class TileLayer extends CanvasLayer
391
409
  {
392
410
  ASSERT(drawContext !== this.context, 'must call redrawEnd() after drawing tiles!');
393
411
 
412
+ if (this.refreshWebGL)
413
+ {
414
+ // update the WebGL texture
415
+ this.textureInfo.createWebGLTexture();
416
+ this.refreshWebGL = false;
417
+ }
418
+
394
419
  // draw the tile layer as a single tile
395
420
  const tileInfo = new TileInfo().setFullImage(this.textureInfo);
396
421
  const size = this.drawSize || this.size;
@@ -427,8 +452,9 @@ class TileLayer extends CanvasLayer
427
452
  drawCanvas = this.canvas;
428
453
  drawContext = this.context;
429
454
  cameraPos = this.size.scale(.5);
430
- cameraScale = this.tileInfo.size.x;
431
- mainCanvasSize = this.size.multiply(this.tileInfo.size);
455
+ const tileSize = this.tileInfo ? this.tileInfo.size : vec2(1);
456
+ cameraScale = tileSize.x;
457
+ mainCanvasSize = this.size.multiply(tileSize);
432
458
  if (clear)
433
459
  {
434
460
  // clear and set size
@@ -588,65 +614,44 @@ class TileCollisionLayer extends TileLayer
588
614
  // check if the object should collide with this tile
589
615
  const tileData = this.collisionData[y*this.size.x+x];
590
616
  if (tileData)
591
- if (!object || object.collideWithTile(tileData, hitPos.set(x, y)))
617
+ if (!object || object.collideWithTile(tileData,
618
+ hitPos.set(x + this.pos.x, y + this.pos.y)))
592
619
  return true;
593
620
  }
594
621
  return false;
595
622
  }
596
623
 
597
- /** Return the center of first tile hit, undefined if nothing was hit.
598
- * This does not return the exact intersection, but the center of the tile hit.
624
+ /** Return the exact position of the boudnary of first tile hit, undefined if nothing was hit.
625
+ * The point will be inside the colliding tile if it hits (may have a tiny shift)
599
626
  * @param {Vector2} posStart
600
627
  * @param {Vector2} posEnd
601
- * @param {EngineObject} [object]
602
- * @return {Vector2} */
603
- collisionRaycast(posStart, posEnd, object)
628
+ * @param {EngineObject} [object] - An object or undefined for generic test
629
+ * @param {Vector2} [normal] - Optional normal of the surface hit
630
+ * @return {Vector2|undefined} */
631
+ collisionRaycast(posStart, posEnd, object, normal)
604
632
  {
605
633
  ASSERT(isVector2(posStart) && isVector2(posEnd), 'positions must be Vector2s');
606
634
  ASSERT(!object || object instanceof EngineObject, 'object must be an EngineObject');
607
-
608
- // transform to local layer space
609
- const posStartX = posStart.x - this.pos.x;
610
- const posStartY = posStart.y - this.pos.y;
611
- const posEndX = posEnd.x - this.pos.x;
612
- const posEndY = posEnd.y - this.pos.y;
613
-
614
- // test if a ray collides with tiles from start to end
615
- const deltaX = posEndX - posStartX;
616
- const deltaY = posEndY - posStartY;
617
- const totalLength = (deltaX**2 + deltaY**2)**.5;
618
- const unitX = abs(totalLength/deltaX);
619
- const unitY = abs(totalLength/deltaY);
620
-
621
- // setup iteration variables
622
- const pos = posStart.floor(), signDeltaX = sign(deltaX), signDeltaY = sign(deltaY);
623
- let xi = unitX * (deltaX < 0 ? posStart.x - pos.x : pos.x - posStart.x + 1) || 0;
624
- let yi = unitY * (deltaY < 0 ? posStart.y - pos.y : pos.y - posStart.y + 1) || 0;
625
-
626
- // use line drawing algorithm to test for collisions
627
- while (true)
635
+
636
+ const localPos = new Vector2;
637
+ const collisionTest = (pos)=>
628
638
  {
629
639
  // check for tile collision
630
- const tileData = this.getCollisionData(pos);
631
- if (tileData && (!object || object.collideWithTile(tileData, pos)))
632
- {
633
- pos.x += .5; pos.y += .5;
634
- debugRaycast && debugLine(posStart, posEnd, '#f00', .02);
635
- debugRaycast && debugPoint(pos, '#ff0');
636
- return pos;
637
- }
638
-
639
- // check if past the end
640
- if (xi >= totalLength && yi >= totalLength)
641
- break;
642
-
643
- // get coordinates of next tile to check
644
- if (xi > yi)
645
- pos.y += signDeltaY, yi += unitY;
646
- else
647
- pos.x += signDeltaX, xi += unitX;
640
+ localPos.set(pos.x - this.pos.x, pos.y - this.pos.y);
641
+ const tileData = this.getCollisionData(localPos);
642
+ return tileData && (!object || object.collideWithTile(tileData, pos));
648
643
  }
649
-
650
644
  debugRaycast && debugLine(posStart, posEnd, '#00f', .02);
645
+ const hitPos = lineTest(posStart, posEnd, collisionTest, normal);
646
+ if (hitPos)
647
+ {
648
+ const tilePos = hitPos.floor().add(vec2(.5));
649
+ debugRaycast && debugRect(tilePos, vec2(1), '#f008');
650
+ debugRaycast && debugLine(posStart, hitPos, '#f00', .02);
651
+ debugRaycast && debugPoint(hitPos, '#0f0');
652
+ debugRaycast && normal &&
653
+ debugLine(hitPos, hitPos.add(normal), '#ff0', .02);
654
+ return hitPos;
655
+ }
651
656
  }
652
657
  }