littlejsengine 1.18.28 → 1.19.3

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.
@@ -1,552 +1,561 @@
1
- /**
2
- * LittleJS Object System
3
- * - EngineObject is the base class for all game objects
4
- * - Handles automatic updating, rendering, physics, and collision
5
- * - Supports parent-child hierarchies with transform inheritance
6
- * - 2D physics with velocity, acceleration, damping, and gravity
7
- * - Collision system with tiles and other objects
8
- * - Renders sprites from tile sheets with color and rotation
9
- * - Objects sorted by renderOrder for layered rendering
10
- */
11
-
12
- 'use strict';
13
-
14
- /**
15
- * LittleJS Object Base Object Class
16
- * - Top level object class used by the engine
17
- * - Automatically adds self to object list
18
- * - Will be updated and rendered each frame
19
- * - Renders as a sprite from a tilesheet by default
20
- * - Can have color and additive color applied
21
- * - 2D Physics and collision system
22
- * - Sorted by renderOrder
23
- * - Objects can have children attached
24
- * - Parents are updated before children, and set child transform
25
- * - Call destroy() to get rid of objects
26
- *
27
- * The physics system used by objects is simple and fast with some caveats...
28
- * - Collision uses the axis aligned size, the object's rotation angle is only for rendering
29
- * - Objects are guaranteed to not intersect tile collision from physics
30
- * - If an object starts or is moved inside tile collision, it will not collide with that tile
31
- * - Collision for objects can be set to be solid to block other objects
32
- * - Objects may get pushed into overlapping other solid objects, if so they will push away
33
- * - Solid objects are more performance intensive and should be used sparingly
34
- * @memberof Engine
35
- * @example
36
- * // create an engine object, normally you would first extend the class with your own
37
- * const pos = vec2(2,3);
38
- * const object = new EngineObject(pos);
39
- */
40
- class EngineObject
41
- {
42
- /** Create an engine object and adds it to the list of objects
43
- * @param {Vector2} [pos=vec2()] - World space position of the object
44
- * @param {Vector2} [size=vec2(1)] - World space size of the object
45
- * @param {TileInfo} [tileInfo] - Tile info to render object (undefined is untextured)
46
- * @param {number} [angle] - Angle the object is rotated by
47
- * @param {Color} [color=WHITE] - Color to apply to tile when rendered
48
- * @param {number} [renderOrder] - Objects sorted by renderOrder before being rendered
49
- */
50
- constructor(pos=vec2(), size=vec2(1), tileInfo, angle=0, color=WHITE, renderOrder=0)
51
- {
52
- // check passed in params
53
- ASSERT(isVector2(pos), 'object pos must be a vec2');
54
- ASSERT(isVector2(size), 'object size must be a vec2');
55
- ASSERT(!tileInfo || tileInfo instanceof TileInfo, 'object tileInfo should be a TileInfo or undefined');
56
- ASSERT(typeof angle === 'number' && isFinite(angle), 'object angle should be a number');
57
- ASSERT(isColor(color), 'object color should be a valid rgba color');
58
- ASSERT(typeof renderOrder === 'number', 'object renderOrder should be a number');
59
-
60
- /** @property {Vector2} - World space position of the object */
61
- this.pos = pos.copy();
62
- /** @property {Vector2} - World space width and height of the object */
63
- this.size = size.copy();
64
- /** @property {Vector2} - Size of object used for drawing, uses size if not set */
65
- this.drawSize = undefined;
66
- /** @property {TileInfo} - Tile info to render object (undefined is untextured) */
67
- this.tileInfo = tileInfo;
68
- /** @property {number} - Angle to rotate the object */
69
- this.angle = angle;
70
- /** @property {Color} - Color to apply when rendered */
71
- this.color = color.copy();
72
- /** @property {Color} - Additive color to apply when rendered */
73
- this.additiveColor = undefined;
74
- /** @property {boolean} - Should the rendered tile flip along the y axis. Affects rendering and the local→world transform of attached children (a mirrored parent flips its children's localPos.x and localAngle). Does not affect this object's own physics, collision, or localToWorld/worldToLocal. */
75
- this.mirror = false;
76
- /** @property {boolean} - Has object been destroyed? */
77
- this.destroyed = false;
78
-
79
- // physical properties
80
- /** @property {number} - How heavy the object is, static if 0 */
81
- this.mass = objectDefaultMass;
82
- /** @property {number} - How much to slow down velocity each frame (0-1) */
83
- this.damping = objectDefaultDamping;
84
- /** @property {number} - How much to slow down rotation each frame (0-1) */
85
- this.angleDamping = objectDefaultAngleDamping;
86
- /** @property {number} - How bouncy the object is when colliding (0-1) */
87
- this.restitution = objectDefaultRestitution;
88
- /** @property {number} - How much friction to apply when sliding (0-1) */
89
- this.friction = objectDefaultFriction;
90
- /** @property {number} - How much to scale gravity by for this object */
91
- this.gravityScale = 1;
92
- /** @property {number} - Objects are sorted by render order */
93
- this.renderOrder = renderOrder;
94
- /** @property {Vector2} - Velocity of the object */
95
- this.velocity = vec2();
96
- /** @property {number} - Angular velocity of the object */
97
- this.angleVelocity = 0;
98
- /** @property {number} - Track when object was created */
99
- this.spawnTime = time;
100
- /** @property {Array<EngineObject>} - List of children of this object */
101
- this.children = [];
102
- /** @property {boolean} - Limit object speed along x and y axis */
103
- this.clampSpeed = true;
104
- /** @property {EngineObject} - Object we are standing on, if any */
105
- this.groundObject = undefined;
106
-
107
- // parent child system
108
- /** @property {EngineObject} - Parent of object if in local space */
109
- this.parent = undefined;
110
- /** @property {Vector2} - Local position if child */
111
- this.localPos = vec2();
112
- /** @property {number} - Local angle if child */
113
- this.localAngle = 0;
114
-
115
- // collision flags
116
- /** @property {boolean} - Object collides with the tile collision */
117
- this.collideTiles = false;
118
- /** @property {boolean} - Object collides with solid objects */
119
- this.collideSolidObjects = false;
120
- /** @property {boolean} - Object collides with and blocks other objects */
121
- this.isSolid = false;
122
- /** @property {boolean} - Object collides with raycasts */
123
- this.collideRaycast = false;
124
-
125
- // add to list of objects
126
- engineObjects.push(this);
127
- }
128
-
129
- /** Update the object transform, called automatically by engine even when paused */
130
- updateTransforms()
131
- {
132
- const parent = this.parent;
133
- if (parent)
134
- {
135
- // compose with parent transform inline to avoid intermediate vector allocs
136
- const mirror = parent.getMirrorSign();
137
- const lp = this.localPos, pp = parent.pos;
138
- const lx = lp.x*mirror, ly = lp.y, pa = parent.angle;
139
- if (pa)
140
- {
141
- const c = cos(-pa), s = sin(-pa);
142
- this.pos.set(lx*c - ly*s + pp.x, lx*s + ly*c + pp.y);
143
- }
144
- else
145
- this.pos.set(lx + pp.x, ly + pp.y);
146
- this.angle = mirror*this.localAngle + pa;
147
- }
148
-
149
- // update children
150
- for (const child of this.children)
151
- child.updateTransforms();
152
- }
153
-
154
- /** Update the object physics, called automatically by engine once each frame. Can be overridden to stop or change how physics works for an object. */
155
- updatePhysics()
156
- {
157
- // child objects do not have physics
158
- ASSERT(!this.parent);
159
-
160
- // bail if a collision callback destroyed us mid-frame
161
- if (this.destroyed) return;
162
-
163
- if (this.clampSpeed)
164
- {
165
- // limit max speed to prevent missing collisions
166
- this.velocity.x = clamp(this.velocity.x, -objectMaxSpeed, objectMaxSpeed);
167
- this.velocity.y = clamp(this.velocity.y, -objectMaxSpeed, objectMaxSpeed);
168
- }
169
-
170
- // apply physics
171
- const oldPos = this.pos.copy();
172
- this.velocity.x *= this.damping;
173
- this.velocity.y *= this.damping;
174
- if (this.mass)
175
- {
176
- // apply gravity only if it has mass
177
- this.velocity.x += gravity.x * this.gravityScale;
178
- this.velocity.y += gravity.y * this.gravityScale;
179
- }
180
- this.pos.x += this.velocity.x;
181
- this.pos.y += this.velocity.y;
182
- this.angle += this.angleVelocity *= this.angleDamping;
183
-
184
- // physics sanity checks
185
- ASSERT(this.angleDamping >= 0 && this.angleDamping <= 1);
186
- ASSERT(this.damping >= 0 && this.damping <= 1);
187
-
188
- // don't do collision for static objects or if solver disabled
189
- if (!enablePhysicsSolver || !this.mass) return;
190
-
191
- const wasFalling = this.velocity.y < 0 && gravity.y < 0 || this.velocity.y > 0 && gravity.y > 0;
192
- if (this.groundObject)
193
- {
194
- // apply friction in local space of ground object
195
- const friction = max(this.friction, this.groundObject.friction);
196
- const groundSpeed = this.groundObject.velocity.x;
197
- this.velocity.x = groundSpeed + (this.velocity.x - groundSpeed) * friction;
198
- this.groundObject = undefined;
199
- }
200
-
201
- if (this.collideSolidObjects)
202
- {
203
- // check collisions against solid objects
204
- const epsilon = .001; // necessary to push slightly outside of the collision
205
- for (const o of engineObjectsCollide)
206
- {
207
- // skip destroyed, child objects, or self collision
208
- if (o.destroyed || o.parent || o === this) continue;
209
-
210
- // non solid objects don't collide with each other
211
- if (!this.isSolid && !o.isSolid) continue;
212
-
213
- // check collision
214
- if (!this.isOverlappingObject(o)) continue;
215
-
216
- // notify objects of collision and check if should be resolved
217
- const collide1 = this.collideWithObject(o);
218
- const collide2 = o.collideWithObject(this);
219
- if (!collide1 || !collide2) continue;
220
-
221
- if (isOverlapping(oldPos, this.size, o.pos, o.size))
222
- {
223
- // if already was touching, try to push away
224
- const deltaPos = oldPos.subtract(o.pos);
225
- const length = deltaPos.length();
226
- const pushAwayAccel = .001;
227
- const velocity = length < .001 ? vec2(0,1) : deltaPos.scale(pushAwayAccel/length);
228
- this.velocity = this.velocity.add(velocity);
229
- if (o.mass) // push away other object if not fixed
230
- o.velocity = o.velocity.subtract(velocity);
231
-
232
- debugPhysics && debugOverlap(this.pos, this.size, o.pos, o.size, '#f00');
233
- continue;
234
- }
235
-
236
- // check for collision
237
- const sizeBoth = this.size.add(o.size);
238
- const smallStepUp = (oldPos.y - o.pos.y)*2 > sizeBoth.y + gravity.y; // prefer to push up if small delta
239
- const isBlockedX = abs(oldPos.y - o.pos.y)*2 < sizeBoth.y;
240
- const isBlockedY = abs(oldPos.x - o.pos.x)*2 < sizeBoth.x;
241
- const restitution = max(this.restitution, o.restitution);
242
-
243
- if (smallStepUp || isBlockedY || !isBlockedX) // resolve y collision
244
- {
245
- // push outside object collision
246
- this.pos.y = o.pos.y + (sizeBoth.y/2 + epsilon) * sign(oldPos.y - o.pos.y);
247
- if ((o.groundObject && wasFalling) || !o.mass)
248
- {
249
- // set ground object if landed on something
250
- if (wasFalling)
251
- this.groundObject = o;
252
-
253
- // bounce if other object is fixed or grounded
254
- this.velocity.y *= -restitution;
255
- }
256
- else if (o.mass)
257
- {
258
- // inelastic collision
259
- const inelastic = (this.mass * this.velocity.y + o.mass * o.velocity.y) / (this.mass + o.mass);
260
-
261
- // elastic collision
262
- const elastic0 = this.velocity.y * (this.mass - o.mass) / (this.mass + o.mass)
263
- + o.velocity.y * 2 * o.mass / (this.mass + o.mass);
264
- const elastic1 = o.velocity.y * (o.mass - this.mass) / (this.mass + o.mass)
265
- + this.velocity.y * 2 * this.mass / (this.mass + o.mass);
266
-
267
- // lerp between elastic or inelastic based on restitution
268
- this.velocity.y = lerp(inelastic, elastic0, restitution);
269
- o.velocity.y = lerp(inelastic, elastic1, restitution);
270
- }
271
- }
272
- if (!smallStepUp && isBlockedX) // resolve x collision
273
- {
274
- // push outside collision
275
- this.pos.x = o.pos.x + (sizeBoth.x/2 + epsilon) * sign(oldPos.x - o.pos.x);
276
- if (o.mass)
277
- {
278
- // inelastic collision
279
- const inelastic = (this.mass * this.velocity.x + o.mass * o.velocity.x) / (this.mass + o.mass);
280
-
281
- // elastic collision
282
- const elastic0 = this.velocity.x * (this.mass - o.mass) / (this.mass + o.mass)
283
- + o.velocity.x * 2 * o.mass / (this.mass + o.mass);
284
- const elastic1 = o.velocity.x * (o.mass - this.mass) / (this.mass + o.mass)
285
- + this.velocity.x * 2 * this.mass / (this.mass + o.mass);
286
-
287
- // lerp between elastic or inelastic based on restitution
288
- this.velocity.x = lerp(inelastic, elastic0, restitution);
289
- o.velocity.x = lerp(inelastic, elastic1, restitution);
290
- }
291
- else // bounce if other object is fixed
292
- this.velocity.x *= -restitution;
293
- }
294
- debugPhysics && debugOverlap(this.pos, this.size, o.pos, o.size, '#f0f');
295
- }
296
- }
297
- if (this.collideTiles)
298
- {
299
- // check collision against tiles
300
- const hitLayer = tileCollisionTest(this.pos, this.size, this);
301
- if (hitLayer)
302
- {
303
- // if already was stuck in collision, don't do anything
304
- // this should not happen unless something starts in collision
305
- if (!tileCollisionTest(oldPos, this.size, this))
306
- {
307
- // test which side we bounced off (or both if a corner)
308
- const isBlockedX = tileCollisionTest(vec2(this.pos.x, oldPos.y), this.size, this);
309
- const isBlockedY = tileCollisionTest(vec2(oldPos.x, this.pos.y), this.size, this);
310
- const restitution = max(this.restitution, hitLayer.restitution);
311
- if (isBlockedX)
312
- {
313
- // try to step over a 1-tile bump (direction follows gravity sign
314
- // so inverted gravity steps down off a ceiling bump instead of up;
315
- // zero gravity defaults to the normal-gravity step-up direction)
316
- const epsilon = 1e-3;
317
- const maxMove = .1;
318
- const gravitySign = gravity.y > 0 ? -1 : 1;
319
- const y = gravitySign > 0 ?
320
- floor(oldPos.y-this.size.y/2+1) + this.size.y/2 + epsilon :
321
- ceil( oldPos.y+this.size.y/2-1) - this.size.y/2 - epsilon;
322
- const delta = abs(y - this.pos.y);
323
- if (delta < maxMove)
324
- if (!tileCollisionTest(vec2(this.pos.x, y), this.size, this))
325
- {
326
- this.pos.y = y;
327
- debugPhysics && debugRect(this.pos, this.size, '#ff0');
328
- return;
329
- }
330
-
331
- // move to previous X position and bounce
332
- this.pos.x = oldPos.x;
333
- this.velocity.x *= -restitution;
334
- }
335
- if (isBlockedY || !isBlockedX)
336
- {
337
- if (wasFalling)
338
- {
339
- // adjust position to slightly away from nearest tile
340
- // this prevents gap between object and ground
341
- const epsilon = .0001;
342
- const offset = this.size.y/2 + epsilon;
343
- this.pos.y = gravity.y < 0 ?
344
- floor(oldPos.y-this.size.y/2) + offset :
345
- ceil( oldPos.y+this.size.y/2) - offset;
346
-
347
- // set ground object for tile collision
348
- this.groundObject = hitLayer;
349
- }
350
- else
351
- {
352
- // move to previous Y position
353
- this.pos.y = oldPos.y;
354
- this.groundObject = undefined;
355
- }
356
- // bounce velocity
357
- this.velocity.y *= -restitution;
358
- }
359
- debugPhysics && debugRect(this.pos, this.size, '#f00');
360
- }
361
- }
362
- }
363
- }
364
-
365
- /** Update the object, called automatically by engine once each frame. Does nothing by default. */
366
- update() {}
367
-
368
- /** Render the object, draws a tile by default, automatically called each frame, sorted by renderOrder */
369
- render()
370
- {
371
- // default object render
372
- drawTile(this.pos, this.drawSize || this.size, this.tileInfo, this.color, this.angle, this.mirror, this.additiveColor);
373
- }
374
-
375
- /** Optional hook called during the light system plugin's lightmap pass to draw this object's lightmap contribution. Does nothing by default. */
376
- renderLight() {}
377
-
378
- /** Destroy this object, destroy its children, detach its parent, and mark it for removal
379
- * @param {boolean} [immediate] - should attached effects be allowed to die off? */
380
- destroy(immediate=false)
381
- {
382
- if (this.destroyed) return;
383
-
384
- // disconnect from parent and destroy children
385
- this.destroyed = true;
386
- this.parent?.removeChild(this);
387
- for (const child of this.children)
388
- {
389
- child.parent = undefined;
390
- child.destroy(immediate);
391
- }
392
- }
393
-
394
- /** Convert from local space to world space
395
- * @param {Vector2} pos - local space point */
396
- localToWorld(pos) { return this.pos.add(pos.rotate(this.angle)); }
397
-
398
- /** Convert from world space to local space
399
- * @param {Vector2} pos - world space point */
400
- worldToLocal(pos) { return pos.subtract(this.pos).rotate(-this.angle); }
401
-
402
- /** Convert from local space to world space for a vector (rotation only)
403
- * @param {Vector2} vec - local space vector */
404
- localToWorldVector(vec) { return vec.rotate(this.angle); }
405
-
406
- /** Convert from world space to local space for a vector (rotation only)
407
- * @param {Vector2} vec - world space vector */
408
- worldToLocalVector(vec) { return vec.rotate(-this.angle); }
409
-
410
- /** Called to check if a tile collision should be resolved. Return true for physics to resolve the collision or false to ignore and resolve it manually.
411
- * @param {number} tileData - the value of the tile at the position
412
- * @param {Vector2} pos - tile where the collision occurred
413
- * @return {boolean} - true if the collision should be resolved by modifying it's position and velocity */
414
- collideWithTile(tileData, pos) { return tileData > 0; }
415
-
416
- /** Called by the engine to check if an object collision should be resolved. Return true for physics to resolve the collision or false to ignore and resolve it manually.
417
- * @param {EngineObject} object - the object to test against
418
- * @return {boolean} - true if the collision should be resolved by modifying it's position and velocity
419
- */
420
- collideWithObject(object) { return true; }
421
-
422
- /** Get this object's up vector
423
- * @param {number} [scale] - length of the vector
424
- * @return {Vector2} */
425
- getUp(scale=1) { return vec2().setAngle(this.angle, scale); }
426
-
427
- /** Get this object's right vector
428
- * @param {number} [scale] - length of the vector
429
- * @return {Vector2} */
430
- getRight(scale=1) { return vec2().setAngle(this.angle+PI/2, scale); }
431
-
432
- /** How long since the object was created
433
- * @return {number} */
434
- getAliveTime() { return time - this.spawnTime; }
435
-
436
- /** Get the speed of this object
437
- * @return {number} */
438
- getSpeed() { return this.velocity.length(); }
439
-
440
- /** Apply acceleration to this object (adjust velocity, not affected by mass)
441
- * @param {Vector2} acceleration */
442
- applyAcceleration(acceleration)
443
- { if (this.mass) this.velocity = this.velocity.add(acceleration); }
444
-
445
- /** Apply angular acceleration to this object
446
- * @param {number} acceleration */
447
- applyAngularAcceleration(acceleration)
448
- { if (this.mass) this.angleVelocity += acceleration; }
449
-
450
- /** Apply force to this object (adjust velocity, affected by mass)
451
- * @param {Vector2} force */
452
- applyForce(force)
453
- { if (this.mass) this.applyAcceleration(force.scale(1/this.mass)); }
454
-
455
- /** Get the direction of the mirror
456
- * @return {number} -1 if this.mirror is true, or 1 if not mirrored */
457
- getMirrorSign() { return this.mirror ? -1 : 1; }
458
-
459
- /** Attaches a child to this with a local transform, returns child for chaining
460
- * @param {EngineObject} child
461
- * @param {Vector2} [localPos=vec2()]
462
- * @param {number} [localAngle]
463
- * @return {EngineObject} The child object added */
464
- addChild(child, localPos=vec2(), localAngle=0)
465
- {
466
- ASSERT(!this.destroyed, 'cannot add child to destroyed object');
467
- if (this.destroyed) return child;
468
- ASSERT(!child.parent && !this.children.includes(child));
469
- ASSERT(child instanceof EngineObject, 'child must be an EngineObject');
470
- ASSERT(child !== this, 'cannot add self as child');
471
- this.children.push(child);
472
- child.parent = this;
473
- child.localPos = localPos.copy();
474
- child.localAngle = localAngle;
475
- child.updateTransforms();
476
- return child;
477
- }
478
-
479
- /** Removes a child from this one
480
- * @param {EngineObject} child */
481
- removeChild(child)
482
- {
483
- ASSERT(child.parent === this && this.children.includes(child));
484
- this.children.splice(this.children.indexOf(child), 1);
485
- child.parent = undefined;
486
- }
487
-
488
- /** Check if overlapping another engine object
489
- * Collisions are resolved to prevent overlaps
490
- * @param {EngineObject} object
491
- * @return {boolean} */
492
- isOverlappingObject(object)
493
- { return this.isOverlapping(object.pos, object.size); }
494
-
495
- /** Check if overlapping a point or aligned bounding box
496
- * @param {Vector2} pos - Center of box
497
- * @param {Vector2} [size=vec2()] - Size of box, uses a point if undefined
498
- * @return {boolean} */
499
- isOverlapping(pos, size=vec2())
500
- { return isOverlapping(this.pos, this.size, pos, size); }
501
-
502
- /** Set how this object collides
503
- * @param {boolean} [collideSolidObjects] - Does it collide with solid objects?
504
- * @param {boolean} [isSolid] - Does it collide with and block other objects? (expensive in large numbers)
505
- * @param {boolean} [collideTiles] - Does it collide with the tile collision?
506
- * @param {boolean} [collideRaycast] - Does it collide with raycasts? */
507
- setCollision(collideSolidObjects=true, isSolid=true, collideTiles=true, collideRaycast=true)
508
- {
509
- ASSERT(collideSolidObjects || !isSolid, 'solid objects must be set to collide');
510
-
511
- this.collideSolidObjects = collideSolidObjects;
512
- this.isSolid = isSolid;
513
- this.collideTiles = collideTiles;
514
- this.collideRaycast = collideRaycast;
515
- }
516
-
517
- /** Returns string containing info about this object for debugging
518
- * @return {string} */
519
- toString()
520
- {
521
- let text = 'type = ' + this.constructor.name;
522
- if (this.pos.x || this.pos.y)
523
- text += '\npos = ' + this.pos;
524
- if (this.velocity.x || this.velocity.y)
525
- text += '\nvelocity = ' + this.velocity;
526
- if (this.size.x || this.size.y)
527
- text += '\nsize = ' + this.size;
528
- if (this.angle)
529
- text += '\nangle = ' + this.angle.toFixed(3);
530
- if (this.color)
531
- text += '\ncolor = ' + this.color;
532
- return text;
533
- }
534
-
535
- /** Render debug info for this object */
536
- renderDebugInfo()
537
- {
538
- if (!debug) return;
539
-
540
- // check if there is anything to show
541
- const hasPhysics = this.collideTiles || this.collideSolidObjects || this.isSolid;
542
- if (!hasPhysics && !this.parent) return;
543
-
544
- // show object info for debugging
545
- const size = vec2(max(this.size.x, .2), max(this.size.y, .2));
546
- const color = rgb(this.collideTiles?1:0, this.collideSolidObjects?1:0, this.isSolid?1:0, .5);
547
- debugRect(this.pos, size, color, 0, this.angle, hasPhysics);
548
- if (this.parent)
549
- debugRect(this.pos, size.scale(.8), rgb(1,1,1,.5), 0, this.angle);
550
- this.parent && debugLine(this.pos, this.parent.pos, rgb(1,1,1,.5), .5);
551
- }
1
+ /**
2
+ * LittleJS Object System
3
+ * - EngineObject is the base class for all game objects
4
+ * - Handles automatic updating, rendering, physics, and collision
5
+ * - Supports parent-child hierarchies with transform inheritance
6
+ * - 2D physics with velocity, acceleration, damping, and gravity
7
+ * - Collision system with tiles and other objects
8
+ * - Renders sprites from tile sheets with color and rotation
9
+ * - Objects sorted by renderOrder for layered rendering
10
+ */
11
+
12
+ 'use strict';
13
+
14
+ /**
15
+ * LittleJS Object Base Object Class
16
+ * - Top level object class used by the engine
17
+ * - Automatically adds self to object list
18
+ * - Will be updated and rendered each frame
19
+ * - Renders as a sprite from a tilesheet by default
20
+ * - Can have color and additive color applied
21
+ * - 2D Physics and collision system
22
+ * - Sorted by renderOrder
23
+ * - Objects can have children attached
24
+ * - Parents are updated before children, and set child transform
25
+ * - Call destroy() to get rid of objects
26
+ *
27
+ * The physics system used by objects is simple and fast with some caveats...
28
+ * - Collision uses the axis aligned size, the object's rotation angle is only for rendering
29
+ * - Objects are guaranteed to not intersect tile collision from physics
30
+ * - If an object starts or is moved inside tile collision, it will not collide with that tile
31
+ * - Collision for objects can be set to be solid to block other objects
32
+ * - Objects may get pushed into overlapping other solid objects, if so they will push away
33
+ * - Solid objects are more performance intensive and should be used sparingly
34
+ * @memberof Engine
35
+ * @example
36
+ * // create an engine object, normally you would first extend the class with your own
37
+ * const pos = vec2(2,3);
38
+ * const object = new EngineObject(pos);
39
+ */
40
+ class EngineObject
41
+ {
42
+ /** Create an engine object and adds it to the list of objects
43
+ * @param {Vector2} [pos=vec2()] - World space position of the object
44
+ * @param {Vector2} [size=vec2(1)] - World space size of the object
45
+ * @param {TileInfo} [tileInfo] - Tile info to render object (undefined is untextured)
46
+ * @param {number} [angle] - Angle the object is rotated by
47
+ * @param {Color} [color=WHITE] - Color to apply to tile when rendered
48
+ * @param {number} [renderOrder] - Objects sorted by renderOrder before being rendered
49
+ */
50
+ constructor(pos=vec2(), size=vec2(1), tileInfo, angle=0, color=WHITE, renderOrder=0)
51
+ {
52
+ // check passed in params
53
+ ASSERT(isVector2(pos), 'object pos must be a vec2');
54
+ ASSERT(isVector2(size), 'object size must be a vec2');
55
+ ASSERT(!tileInfo || tileInfo instanceof TileInfo, 'object tileInfo should be a TileInfo or undefined');
56
+ ASSERT(typeof angle === 'number' && isFinite(angle), 'object angle should be a number');
57
+ ASSERT(isColor(color), 'object color should be a valid rgba color');
58
+ ASSERT(typeof renderOrder === 'number', 'object renderOrder should be a number');
59
+
60
+ /** @property {Vector2} - World space position of the object */
61
+ this.pos = pos.copy();
62
+ /** @property {Vector2} - World space width and height of the object */
63
+ this.size = size.copy();
64
+ /** @property {Vector2} - Size of object used for drawing, uses size if not set */
65
+ this.drawSize = undefined;
66
+ /** @property {TileInfo} - Tile info to render object (undefined is untextured) */
67
+ this.tileInfo = tileInfo;
68
+ /** @property {number} - Angle to rotate the object */
69
+ this.angle = angle;
70
+ /** @property {Color} - Color to apply when rendered */
71
+ this.color = color.copy();
72
+ /** @property {Color} - Additive color to apply when rendered */
73
+ this.additiveColor = undefined;
74
+ /** @property {Shader|undefined} - Custom shader to render with, undefined for the engine's own
75
+ * @type {Shader|undefined} */
76
+ this.shader = undefined;
77
+ /** @property {boolean} - Should the rendered tile flip along the y axis. Affects rendering and the local→world transform of attached children (a mirrored parent flips its children's localPos.x and localAngle). Does not affect this object's own physics, collision, or localToWorld/worldToLocal. */
78
+ this.mirror = false;
79
+ /** @property {boolean} - Has object been destroyed? */
80
+ this.destroyed = false;
81
+
82
+ // physical properties
83
+ /** @property {number} - How heavy the object is, static if 0 */
84
+ this.mass = objectDefaultMass;
85
+ /** @property {number} - How much to slow down velocity each frame (0-1) */
86
+ this.damping = objectDefaultDamping;
87
+ /** @property {number} - How much to slow down rotation each frame (0-1) */
88
+ this.angleDamping = objectDefaultAngleDamping;
89
+ /** @property {number} - How bouncy the object is when colliding (0-1) */
90
+ this.restitution = objectDefaultRestitution;
91
+ /** @property {number} - How much friction to apply when sliding (0-1) */
92
+ this.friction = objectDefaultFriction;
93
+ /** @property {number} - How much to scale gravity by for this object */
94
+ this.gravityScale = 1;
95
+ /** @property {number} - Objects are sorted by render order */
96
+ this.renderOrder = renderOrder;
97
+ /** @property {Vector2} - Velocity of the object */
98
+ this.velocity = vec2();
99
+ /** @property {number} - Angular velocity of the object */
100
+ this.angleVelocity = 0;
101
+ /** @property {number} - Track when object was created */
102
+ this.spawnTime = time;
103
+ /** @property {Array<EngineObject>} - List of children of this object */
104
+ this.children = [];
105
+ /** @property {boolean} - Limit object speed along x and y axis */
106
+ this.clampSpeed = true;
107
+ /** @property {EngineObject} - Object we are standing on, if any */
108
+ this.groundObject = undefined;
109
+
110
+ // parent child system
111
+ /** @property {EngineObject} - Parent of object if in local space */
112
+ this.parent = undefined;
113
+ /** @property {Vector2} - Local position if child */
114
+ this.localPos = vec2();
115
+ /** @property {number} - Local angle if child */
116
+ this.localAngle = 0;
117
+
118
+ // collision flags
119
+ /** @property {boolean} - Object collides with the tile collision */
120
+ this.collideTiles = false;
121
+ /** @property {boolean} - Object collides with solid objects */
122
+ this.collideSolidObjects = false;
123
+ /** @property {boolean} - Object collides with and blocks other objects */
124
+ this.isSolid = false;
125
+ /** @property {boolean} - Object collides with raycasts */
126
+ this.collideRaycast = false;
127
+
128
+ /** @property {boolean} - Object is skipped by engineObjectsDestroy, for things that outlive a level like a camera
129
+ * - Calling destroy on it still destroys it, and its children go with it either way */
130
+ this.persistent = false;
131
+
132
+ // add to list of objects
133
+ engineObjects.push(this);
134
+ }
135
+
136
+ /** Update the object transform, called automatically by engine even when paused */
137
+ updateTransforms()
138
+ {
139
+ const parent = this.parent;
140
+ if (parent)
141
+ {
142
+ // compose with parent transform inline to avoid intermediate vector allocs
143
+ const mirror = parent.getMirrorSign();
144
+ const lp = this.localPos, pp = parent.pos;
145
+ const lx = lp.x*mirror, ly = lp.y, pa = parent.angle;
146
+ if (pa)
147
+ {
148
+ const c = cos(-pa), s = sin(-pa);
149
+ this.pos.set(lx*c - ly*s + pp.x, lx*s + ly*c + pp.y);
150
+ }
151
+ else
152
+ this.pos.set(lx + pp.x, ly + pp.y);
153
+ this.angle = mirror*this.localAngle + pa;
154
+ }
155
+
156
+ // update children
157
+ for (const child of this.children)
158
+ child.updateTransforms();
159
+ }
160
+
161
+ /** Update the object physics, called automatically by engine once each frame. Can be overridden to stop or change how physics works for an object. */
162
+ updatePhysics()
163
+ {
164
+ // child objects do not have physics
165
+ ASSERT(!this.parent);
166
+
167
+ // bail if a collision callback destroyed us mid-frame
168
+ if (this.destroyed) return;
169
+
170
+ if (this.clampSpeed)
171
+ {
172
+ // limit max speed to prevent missing collisions
173
+ this.velocity.x = clamp(this.velocity.x, -objectMaxSpeed, objectMaxSpeed);
174
+ this.velocity.y = clamp(this.velocity.y, -objectMaxSpeed, objectMaxSpeed);
175
+ }
176
+
177
+ // apply physics
178
+ const oldPos = this.pos.copy();
179
+ this.velocity.x *= this.damping;
180
+ this.velocity.y *= this.damping;
181
+ if (this.mass)
182
+ {
183
+ // apply gravity only if it has mass
184
+ this.velocity.x += gravity.x * this.gravityScale;
185
+ this.velocity.y += gravity.y * this.gravityScale;
186
+ }
187
+ this.pos.x += this.velocity.x;
188
+ this.pos.y += this.velocity.y;
189
+ this.angle += this.angleVelocity *= this.angleDamping;
190
+
191
+ // physics sanity checks
192
+ ASSERT(this.angleDamping >= 0 && this.angleDamping <= 1);
193
+ ASSERT(this.damping >= 0 && this.damping <= 1);
194
+
195
+ // don't do collision for static objects or if solver disabled
196
+ if (!enablePhysicsSolver || !this.mass) return;
197
+
198
+ const wasFalling = this.velocity.y < 0 && gravity.y < 0 || this.velocity.y > 0 && gravity.y > 0;
199
+ if (this.groundObject)
200
+ {
201
+ // apply friction in local space of ground object
202
+ const friction = max(this.friction, this.groundObject.friction);
203
+ const groundSpeed = this.groundObject.velocity.x;
204
+ this.velocity.x = groundSpeed + (this.velocity.x - groundSpeed) * friction;
205
+ this.groundObject = undefined;
206
+ }
207
+
208
+ // an object with no width or height has no box to push out of, or to be pushed out of
209
+ if (this.collideSolidObjects && this.size.x && this.size.y)
210
+ {
211
+ // check collisions against solid objects
212
+ const epsilon = .001; // necessary to push slightly outside of the collision
213
+ for (const o of engineObjectsCollide)
214
+ {
215
+ // skip destroyed, child objects, self collision, or objects with no box
216
+ if (o.destroyed || o.parent || o === this || !o.size.x || !o.size.y) continue;
217
+
218
+ // non solid objects don't collide with each other
219
+ if (!this.isSolid && !o.isSolid) continue;
220
+
221
+ // check collision
222
+ if (!this.isOverlappingObject(o)) continue;
223
+
224
+ // notify objects of collision and check if should be resolved
225
+ const collide1 = this.collideWithObject(o);
226
+ const collide2 = o.collideWithObject(this);
227
+ if (!collide1 || !collide2) continue;
228
+
229
+ if (isOverlapping(oldPos, this.size, o.pos, o.size))
230
+ {
231
+ // if already was touching, try to push away
232
+ const deltaPos = oldPos.subtract(o.pos);
233
+ const length = deltaPos.length();
234
+ const pushAwayAccel = .001;
235
+ const velocity = length < .001 ? vec2(0,1) : deltaPos.scale(pushAwayAccel/length);
236
+ this.velocity = this.velocity.add(velocity);
237
+ if (o.mass) // push away other object if not fixed
238
+ o.velocity = o.velocity.subtract(velocity);
239
+
240
+ debugPhysics && debugOverlap(this.pos, this.size, o.pos, o.size, '#f00');
241
+ continue;
242
+ }
243
+
244
+ // check for collision
245
+ const sizeBoth = this.size.add(o.size);
246
+ const smallStepUp = (oldPos.y - o.pos.y)*2 > sizeBoth.y + gravity.y; // prefer to push up if small delta
247
+ const isBlockedX = abs(oldPos.y - o.pos.y)*2 < sizeBoth.y;
248
+ const isBlockedY = abs(oldPos.x - o.pos.x)*2 < sizeBoth.x;
249
+ const restitution = max(this.restitution, o.restitution);
250
+
251
+ if (smallStepUp || isBlockedY || !isBlockedX) // resolve y collision
252
+ {
253
+ // push outside object collision
254
+ this.pos.y = o.pos.y + (sizeBoth.y/2 + epsilon) * sign(oldPos.y - o.pos.y);
255
+ if ((o.groundObject && wasFalling) || !o.mass)
256
+ {
257
+ // set ground object if landed on something
258
+ if (wasFalling)
259
+ this.groundObject = o;
260
+
261
+ // bounce if other object is fixed or grounded
262
+ this.velocity.y *= -restitution;
263
+ }
264
+ else if (o.mass)
265
+ {
266
+ // inelastic collision
267
+ const inelastic = (this.mass * this.velocity.y + o.mass * o.velocity.y) / (this.mass + o.mass);
268
+
269
+ // elastic collision
270
+ const elastic0 = this.velocity.y * (this.mass - o.mass) / (this.mass + o.mass)
271
+ + o.velocity.y * 2 * o.mass / (this.mass + o.mass);
272
+ const elastic1 = o.velocity.y * (o.mass - this.mass) / (this.mass + o.mass)
273
+ + this.velocity.y * 2 * this.mass / (this.mass + o.mass);
274
+
275
+ // lerp between elastic or inelastic based on restitution
276
+ this.velocity.y = lerp(inelastic, elastic0, restitution);
277
+ o.velocity.y = lerp(inelastic, elastic1, restitution);
278
+ }
279
+ }
280
+ if (!smallStepUp && isBlockedX) // resolve x collision
281
+ {
282
+ // push outside collision
283
+ this.pos.x = o.pos.x + (sizeBoth.x/2 + epsilon) * sign(oldPos.x - o.pos.x);
284
+ if (o.mass)
285
+ {
286
+ // inelastic collision
287
+ const inelastic = (this.mass * this.velocity.x + o.mass * o.velocity.x) / (this.mass + o.mass);
288
+
289
+ // elastic collision
290
+ const elastic0 = this.velocity.x * (this.mass - o.mass) / (this.mass + o.mass)
291
+ + o.velocity.x * 2 * o.mass / (this.mass + o.mass);
292
+ const elastic1 = o.velocity.x * (o.mass - this.mass) / (this.mass + o.mass)
293
+ + this.velocity.x * 2 * this.mass / (this.mass + o.mass);
294
+
295
+ // lerp between elastic or inelastic based on restitution
296
+ this.velocity.x = lerp(inelastic, elastic0, restitution);
297
+ o.velocity.x = lerp(inelastic, elastic1, restitution);
298
+ }
299
+ else // bounce if other object is fixed
300
+ this.velocity.x *= -restitution;
301
+ }
302
+ debugPhysics && debugOverlap(this.pos, this.size, o.pos, o.size, '#f0f');
303
+ }
304
+ }
305
+ if (this.collideTiles)
306
+ {
307
+ // check collision against tiles
308
+ const hitLayer = tileCollisionTest(this.pos, this.size, this);
309
+ if (hitLayer)
310
+ {
311
+ // if already was stuck in collision, don't do anything
312
+ // this should not happen unless something starts in collision
313
+ if (!tileCollisionTest(oldPos, this.size, this))
314
+ {
315
+ // test which side we bounced off (or both if a corner)
316
+ const isBlockedX = tileCollisionTest(vec2(this.pos.x, oldPos.y), this.size, this);
317
+ const isBlockedY = tileCollisionTest(vec2(oldPos.x, this.pos.y), this.size, this);
318
+ const restitution = max(this.restitution, hitLayer.restitution);
319
+ if (isBlockedX)
320
+ {
321
+ // try to step over a 1-tile bump (direction follows gravity sign
322
+ // so inverted gravity steps down off a ceiling bump instead of up;
323
+ // zero gravity defaults to the normal-gravity step-up direction)
324
+ const epsilon = 1e-3;
325
+ const maxMove = .1;
326
+ const gravitySign = gravity.y > 0 ? -1 : 1;
327
+ const y = gravitySign > 0 ?
328
+ floor(oldPos.y-this.size.y/2+1) + this.size.y/2 + epsilon :
329
+ ceil( oldPos.y+this.size.y/2-1) - this.size.y/2 - epsilon;
330
+ const delta = abs(y - this.pos.y);
331
+ if (delta < maxMove)
332
+ if (!tileCollisionTest(vec2(this.pos.x, y), this.size, this))
333
+ {
334
+ this.pos.y = y;
335
+ debugPhysics && debugRect(this.pos, this.size, '#ff0');
336
+ return;
337
+ }
338
+
339
+ // move to previous X position and bounce
340
+ this.pos.x = oldPos.x;
341
+ this.velocity.x *= -restitution;
342
+ }
343
+ if (isBlockedY || !isBlockedX)
344
+ {
345
+ if (wasFalling)
346
+ {
347
+ // adjust position to slightly away from nearest tile
348
+ // this prevents gap between object and ground
349
+ const epsilon = .0001;
350
+ const offset = this.size.y/2 + epsilon;
351
+ this.pos.y = gravity.y < 0 ?
352
+ floor(oldPos.y-this.size.y/2) + offset :
353
+ ceil( oldPos.y+this.size.y/2) - offset;
354
+
355
+ // set ground object for tile collision
356
+ this.groundObject = hitLayer;
357
+ }
358
+ else
359
+ {
360
+ // move to previous Y position
361
+ this.pos.y = oldPos.y;
362
+ this.groundObject = undefined;
363
+ }
364
+ // bounce velocity
365
+ this.velocity.y *= -restitution;
366
+ }
367
+ debugPhysics && debugRect(this.pos, this.size, '#f00');
368
+ }
369
+ }
370
+ }
371
+ }
372
+
373
+ /** Update the object, called automatically by engine once each frame. Does nothing by default. */
374
+ update() {}
375
+
376
+ /** Render the object, draws a tile by default, automatically called each frame, sorted by renderOrder */
377
+ render()
378
+ {
379
+ // default object render
380
+ drawTile(this.pos, this.drawSize || this.size, this.tileInfo, this.color, this.angle, this.mirror, this.additiveColor);
381
+ }
382
+
383
+ /** Optional hook called during the light system plugin's lightmap pass to draw this object's lightmap contribution. Does nothing by default. */
384
+ renderLight() {}
385
+
386
+ /** Destroy this object, destroy its children, detach its parent, and mark it for removal
387
+ * @param {boolean} [immediate] - should attached effects be allowed to die off? */
388
+ destroy(immediate=false)
389
+ {
390
+ if (this.destroyed) return;
391
+
392
+ // disconnect from parent and destroy children
393
+ this.destroyed = true;
394
+ this.parent?.removeChild(this);
395
+ for (const child of this.children)
396
+ {
397
+ child.parent = undefined;
398
+ child.destroy(immediate);
399
+ }
400
+ }
401
+
402
+ /** Convert from local space to world space
403
+ * @param {Vector2} pos - local space point */
404
+ localToWorld(pos) { return this.pos.add(pos.rotate(this.angle)); }
405
+
406
+ /** Convert from world space to local space
407
+ * @param {Vector2} pos - world space point */
408
+ worldToLocal(pos) { return pos.subtract(this.pos).rotate(-this.angle); }
409
+
410
+ /** Convert from local space to world space for a vector (rotation only)
411
+ * @param {Vector2} vec - local space vector */
412
+ localToWorldVector(vec) { return vec.rotate(this.angle); }
413
+
414
+ /** Convert from world space to local space for a vector (rotation only)
415
+ * @param {Vector2} vec - world space vector */
416
+ worldToLocalVector(vec) { return vec.rotate(-this.angle); }
417
+
418
+ /** Called to check if a tile collision should be resolved. Return true for physics to resolve the collision or false to ignore and resolve it manually.
419
+ * @param {number} tileData - the value of the tile at the position
420
+ * @param {Vector2} pos - tile where the collision occurred
421
+ * @return {boolean} - true if the collision should be resolved by modifying it's position and velocity */
422
+ collideWithTile(tileData, pos) { return tileData > 0; }
423
+
424
+ /** Called by the engine to check if an object collision should be resolved. Return true for physics to resolve the collision or false to ignore and resolve it manually.
425
+ * @param {EngineObject} object - the object to test against
426
+ * @param {Object} [push] - what it would take to move this object clear, a Vector3 from the 3D plugin, undefined in 2D
427
+ * @return {boolean} - true if the collision should be resolved by modifying it's position and velocity
428
+ */
429
+ collideWithObject(object, push) { return true; }
430
+
431
+ /** Get this object's up vector
432
+ * @param {number} [scale] - length of the vector
433
+ * @return {Vector2} */
434
+ getUp(scale=1) { return vec2().setAngle(this.angle, scale); }
435
+
436
+ /** Get this object's right vector
437
+ * @param {number} [scale] - length of the vector
438
+ * @return {Vector2} */
439
+ getRight(scale=1) { return vec2().setAngle(this.angle+PI/2, scale); }
440
+
441
+ /** How long since the object was created
442
+ * @return {number} */
443
+ getAliveTime() { return time - this.spawnTime; }
444
+
445
+ /** Get the speed of this object
446
+ * @return {number} */
447
+ getSpeed() { return this.velocity.length(); }
448
+
449
+ /** Apply acceleration to this object (adjust velocity, not affected by mass)
450
+ * @param {Vector2} acceleration */
451
+ applyAcceleration(acceleration)
452
+ { if (this.mass) this.velocity = this.velocity.add(acceleration); }
453
+
454
+ /** Apply angular acceleration to this object
455
+ * @param {number} acceleration */
456
+ applyAngularAcceleration(acceleration)
457
+ { if (this.mass) this.angleVelocity += acceleration; }
458
+
459
+ /** Apply force to this object (adjust velocity, affected by mass)
460
+ * @param {Vector2} force */
461
+ applyForce(force)
462
+ { if (this.mass) this.applyAcceleration(force.scale(1/this.mass)); }
463
+
464
+ /** Get the direction of the mirror
465
+ * @return {number} -1 if this.mirror is true, or 1 if not mirrored */
466
+ getMirrorSign() { return this.mirror ? -1 : 1; }
467
+
468
+ /** Attaches a child to this with a local transform, returns child for chaining
469
+ * @param {EngineObject} child
470
+ * @param {Vector2} [localPos=vec2()]
471
+ * @param {number} [localAngle]
472
+ * @return {EngineObject} The child object added */
473
+ addChild(child, localPos=vec2(), localAngle=0)
474
+ {
475
+ ASSERT(!this.destroyed, 'cannot add child to destroyed object');
476
+ if (this.destroyed) return child;
477
+ ASSERT(!child.parent && !this.children.includes(child));
478
+ ASSERT(child instanceof EngineObject, 'child must be an EngineObject');
479
+ ASSERT(child !== this, 'cannot add self as child');
480
+ this.children.push(child);
481
+ child.parent = this;
482
+ child.localPos = localPos.copy();
483
+ child.localAngle = localAngle;
484
+ child.updateTransforms();
485
+ return child;
486
+ }
487
+
488
+ /** Removes a child from this one
489
+ * @param {EngineObject} child */
490
+ removeChild(child)
491
+ {
492
+ ASSERT(child.parent === this && this.children.includes(child));
493
+ this.children.splice(this.children.indexOf(child), 1);
494
+ child.parent = undefined;
495
+ }
496
+
497
+ /** Check if overlapping another engine object
498
+ * Collisions are resolved to prevent overlaps
499
+ * @param {EngineObject} object
500
+ * @return {boolean} */
501
+ isOverlappingObject(object)
502
+ { return this.isOverlapping(object.pos, object.size); }
503
+
504
+ /** Check if overlapping a point or aligned bounding box
505
+ * @param {Vector2} pos - Center of box
506
+ * @param {Vector2} [size=vec2()] - Size of box, uses a point if undefined
507
+ * @return {boolean} */
508
+ isOverlapping(pos, size=vec2())
509
+ { return isOverlapping(this.pos, this.size, pos, size); }
510
+
511
+ /** Set how this object collides
512
+ * @param {boolean} [collideSolidObjects] - Does it collide with solid objects?
513
+ * @param {boolean} [isSolid] - Does it collide with and block other objects? (expensive in large numbers)
514
+ * @param {boolean} [collideTiles] - Does it collide with the tile collision?
515
+ * @param {boolean} [collideRaycast] - Does it collide with raycasts? */
516
+ setCollision(collideSolidObjects=true, isSolid=true, collideTiles=true, collideRaycast=true)
517
+ {
518
+ ASSERT(collideSolidObjects || !isSolid, 'solid objects must be set to collide');
519
+
520
+ this.collideSolidObjects = collideSolidObjects;
521
+ this.isSolid = isSolid;
522
+ this.collideTiles = collideTiles;
523
+ this.collideRaycast = collideRaycast;
524
+ }
525
+
526
+ /** Returns string containing info about this object for debugging
527
+ * @return {string} */
528
+ toString()
529
+ {
530
+ let text = 'type = ' + this.constructor.name;
531
+ if (this.pos.x || this.pos.y)
532
+ text += '\npos = ' + this.pos;
533
+ if (this.velocity.x || this.velocity.y)
534
+ text += '\nvelocity = ' + this.velocity;
535
+ if (this.size.x || this.size.y)
536
+ text += '\nsize = ' + this.size;
537
+ if (this.angle)
538
+ text += '\nangle = ' + this.angle.toFixed(3);
539
+ if (this.color)
540
+ text += '\ncolor = ' + this.color;
541
+ return text;
542
+ }
543
+
544
+ /** Render debug info for this object */
545
+ renderDebugInfo()
546
+ {
547
+ if (!debug) return;
548
+
549
+ // check if there is anything to show
550
+ const hasPhysics = this.collideTiles || this.collideSolidObjects || this.isSolid;
551
+ if (!hasPhysics && !this.parent) return;
552
+
553
+ // show object info for debugging
554
+ const size = vec2(max(this.size.x, .2), max(this.size.y, .2));
555
+ const color = rgb(this.collideTiles?1:0, this.collideSolidObjects?1:0, this.isSolid?1:0, .5);
556
+ debugRect(this.pos, size, color, 0, this.angle, hasPhysics);
557
+ if (this.parent)
558
+ debugRect(this.pos, size.scale(.8), rgb(1,1,1,.5), 0, this.angle);
559
+ this.parent && debugLine(this.pos, this.parent.pos, rgb(1,1,1,.5), .5);
560
+ }
552
561
  }