/**
* LittleJS Box2D Physics Plugin
* - Box2dObject extends EngineObject with Box2D physics
* - Call box2dInit() to enable
* - You will also need to include box2d.wasm.js
* - Uses a super fast web assembly port of Box2D v2.3.1
* - More info: https://github.com/kripken/box2d.js
* - Functions to create polygon, circle, and edge shapes
* - Contact begin and end callbacks
* - Wraps b2Vec2 type to/from Vector2
* - Raycasting and querying
* - Box2dTileLayer for grid based collision
* - Every type of joint
* - Debug physics drawing
* - Box2D works per second: its velocities and accelerations are in units per second, and it reads the engine's
* gravity as units per second squared, where an EngineObject's are per frame
* @namespace Box2D
*/
'use strict';
/** Global Box2d Plugin object
* @type {Box2dPlugin}
* @memberof Box2D */
let box2d;
/** Enable Box2D debug drawing
* @type {boolean}
* @default
* @memberof Box2D */
let box2dDebug = false;
// Box2D copies every vector it is given, so the plugin hands it these two instead of making a new one each call,
// which the binding would keep; a call that takes two vectors uses both
let box2dTempVectors;
function box2dTemp(v, slot=0)
{
ASSERT(isVector2(v));
const temp = (box2dTempVectors ||= [new box2d.instance.b2Vec2(), new box2d.instance.b2Vec2()])[slot];
temp.Set(v.x, v.y);
return temp;
}
// the native objects a query needs, one of each kind made once and reused, since the binding keeps every one made;
// a query sets the callback's ReportFixture before each use, so the one callback serves every query of its kind
const box2dQueryObjects = {};
const box2dGravity = {x:NaN, y:NaN}; // the gravity the world was last given
function box2dQueryObject(key, type) { return box2dQueryObjects[key] ||= new box2d.instance[type](); }
// Box2D finds fixtures by boxes it pads and stretches ahead along the velocity, so a query checks the shape's own box
function box2dFixtureOverlaps(fixture, aabb)
{
const shape = fixture.GetShape(), transform = fixture.GetBody().GetTransform();
const shapeBox = box2dQueryObject('shapeAABB', 'b2AABB');
const lower = aabb.get_lowerBound(), upper = aabb.get_upperBound();
for (let i = shape.GetChildCount(); i--;)
{
shape.ComputeAABB(shapeBox, transform, i);
const a = shapeBox.get_lowerBound(), b = shapeBox.get_upperBound();
if (a.get_x() <= upper.get_x() && b.get_x() >= lower.get_x() &&
a.get_y() <= upper.get_y() && b.get_y() >= lower.get_y())
return true;
}
return false;
}
// the points of an edge list or loop, without one that repeats the one before, or a loop's closing repeat of the first;
// an edge of no length gives its neighbors a ghost vertex on their own end, and bodies fall through them
function box2dEdgePoints(points, loop)
{
// each point is kept if it is far enough from the last one kept, so a densely sampled curve keeps every few of
// its points rather than losing them all
const slop2 = .005**2; // Box2D's linear slop, what its own chain shape asserts consecutive points are apart
const kept = [];
for (const p of points)
if (!kept.length || p.distanceSquared(kept[kept.length-1]) > slop2)
kept.push(p);
while (loop && kept.length > 1 && kept[kept.length-1].distanceSquared(kept[0]) <= slop2)
kept.pop();
return kept;
}
// wake a body and whatever touches it, Box2D does not when a body is moved, and a sleeping pair never updates
function box2dWakeWithContacts(body)
{
body.SetAwake(true);
for (let edge = body.GetContactList(); !box2d.isNull(edge); edge = edge.get_next())
edge.get_other().SetAwake(true);
}
// wake both bodies of a joint whose length, spring, ratio or strength changed, a sleeping body would stay where it was
function box2dWakeJoint(joint)
{
joint.GetBodyA().SetAwake(true);
joint.GetBodyB().SetAwake(true);
}
// what Box2D adds to the inertia for a center of mass away from the origin, in float32 as it does it, so taking it
// off again gives exactly what Box2D keeps: an inertia of 0, a locked rotation, stays 0 and not a speck either side
function box2dCenterInertia(mass, x, y)
{
const f = Math.fround;
return f(f(mass)*f(f(f(x)*f(x)) + f(f(y)*f(y))));
}
// what cannot happen while the world steps, like losing a body from a contact callback: done now, or queued until
// the step is done; a body going calls endContact for what it touched, and a destroy from there waits in the queue
// too, since the contact it would free is still in use, so the queue runs one at a time in the order things came
const box2dPending = [];
let box2dPendingBusy = 0;
function box2dWhenUnlocked(f)
{
box2dPending.push(f);
if (!box2d.world.IsLocked() && !box2dPendingBusy)
box2dRunPending();
}
function box2dRunPending()
{
++box2dPendingBusy;
try { while (box2dPending.length) box2dPending.shift()(); }
finally { --box2dPendingBusy; }
}
// each Box2dJoint by its native pointer, so a joint Box2D destroys along with a body can let go of its wrapper
const box2dJoints = new Map;
// the gear joints, kept apart from the others, since every joint that goes looks through them
const box2dGearJoints = new Set;
// a gear joint keeps pointers to the joints it gears and to their bodies, so it goes before either joint does
function box2dDestroyGears(joint)
{
for (const gear of box2dGearJoints)
{
if (!gear.box2dJoint)
box2dGearJoints.delete(gear); // gone already, with a body
else if (gear.joint1 === joint || gear.joint2 === joint)
{
box2dGearJoints.delete(gear);
gear.destroy();
}
}
}
/** Enable Box2D debug drawing
* @param {boolean} enable
* @memberof Box2D */
function box2dSetDebug(enable) { box2dDebug = enable; }
///////////////////////////////////////////////////////////////////////////////
/**
* Box2D Object - extend with your own custom physics objects
* - A LittleJS object with Box2D physics, dynamic by default
* - Provides interface for Box2D body and fixture functions
* - Each object can have multiple fixtures and joints
* - Angular values are clockwise like angle: angular velocity, torque, joint angles, limits and motor speeds
* @extends EngineObject
* @memberof Box2D
*/
class Box2dObject extends EngineObject
{
/** Create a LittleJS object with Box2d physics
* @param {Vector2} [pos]
* @param {Vector2} [size]
* @param {TileInfo} [tileInfo]
* @param {number} [angle]
* @param {Color} [color]
* @param {number} [bodyType]
* @param {number} [renderOrder] */
constructor(pos=vec2(), size=vec2(), tileInfo, angle=0, color, bodyType=box2d.bodyTypeDynamic, renderOrder=0)
{
ASSERT(!box2d.world.IsLocked(), 'cannot create Box2D bodies during a contact callback');
super(pos, size, tileInfo, angle, color, renderOrder);
// create physics body, Box2D copies the def
const bodyDef = new box2d.instance.b2BodyDef();
bodyDef.set_type(bodyType);
bodyDef.set_position(box2dTemp(pos));
bodyDef.set_angle(-angle);
/** @property {Object} - The Box2d body, undefined once it is destroyed */
this.body = box2d.world.CreateBody(bodyDef);
box2d.instance.destroy(bodyDef);
/** @property {Color} - Line color used for default box2d drawing */
this.lineColor = BLACK.copy();
/** @property {number} - Line width used for default box2d drawing */
this.lineWidth = .1;
/** @property {Array<Array<Vector2>>} - List of all edges for default box2d drawing
* @type {Array<Array<Vector2>>} */
this.edgeLists = [];
/** @property {Array<Array<Vector2>>} - List of all edge loops for default box2d drawing
* @type {Array<Array<Vector2>>} */
this.edgeLoops = [];
// the fixtures of the edge lists and loops, by pointer, each to the points it is drawn with
this.edgeListFixtures = new Map;
this.body.object = this; // link body to this object
box2d.objects.push(this); // keep track of all box2d objects
}
/** Destroy this object and its physics body
* @param {boolean} [immediate] - Remove it now, as EngineObject.destroy does, children included */
destroy(immediate=false)
{
if (this.destroyed) return;
// destroy physics body, fixtures, and joints; from a contact callback the world is still
// stepping and cannot lose a body, so it goes as soon as the step is done; the object
// leaves box2d.objects at the next step, or the next frame while paused or time is stopped;
// the body lets go of it after, since destroying it calls endContact, which finds it there;
// that endContact can destroy this same object again, before it is marked destroyed, so the body is
// destroyed only by the call that still finds it, Box2D would lose count of its bodies on a second
ASSERT(this.body, 'Box2dObject has no body to destroy');
const body = this.body;
box2dWhenUnlocked(()=>
{
if (this.body !== body) return; // destroyed already
box2d.world.DestroyBody(body);
body.object = undefined;
this.body = undefined;
});
super.destroy(immediate);
}
/** Box2d objects updated with Box2d world step */
updatePhysics() {}
/** Update the object transform, called automatically by engine even when paused;
* its body places it, so it can be a parent but not a child, connect it to another with a joint
* @param {boolean} [updateChildren] - Also update the children's transforms */
updateTransforms(updateChildren=true)
{
ASSERT(!this.parent, 'a Box2dObject cannot be a child, its body would stay behind, connect it with a joint');
super.updateTransforms(updateChildren);
}
/** Render the object, uses box2d drawing if no tile info exists */
render()
{
// use default render or draw fixtures
if (this.tileInfo)
super.render();
else
this.drawFixtures(this.color, this.lineColor, this.lineWidth);
}
/** Render debug info */
renderDebugInfo()
{
const isAsleep = !this.getIsAwake();
const isStatic = this.getBodyType() === box2d.bodyTypeStatic;
const color = rgb(isAsleep?1:0, isAsleep?1:0, isStatic?1:0, .5);
this.drawFixtures(color);
}
/** Draws all this object's fixtures
* @param {Color} [color]
* @param {Color} [lineColor]
* @param {number} [lineWidth]
* @param {boolean} [useWebGL=glEnable]
* @param {CanvasRenderingContext2D} [context] */
drawFixtures(color=WHITE, lineColor=BLACK, lineWidth=.1, useWebGL, context)
{
// draw each fixture, but the edges of an edge list or loop, which draw below as one line
const edgeFixtures = this.edgeListFixtures;
this.getFixtureList().forEach((fixture)=>
{
if (!edgeFixtures.has(box2d.instance.getPointer(fixture)))
box2d.drawFixture(fixture, this.pos, this.angle, color, lineColor, lineWidth, useWebGL, context);
});
// draw edges using a single draw line for better connections
this.edgeLists.forEach(points=>
drawLineList(points, lineWidth, lineColor, false, this.pos, this.angle, useWebGL, false, context));
this.edgeLoops.forEach(points=>
drawLineList(points, lineWidth, lineColor, true, this.pos, this.angle, useWebGL, false, context));
}
///////////////////////////////////////////////////////////////////////////////
// physics contact callbacks
/** Called when a contact begins, while the world steps: a destroy or a setter waits until the step is done,
* and creating objects, fixtures or joints must wait until after the step
* - The fixtures say which shapes touched, the same objects addBox and the others returned, so a small sensor
* under a player's feet can tell standing on the ground from touching a wall
* @param {Box2dObject} otherObject
* @param {Object} [fixture] - This object's fixture that touched
* @param {Object} [otherFixture] - The other object's fixture that touched */
beginContact(otherObject, fixture, otherFixture) {}
/** Called when a contact ends, while the world steps or a body is destroyed: a destroy or a setter waits
* until the step is done, and creating objects, fixtures or joints must wait until after the step
* @param {Box2dObject} otherObject
* @param {Object} [fixture] - This object's fixture that touched
* @param {Object} [otherFixture] - The other object's fixture that touched */
endContact(otherObject, fixture, otherFixture) {}
///////////////////////////////////////////////////////////////////////////////
// physics fixtures and shapes
/** Add a shape fixture to the body
* @param {Object} shape
* @param {number} [density]
* @param {number} [friction]
* @param {number} [restitution]
* @param {boolean} [isSensor] */
addShape(shape, density=1, friction=.2, restitution=0, isSensor=false)
{
ASSERT(isNumber(density), 'density must be a number');
ASSERT(isNumber(friction), 'friction must be a number');
ASSERT(isNumber(restitution), 'restitution must be a number');
ASSERT(!box2d.world.IsLocked(), 'cannot create Box2D fixtures during a contact callback');
// Box2D copies the def and the shape
const fd = new box2d.instance.b2FixtureDef();
fd.set_shape(shape);
fd.set_density(density);
fd.set_friction(friction);
fd.set_restitution(restitution);
fd.set_isSensor(isSensor);
const fixture = this.body.CreateFixture(fd);
box2d.instance.destroy(fd);
return fixture;
}
/** Add a box shape to the body
* @param {Vector2} [size]
* @param {Vector2} [offset]
* @param {number} [angle] - LittleJS convention (clockwise positive).
* Negated internally to match Box2D's CCW-positive convention so the
* fixture aligns with the same angle passed to drawRect/drawTile.
* @param {number} [density]
* @param {number} [friction]
* @param {number} [restitution]
* @param {boolean} [isSensor] */
addBox(size=vec2(1), offset=vec2(), angle=0, density, friction, restitution, isSensor)
{
ASSERT(isVector2(size), 'size must be a Vector2');
ASSERT(size.x > 0 && size.y > 0, 'size must be positive');
ASSERT(isVector2(offset), 'offset must be a Vector2');
ASSERT(isNumber(angle), 'angle must be a number');
// Box2D stops for good on a box with almost no area, like addPoly no fixture is made from one
ASSERT(size.x * size.y > 1e-6, 'box is too small for Box2D');
if (!(size.x * size.y > 1e-6)) return;
const shape = new box2d.instance.b2PolygonShape();
shape.SetAsBox(size.x/2, size.y/2, box2dTemp(offset), -angle);
const fixture = this.addShape(shape, density, friction, restitution, isSensor);
box2d.instance.destroy(shape); // the fixture has its own copy
return fixture;
}
/** Add a polygon shape to the body, the convex hull of its points; Box2D takes 3 to 8 points,
* not all in a line, and no fixture is made from any other; points closer than .001 count as one
* @param {Array<Vector2>} points
* @param {number} [density]
* @param {number} [friction]
* @param {number} [restitution]
* @param {boolean} [isSensor] */
addPoly(points, density, friction, restitution, isSensor)
{
ASSERT(isArray(points), 'points must be an array');
function box2dCreatePolygonShape(points)
{
// Box2D stops for good on two hull points that nearly meet, like a loop whose last point is its first
points = points.filter((p, i)=> points.slice(0, i).every(q=> p.distanceSquared(q) > 1e-6));
// Box2D stops for good on a polygon it cannot take, so one with too many points or no area makes none;
// it takes the convex hull, which is at least as big as the biggest triangle of the points
let area = 0; // twice the biggest triangle's
if (3 <= points.length && points.length <= 8)
for (const a of points) for (const b of points) for (const c of points)
area = max(area, abs(b.subtract(a).cross(c.subtract(a))));
ASSERT(area >= 1e-6, 'Box2D polygons need 3 to 8 points, not all in a line');
if (!(area >= 1e-6)) return;
const buffer = box2d.instance._malloc(points.length * 8);
for (let i=0, offset=0; i<points.length; ++i)
{
box2d.instance.HEAPF32[buffer + offset >> 2] = points[i].x;
offset += 4;
box2d.instance.HEAPF32[buffer + offset >> 2] = points[i].y;
offset += 4;
}
const box2dPoints = box2d.instance.wrapPointer(buffer, box2d.instance.b2Vec2);
const shape = new box2d.instance.b2PolygonShape();
shape.Set(box2dPoints, points.length);
box2d.instance._free(buffer);
return shape;
}
const shape = box2dCreatePolygonShape(points);
if (!shape) return;
const fixture = this.addShape(shape, density, friction, restitution, isSensor);
box2d.instance.destroy(shape); // the fixture has its own copy
return fixture;
}
/** Add a regular polygon shape to the body
* @param {number} [diameter]
* @param {number} [sides] - 3 to 8, the most Box2D polygons have
* @param {number} [density]
* @param {number} [friction]
* @param {number} [restitution]
* @param {boolean} [isSensor] */
addRegularPoly(diameter=1, sides=8, density, friction, restitution, isSensor)
{
ASSERT(isNumber(diameter) && diameter>0, 'diameter must be a positive number');
ASSERT(isNumber(sides) && sides>2, 'sides must be a positive number greater than 2');
ASSERT(sides <= 8, 'Box2D polygons have at most 8 sides');
sides = min(sides, 8); // more would stop Box2D for good
const points = [];
const radius = diameter/2;
for (let i=sides; i--;)
points.push(vec2(radius,0).rotate((i+.5)/sides*PI*2));
return this.addPoly(points, density, friction, restitution, isSensor);
}
/** Add a random polygon shape to the body
* @param {number} [diameter]
* @param {number} [density]
* @param {number} [friction]
* @param {number} [restitution]
* @param {boolean} [isSensor] */
addRandomPoly(diameter=1, density, friction, restitution, isSensor)
{
ASSERT(isNumber(diameter) && diameter>0, 'diameter must be a positive number');
const sides = randInt(3, 9);
const points = [];
const radius = diameter/2;
for (let i=sides; i--;)
points.push(vec2(rand(radius/2,radius*1.5),0).rotate(i/sides*PI*2));
return this.addPoly(points, density, friction, restitution, isSensor);
}
/** Add a circle shape to the body
* @param {number} [diameter]
* @param {Vector2} [offset]
* @param {number} [density]
* @param {number} [friction]
* @param {number} [restitution]
* @param {boolean} [isSensor] */
addCircle(diameter=1, offset=vec2(), density, friction, restitution, isSensor)
{
ASSERT(isNumber(diameter) && diameter>0, 'diameter must be a positive number');
ASSERT(isVector2(offset), 'offset must be a Vector2');
const shape = new box2d.instance.b2CircleShape();
shape.set_m_p(box2dTemp(offset));
shape.set_m_radius(diameter/2);
const fixture = this.addShape(shape, density, friction, restitution, isSensor);
box2d.instance.destroy(shape); // the fixture has its own copy
return fixture;
}
/** Add an edge shape to the body
* @param {Vector2} point1
* @param {Vector2} point2
* @param {number} [density]
* @param {number} [friction]
* @param {number} [restitution]
* @param {boolean} [isSensor] */
addEdge(point1, point2, density, friction, restitution, isSensor)
{
ASSERT(isVector2(point1), 'point1 must be a Vector2');
ASSERT(isVector2(point2), 'point2 must be a Vector2');
const shape = new box2d.instance.b2EdgeShape();
shape.Set(box2dTemp(point1), box2dTemp(point2, 1));
const fixture = this.addShape(shape, density, friction, restitution, isSensor);
box2d.instance.destroy(shape); // the fixture has its own copy
return fixture;
}
/** Add an edge list to the body
* @param {Array<Vector2>} points
* @param {number} [density]
* @param {number} [friction]
* @param {number} [restitution]
* @param {boolean} [isSensor] */
addEdgeList(points, density, friction, restitution, isSensor)
{
ASSERT(isArray(points), 'points must be an array');
points = box2dEdgePoints(points);
const fixtures = [], edgePoints = [];
if (points.length < 2) return fixtures;
for (let i=0; i<points.length-1; ++i)
{
// the ghost vertices, where there is a neighbor, make the edges one smooth surface
const shape = new box2d.instance.b2EdgeShape();
points[i-1] && shape.set_m_vertex0(box2dTemp(points[i-1]));
points[i+0] && shape.set_m_vertex1(box2dTemp(points[i+0]));
points[i+1] && shape.set_m_vertex2(box2dTemp(points[i+1]));
points[i+2] && shape.set_m_vertex3(box2dTemp(points[i+2]));
shape.set_m_hasVertex0(!!points[i-1]);
shape.set_m_hasVertex3(!!points[i+2]);
const f = this.addShape(shape, density, friction, restitution, isSensor);
box2d.instance.destroy(shape); // the fixture has its own copy
fixtures.push(f);
edgePoints.push(points[i].copy());
}
edgePoints.push(points[points.length-1].copy());
this.edgeLists.push(edgePoints);
fixtures.forEach(f=> this.edgeListFixtures.set(box2d.instance.getPointer(f), edgePoints));
return fixtures;
}
/** Add an edge loop to the body, an edge loop connects the end points
* @param {Array<Vector2>} points
* @param {number} [density]
* @param {number} [friction]
* @param {number} [restitution]
* @param {boolean} [isSensor] */
addEdgeLoop(points, density, friction, restitution, isSensor)
{
ASSERT(isArray(points), 'points must be an array');
points = box2dEdgePoints(points, true);
const fixtures = [], edgePoints = [];
if (points.length < 2) return fixtures;
const getPoint = i=> points[mod(i,points.length)];
for (let i=0; i<points.length; ++i)
{
const shape = new box2d.instance.b2EdgeShape();
shape.set_m_vertex0(box2dTemp(getPoint(i-1)));
shape.set_m_vertex1(box2dTemp(getPoint(i+0)));
shape.set_m_vertex2(box2dTemp(getPoint(i+1)));
shape.set_m_vertex3(box2dTemp(getPoint(i+2)));
shape.set_m_hasVertex0(true);
shape.set_m_hasVertex3(true);
const f = this.addShape(shape, density, friction, restitution, isSensor);
box2d.instance.destroy(shape); // the fixture has its own copy
fixtures.push(f);
edgePoints.push(points[i].copy());
}
this.edgeLoops.push(edgePoints);
fixtures.forEach(f=> this.edgeListFixtures.set(box2d.instance.getPointer(f), edgePoints));
return fixtures;
}
/** Destroy a fixture from the body, from a contact callback once the step is done
* @param {Object} fixture */
destroyFixture(fixture)
{
const pointer = box2d.instance.getPointer(fixture);
// an edge list or loop that loses a fixture is no longer one line, what is left of it draws edge by edge
const edgeFixtures = this.edgeListFixtures, points = edgeFixtures.get(pointer);
if (points)
{
this.edgeLists = this.edgeLists.filter(p=> p !== points);
this.edgeLoops = this.edgeLoops.filter(p=> p !== points);
edgeFixtures.forEach((p, pointer)=> p === points && edgeFixtures.delete(pointer));
}
// not once the body is gone, which takes its fixtures with it, or once the fixture is,
// since a second destroy of it, like one from each of two contacts in a step, stops Box2D for good
box2dWhenUnlocked(()=> this.body && this.getFixtureList().some(f=> box2d.instance.getPointer(f) === pointer)
&& this.body.DestroyFixture(fixture));
}
/** Destroy all fixtures from the body, from a contact callback once the step is done */
destroyAllFixtures()
{
// the fixtures it has now, each destroyed if still there, in one pass so a big tile layer rebuilds quickly
this.edgeLists = [];
this.edgeLoops = [];
this.edgeListFixtures.clear();
const fixtures = this.getFixtureList(), getPointer = box2d.instance.getPointer;
box2dWhenUnlocked(()=>
{
if (!this.body) return;
const alive = new Set(this.getFixtureList().map(getPointer));
for (const fixture of fixtures)
alive.has(getPointer(fixture)) && this.body.DestroyFixture(fixture);
});
}
///////////////////////////////////////////////////////////////////////////////
// physics get functions
/** Gets the center of mass in world space
* @return {Vector2} */
getCenterOfMass() { return box2d.vec2From(this.body.GetWorldCenter()); }
/** Gets the linear velocity
* @return {Vector2} */
getLinearVelocity() { return box2d.vec2From(this.body.GetLinearVelocity()); }
/** Gets the angular velocity, clockwise like angle
* @return {number} */
getAngularVelocity() { return -this.body.GetAngularVelocity(); } // box2d uses reverse angle
/** Gets the mass
* @return {number} */
getMass() { return this.body.GetMass(); }
/** Gets the rotational inertia about the center of mass
* @return {number} */
getInertia()
{
// Box2D gives it about the body origin
const center = this.body.GetLocalCenter();
return max(0, Math.fround(this.body.GetInertia() - box2dCenterInertia(this.getMass(), center.get_x(), center.get_y())));
}
/** Check if this object is awake
* @return {boolean} */
getIsAwake() { return this.body.IsAwake(); }
/** Gets the physics body type
* @return {number} */
getBodyType() { return this.body.GetType(); }
/** Get the speed of this object
* @return {number} */
getSpeed() { return this.getLinearVelocity().length(); }
///////////////////////////////////////////////////////////////////////////////
// physics set functions
/** Sets the position and angle, from a contact callback the body moves once the step is done
* @param {Vector2} pos
* @param {number} angle */
setTransform(pos, angle)
{
this.pos = pos.copy();
this.angle = angle;
// box2d uses reverse angle
const x = pos.x, y = pos.y;
box2dWhenUnlocked(()=>
{
if (!this.body) return;
this.body.SetTransform(box2dTemp(vec2(x, y)), -angle);
box2dWakeWithContacts(this.body); // Box2D leaves a sleeping body, and what rests on it, in the air
});
}
/** Sets the position, from a contact callback the body moves once the step is done, keeping the angle it has then
* @param {Vector2} pos */
setPosition(pos)
{
this.pos = pos.copy();
const x = pos.x, y = pos.y;
box2dWhenUnlocked(()=>
{
if (!this.body) return;
this.body.SetTransform(box2dTemp(vec2(x, y)), this.body.GetAngle());
box2dWakeWithContacts(this.body);
});
}
/** Sets the angle, from a contact callback the body turns once the step is done, keeping the position it has then
* @param {number} angle */
setAngle(angle)
{
this.angle = angle;
box2dWhenUnlocked(()=>
{
if (!this.body) return;
this.body.SetTransform(this.body.GetPosition(), -angle); // box2d uses reverse angle
box2dWakeWithContacts(this.body);
});
}
/** Sets the linear velocity
* @param {Vector2} velocity */
setLinearVelocity(velocity)
{ this.body.SetLinearVelocity(box2dTemp(velocity)); }
/** Sets the angular velocity, clockwise like angle
* @param {number} angularVelocity */
setAngularVelocity(angularVelocity)
{ this.body.SetAngularVelocity(-angularVelocity); }
/** Sets the linear damping
* @param {number} damping */
setLinearDamping(damping)
{ this.body.SetLinearDamping(damping); }
/** Sets the angular damping
* @param {number} damping */
setAngularDamping(damping)
{ this.body.SetAngularDamping(damping); }
/** Sets the gravity scale
* @param {number} [scale] */
setGravityScale(scale=1)
{
this.body.SetGravityScale(this.gravityScale = scale);
this.body.SetAwake(true); // a sleeping body would not feel it
}
/** Should be like a bullet for continuous collision detection?
* @param {boolean} [isBullet] */
setBullet(isBullet=true) { this.body.SetBullet(isBullet); }
/** Set the sleep state of the body
* @param {boolean} [isAwake] */
setAwake(isAwake=true) { this.body.SetAwake(isAwake); }
/** Set the physics body type, from a contact callback it changes once the step is done
* @param {number} type */
setBodyType(type) { box2dWhenUnlocked(()=> this.body && this.body.SetType(type)); }
/** Set whether the body is allowed to sleep
* @param {boolean} [isAllowed] */
setSleepingAllowed(isAllowed=true)
{ this.body.SetSleepingAllowed(isAllowed); }
/** Set whether the body can rotate
* @param {boolean} [isFixed] */
setFixedRotation(isFixed=true)
{
this.body.SetFixedRotation(isFixed);
this.body.SetAwake(true); // a sleeping body would not tip over once it can turn
}
/** Set the center of mass of the body, local to it
* @param {Vector2} center */
setCenterOfMass(center) { this.setMassData(center) }
/** Set the mass of the body
* @param {number} mass */
setMass(mass) { this.setMassData(undefined, mass) }
/** Set the moment of inertia of the body, about its center of mass
* @param {number} momentOfInertia */
setMomentOfInertia(momentOfInertia)
{ this.setMassData(undefined, undefined, momentOfInertia) }
/** Reset the mass, center of mass, and moment, from a contact callback once the step is done */
resetMassData() { box2dWhenUnlocked(()=> this.body && this.body.ResetMassData()); }
/** Set the mass data of the body, from a contact callback once the step is done;
* a mass of 0 or less becomes 1, use setBodyType for a static body; call it after adding fixtures and after
* setFixedRotation, both of which put the mass back to what the fixtures give
* @param {Vector2} [localCenter]
* @param {number} [mass]
* @param {number} [momentOfInertia] - About the center of mass */
setMassData(localCenter, mass, momentOfInertia)
{
localCenter = localCenter && localCenter.copy(); // as it is now, even if it waits for the step
box2dWhenUnlocked(()=>
{
if (!this.body) return;
const data = box2dQueryObject('massData', 'b2MassData'); // reused, GetMassData fills it in
this.body.GetMassData(data);
// Box2D's inertia is about the body origin, so it is turned to the center of mass and back; kept as
// it was, one for the old mass and center goes below 0 about the new ones, which stops Box2D for good
const center = data.get_center(), oldMass = data.get_mass();
const cx = localCenter ? localCenter.x : center.get_x(), cy = localCenter ? localCenter.y : center.get_y();
// it is worked out in float32 as Box2D does it, which must come out above 0 or the rotation is locked
const f = Math.fround;
const oldInertia = f(data.get_I() - box2dCenterInertia(oldMass, center.get_x(), center.get_y()));
const inertia = momentOfInertia ?? oldInertia;
mass ??= oldMass;
const offset = box2dCenterInertia(mass > 0 ? mass : 1, cx, cy); // a mass of 0 or less is 1 to Box2D
const I = f(inertia + offset);
data.set_mass(mass);
data.set_center(box2dTemp(vec2(cx, cy)));
data.set_I(inertia > 0 && f(I - offset) > 0 ? I : 0);
this.body.SetMassData(data);
this.body.SetAwake(true); // a sleeping body would not tip over a new center of mass
});
}
/** Set the collision filter data for the fixtures this body has now, a fixture added later has the default
* filter, category 1 colliding with everything
* @param {number} [categoryBits]
* @param {number} [ignoreCategoryBits]
* @param {number} [groupIndex] */
setFilterData(categoryBits=1, ignoreCategoryBits=0, groupIndex=0)
{
this.getFixtureList().forEach(fixture=>
{
const filter = fixture.GetFilterData();
filter.set_categoryBits(categoryBits);
filter.set_maskBits(0xffff & ~ignoreCategoryBits);
filter.set_groupIndex(groupIndex);
fixture.SetFilterData(filter); // applies and refilters contacts
});
}
/** Set if this body is a sensor
* @param {boolean} [isSensor] */
setSensor(isSensor=true)
{
this.getFixtureList().forEach(f=>f.SetSensor(isSensor));
box2dWakeWithContacts(this.body); // what rests on it or sits in it moves again, a sleeping pair never updates
}
///////////////////////////////////////////////////////////////////////////////
// physics force and torque functions
/** Apply force to this object
* @param {Vector2} force
* @param {Vector2} [pos] */
applyForce(force, pos)
{
pos ||= this.getCenterOfMass();
this.setAwake();
this.body.ApplyForce(box2dTemp(force), box2dTemp(pos, 1));
}
/** Apply acceleration to this object (changes velocity by acceleration,
* mass-independent like EngineObject.applyAcceleration, but in units per second).
* Use applyImpulse if you want the mass-dependent velocity change
* Δv = impulse / mass, or applyForce for a Newton-style sustained force.
* @param {Vector2} acceleration
* @param {Vector2} [pos] */
applyAcceleration(acceleration, pos)
{
pos ||= this.getCenterOfMass();
this.setAwake();
const impulse = acceleration.scale(this.getMass());
this.body.ApplyLinearImpulse(box2dTemp(impulse), box2dTemp(pos, 1));
}
/** Apply an instantaneous linear impulse. Changes velocity immediately by
* impulse / mass (so heavier bodies move less for the same impulse).
* @param {Vector2} impulse
* @param {Vector2} [pos] */
applyImpulse(impulse, pos)
{
pos ||= this.getCenterOfMass();
this.setAwake();
this.body.ApplyLinearImpulse(box2dTemp(impulse), box2dTemp(pos, 1));
}
/** Apply torque to this object, clockwise like angle
* @param {number} torque */
applyTorque(torque)
{
this.setAwake();
this.body.ApplyTorque(-torque);
}
/** Apply angular acceleration to this object (changes angular velocity by
* acceleration, mass-independent, clockwise — matches EngineObject.applyAngularAcceleration).
* @param {number} acceleration */
applyAngularAcceleration(acceleration)
{
// the velocity itself, since the inertia Box2D gives is about the origin, not the center of mass;
// like the impulse this was, only a dynamic body turns
if (this.getBodyType() !== box2d.bodyTypeDynamic || this.body.IsFixedRotation()) return;
this.setAwake();
this.setAngularVelocity(this.getAngularVelocity() + acceleration);
}
/** Apply an instantaneous angular impulse. Changes angular velocity by
* impulse / inertia immediately, clockwise like angle.
* @param {number} impulse */
applyAngularImpulse(impulse)
{
this.setAwake();
this.body.ApplyAngularImpulse(-impulse);
}
///////////////////////////////////////////////////////////////////////////////
// lists of fixtures and joints
/** Check if this object has any fixtures
* @return {boolean} */
hasFixtures() { return !box2d.isNull(this.body.GetFixtureList()); }
/** Get list of fixtures for this object
* @return {Array<Object>} */
getFixtureList()
{
const fixtures = [];
for (let fixture=this.body.GetFixtureList(); !box2d.isNull(fixture); )
{
fixtures.push(fixture);
fixture = fixture.GetNext();
}
return fixtures;
}
/** Check if this object has any joints
* @return {boolean} */
hasJoints() { return this.getJointList().length > 0; } // not the ones destroyed and waiting for the step to end
/** Get list of joints for this object, the Box2dJoint for each one made through LittleJS,
* and the Box2D joint, cast to its type, for any made on the world directly
* @return {Array<Box2dJoint|Object>} */
getJointList()
{
// the body keeps a list of edges, each holding a joint and the next edge
const joints = [];
for (let edge=this.body.GetJointList(); !box2d.isNull(edge); edge = edge.get_next())
{
const joint = edge.get_joint(), wrapper = box2dJoints.get(box2d.instance.getPointer(joint));
if (wrapper && !wrapper.box2dJoint)
continue; // destroyed in a contact callback, Box2D lets go of it once the step is done
joints.push(wrapper || box2d.castJointObject(joint));
}
return joints;
}
}
///////////////////////////////////////////////////////////////////////////////
/**
* Box2D Static Object - Box2d with a static physics body
* @extends Box2dObject
* @memberof Box2D
*/
class Box2dStaticObject extends Box2dObject
{
/** Create a LittleJS object with Box2d physics
* @param {Vector2} [pos]
* @param {Vector2} [size]
* @param {TileInfo} [tileInfo]
* @param {number} [angle]
* @param {Color} [color]
* @param {number} [renderOrder] */
constructor(pos, size, tileInfo, angle=0, color, renderOrder=0)
{
const bodyType = box2d.bodyTypeStatic;
super(pos, size, tileInfo, angle, color, bodyType, renderOrder);
}
}
///////////////////////////////////////////////////////////////////////////////
/**
* Box2D Kinematic Object - Box2d with a kinematic physics body
* @extends Box2dObject
* @memberof Box2D
*/
class Box2dKinematicObject extends Box2dObject
{
/** Create a LittleJS object with Box2d physics
* @param {Vector2} [pos]
* @param {Vector2} [size]
* @param {TileInfo} [tileInfo]
* @param {number} [angle]
* @param {Color} [color]
* @param {number} [renderOrder] */
constructor(pos, size, tileInfo, angle=0, color, renderOrder=0)
{
const bodyType = box2d.bodyTypeKinematic;
super(pos, size, tileInfo, angle, color, bodyType, renderOrder);
}
}
///////////////////////////////////////////////////////////////////////////////
/**
* Box2d Tile Layer
* - adds Box2d support to tile layers
* - creates static box2d fixtures for solid tiles, call buildCollision to rebuild them after the tiles change
* @extends Box2dStaticObject
* @memberof Box2D
*/
class Box2dTileLayer extends Box2dStaticObject
{
/** Create a Box2d tile layer object
* @param {TileCollisionLayer} tileLayer - Tile layer for this object */
constructor(tileLayer)
{
ASSERT(tileLayer instanceof TileCollisionLayer, 'tileLayer must be a TileCollisionLayer');
super(tileLayer.pos, tileLayer.size);
/** @property {TileCollisionLayer} - The tile layer */
this.tileLayer = tileLayer;
this.addChild(tileLayer);
// collision for the solid tiles it has now, call buildCollision again after changing them
this.buildCollision();
}
render()
{
// do not render fixtures, tile layer handles rendering
}
/** Create box2d collision fixtures for solid tiles
* @param {number} [friction]
* @param {number} [restitution] */
buildCollision(friction=.2, restitution=0)
{
// destroy all fixtures and create new ones
this.destroyAllFixtures();
// track which tiles have been processed
const processed = [];
const getIndex = (x, y)=> x + y * this.size.x;
const isSolidUnprocessed = (x, y)=>
!processed[getIndex(x, y)] &&
this.tileLayer.getCollisionData(vec2(x, y)) > 0;
// combine tiles into larger boxes
for (let x = 0; x < this.size.x; ++x)
for (let y = 0; y < this.size.y; ++y)
{
if (!isSolidUnprocessed(x, y)) continue;
// find max width by scanning right
let width = 1, height = 1, canExpand = true;
while (isSolidUnprocessed(x + width, y))
++width;
// find max height by scanning up, ensuring all rows have the same width
while (canExpand)
{
for (let checkX = 0; checkX < width; ++checkX)
{
if (!isSolidUnprocessed(x + checkX, y + height))
{
canExpand = false;
break;
}
}
if (canExpand)
++height;
}
// mark all tiles in this rectangle as processed
for (let rectX = width; rectX--;)
for (let rectY = height; rectY--;)
processed[getIndex(x + rectX, y + rectY)] = true;
// create a single fixture for the entire rectangle
const shapeSize = vec2(width, height);
const offset = vec2(x + width/2, y + height/2);
this.addBox(shapeSize, offset, 0, 0, friction, restitution);
}
}
}
///////////////////////////////////////////////////////////////////////////////
/**
* Box2D Raycast Result
* - Holds results from a box2d raycast queries
* - Automatically created by box2d raycast functions
* @memberof Box2D
*/
class Box2dRaycastResult
{
/** Create a raycast result
* @param {Object} fixture
* @param {Vector2} point
* @param {Vector2} normal
* @param {number} fraction */
constructor(fixture, point, normal, fraction)
{
/** @property {Box2dObject} - The box2d object
* @type {Box2dObject} */
this.object = fixture.GetBody().object;
/** @property {Object} - The fixture that was hit */
this.fixture = fixture;
/** @property {Vector2} - The hit point */
this.point = point;
/** @property {Vector2} - The hit normal */
this.normal = normal;
/** @property {number} - Distance fraction at the point of intersection */
this.fraction = fraction;
}
}
///////////////////////////////////////////////////////////////////////////////
/**
* Box2D Joint
* - Base class for Box2D joints
* - A joint is used to connect objects together
* - Angular values are clockwise like angle: joint angles and speeds, limits, motor speeds and torques
* @memberof Box2D
*/
class Box2dJoint
{
/** Create a box2d joint, the base class is not intended to be used directly
* @param {Object} jointDef - Freed once the joint is made, Box2D copies it */
constructor(jointDef)
{
ASSERT(!box2d.world.IsLocked(), 'cannot create Box2D joints during a contact callback');
const bodyA = jointDef.get_bodyA(), bodyB = jointDef.get_bodyB();
const bothLive = !box2d.isNull(bodyA) && !box2d.isNull(bodyB);
ASSERT(bothLive, 'a joint needs two objects that are not destroyed');
ASSERT(box2d.instance.getPointer(bodyA) !== box2d.instance.getPointer(bodyB), 'a joint needs two different objects');
/** @property {Object} - The Box2d joint, 0 once it is destroyed, as it is when either object is */
this.box2dJoint = 0;
if (!bothLive)
{
// one of its objects is gone, so it is made destroyed, as it would be had it gone after
box2d.instance.destroy(jointDef);
return;
}
this.box2dJoint = box2d.castJointObject(box2d.world.CreateJoint(jointDef));
box2d.instance.destroy(jointDef);
box2dJoints.set(box2d.instance.getPointer(this.box2dJoint), this);
}
/** Destroy this joint */
destroy()
{
const joint = this.box2dJoint;
if (!joint) return; // destroyed already, or with one of its objects
this.box2dJoint = 0;
box2dDestroyGears(this);
// a body destroyed before it, in the same step, takes the joint with it and lets go of it here
const pointer = box2d.instance.getPointer(joint);
box2dWhenUnlocked(()=>
{
if (box2dJoints.get(pointer) !== this) return;
box2dJoints.delete(pointer);
box2d.world.DestroyJoint(joint);
});
}
/** Get the first object attached to this joint
* @return {Box2dObject} */
getObjectA() { return this.box2dJoint.GetBodyA().object; }
/** Get the second object attached to this joint
* @return {Box2dObject} */
getObjectB() { return this.box2dJoint.GetBodyB().object; }
/** Get the first anchor for this joint in world coordinates
* @return {Vector2} */
getAnchorA() { return box2d.vec2From(this.box2dJoint.GetAnchorA());}
/** Get the second anchor for this joint in world coordinates
* @return {Vector2} */
getAnchorB() { return box2d.vec2From(this.box2dJoint.GetAnchorB());}
/** Get the reaction force on bodyB at the joint anchor over the last step
* @param {number} [time] - The step length in seconds, the world steps by timeDelta
* @return {Vector2} */
getReactionForce(time=timeDelta) { return box2d.vec2From(this.box2dJoint.GetReactionForce(1/time));}
/** Get the reaction torque on bodyB in N*m over the last step, clockwise like angle
* @param {number} [time] - The step length in seconds, the world steps by timeDelta
* @return {number} */
getReactionTorque(time=timeDelta) { return -this.box2dJoint.GetReactionTorque(1/time);} // box2d uses reverse angle
/** Check if the connected bodies should collide
* @return {boolean} */
getCollideConnected() { return this.box2dJoint.GetCollideConnected();}
/** Check if either connected body is active
* @return {boolean} */
isActive() { return !!this.box2dJoint && this.box2dJoint.IsActive(); }
/** Check if the joint is gone, destroyed or taken along with one of its objects; its other methods can not be used then
* @return {boolean} */
isDestroyed() { return !this.box2dJoint; }
}
///////////////////////////////////////////////////////////////////////////////
/**
* Box2D Target Joint, also known as a mouse joint
* - Used to make a point on a object track a specific world point target
* - This a soft constraint with a max force
* - This allows the constraint to stretch and without applying huge forces
* - The object must be dynamic, and stay dynamic while the joint holds it, Box2D stops for good on one with no mass
* @extends Box2dJoint
* @memberof Box2D
*/
class Box2dTargetJoint extends Box2dJoint
{
/** Create a target joint
* @param {Box2dObject} object
* @param {Box2dObject} fixedObject
* @param {Vector2} worldPos */
constructor(object, fixedObject, worldPos)
{
ASSERT(object.getBodyType() === box2d.bodyTypeDynamic, 'a target joint needs a dynamic object');
object.setAwake();
const jointDef = new box2d.instance.b2MouseJointDef();
jointDef.set_bodyA(fixedObject.body);
jointDef.set_bodyB(object.body);
jointDef.set_target(box2dTemp(worldPos));
jointDef.set_maxForce(2e3 * object.getMass());
super(jointDef);
}
/** Set the target point in world coordinates
* @param {Vector2} pos */
setTarget(pos) { this.box2dJoint.SetTarget(box2dTemp(pos)); }
/** Get the target point in world coordinates
* @return {Vector2} */
getTarget(){ return box2d.vec2From(this.box2dJoint.GetTarget()); }
/** Sets the maximum force in Newtons
* @param {number} force */
setMaxForce(force) { this.box2dJoint.SetMaxForce(force); box2dWakeJoint(this.box2dJoint); }
/** Gets the maximum force in Newtons
* @return {number} */
getMaxForce() { return this.box2dJoint.GetMaxForce(); }
/** Sets the joint frequency in Hertz, above 0, Box2D stops for good on 0
* @param {number} hz */
setFrequency(hz) { this.box2dJoint.SetFrequency(max(hz, 1e-3)); box2dWakeJoint(this.box2dJoint); }
/** Gets the joint frequency in Hertz
* @return {number} */
getFrequency() { return this.box2dJoint.GetFrequency(); }
}
///////////////////////////////////////////////////////////////////////////////
/**
* Box2D Distance Joint
* - Constrains two points on two objects to remain at a fixed distance
* - You can view this as a massless, rigid rod
* @extends Box2dJoint
* @memberof Box2D
*/
class Box2dDistanceJoint extends Box2dJoint
{
/** Create a distance joint
* @param {Box2dObject} objectA
* @param {Box2dObject} objectB
* @param {Vector2} [anchorA] - World position, objectA's position if not given
* @param {Vector2} [anchorB] - World position, objectB's position if not given
* @param {boolean} [collide] */
constructor(objectA, objectB, anchorA, anchorB, collide=false)
{
anchorA ||= box2d.vec2From(objectA.body.GetPosition());
anchorB ||= box2d.vec2From(objectB.body.GetPosition());
const localAnchorA = objectA.worldToLocal(anchorA);
const localAnchorB = objectB.worldToLocal(anchorB);
const jointDef = new box2d.instance.b2DistanceJointDef();
jointDef.set_bodyA(objectA.body);
jointDef.set_bodyB(objectB.body);
jointDef.set_localAnchorA(box2dTemp(localAnchorA));
jointDef.set_localAnchorB(box2dTemp(localAnchorB));
jointDef.set_length(anchorA.distance(anchorB));
jointDef.set_collideConnected(collide);
super(jointDef);
}
/** Get the local anchor point relative to objectA's origin
* @return {Vector2} */
getLocalAnchorA() { return box2d.vec2From(this.box2dJoint.GetLocalAnchorA()); }
/** Get the local anchor point relative to objectB's origin
* @return {Vector2} */
getLocalAnchorB() { return box2d.vec2From(this.box2dJoint.GetLocalAnchorB()); }
/** Set the length of the joint
* @param {number} length */
setLength(length) { this.box2dJoint.SetLength(length); box2dWakeJoint(this.box2dJoint); }
/** Get the length of the joint
* @return {number} */
getLength() { return this.box2dJoint.GetLength(); }
/** Set the frequency in Hertz
* @param {number} hz */
setFrequency(hz) { this.box2dJoint.SetFrequency(hz); box2dWakeJoint(this.box2dJoint); }
/** Get the frequency in Hertz
* @return {number} */
getFrequency() { return this.box2dJoint.GetFrequency(); }
/** Set the damping ratio
* @param {number} ratio */
setDampingRatio(ratio) { this.box2dJoint.SetDampingRatio(ratio); box2dWakeJoint(this.box2dJoint); }
/** Get the damping ratio
* @return {number} */
getDampingRatio() { return this.box2dJoint.GetDampingRatio(); }
}
///////////////////////////////////////////////////////////////////////////////
/**
* Box2D Rope Joint
* - Enforces a maximum distance between two points on two objects
* @extends Box2dJoint
* @memberof Box2D
*/
class Box2dRopeJoint extends Box2dJoint
{
/** Create a rope joint
* @param {Box2dObject} objectA
* @param {Box2dObject} objectB
* @param {Vector2} [anchorA] - World position, objectA's position if not given
* @param {Vector2} [anchorB] - World position, objectB's position if not given
* @param {number} [extraLength]
* @param {boolean} [collide] */
constructor(objectA, objectB, anchorA, anchorB, extraLength=0, collide=false)
{
anchorA ||= box2d.vec2From(objectA.body.GetPosition());
anchorB ||= box2d.vec2From(objectB.body.GetPosition());
const localAnchorA = objectA.worldToLocal(anchorA);
const localAnchorB = objectB.worldToLocal(anchorB);
const jointDef = new box2d.instance.b2RopeJointDef();
jointDef.set_bodyA(objectA.body);
jointDef.set_bodyB(objectB.body);
jointDef.set_localAnchorA(box2dTemp(localAnchorA));
jointDef.set_localAnchorB(box2dTemp(localAnchorB));
jointDef.set_maxLength(anchorA.distance(anchorB)+extraLength);
jointDef.set_collideConnected(collide);
super(jointDef);
}
/** Get the local anchor point relative to objectA's origin
* @return {Vector2} */
getLocalAnchorA() { return box2d.vec2From(this.box2dJoint.GetLocalAnchorA()); }
/** Get the local anchor point relative to objectB's origin
* @return {Vector2} */
getLocalAnchorB() { return box2d.vec2From(this.box2dJoint.GetLocalAnchorB()); }
/** Set the max length of the joint
* @param {number} length */
setMaxLength(length) { this.box2dJoint.SetMaxLength(length); box2dWakeJoint(this.box2dJoint); }
/** Get the max length of the joint
* @return {number} */
getMaxLength() { return this.box2dJoint.GetMaxLength(); }
}
///////////////////////////////////////////////////////////////////////////////
/**
* Box2D Revolute Joint
* - Constrains two objects to share a point while they are free to rotate around the point
* - The relative rotation about the shared point is the joint angle
* - You can limit the relative rotation with a joint limit
* - You can use a motor to drive the relative rotation about the shared point
* - A maximum motor torque is provided so that infinite forces are not generated
* @extends Box2dJoint
* @memberof Box2D
*/
class Box2dRevoluteJoint extends Box2dJoint
{
/** Create a revolute joint
* @param {Box2dObject} objectA
* @param {Box2dObject} objectB
* @param {Vector2} [anchor] - World position, objectB's position if not given
* @param {boolean} [collide] */
constructor(objectA, objectB, anchor, collide=false)
{
anchor ||= box2d.vec2From(objectB.body.GetPosition());
const localAnchorA = objectA.worldToLocal(anchor);
const localAnchorB = objectB.worldToLocal(anchor);
const jointDef = new box2d.instance.b2RevoluteJointDef();
jointDef.set_bodyA(objectA.body);
jointDef.set_bodyB(objectB.body);
jointDef.set_localAnchorA(box2dTemp(localAnchorA));
jointDef.set_localAnchorB(box2dTemp(localAnchorB));
jointDef.set_referenceAngle(objectB.body.GetAngle() - objectA.body.GetAngle());
jointDef.set_collideConnected(collide);
super(jointDef);
}
/** Get the local anchor point relative to objectA's origin
* @return {Vector2} */
getLocalAnchorA() { return box2d.vec2From(this.box2dJoint.GetLocalAnchorA()); }
/** Get the local anchor point relative to objectB's origin
* @return {Vector2} */
getLocalAnchorB() { return box2d.vec2From(this.box2dJoint.GetLocalAnchorB()); }
/** Get the reference angle, objectB angle minus objectA angle in the reference state
* @return {number} */
getReferenceAngle() { return -this.box2dJoint.GetReferenceAngle(); } // box2d uses reverse angle
/** Get the current joint angle, clockwise like angle
* @return {number} */
getJointAngle() { return -this.box2dJoint.GetJointAngle(); }
/** Get the current joint angle speed in radians per second, clockwise like angle
* @return {number} */
getJointSpeed() { return -this.box2dJoint.GetJointSpeed(); }
/** Is the joint limit enabled?
* @return {boolean} */
isLimitEnabled() { return this.box2dJoint.IsLimitEnabled(); }
/** Enable/disable the joint limit
* @param {boolean} [enable] */
enableLimit(enable=true) { this.box2dJoint.EnableLimit(enable); }
/** Get the lower joint limit, clockwise like angle
* @return {number} */
getLowerLimit() { return -this.box2dJoint.GetUpperLimit(); } // reversed, so Box2D's upper is the lower
/** Get the upper joint limit, clockwise like angle
* @return {number} */
getUpperLimit() { return -this.box2dJoint.GetLowerLimit(); }
/** Set the joint limits, clockwise like angle
* @param {number} min
* @param {number} max */
setLimits(min, max)
{
ASSERT(min <= max, 'the lower limit must not be above the upper one');
if (min > max) [min, max] = [max, min]; // Box2D stops on them reversed
this.box2dJoint.SetLimits(-max, -min);
}
/** Is the joint motor enabled?
* @return {boolean} */
isMotorEnabled() { return this.box2dJoint.IsMotorEnabled(); }
/** Enable/disable the joint motor
* @param {boolean} [enable] */
enableMotor(enable=true) { this.box2dJoint.EnableMotor(enable); }
/** Set the motor speed, clockwise like angle
* @param {number} speed */
setMotorSpeed(speed) { this.box2dJoint.SetMotorSpeed(-speed); }
/** Get the motor speed, clockwise like angle
* @return {number} */
getMotorSpeed() { return -this.box2dJoint.GetMotorSpeed(); }
/** Set the max motor torque, a magnitude
* @param {number} torque */
setMaxMotorTorque(torque) { this.box2dJoint.SetMaxMotorTorque(torque); }
/** Get the max motor torque
* @return {number} */
getMaxMotorTorque() { return this.box2dJoint.GetMaxMotorTorque(); }
/** Get the motor torque over the last step, clockwise like angle
* @param {number} [time] - The step length in seconds, the world steps by timeDelta
* @return {number} */
getMotorTorque(time=timeDelta) { return -this.box2dJoint.GetMotorTorque(1/time); }
}
///////////////////////////////////////////////////////////////////////////////
/**
* Box2D Pin Joint
* - Pins two objects together at a point, where they still turn freely, like a nail through two boards
* - A revolute joint at that point, so it holds exactly and its limits and motor work too
* @extends Box2dRevoluteJoint
* @memberof Box2D
*/
class Box2dPinJoint extends Box2dRevoluteJoint
{
/** Create a pin joint
* @param {Box2dObject} objectA
* @param {Box2dObject} objectB
* @param {Vector2} [pos] - World position, objectA's position if not given
* @param {boolean} [collide] */
constructor(objectA, objectB, pos=objectA.pos, collide=false)
{
super(objectA, objectB, pos, collide);
}
}
///////////////////////////////////////////////////////////////////////////////
/**
* Box2D Gear Joint
* - A gear joint is used to connect two joints together
* - Either joint can be a revolute or prismatic joint
* - You specify a gear ratio to bind the motions together
* - joint1's angle or translation plus ratio times joint2's stays constant, angles clockwise like angle
* - It is destroyed along with either joint, or an object either joint is on
* - It turns objectB of each joint, so make each with its fixed or carrying object first, and a dynamic objectB
* @extends Box2dJoint
* @memberof Box2D
*/
class Box2dGearJoint extends Box2dJoint
{
/** Create a gear joint
* @param {Box2dObject} objectA - objectB of joint1, Box2D joins that one whatever is passed
* @param {Box2dObject} objectB - objectB of joint2, Box2D joins that one whatever is passed
* @param {Box2dJoint} joint1
* @param {Box2dJoint} joint2
* @param {number} [ratio] */
constructor(objectA, objectB, joint1, joint2, ratio=1)
{
// Box2D's angles are reversed and its translations are not, so a revolute joint geared to a prismatic one
// needs the ratio reversed too, two of a kind keep it
const isGearable = (j)=> (j instanceof Box2dRevoluteJoint || j instanceof Box2dPrismaticJoint) && !!j.box2dJoint;
ASSERT(isGearable(joint1) && isGearable(joint2), 'a gear joint needs two revolute or prismatic joints that exist');
// Box2D turns objectB of each joint, with objectA as its carrier, so a static objectB leaves the gear doing nothing
ASSERT(joint1.getObjectB()?.getBodyType() === box2d.bodyTypeDynamic &&
joint2.getObjectB()?.getBodyType() === box2d.bodyTypeDynamic,
'a gear joint turns objectB of each joint, make each joint with its fixed or carrying object first');
ASSERT(objectA === joint1.getObjectB() && objectB === joint2.getObjectB(),
'a gear joint joins objectB of joint1 and objectB of joint2, pass those');
const ratioSign = (joint1 instanceof Box2dRevoluteJoint) === (joint2 instanceof Box2dRevoluteJoint) ? 1 : -1;
const jointDef = new box2d.instance.b2GearJointDef();
jointDef.set_bodyA(objectA.body);
jointDef.set_bodyB(objectB.body);
jointDef.set_joint1(joint1.box2dJoint);
jointDef.set_joint2(joint2.box2dJoint);
jointDef.set_ratio(ratio * ratioSign);
super(jointDef);
this.joint1 = joint1;
this.joint2 = joint2;
this.ratioSign = ratioSign;
box2dGearJoints.add(this);
}
/** Get the first joint
* @return {Box2dJoint} */
getJoint1() { return this.joint1; }
/** Get the second joint
* @return {Box2dJoint} */
getJoint2() { return this.joint2; }
/** Set the gear ratio
* @param {number} ratio */
setRatio(ratio) { this.box2dJoint.SetRatio(ratio * this.ratioSign); box2dWakeJoint(this.box2dJoint); }
/** Get the gear ratio
* @return {number} */
getRatio() { return this.box2dJoint.GetRatio() * this.ratioSign; }
}
///////////////////////////////////////////////////////////////////////////////
/**
* Box2D Prismatic Joint
* - Provides one degree of freedom: translation along an axis fixed in objectA
* - Relative rotation is prevented
* - You can use a joint limit to restrict the range of motion
* - You can use a joint motor to drive the motion or to model joint friction
* @extends Box2dJoint
* @memberof Box2D
*/
class Box2dPrismaticJoint extends Box2dJoint
{
/** Create a prismatic joint
* @param {Box2dObject} objectA
* @param {Box2dObject} objectB
* @param {Vector2} [anchor] - World position, objectB's position if not given
* @param {Vector2} [worldAxis]
* @param {boolean} [collide] */
constructor(objectA, objectB, anchor, worldAxis=vec2(0,1), collide=false)
{
anchor ||= box2d.vec2From(objectB.body.GetPosition());
const localAnchorA = objectA.worldToLocal(anchor);
const localAnchorB = objectB.worldToLocal(anchor);
const localAxisA = objectA.worldToLocalVector(worldAxis);
const jointDef = new box2d.instance.b2PrismaticJointDef();
jointDef.set_bodyA(objectA.body);
jointDef.set_bodyB(objectB.body);
jointDef.set_localAnchorA(box2dTemp(localAnchorA));
jointDef.set_localAnchorB(box2dTemp(localAnchorB));
jointDef.set_localAxisA(box2dTemp(localAxisA));
jointDef.set_referenceAngle(objectB.body.GetAngle() - objectA.body.GetAngle());
jointDef.set_collideConnected(collide);
super(jointDef);
}
/** Get the local anchor point relative to objectA's origin
* @return {Vector2} */
getLocalAnchorA() { return box2d.vec2From(this.box2dJoint.GetLocalAnchorA()); }
/** Get the local anchor point relative to objectB's origin
* @return {Vector2} */
getLocalAnchorB() { return box2d.vec2From(this.box2dJoint.GetLocalAnchorB()); }
/** Get the local joint axis relative to bodyA
* @return {Vector2} */
getLocalAxisA() { return box2d.vec2From(this.box2dJoint.GetLocalAxisA()); }
/** Get the reference angle, objectB angle minus objectA angle in the reference state
* @return {number} */
getReferenceAngle() { return -this.box2dJoint.GetReferenceAngle(); } // box2d uses reverse angle
/** Get the current joint translation
* @return {number} */
getJointTranslation() { return this.box2dJoint.GetJointTranslation(); }
/** Get the current joint translation speed
* @return {number} */
getJointSpeed() { return this.box2dJoint.GetJointSpeed(); }
/** Is the joint limit enabled?
* @return {boolean} */
isLimitEnabled() { return this.box2dJoint.IsLimitEnabled(); }
/** Enable/disable the joint limit
* @param {boolean} [enable] */
enableLimit(enable=true) { this.box2dJoint.EnableLimit(enable); }
/** Get the lower joint limit
* @return {number} */
getLowerLimit() { return this.box2dJoint.GetLowerLimit(); }
/** Get the upper joint limit
* @return {number} */
getUpperLimit() { return this.box2dJoint.GetUpperLimit(); }
/** Set the joint limits
* @param {number} min
* @param {number} max */
setLimits(min, max)
{
ASSERT(min <= max, 'the lower limit must not be above the upper one');
if (min > max) [min, max] = [max, min]; // Box2D stops on them reversed
this.box2dJoint.SetLimits(min, max);
}
/** Is the motor enabled?
* @return {boolean} */
isMotorEnabled() { return this.box2dJoint.IsMotorEnabled(); }
/** Enable/disable the joint motor
* @param {boolean} [enable] */
enableMotor(enable=true) { this.box2dJoint.EnableMotor(enable); }
/** Set the motor speed
* @param {number} speed */
setMotorSpeed(speed) { this.box2dJoint.SetMotorSpeed(speed); }
/** Get the motor speed
* @return {number} */
getMotorSpeed() { return this.box2dJoint.GetMotorSpeed(); }
/** Set the maximum motor force
* @param {number} force */
setMaxMotorForce(force) { this.box2dJoint.SetMaxMotorForce(force); }
/** Get the maximum motor force
* @return {number} */
getMaxMotorForce() { return this.box2dJoint.GetMaxMotorForce(); }
/** Get the motor force over the last step
* @param {number} [time] - The step length in seconds, the world steps by timeDelta
* @return {number} */
getMotorForce(time=timeDelta) { return this.box2dJoint.GetMotorForce(1/time); }
}
///////////////////////////////////////////////////////////////////////////////
/**
* Box2D Wheel Joint
* - Provides two degrees of freedom: translation along an axis fixed in objectA and rotation
* - You can use a joint motor to drive the motion or to model joint friction
* - This joint is designed for vehicle suspensions
* @extends Box2dJoint
* @memberof Box2D
*/
class Box2dWheelJoint extends Box2dJoint
{
/** Create a wheel joint
* @param {Box2dObject} objectA
* @param {Box2dObject} objectB
* @param {Vector2} [anchor] - World position, objectB's position if not given
* @param {Vector2} [worldAxis]
* @param {boolean} [collide] */
constructor(objectA, objectB, anchor, worldAxis=vec2(0,1), collide=false)
{
anchor ||= box2d.vec2From(objectB.body.GetPosition());
const localAnchorA = objectA.worldToLocal(anchor);
const localAnchorB = objectB.worldToLocal(anchor);
const localAxisA = objectA.worldToLocalVector(worldAxis).normalize(); // Box2D uses the wheel axis as given
const jointDef = new box2d.instance.b2WheelJointDef();
jointDef.set_bodyA(objectA.body);
jointDef.set_bodyB(objectB.body);
jointDef.set_localAnchorA(box2dTemp(localAnchorA));
jointDef.set_localAnchorB(box2dTemp(localAnchorB));
jointDef.set_localAxisA(box2dTemp(localAxisA));
jointDef.set_collideConnected(collide);
super(jointDef);
}
/** Get the local anchor point relative to objectA's origin
* @return {Vector2} */
getLocalAnchorA() { return box2d.vec2From(this.box2dJoint.GetLocalAnchorA()); }
/** Get the local anchor point relative to objectB's origin
* @return {Vector2} */
getLocalAnchorB() { return box2d.vec2From(this.box2dJoint.GetLocalAnchorB()); }
/** Get the local joint axis relative to bodyA
* @return {Vector2} */
getLocalAxisA() { return box2d.vec2From(this.box2dJoint.GetLocalAxisA()); }
/** Get the current joint translation
* @return {number} */
getJointTranslation() { return this.box2dJoint.GetJointTranslation(); }
/** Get the current joint rotation speed in radians per second, clockwise like angle,
* which is what this version of Box2D measures for a wheel joint
* @return {number} */
getJointSpeed() { return -this.box2dJoint.GetJointSpeed(); } // box2d uses reverse angle
/** Is the joint motor enabled?
* @return {boolean} */
isMotorEnabled() { return this.box2dJoint.IsMotorEnabled(); }
/** Enable/disable the joint motor
* @param {boolean} [enable] */
enableMotor(enable=true) { this.box2dJoint.EnableMotor(enable); }
/** Set the motor speed, the wheel's turn in radians per second, clockwise like angle
* @param {number} speed */
setMotorSpeed(speed) { this.box2dJoint.SetMotorSpeed(-speed); }
/** Get the motor speed, clockwise like angle
* @return {number} */
getMotorSpeed() { return -this.box2dJoint.GetMotorSpeed(); }
/** Set the maximum motor torque, a magnitude
* @param {number} torque */
setMaxMotorTorque(torque) { this.box2dJoint.SetMaxMotorTorque(torque); }
/** Get the max motor torque
* @return {number} */
getMaxMotorTorque() { return this.box2dJoint.GetMaxMotorTorque(); }
/** Get the motor torque over the last step, clockwise like angle
* @param {number} [time] - The step length in seconds, the world steps by timeDelta
* @return {number} */
getMotorTorque(time=timeDelta) { return -this.box2dJoint.GetMotorTorque(1/time); }
/** Set the spring frequency in Hertz
* @param {number} hz */
setSpringFrequencyHz(hz) { this.box2dJoint.SetSpringFrequencyHz(hz); box2dWakeJoint(this.box2dJoint); }
/** Get the spring frequency in Hertz
* @return {number} */
getSpringFrequencyHz() { return this.box2dJoint.GetSpringFrequencyHz(); }
/** Set the spring damping ratio
* @param {number} ratio */
setSpringDampingRatio(ratio) { this.box2dJoint.SetSpringDampingRatio(ratio); box2dWakeJoint(this.box2dJoint); }
/** Get the spring damping ratio
* @return {number} */
getSpringDampingRatio() { return this.box2dJoint.GetSpringDampingRatio(); }
}
///////////////////////////////////////////////////////////////////////////////
/**
* Box2D Weld Joint
* - Glues two objects together
* @extends Box2dJoint
* @memberof Box2D
*/
class Box2dWeldJoint extends Box2dJoint
{
/** Create a weld joint
* @param {Box2dObject} objectA
* @param {Box2dObject} objectB
* @param {Vector2} [anchor] - World position, objectB's position if not given
* @param {boolean} [collide] */
constructor(objectA, objectB, anchor, collide=false)
{
anchor ||= box2d.vec2From(objectB.body.GetPosition());
const localAnchorA = objectA.worldToLocal(anchor);
const localAnchorB = objectB.worldToLocal(anchor);
const referenceAngle = objectB.body.GetAngle() - objectA.body.GetAngle();
const jointDef = new box2d.instance.b2WeldJointDef();
jointDef.set_bodyA(objectA.body);
jointDef.set_bodyB(objectB.body);
jointDef.set_localAnchorA(box2dTemp(localAnchorA));
jointDef.set_localAnchorB(box2dTemp(localAnchorB));
jointDef.set_referenceAngle(referenceAngle);
jointDef.set_collideConnected(collide);
super(jointDef);
// kept here, since the binding cannot read it back from a weld joint; box2d uses reverse angle
this.referenceAngle = -referenceAngle;
}
/** Get the local anchor point relative to objectA's origin
* @return {Vector2} */
getLocalAnchorA() { return box2d.vec2From(this.box2dJoint.GetLocalAnchorA()); }
/** Get the local anchor point relative to objectB's origin
* @return {Vector2} */
getLocalAnchorB() { return box2d.vec2From(this.box2dJoint.GetLocalAnchorB()); }
/** Get the reference angle, objectB angle minus objectA angle in the reference state
* @return {number} */
getReferenceAngle() { return this.referenceAngle; }
/** Set the frequency in Hertz
* @param {number} hz */
setFrequency(hz) { this.box2dJoint.SetFrequency(hz); box2dWakeJoint(this.box2dJoint); }
/** Get the frequency in Hertz
* @return {number} */
getFrequency() { return this.box2dJoint.GetFrequency(); }
/** Set the damping ratio
* @param {number} ratio */
setDampingRatio(ratio) { this.box2dJoint.SetDampingRatio(ratio); box2dWakeJoint(this.box2dJoint); }
/** Get the damping ratio
* @return {number} */
getDampingRatio() { return this.box2dJoint.GetDampingRatio(); }
/** @deprecated since 1.20, use setDampingRatio
* @param {number} ratio */
setSpringDampingRatio(ratio) { this.setDampingRatio(ratio); }
/** @deprecated since 1.20, use getDampingRatio
* @return {number} */
getSpringDampingRatio() { return this.getDampingRatio(); }
}
///////////////////////////////////////////////////////////////////////////////
/**
* Box2D Friction Joint
* - Used to apply top-down friction
* - Provides 2D translational friction and angular friction
* @extends Box2dJoint
* @memberof Box2D
*/
class Box2dFrictionJoint extends Box2dJoint
{
/** Create a friction joint
* @param {Box2dObject} objectA
* @param {Box2dObject} objectB
* @param {Vector2} [anchor] - World position, objectB's position if not given
* @param {boolean} [collide] */
constructor(objectA, objectB, anchor, collide=false)
{
anchor ||= box2d.vec2From(objectB.body.GetPosition());
const localAnchorA = objectA.worldToLocal(anchor);
const localAnchorB = objectB.worldToLocal(anchor);
const jointDef = new box2d.instance.b2FrictionJointDef();
jointDef.set_bodyA(objectA.body);
jointDef.set_bodyB(objectB.body);
jointDef.set_localAnchorA(box2dTemp(localAnchorA));
jointDef.set_localAnchorB(box2dTemp(localAnchorB));
jointDef.set_collideConnected(collide);
super(jointDef);
}
/** Get the local anchor point relative to objectA's origin
* @return {Vector2} */
getLocalAnchorA() { return box2d.vec2From(this.box2dJoint.GetLocalAnchorA()); }
/** Get the local anchor point relative to objectB's origin
* @return {Vector2} */
getLocalAnchorB() { return box2d.vec2From(this.box2dJoint.GetLocalAnchorB()); }
/** Set the maximum friction force
* @param {number} force */
setMaxForce(force) { this.box2dJoint.SetMaxForce(max(force, 0)); } // Box2D stops on a negative one
/** Get the maximum friction force
* @return {number} */
getMaxForce() { return this.box2dJoint.GetMaxForce(); }
/** Set the maximum friction torque
* @param {number} torque */
setMaxTorque(torque) { this.box2dJoint.SetMaxTorque(max(torque, 0)); } // Box2D stops on a negative one
/** Get the maximum friction torque
* @return {number} */
getMaxTorque() { return this.box2dJoint.GetMaxTorque(); }
}
///////////////////////////////////////////////////////////////////////////////
/**
* Box2D Pulley Joint
* - Connects to two objects and two fixed ground points
* - The pulley supports a ratio such that: length1 + ratio * length2 <= constant
* - The force transmitted is scaled by the ratio
* @extends Box2dJoint
* @memberof Box2D
*/
class Box2dPulleyJoint extends Box2dJoint
{
/** Create a pulley joint
* @param {Box2dObject} objectA
* @param {Box2dObject} objectB
* @param {Vector2} groundAnchorA
* @param {Vector2} groundAnchorB
* @param {Vector2} [anchorA] - World position, objectA's position if not given
* @param {Vector2} [anchorB] - World position, objectB's position if not given
* @param {number} [ratio]
* @param {boolean} [collide] */
constructor(objectA, objectB, groundAnchorA, groundAnchorB, anchorA, anchorB, ratio=1, collide=false)
{
anchorA ||= box2d.vec2From(objectA.body.GetPosition());
anchorB ||= box2d.vec2From(objectB.body.GetPosition());
const localAnchorA = objectA.worldToLocal(anchorA);
const localAnchorB = objectB.worldToLocal(anchorB);
const jointDef = new box2d.instance.b2PulleyJointDef();
jointDef.set_bodyA(objectA.body);
jointDef.set_bodyB(objectB.body);
jointDef.set_groundAnchorA(box2dTemp(groundAnchorA));
jointDef.set_groundAnchorB(box2dTemp(groundAnchorB));
jointDef.set_localAnchorA(box2dTemp(localAnchorA));
jointDef.set_localAnchorB(box2dTemp(localAnchorB));
ASSERT(ratio, 'a pulley ratio can not be 0');
jointDef.set_ratio(ratio);
jointDef.set_lengthA(groundAnchorA.distance(anchorA));
jointDef.set_lengthB(groundAnchorB.distance(anchorB));
jointDef.set_collideConnected(collide);
super(jointDef);
}
/** Get the first ground anchor
* @return {Vector2} */
getGroundAnchorA() { return box2d.vec2From(this.box2dJoint.GetGroundAnchorA()); }
/** Get the second ground anchor
* @return {Vector2} */
getGroundAnchorB() { return box2d.vec2From(this.box2dJoint.GetGroundAnchorB()); }
/** Get the rest length of the segment attached to objectA, set at creation
* @return {number} */
getLengthA() { return this.box2dJoint.GetLengthA(); }
/** Get the rest length of the segment attached to objectB, set at creation
* @return {number} */
getLengthB(){ return this.box2dJoint.GetLengthB(); }
/** Get the pulley ratio
* @return {number} */
getRatio() { return this.box2dJoint.GetRatio(); }
/** Get the current length of the segment attached to objectA
* @return {number} */
getCurrentLengthA() { return this.box2dJoint.GetCurrentLengthA(); }
/** Get the current length of the segment attached to objectB
* @return {number} */
getCurrentLengthB() { return this.box2dJoint.GetCurrentLengthB(); }
}
///////////////////////////////////////////////////////////////////////////////
/**
* Box2D Motor Joint
* - Controls the relative motion between two objects
* - Typical usage is to control the movement of a object with respect to the ground
* @extends Box2dJoint
* @memberof Box2D
*/
class Box2dMotorJoint extends Box2dJoint
{
/** Create a motor joint
* @param {Box2dObject} objectA
* @param {Box2dObject} objectB */
constructor(objectA, objectB)
{
const linearOffset = objectA.worldToLocal(box2d.vec2From(objectB.body.GetPosition()));
const angularOffset = objectB.body.GetAngle() - objectA.body.GetAngle();
const jointDef = new box2d.instance.b2MotorJointDef();
jointDef.set_bodyA(objectA.body);
jointDef.set_bodyB(objectB.body);
jointDef.set_linearOffset(box2dTemp(linearOffset));
jointDef.set_angularOffset(angularOffset);
super(jointDef);
}
/** Set the target linear offset, in frame A, in meters.
* @param {Vector2} offset */
setLinearOffset(offset) { this.box2dJoint.SetLinearOffset(box2dTemp(offset)); }
/** Get the target linear offset, in frame A, in meters.
* @return {Vector2} */
getLinearOffset() { return box2d.vec2From(this.box2dJoint.GetLinearOffset()); }
/** Set the target angular offset, objectB angle minus objectA angle, clockwise like angle
* @param {number} offset */
setAngularOffset(offset) { this.box2dJoint.SetAngularOffset(-offset); } // box2d uses reverse angle
/** Get the target angular offset, objectB angle minus objectA angle, clockwise like angle
* @return {number} */
getAngularOffset() { return -this.box2dJoint.GetAngularOffset(); }
/** Set the maximum force
* @param {number} force */
setMaxForce(force) { this.box2dJoint.SetMaxForce(max(force, 0)); box2dWakeJoint(this.box2dJoint); } // Box2D stops on a negative one
/** Get the maximum force
* @return {number} */
getMaxForce() { return this.box2dJoint.GetMaxForce(); }
/** Set the maximum torque
* @param {number} torque */
setMaxTorque(torque) { this.box2dJoint.SetMaxTorque(max(torque, 0)); box2dWakeJoint(this.box2dJoint); } // Box2D stops on a negative one
/** Get the maximum torque
* @return {number} */
getMaxTorque() { return this.box2dJoint.GetMaxTorque(); }
/** Set the position correction factor in the range [0,1]
* @param {number} factor */
setCorrectionFactor(factor) { this.box2dJoint.SetCorrectionFactor(clamp(factor)); box2dWakeJoint(this.box2dJoint); }
/** Get the position correction factor in the range [0,1]
* @return {number} */
getCorrectionFactor() { return this.box2dJoint.GetCorrectionFactor(); }
}
///////////////////////////////////////////////////////////////////////////////
/**
* Box2D Global Object
* - Wraps Box2d world and provides global functions
* @memberof Box2D
*/
class Box2dPlugin
{
/** Create the global Box2D plugin object, box2dInit does this
* @param {Object} instance */
constructor(instance)
{
ASSERT(!box2d, 'Box2D already initialized');
box2d = this;
/** @property {Object} - The Box2d instance */
this.instance = instance;
/** @property {Object} - The Box2d world */
this.world = new box2d.instance.b2World();
/** @property {Array<Box2dObject>} - List of all Box2d objects, a destroyed one stays until the next step
* with its body undefined
* @type {Array<Box2dObject>} */
this.objects = [];
/** @property {number} - Velocity iterations per update*/
this.velocityIterations = 8;
/** @property {number} - Position iterations per update*/
this.positionIterations = 3;
/** @property {number} - Static, zero mass, zero velocity, may be manually moved
* @type {number} */
this.bodyTypeStatic = instance.b2_staticBody;
/** @property {number} - Kinematic, zero mass, non-zero velocity set by user, moved by solver
* @type {number} */
this.bodyTypeKinematic = instance.b2_kinematicBody;
/** @property {number} - Dynamic, positive mass, non-zero velocity determined by forces, moved by solver
* @type {number} */
this.bodyTypeDynamic = instance.b2_dynamicBody;
// a body that goes takes its joints with it, their wrappers let go of them, and a gear joint on one of them
// goes too, since it would keep pointers to what was freed; that destroy waits in the queue, which DestroyBody
// runs from, and is skipped if the gear hung off this same body and went with it
const destructionListener = new box2d.instance.JSDestructionListener();
destructionListener.SayGoodbyeJoint = function(jointPointer)
{
const joint = box2dJoints.get(jointPointer);
if (joint)
{
box2dDestroyGears(joint);
joint.box2dJoint = 0;
}
box2dJoints.delete(jointPointer);
};
destructionListener.SayGoodbyeFixture = function() {};
box2d.world.SetDestructionListener(destructionListener);
// setup contact listener
const listener = new box2d.instance.JSContactListener();
listener.BeginContact = function(contactPtr)
{
const contact = box2d.instance.wrapPointer(contactPtr, box2d.instance.b2Contact);
const fixtureA = contact.GetFixtureA();
const fixtureB = contact.GetFixtureB();
const objectA = fixtureA.GetBody().object;
const objectB = fixtureB.GetBody().object;
// raw user-created b2Bodies may have no .object — skip those
if (!objectA || !objectB) return;
objectA.beginContact(objectB, fixtureA, fixtureB);
objectB.beginContact(objectA, fixtureB, fixtureA);
}
listener.EndContact = function(contactPtr)
{
const contact = box2d.instance.wrapPointer(contactPtr, box2d.instance.b2Contact);
const fixtureA = contact.GetFixtureA();
const fixtureB = contact.GetFixtureB();
const objectA = fixtureA.GetBody().object;
const objectB = fixtureB.GetBody().object;
if (!objectA || !objectB) return;
objectA.endContact(objectB, fixtureA, fixtureB);
objectB.endContact(objectA, fixtureB, fixtureA);
};
listener.PreSolve = function() {};
listener.PostSolve = function() {};
box2d.world.SetContactListener(listener);
}
/** Step the physics world simulation
* @param {number} [frames] */
step(frames=1)
{
// the engine's gravity, Box2D does not wake a sleeping body for a new one, so a change wakes them all
if (gravity.x !== box2dGravity.x || gravity.y !== box2dGravity.y)
{
box2dGravity.x = gravity.x, box2dGravity.y = gravity.y;
box2d.world.SetGravity(box2dTemp(gravity));
for (let b = box2d.world.GetBodyList(); !box2d.isNull(b); b = b.GetNext())
b.SetAwake(true);
}
for (let i=frames; i--;)
{
box2d.world.Step(timeDelta, this.velocityIterations, this.positionIterations);
// what a contact callback destroyed or changed, now the world can take it
box2dRunPending();
}
// remove destroyed objects, once for all of them
this.objects = this.objects.filter(o=>!o.destroyed);
}
///////////////////////////////////////////////////////////////////////////////
// raycasting and querying
/** raycast and return a list of all the results, nearest first
* @param {Vector2} start
* @param {Vector2} end
* @param {boolean} [includeSensors] - Also hit sensors, trigger zones are passed through by default
* @return {Array<Box2dRaycastResult>} */
raycastAll(start, end, includeSensors=false)
{
// a ray with no length fails an assert that stops Box2D for good, measured as Box2D does in 32 bit floats,
// where two ends a float apart are one point; one that is not a number has no length either
const f = Math.fround, dx = f(f(end.x) - f(start.x)), dy = f(f(end.y) - f(start.y));
const lengthSquared = f(f(dx*dx) + f(dy*dy));
if (!(lengthSquared > 0 && lengthSquared < Infinity))
return [];
const raycastCallback = box2dQueryObject('rayCast', 'JSRayCastCallback');
raycastCallback.ReportFixture = function(fixturePointer, point, normal, fraction)
{
const fixture = box2d.instance.wrapPointer(fixturePointer, box2d.instance.b2Fixture);
const o = fixture.GetBody().object;
if (!o || o.destroyed)
return 1; // a raw body with no Box2dObject or one destroyed this step, continue getting results
if (!includeSensors && fixture.IsSensor())
return -1; // skip it, Box2D goes on as if it were not there
point = box2d.vec2FromPointer(point);
normal = box2d.vec2FromPointer(normal);
raycastResults.push(new Box2dRaycastResult(fixture, point, normal, fraction));
return 1; // continue getting results
};
const raycastResults = [];
box2d.world.RayCast(raycastCallback, box2dTemp(start), box2dTemp(end, 1));
raycastResults.sort((a,b)=> a.fraction - b.fraction); // Box2D reports them in its tree's order
debugRaycast && debugLine(start, end, raycastResults.length ? '#f00' : '#00f', .02, 0, false);
return raycastResults;
}
/** raycast and return the first result
* @param {Vector2} start
* @param {Vector2} end
* @param {boolean} [includeSensors] - Also hit sensors, trigger zones are passed through by default
* @return {Box2dRaycastResult|undefined} */
raycast(start, end, includeSensors=false)
{
return box2d.raycastAll(start, end, includeSensors)[0];
}
/** box aabb cast and return all the objects
* @param {Vector2} pos
* @param {Vector2} size
* @param {boolean} [includeSensors] - Also find sensors, trigger zones are passed through by default
* @return {Array<Box2dObject>} */
boxCastAll(pos, size, includeSensors=false)
{
const queryCallback = box2dQueryObject('query', 'JSQueryCallback');
queryCallback.ReportFixture = function(fixturePointer)
{
const fixture = box2d.instance.wrapPointer(fixturePointer, box2d.instance.b2Fixture);
if (!includeSensors && fixture.IsSensor())
return true; // a trigger zone, continue getting results
const o = fixture.GetBody().object;
if (o && !o.destroyed && !queryObjects.includes(o) // skip raw bodies and ones destroyed this step
&& box2dFixtureOverlaps(fixture, aabb))
queryObjects.push(o); // add if not already in list
return true; // continue getting results
};
const aabb = box2dQueryObject('aabb', 'b2AABB');
aabb.set_lowerBound(box2dTemp(pos.subtract(size.scale(.5))));
aabb.set_upperBound(box2dTemp(pos.add(size.scale(.5))));
let queryObjects = [];
box2d.world.QueryAABB(queryCallback, aabb);
debugRaycast && debugRect(pos, size, queryObjects.length ? '#f00' : '#00f', 0, 0, false, false);
return queryObjects;
}
/** box aabb cast and return the first object
* @param {Vector2} pos
* @param {Vector2} size
* @param {boolean} [includeSensors] - Also find sensors, trigger zones are passed through by default
* @return {Box2dObject|undefined} */
boxCast(pos, size, includeSensors=false)
{
const queryCallback = box2dQueryObject('query', 'JSQueryCallback');
queryCallback.ReportFixture = function(fixturePointer)
{
const fixture = box2d.instance.wrapPointer(fixturePointer, box2d.instance.b2Fixture);
if (!includeSensors && fixture.IsSensor())
return true; // a trigger zone, continue getting results
const o = fixture.GetBody().object;
if (!o || o.destroyed)
return true; // a raw body with no Box2dObject or one destroyed this step, continue getting results
if (!box2dFixtureOverlaps(fixture, aabb))
return true; // only near the box, continue getting results
queryObject = o;
return false; // stop getting results
};
const aabb = box2dQueryObject('aabb', 'b2AABB');
aabb.set_lowerBound(box2dTemp(pos.subtract(size.scale(.5))));
aabb.set_upperBound(box2dTemp(pos.add(size.scale(.5))));
let queryObject;
box2d.world.QueryAABB(queryCallback, aabb);
debugRaycast && debugRect(pos, size, queryObject ? '#f00' : '#00f', 0, 0, false, false);
return queryObject;
}
/** circle cast and return all the objects whose position is within the circle, wherever their shapes are
* @param {Vector2} pos
* @param {number} diameter
* @param {boolean} [includeSensors] - Also find objects whose shapes are all sensors, like trigger zones
* @return {Array<Box2dObject>} */
circleCastAll(pos, diameter, includeSensors=false)
{
// by each object's position, so an object whose shapes are offset from it is still found
const radius2 = (diameter/2)**2;
const results = box2d.objects.filter(o=> !o.destroyed && o.body && o.pos.distanceSquared(pos) < radius2 &&
(includeSensors || o.getFixtureList().some(fixture=> !fixture.IsSensor())));
debugRaycast && debugCircle(pos, diameter, results.length ? '#f00' : '#00f', 0, false, false);
return results;
}
/** circle cast and return the object whose position is nearest, of those within the circle
* @param {Vector2} pos
* @param {number} diameter
* @param {boolean} [includeSensors] - Also find objects whose shapes are all sensors, like trigger zones
* @return {Box2dObject|undefined} */
circleCast(pos, diameter, includeSensors=false)
{
let bestResult, bestDistance2;
for (const result of box2d.circleCastAll(pos, diameter, includeSensors))
{
const distance2 = result.pos.distanceSquared(pos);
if (!bestResult || distance2 < bestDistance2)
{
bestResult = result;
bestDistance2 = distance2;
}
}
return bestResult;
}
/** point cast and return the first object
* @param {Vector2} pos
* @param {boolean} [dynamicOnly]
* @param {boolean} [includeSensors] - Also find sensors, so a pickup radius does not grab its object from afar
* @return {Box2dObject|undefined} */
pointCast(pos, dynamicOnly=true, includeSensors=false)
{
const queryCallback = box2dQueryObject('query', 'JSQueryCallback');
queryCallback.ReportFixture = function(fixturePointer)
{
const fixture = box2d.instance.wrapPointer(fixturePointer, box2d.instance.b2Fixture);
if (!includeSensors && fixture.IsSensor())
return true; // a trigger zone, continue getting results
if (dynamicOnly && fixture.GetBody().GetType() !== box2d.bodyTypeDynamic)
return true; // continue getting results
if (!fixture.TestPoint(box2dTemp(pos)))
return true; // continue getting results
const o = fixture.GetBody().object;
if (!o || o.destroyed)
return true; // a raw body with no Box2dObject or one destroyed this step, continue getting results
queryObject = o;
return false; // stop getting results
};
const aabb = box2dQueryObject('aabb', 'b2AABB');
aabb.set_lowerBound(box2dTemp(pos));
aabb.set_upperBound(box2dTemp(pos));
let queryObject;
box2d.world.QueryAABB(queryCallback, aabb);
debugRaycast && debugRect(pos, vec2(), queryObject ? '#f00' : '#00f', 0, 0, false, false);
return queryObject;
}
///////////////////////////////////////////////////////////////////////////////
// drawing
/** draws a fixture
* @param {Object} fixture
* @param {Vector2} pos
* @param {number} angle
* @param {Color} [color]
* @param {Color} [lineColor]
* @param {number} [lineWidth]
* @param {boolean} [useWebGL=glEnable]
* @param {CanvasRenderingContext2D} [context] */
drawFixture(fixture, pos, angle, color=WHITE, lineColor=BLACK, lineWidth=.1, useWebGL, context)
{
const shape = box2d.castShapeObject(fixture.GetShape());
switch (shape.GetType())
{
case box2d.instance.b2Shape.e_polygon:
{
let points = [];
for (let i=shape.GetVertexCount(); i--;)
points.push(box2d.vec2From(shape.GetVertex(i)));
drawPoly(points, color, lineWidth, lineColor, pos, angle, useWebGL, false, context);
break;
}
case box2d.instance.b2Shape.e_circle:
{
const radius = shape.get_m_radius(), offset = box2d.vec2From(shape.get_m_p());
drawCircle(pos.add(offset.rotate(angle)), radius*2, color, lineWidth, lineColor, useWebGL, false, context);
break;
}
case box2d.instance.b2Shape.e_edge:
{
const v1 = box2d.vec2From(shape.get_m_vertex1());
const v2 = box2d.vec2From(shape.get_m_vertex2());
drawLine(v1, v2, lineWidth, lineColor, pos, angle, useWebGL, false, context);
break;
}
}
}
///////////////////////////////////////////////////////////////////////////////
// helper functions
/** converts a box2d vec2 to a Vector2
* @param {Object} v */
vec2From(v)
{
ASSERT(v instanceof box2d.instance.b2Vec2);
return new Vector2(v.get_x(), v.get_y());
}
/** converts a box2d vec2 pointer to a Vector2
* @param {Object} vp */
vec2FromPointer(vp)
{
const v = box2d.instance.wrapPointer(vp, box2d.instance.b2Vec2);
return box2d.vec2From(v);
}
/** converts a Vector2 to a new box2d vec2, which stays until destroyed with box2d.instance.destroy;
* the plugin itself passes Box2D reused ones, since Box2D copies every vector it is given
* @param {Vector2} v */
vec2dTo(v)
{
ASSERT(isVector2(v));
return new box2d.instance.b2Vec2(v.x, v.y);
}
/** checks if a box2d object is null
* @param {Object} o */
isNull(o) { return !box2d.instance.getPointer(o); }
/** casts a box2d object to a shape type
* @param {Object} o */
castShapeObject(o)
{
switch (o.GetType())
{
case box2d.instance.b2Shape.e_circle:
return box2d.instance.castObject(o, box2d.instance.b2CircleShape);
case box2d.instance.b2Shape.e_edge:
return box2d.instance.castObject(o, box2d.instance.b2EdgeShape);
case box2d.instance.b2Shape.e_polygon:
return box2d.instance.castObject(o, box2d.instance.b2PolygonShape);
case box2d.instance.b2Shape.e_chain:
return box2d.instance.castObject(o, box2d.instance.b2ChainShape);
}
ASSERT(false, 'Unknown box2d object type');
}
/** casts a box2d object to a joint type
* @param {Object} o */
castJointObject(o)
{
switch (o.GetType())
{
case box2d.instance.e_revoluteJoint:
return box2d.instance.castObject(o, box2d.instance.b2RevoluteJoint);
case box2d.instance.e_prismaticJoint:
return box2d.instance.castObject(o, box2d.instance.b2PrismaticJoint);
case box2d.instance.e_distanceJoint:
return box2d.instance.castObject(o, box2d.instance.b2DistanceJoint);
case box2d.instance.e_pulleyJoint:
return box2d.instance.castObject(o, box2d.instance.b2PulleyJoint);
case box2d.instance.e_mouseJoint:
return box2d.instance.castObject(o, box2d.instance.b2MouseJoint);
case box2d.instance.e_gearJoint:
return box2d.instance.castObject(o, box2d.instance.b2GearJoint);
case box2d.instance.e_wheelJoint:
return box2d.instance.castObject(o, box2d.instance.b2WheelJoint);
case box2d.instance.e_weldJoint:
return box2d.instance.castObject(o, box2d.instance.b2WeldJoint);
case box2d.instance.e_frictionJoint:
return box2d.instance.castObject(o, box2d.instance.b2FrictionJoint);
case box2d.instance.e_ropeJoint:
return box2d.instance.castObject(o, box2d.instance.b2RopeJoint);
case box2d.instance.e_motorJoint:
return box2d.instance.castObject(o, box2d.instance.b2MotorJoint);
}
ASSERT(false, 'Unknown box2d object type');
}
}
///////////////////////////////////////////////////////////////////////////////
/** Box2d Init - Call with await to init box2d
* @example
* await box2dInit();
* @return {Promise<Box2dPlugin>}
* @memberof Box2D */
async function box2dInit()
{
// load box2d
// @ts-ignore - Box2D is the global that box2d.wasm.js defines
new Box2dPlugin(await Box2D());
setupDebugDraw();
engineAddPlugin(box2dUpdate, box2dRender);
return box2d;
// add the box2d plugin to the engine
function box2dUpdate()
{
// frozen like the engine objects while paused or time is stopped
if (paused || !timeScale)
{
// what was destroyed while frozen leaves the list, the step does it otherwise
box2d.objects = box2d.objects.filter(o=>!o.destroyed);
return;
}
box2d.step();
// copy box2d physics results to engine objects
for (const o of box2d.objects)
{
if (o.body)
{
// box2d uses reverse angle
o.pos = box2d.vec2From(o.body.GetPosition());
o.angle = -o.body.GetAngle();
}
}
}
function box2dRender()
{
if (box2dDebug || debugPhysics)
box2d.world.DrawDebugData();
}
// box2d debug drawing
function setupDebugDraw()
{
// setup debug draw
const debugLineWidth = .1;
const debugDraw = new box2d.instance.JSDraw();
const box2dColor = (c)=> new Color(c.get_r(), c.get_g(), c.get_b());
const box2dColorPointer = (c)=>
box2dColor(box2d.instance.wrapPointer(c, box2d.instance.b2Color));
const getDebugColor = (color)=>box2dColorPointer(color).scale(1,.8);
const getPointsList = (vertices, vertexCount)=>
{
const points = [];
for (let i=vertexCount; i--;)
points.push(box2d.vec2FromPointer(vertices+i*8));
return points;
}
debugDraw.DrawSegment = function(point1, point2, color)
{
color = getDebugColor(color);
point1 = box2d.vec2FromPointer(point1);
point2 = box2d.vec2FromPointer(point2);
drawLine(point1, point2, debugLineWidth, color, vec2(), 0, false, false);
};
debugDraw.DrawPolygon = function(vertices, vertexCount, color)
{
color = getDebugColor(color);
const points = getPointsList(vertices, vertexCount);
drawPoly(points, CLEAR_WHITE, debugLineWidth, color, vec2(), 0, false, false);
};
debugDraw.DrawSolidPolygon = function(vertices, vertexCount, color)
{
color = getDebugColor(color);
const points = getPointsList(vertices, vertexCount);
drawPoly(points, color, 0, color, vec2(), 0, false, false);
};
debugDraw.DrawCircle = function(center, radius, color)
{
color = getDebugColor(color);
center = box2d.vec2FromPointer(center);
drawCircle(center, radius*2, CLEAR_WHITE, debugLineWidth, color, false, false);
};
debugDraw.DrawSolidCircle = function(center, radius, axis, color)
{
color = getDebugColor(color);
center = box2d.vec2FromPointer(center);
axis = box2d.vec2FromPointer(axis).scale(radius);
drawCircle(center, radius*2, color, debugLineWidth, color, false, false);
drawLine(vec2(), axis, debugLineWidth, color, center, 0, false, false);
};
debugDraw.DrawTransform = function(transform)
{
transform = box2d.instance.wrapPointer(transform, box2d.instance.b2Transform);
const pos = box2d.vec2From(transform.get_p());
const angle = -transform.get_q().GetAngle();
const p1 = vec2(1,0), c1 = rgb(.75,0,0,.8);
const p2 = vec2(0,1), c2 = rgb(0,.75,0,.8);
drawLine(vec2(), p1, debugLineWidth, c1, pos, angle, false, false);
drawLine(vec2(), p2, debugLineWidth, c2, pos, angle, false, false);
}
debugDraw.AppendFlags(box2d.instance.b2Draw.e_shapeBit);
debugDraw.AppendFlags(box2d.instance.b2Draw.e_jointBit);
//debugDraw.AppendFlags(box2d.instance.b2Draw.e_aabbBit);
//debugDraw.AppendFlags(box2d.instance.b2Draw.e_pairBit);
//debugDraw.AppendFlags(box2d.instance.b2Draw.e_centerOfMassBit);
box2d.world.SetDebugDraw(debugDraw);
}
}