/**
* LittleJS Object System
* - EngineObject is the base class for all game objects
* - Handles automatic updating, rendering, physics, and collision
* - Supports parent-child hierarchies with transform inheritance
* - 2D physics with velocity, acceleration, damping, and gravity
* - Collision system with tiles and other objects
* - Renders sprites from tile sheets with color and rotation
* - Objects sorted by renderOrder for layered rendering
*/
'use strict';
/**
* LittleJS Object Base Object Class
* - Top level object class used by the engine
* - Automatically adds self to object list
* - Will be updated and rendered each frame
* - Renders as a sprite from a tilesheet by default
* - Can have color and additive color applied
* - 2D Physics and collision system
* - Sorted by renderOrder
* - Objects can have children attached
* - Parents are updated before children, and set child transform
* - Call destroy() to get rid of objects
*
* The physics system used by objects is simple and fast with some caveats...
* - Collision uses the axis aligned size, the object's rotation angle is only for rendering
* - Objects are guaranteed to not intersect tile collision from physics
* - If an object starts or is moved inside tile collision, it will not collide with that tile
* - Collision for objects can be set to be solid to block other objects
* - Objects may get pushed into overlapping other solid objects, if so they will push away
* - A static solid (mass 0) moved by its velocity, like a door or an elevator, pushes objects out of its way
* - Solid objects are more performance intensive and should be used sparingly
* @memberof Engine
* @example
* // create an engine object, normally you would first extend the class with your own
* const pos = vec2(2,3);
* const object = new EngineObject(pos);
*/
class EngineObject
{
/** Create an engine object and adds it to the list of objects
* @param {Vector2} [pos=vec2()] - World space position of the object
* @param {Vector2} [size=vec2(1)] - World space size of the object
* @param {TileInfo} [tileInfo] - Tile info to render object (undefined is untextured)
* @param {number} [angle] - Angle the object is rotated by
* @param {Color} [color=WHITE] - Color to apply to tile when rendered
* @param {number} [renderOrder] - Objects sorted by renderOrder before being rendered
*/
constructor(pos=vec2(), size=vec2(1), tileInfo, angle=0, color=WHITE, renderOrder=0)
{
// check passed in params
ASSERT(isVector2(pos), 'object pos must be a vec2');
ASSERT(isVector2(size), 'object size must be a vec2');
ASSERT(!tileInfo || tileInfo instanceof TileInfo, 'object tileInfo should be a TileInfo or undefined');
ASSERT(typeof angle === 'number' && isFinite(angle), 'object angle should be a number');
ASSERT(isColor(color), 'object color should be a valid rgba color');
ASSERT(typeof renderOrder === 'number', 'object renderOrder should be a number');
/** @property {Vector2} - World space position of the object */
this.pos = pos.copy();
/** @property {Vector2} - World space width and height of the object */
this.size = size.copy();
/** @property {Vector2|undefined} - Size of object used for drawing, uses size if not set
* @type {Vector2|undefined} */
this.drawSize = undefined;
/** @property {TileInfo|undefined} - Tile info to render object (undefined is untextured)
* @type {TileInfo|undefined} */
this.tileInfo = tileInfo;
/** @property {number} - Angle to rotate the object */
this.angle = angle;
/** @property {Color} - Color to apply when rendered */
this.color = color.copy();
/** @property {Color|undefined} - Additive color to apply when rendered
* @type {Color|undefined} */
this.additiveColor = undefined;
/** @property {Shader|undefined} - Custom shader to render with, undefined for the engine's own
* @type {Shader|undefined} */
this.shader = undefined;
/** @property {boolean} - Does this object draw into the light system's shadow map; false for a floor layer, a background, a pickup */
this.castShadow = true;
/** @property {number} - With the light system, how much it lights itself: 0 lit only by the lights, 1 full
* brightness in its own colors whatever the lights do, between partly; drawn into the lightmap through
* renderEmissive, as 3D's emissive. Exact for solid pixels; a partly transparent one is self lit by its alpha
* too, so a half alpha pixel shows at a quarter and a fading emissive sprite fades a little faster */
this.emissive = 0;
/** @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. */
this.mirror = false;
/** @property {boolean} - Has object been destroyed? */
this.destroyed = false;
this.updatePass = 0; // the engine update pass it was last updated in, so nothing updates twice in one
// physical properties
/** @property {number} - How heavy the object is, static if 0: a static object moves by its velocity but does not
* collide on its own, the moving ones collide with it */
this.mass = objectDefaultMass;
/** @property {number} - Fraction of velocity kept each frame, 1 keeps all of it, 0 stops at once */
this.damping = objectDefaultDamping;
/** @property {number} - Fraction of angular velocity kept each frame, 1 keeps all of it, 0 stops at once */
this.angleDamping = objectDefaultAngleDamping;
/** @property {number} - How bouncy the object is when colliding (0-1) */
this.restitution = objectDefaultRestitution;
/** @property {number} - Fraction of sliding speed kept each frame on the ground, 1 is no friction, 0 stops at
* once, the more slippery of the object and its ground is used */
this.friction = objectDefaultFriction;
/** @property {number} - How much to scale gravity by for this object */
this.gravityScale = 1;
/** @property {number} - Objects are sorted by render order */
this.renderOrder = renderOrder;
/** @property {Vector2} - Velocity of the object, in world units per frame */
this.velocity = vec2();
/** @property {number} - Angular velocity of the object, in radians per frame */
this.angleVelocity = 0;
/** @property {number} - Track when object was created */
this.spawnTime = time;
/** @property {Array<EngineObject>} - List of children of this object
* @type {Array<EngineObject>} */
this.children = [];
/** @property {boolean} - Limit object speed along x and y axis */
this.clampSpeed = true;
/** @property {EngineObject|undefined} - Object we are standing on, if any
* @type {EngineObject|undefined} */
this.groundObject = undefined;
// parent child system
/** @property {EngineObject|undefined} - Parent of object if in local space
* @type {EngineObject|undefined} */
this.parent = undefined;
/** @property {Vector2|undefined} - Position relative to the parent, only while attached to one
* @type {Vector2|undefined} */
this.localPos = undefined;
/** @property {number} - Local angle if child */
this.localAngle = 0;
// collision flags
/** @property {boolean} - Object collides with the level, its tile collision layers
* @type {boolean} */
this.collideLevel = false;
/** @property {boolean} - Object collides with solid objects */
this.collideSolidObjects = false;
/** @property {boolean} - Object collides with and blocks other objects */
this.isSolid = false;
/** @property {boolean} - Object collides with raycasts */
this.collideRaycast = false;
/** @property {boolean} - Object is skipped by engineObjectsDestroy, for things that outlive a level like a camera
* - Calling destroy on it still destroys it, and its children go with it either way */
this.persistent = false;
// add to list of objects
engineObjects.push(this);
}
/** Update the object transform, called automatically by engine even when paused
* @param {boolean} [updateChildren] - Also update the children's transforms */
updateTransforms(updateChildren=true)
{
const parent = this.parent;
if (parent)
{
// compose with parent transform inline to avoid intermediate vector allocs
const mirror = parent.getMirrorSign();
const lp = this.localPos, pp = parent.pos;
const lx = lp.x*mirror, ly = lp.y, pa = parent.angle;
if (pa)
{
const c = cos(-pa), s = sin(-pa);
this.pos.set(lx*c - ly*s + pp.x, lx*s + ly*c + pp.y);
}
else
this.pos.set(lx + pp.x, ly + pp.y);
this.angle = mirror*this.localAngle + pa;
}
// update children
if (updateChildren)
for (const child of this.children)
child.updateTransforms();
}
/** Update the object physics, called automatically by engine once each frame. Can be overridden to stop or change how physics works for an object. */
updatePhysics()
{
// child objects do not have physics
ASSERT(!this.parent);
// bail if a collision callback destroyed us mid-frame
if (this.destroyed) return;
// limit max speed to prevent missing collisions, only for what collides: with solids, or with tiles while it
// has a mass, which tile collision needs; anything else moves as fast as it is told
if (this.clampSpeed && enablePhysicsSolver && (this.collideSolidObjects || this.collideLevel && this.mass))
{
this.velocity.x = clamp(this.velocity.x, -objectMaxSpeed, objectMaxSpeed);
this.velocity.y = clamp(this.velocity.y, -objectMaxSpeed, objectMaxSpeed);
}
// physics sanity checks
ASSERT(this.angleDamping >= 0 && this.angleDamping <= 1, 'angleDamping must be 0 to 1, the fraction kept each frame');
ASSERT(this.damping >= 0 && this.damping <= 1, 'damping must be 0 to 1, the fraction of velocity kept each frame');
// apply physics; only the solver needs where the object was, so only then is it copied
const solve = enablePhysicsSolver && this.mass;
const oldPos = solve ? this.pos.copy() : undefined;
this.velocity.x *= this.damping;
this.velocity.y *= this.damping;
if (this.mass)
{
// apply gravity only if it has mass
this.velocity.x += gravity.x * this.gravityScale;
this.velocity.y += gravity.y * this.gravityScale;
}
this.pos.x += this.velocity.x;
this.pos.y += this.velocity.y;
this.angle += this.angleVelocity *= this.angleDamping;
// don't do collision for static objects or if solver disabled
if (!solve) return;
// which way is down for this object, a negative gravityScale falls up and lands on ceilings
const gravityY = this.gravityScale < 0 ? -gravity.y : gravity.y;
const wasFalling = this.velocity.y < 0 && gravityY < 0 || this.velocity.y > 0 && gravityY > 0;
if (this.groundObject)
{
// apply friction in local space of ground object
const friction = max(this.friction, this.groundObject.friction);
const groundSpeed = this.groundObject.velocity.x;
this.velocity.x = groundSpeed + (this.velocity.x - groundSpeed) * friction;
this.groundObject = undefined;
}
// an object with no width or height has no box to push out of, or to be pushed out of
if (this.collideSolidObjects && this.size.x && this.size.y)
{
// check collisions against solid objects
const epsilon = .001; // necessary to push slightly outside of the collision
for (const o of engineObjectsCollideStaticLast)
{
// skip destroyed, child objects, self collision, or objects with no box
if (o.destroyed || o.parent || o === this || !o.size.x || !o.size.y) continue;
// non solid objects don't collide with each other
if (!this.isSolid && !o.isSolid) continue;
// check collision
if (!this.isOverlappingObject(o)) continue;
// each moving object checks its own contacts, so a pair the other one already asked about this frame
// is not asked twice: left overlapping, ignored or only nudged apart it is skipped, and one both said
// to resolve, pushed back together since, is resolved again
const answer = engineObjectsCollidePairAnswer(o, this);
if (answer === false) continue;
if (!answer)
{
// notify objects of collision and check if should be resolved
const collide1 = this.collideWithObject(o);
const collide2 = o.collideWithObject(this);
if (!collide1 || !collide2)
{
engineObjectsCollidePairAdd(this, o);
continue;
}
}
const wasOverlapping = isOverlapping(oldPos, this.size, o.pos, o.size);
if (wasOverlapping && (!o.mass || o.groundObject))
{
// a static solid that moved into it, like a door or an elevator, pushes it out the shortest way
// at once and carries it along, it would only drift out slowly and the solid would pass through;
// an object standing on something counts as fixed too, so a stack on an elevator rides together;
// it bounces off relative to the mover, a paddle moved by setting pos bounces a ball as a wall does
const push = collideBoxBox(this.pos, this.size, o.pos, o.size);
if (push)
{
this.pos.x += push.x + sign(push.x) * epsilon;
this.pos.y += push.y + sign(push.y) * epsilon;
const restitution = max(this.restitution, o.restitution);
if (push.x)
{
const v = this.velocity.x - o.velocity.x;
if (v * push.x < 0) // moving into it
this.velocity.x = o.velocity.x - v * restitution;
}
else
{
const v = this.velocity.y - o.velocity.y;
if (v * push.y < 0) // moving into it
this.velocity.y = o.velocity.y - v * restitution;
if (push.y * gravityY < 0) // pushed up against gravity, it stands on it
this.groundObject = o;
}
}
engineObjectsCollidePairAdd(this, o);
debugPhysics && debugOverlap(this.pos, this.size, o.pos, o.size, '#f00', undefined, false);
continue;
}
if (wasOverlapping)
{
// if already was touching, try to push away
const deltaPos = oldPos.subtract(o.pos);
const length = deltaPos.length();
const pushAwayAccel = .001;
const velocity = length < .001 ? vec2(0, pushAwayAccel) : deltaPos.scale(pushAwayAccel/length);
this.velocity = this.velocity.add(velocity);
if (o.mass) // push away other object if not fixed
o.velocity = o.velocity.subtract(velocity);
engineObjectsCollidePairAdd(this, o);
debugPhysics && debugOverlap(this.pos, this.size, o.pos, o.size, '#f00', undefined, false);
continue;
}
// check for collision
const sizeBoth = this.size.add(o.size);
// prefer to push up if small delta, up away from this object's own gravity
const smallStepUp = (gravityY > 0 ? o.pos.y - oldPos.y : oldPos.y - o.pos.y)*2 > sizeBoth.y - abs(gravityY);
const isBlockedX = abs(oldPos.y - o.pos.y)*2 < sizeBoth.y;
const isBlockedY = abs(oldPos.x - o.pos.x)*2 < sizeBoth.x;
const restitution = max(this.restitution, o.restitution);
if (smallStepUp || isBlockedY || !isBlockedX) // resolve y collision
{
// push outside object collision
this.pos.y = o.pos.y + (sizeBoth.y/2 + epsilon) * sign(oldPos.y - o.pos.y);
if ((o.groundObject && wasFalling) || !o.mass)
{
// set ground object if landed on something
if (wasFalling)
this.groundObject = o;
// bounce if other object is fixed or grounded, relative to it so a rider keeps up with a
// platform moving down instead of landing on it again every few frames
this.velocity.y = o.velocity.y - (this.velocity.y - o.velocity.y) * restitution;
}
else // o has mass here, the other branch already handled the massless case
{
// inelastic collision
const inelastic = (this.mass * this.velocity.y + o.mass * o.velocity.y) / (this.mass + o.mass);
// elastic collision
const elastic0 = this.velocity.y * (this.mass - o.mass) / (this.mass + o.mass)
+ o.velocity.y * 2 * o.mass / (this.mass + o.mass);
const elastic1 = o.velocity.y * (o.mass - this.mass) / (this.mass + o.mass)
+ this.velocity.y * 2 * this.mass / (this.mass + o.mass);
// lerp between elastic or inelastic based on restitution
this.velocity.y = lerp(inelastic, elastic0, restitution);
o.velocity.y = lerp(inelastic, elastic1, restitution);
}
}
if (!smallStepUp && isBlockedX) // resolve x collision
{
// push outside collision
this.pos.x = o.pos.x + (sizeBoth.x/2 + epsilon) * sign(oldPos.x - o.pos.x);
if (o.mass)
{
// inelastic collision
const inelastic = (this.mass * this.velocity.x + o.mass * o.velocity.x) / (this.mass + o.mass);
// elastic collision
const elastic0 = this.velocity.x * (this.mass - o.mass) / (this.mass + o.mass)
+ o.velocity.x * 2 * o.mass / (this.mass + o.mass);
const elastic1 = o.velocity.x * (o.mass - this.mass) / (this.mass + o.mass)
+ this.velocity.x * 2 * this.mass / (this.mass + o.mass);
// lerp between elastic or inelastic based on restitution
this.velocity.x = lerp(inelastic, elastic0, restitution);
o.velocity.x = lerp(inelastic, elastic1, restitution);
}
else // bounce if other object is fixed, relative to it as a landing is
this.velocity.x = o.velocity.x - (this.velocity.x - o.velocity.x) * restitution;
}
engineObjectsCollidePairAdd(this, o, true);
debugPhysics && debugOverlap(this.pos, this.size, o.pos, o.size, '#f0f', undefined, false);
}
}
if (this.collideLevel)
{
// check collision against tiles
const hitLayer = tileCollisionTest(this.pos, this.size, this);
if (hitLayer)
{
// if already was stuck in collision, don't do anything
// this should not happen unless something starts in collision
if (!tileCollisionTest(oldPos, this.size, this))
{
// test which side we bounced off (or both if a corner)
const isBlockedX = tileCollisionTest(vec2(this.pos.x, oldPos.y), this.size, this);
const isBlockedY = tileCollisionTest(vec2(oldPos.x, this.pos.y), this.size, this);
const restitution = max(this.restitution, hitLayer.restitution);
if (isBlockedX)
{
// a ledge caught less than maxMove below its top lifts the object onto it instead of stopping it,
// down off a ceiling ledge when gravity points up; zero gravity counts as down
const epsilon = 1e-3;
const maxMove = .1;
const y = gravityY > 0 ?
ceil( oldPos.y+this.size.y/2-1) - this.size.y/2 - epsilon :
floor(oldPos.y-this.size.y/2+1) + this.size.y/2 + epsilon;
if (abs(y - this.pos.y) < maxMove && !tileCollisionTest(vec2(this.pos.x, y), this.size, this))
{
this.pos.y = y;
debugPhysics && debugRect(this.pos, this.size, '#ff0', 0, 0, false, false);
return;
}
// move to previous X position and bounce
this.pos.x = oldPos.x;
this.velocity.x *= -restitution;
}
if (isBlockedY || !isBlockedX)
{
if (wasFalling)
{
// adjust position to slightly away from nearest tile
// this prevents gap between object and ground
const epsilon = .0001;
const offset = this.size.y/2 + epsilon;
// rounded in the layer's space as its collision test is, or a bottom a hair below a
// grid line would round to the row under it, inside the floor, and fall through
const layerY = hitLayer.pos.y;
this.pos.y = layerY + (gravityY < 0 ?
floor(oldPos.y - layerY - this.size.y/2) + offset :
ceil( oldPos.y - layerY + this.size.y/2) - offset);
// set ground object for tile collision
this.groundObject = hitLayer;
}
else
{
// move to previous Y position
this.pos.y = oldPos.y;
this.groundObject = undefined;
}
// bounce velocity
this.velocity.y *= -restitution;
}
debugPhysics && debugRect(this.pos, this.size, '#f00', 0, 0, false, false);
}
}
}
}
/** Update the object, called automatically by engine once each frame. Does nothing by default. */
update() {}
/** Render the object, draws a tile by default, automatically called each frame, sorted by renderOrder */
render()
{
// default object render
drawTile(this.pos, this.drawSize || this.size, this.tileInfo, this.color, this.angle, this.mirror, this.additiveColor, glEnable, false);
}
/** Optional hook called during the light system plugin's lightmap pass to draw this object's lightmap contribution. Does nothing by default. */
renderLight() {}
/** Draw this object into the light system's shadow map, called during its shadow pass when castShadow is set.
* Calls render() by default so the object casts its own shape; override to cast a different one, like a blob at a character's feet so its body stays lit;
* screen space WebGL draws in render() are skipped during the pass */
renderShadow() { this.render(); }
/** Draw this object's shape into the light system's lightmap, called during its light pass when emissive is above
* 0; what it draws shows in its own colors that much brighter. Calls render() by default so the whole object
* glows; override to glow a part, like a robot's eyes */
renderEmissive() { this.render(); }
/** Destroy this object, destroy its children, detach its parent, and mark it for removal
* @param {boolean} [immediate] - true removes attached effects like particle emitters at once, false lets them finish first */
destroy(immediate=false)
{
if (this.destroyed) return;
// disconnect from parent and destroy children
this.destroyed = true;
this.parent?.removeChild(this);
for (const child of this.children)
{
child.parent = undefined;
child.destroy(immediate);
}
}
/** Convert from local space to world space
* @param {Vector2} pos - local space point */
localToWorld(pos) { return this.pos.add(pos.rotate(this.angle)); }
/** Convert from world space to local space
* @param {Vector2} pos - world space point */
worldToLocal(pos) { return pos.subtract(this.pos).rotate(-this.angle); }
/** Convert from local space to world space for a vector (rotation only)
* @param {Vector2} vec - local space vector */
localToWorldVector(vec) { return vec.rotate(this.angle); }
/** Convert from world space to local space for a vector (rotation only)
* @param {Vector2} vec - world space vector */
worldToLocalVector(vec) { return vec.rotate(-this.angle); }
/** 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.
* - Called for each solid tile the physics tests, which can be several times a frame for the same tile, and for
* positions it only tries, so keep it free of side effects or guard them to once a frame
* - this.pos has already moved, so a check on where it came from, like a one way platform, needs the position
* saved in update, as the platformer example does
* @param {number} tileData - the value of the tile at the position
* @param {Vector2} pos - the tile's bottom left corner in world space
* @return {boolean} - true if the collision should be resolved by modifying it's position and velocity */
collideWithTile(tileData, pos) { return tileData > 0; }
/** 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.
* - Both objects of a touching pair are asked once a frame, whichever order they update in; an object that
* destroys itself here is gone at the end of the frame and is still asked about the pairs left this frame, so a
* bullet that should hit one thing checks its own destroyed flag first
* @param {EngineObject} object - the object to test against
* @param {Vector3} [push] - what it would take to move this object clear, a Vector3 from the 3D plugin, undefined in 2D
* @return {boolean} - true if the collision should be resolved by modifying it's position and velocity
*/
collideWithObject(object, push) { return true; }
/** Get this object's up vector
* @param {number} [scale] - length of the vector
* @return {Vector2} */
getUp(scale=1) { return vec2().setAngle(this.angle, scale); }
/** Get this object's right vector
* @param {number} [scale] - length of the vector
* @return {Vector2} */
getRight(scale=1) { return vec2().setAngle(this.angle+PI/2, scale); }
/** How long since the object was created
* @return {number} */
getAliveTime() { return time - this.spawnTime; }
/** Get the speed of this object
* @return {number} */
getSpeed() { return this.velocity.length(); }
/** Apply acceleration to this object (adjust velocity, not affected by mass)
* - Does nothing on a static object (mass 0), set its velocity instead
* @param {Vector2} acceleration */
applyAcceleration(acceleration)
{ if (this.mass) this.velocity = this.velocity.add(acceleration); }
/** Apply angular acceleration to this object
* - Does nothing on a static object (mass 0), set its angleVelocity instead
* @param {number} acceleration */
applyAngularAcceleration(acceleration)
{ if (this.mass) this.angleVelocity += acceleration; }
/** Apply force to this object (adjust velocity, affected by mass)
* - Does nothing on a static object (mass 0), set its velocity instead
* @param {Vector2} force */
applyForce(force)
{ if (this.mass) this.applyAcceleration(force.scale(1/this.mass)); }
/** Get the direction of the mirror
* @return {number} -1 if this.mirror is true, or 1 if not mirrored */
getMirrorSign() { return this.mirror ? -1 : 1; }
/** Attaches a child to this with a local transform, returns child for chaining
* @param {EngineObject} child
* @param {Vector2} [localPos=vec2()]
* @param {number} [localAngle]
* @return {EngineObject} The child object added */
addChild(child, localPos=vec2(), localAngle=0)
{
ASSERT(!this.destroyed, 'cannot add child to destroyed object');
if (this.destroyed) return child;
ASSERT(!child.parent && !this.children.includes(child), 'child already has a parent, removeChild it first or use attach');
ASSERT(child instanceof EngineObject, 'child must be an EngineObject');
ASSERT(!child.destroyed, 'cannot add a destroyed child');
for (let p = /** @type {EngineObject} */ (this); p; p = p.parent)
ASSERT(p !== child, 'cannot add an object as a child of itself or of its own child');
this.children.push(child);
child.parent = this;
child.localPos = localPos.copy();
child.localAngle = localAngle;
child.updateTransforms();
return child;
}
/** Attaches a child to this without moving it: the local transform is worked out from where the child is now,
* where addChild takes one; a child of something else is moved over, returns child for chaining
* @param {EngineObject} child
* @return {EngineObject} The child object attached */
attach(child)
{
ASSERT(child instanceof EngineObject, 'child must be an EngineObject');
ASSERT(child !== this, 'cannot attach self');
child.parent?.removeChild(child);
// the local values that updateTransforms turns back into the child's current pos and angle
const mirror = this.getMirrorSign(), local = this.worldToLocal(child.pos);
local.x *= mirror;
return this.addChild(child, local, mirror * (child.angle - this.angle));
}
/** Removes a child from this one, it stays where it is in the world
* @param {EngineObject} child */
removeChild(child)
{
ASSERT(child.parent === this && this.children.includes(child));
const i = this.children.indexOf(child);
if (i < 0) return; // not a child of this one, release has no assert
this.children.splice(i, 1);
child.parent = child.localPos = undefined;
}
/** Check if this object's box overlaps another object's box
* @param {EngineObject} object
* @return {boolean} */
isOverlappingObject(object)
{ return this.isOverlapping(object.pos, object.size); }
/** Check if overlapping a point or aligned bounding box
* @param {Vector2} pos - Center of box
* @param {Vector2} [size=vec2()] - Size of box, uses a point if undefined
* @return {boolean} */
isOverlapping(pos, size=vec2())
{ return isOverlapping(this.pos, this.size, pos, size); }
/** Set how this object collides
* @param {boolean} [collideSolidObjects] - Does it collide with solid objects?
* @param {boolean} [isSolid] - Does it collide with and block other objects? (expensive in large numbers)
* @param {boolean} [collideLevel] - Does it collide with the level, its tile collision layers?
* @param {boolean} [collideRaycast] - Does it collide with raycasts? */
setCollision(collideSolidObjects=true, isSolid=true, collideLevel=true, collideRaycast=true)
{
ASSERT(collideSolidObjects || !isSolid, 'solid objects must be set to collide');
this.collideSolidObjects = collideSolidObjects;
this.isSolid = isSolid;
this.collideLevel = collideLevel;
this.collideRaycast = collideRaycast;
}
/** @deprecated since 1.20, use collideLevel
* @type {boolean} */
get collideTiles() { return this.collideLevel; }
set collideTiles(collide) { this.collideLevel = collide; }
/** Returns string containing info about this object for debugging
* @return {string} */
toString()
{
let text = 'type = ' + this.constructor.name;
if (this.pos.x || this.pos.y)
text += '\npos = ' + this.pos;
if (this.velocity.x || this.velocity.y)
text += '\nvelocity = ' + this.velocity;
if (this.size.x || this.size.y)
text += '\nsize = ' + this.size;
if (this.angle)
text += '\nangle = ' + this.angle.toFixed(3);
text += '\ncolor = ' + this.color;
return text;
}
/** Render debug info for this object */
renderDebugInfo()
{
if (!debug) return;
// check if there is anything to show
const hasPhysics = this.collideLevel || this.collideSolidObjects || this.isSolid;
if (!hasPhysics && !this.parent) return;
// show object info for debugging
const size = vec2(max(this.size.x, .2), max(this.size.y, .2));
const color = rgb(this.collideLevel?1:0, this.collideSolidObjects?1:0, this.isSolid?1:0, .5);
debugRect(this.pos, size, color, 0, hasPhysics ? 0 : this.angle, hasPhysics, false); // collision ignores the angle
if (this.parent)
debugRect(this.pos, size.scale(.8), rgb(1,1,1,.5), 0, this.angle, false, false);
this.parent && debugLine(this.pos, this.parent.pos, rgb(1,1,1,.5), .5, 0, false);
}
}