UNPKG

planck-js

Version:

2D physics engine for JavaScript/HTML5 game development

1,013 lines (865 loc) 26.6 kB
/* * Copyright (c) 2016 Ali Shakiba http://shakiba.me/planck.js * Copyright (c) 2006-2011 Erin Catto http://www.box2d.org * * This software is provided 'as-is', without any express or implied * warranty. In no event will the authors be held liable for any damages * arising from the use of this software. * Permission is granted to anyone to use this software for any purpose, * including commercial applications, and to alter it and redistribute it * freely, subject to the following restrictions: * 1. The origin of this software must not be misrepresented; you must not * claim that you wrote the original software. If you use this software * in a product, an acknowledgment in the product documentation would be * appreciated but is not required. * 2. Altered source versions must be plainly marked as such, and must not be * misrepresented as being the original software. * 3. This notice may not be removed or altered from any source distribution. */ module.exports = World; var options = require('./util/options'); var Timer = require('./util/Timer'); var Vec2 = require('./common/Vec2'); var BroadPhase = require('./collision/BroadPhase'); var Solver = require('./Solver'); var Body = require('./Body'); var Contact = require('./Contact'); /** * @prop {Vec2} [gravity = { x : 0, y : 0}] * @prop {boolean} [allowSleep = true] * @prop {boolean} [warmStarting = false] * @prop {boolean} [continuousPhysics = false] * @prop {boolean} [subStepping = false] * @prop {boolean} [blockSolve = true] * @prop {int} [velocityIterations = 8] For the velocity constraint solver. * @prop {int} [positionIterations = 3] For the position constraint solver. */ function WorldDef() { this.gravity = new Vec2(); this.allowSleep = true; this.warmStarting = false; this.continuousPhysics = true; this.subStepping = true; this.blockSolve = true; this.velocityIterations = 8; this.positionIterations = 3; } WorldDef.DEFAULTS = new WorldDef(); /** * @param {WordDef|Vec2} def World definition or gravity vector. */ function World(def) { if (!(this instanceof World)) { return new World(def); } if (def && Vec2.IsValid(def)) { def = { gravity : def }; } def = options(def, WorldDef.DEFAULTS); this.m_solver = new Solver(this); this.m_broadPhase = new BroadPhase(); this.m_contactList = null; this.m_contactCount = 0; this.m_bodyList = null; this.m_bodyCount = 0; this.m_jointList = null; this.m_jointCount = 0; this.m_stepComplete = true; this.m_allowSleep = def.allowSleep; this.m_gravity = Vec2.Clone(def.gravity); this.m_clearForces = true; this.m_newFixture = false; this.m_locked = false; // These are for debugging the solver. this.m_warmStarting = def.warmStarting; this.m_continuousPhysics = def.continuousPhysics; this.m_subStepping = def.subStepping; this.m_blockSolve = def.blockSolve; this.m_velocityIterations = def.velocityIterations; this.m_positionIterations = def.positionIterations; this.m_t = 0; } /** * Get the world body list. With the returned body, use Body.GetNext to get the * next body in the world list. A null body indicates the end of the list. * * @return the head of the world body list. */ World.prototype.GetBodyList = function() { return this.m_bodyList; } /** * Get the world joint list. With the returned joint, use Joint.GetNext to get * the next joint in the world list. A null joint indicates the end of the list. * * @return the head of the world joint list. */ World.prototype.GetJointList = function() { return this.m_jointList; } /** * Get the world contact list. With the returned contact, use Contact.GetNext to * get the next contact in the world list. A null contact indicates the end of * the list. * * @return the head of the world contact list. Warning: contacts are created and * destroyed in the middle of a time step. Use ContactListener to avoid * missing contacts. */ World.prototype.GetContactList = function() { return this.m_contactList; } World.prototype.GetBodyCount = function() { return this.m_bodyCount; } World.prototype.GetJointCount = function() { return this.m_jointCount; } /** * Get the number of contacts (each may have 0 or more contact points). */ World.prototype.GetContactCount = function() { return this.m_contactCount; } /** * Change the global gravity vector. */ World.prototype.SetGravity = function(gravity) { this.m_gravity = gravity; } /** * Get the global gravity vector. */ World.prototype.GetGravity = function() { return this.m_gravity; } /** * Is the world locked (in the middle of a time step). */ World.prototype.IsLocked = function() { return this.m_locked; } /** * Enable/disable sleep. */ World.prototype.SetAllowSleeping = function(flag) { if (flag == this.m_allowSleep) { return; } this.m_allowSleep = flag; if (this.m_allowSleep == false) { for (var b = this.m_bodyList; b; b = b.m_next) { b.SetAwake(true); } } } World.prototype.GetAllowSleeping = function() { return this.m_allowSleep; } /** * Enable/disable warm starting. For testing. */ World.prototype.SetWarmStarting = function(flag) { this.m_warmStarting = flag; } World.prototype.GetWarmStarting = function() { return this.m_warmStarting; } /** * Enable/disable continuous physics. For testing. */ World.prototype.SetContinuousPhysics = function(flag) { this.m_continuousPhysics = flag; } World.prototype.GetContinuousPhysics = function() { return this.m_continuousPhysics; } /** * Enable/disable single stepped continuous physics. For testing. */ World.prototype.SetSubStepping = function(flag) { this.m_subStepping = flag; } World.prototype.GetSubStepping = function() { return this.m_subStepping; } /** * Set flag to control automatic clearing of forces after each time step. */ World.prototype.SetAutoClearForces = function(flag) { this.m_clearForces = flag; } /** * Get the flag that controls automatic clearing of forces after each time step. */ World.prototype.GetAutoClearForces = function() { return this.m_clearForces; } /** * Manually clear the force buffer on all bodies. By default, forces are cleared * automatically after each call to Step. The default behavior is modified by * calling SetAutoClearForces. The purpose of this function is to support * sub-stepping. Sub-stepping is often used to maintain a fixed sized time step * under a variable frame-rate. When you perform sub-stepping you will disable * auto clearing of forces and instead call ClearForces after all sub-steps are * complete in one pass of your game loop. * * @see SetAutoClearForces */ World.prototype.ClearForces = function() { for (var body = this.m_bodyList; body; body = body.GetNext()) { body.m_force.SetZero(); body.m_torque = 0.0; } } function WorldQueryWrapper() { this.broadPhase; this.callback; }; WorldQueryWrapper.prototype.QueryCallback = function(proxyId) { var proxy = this.broadPhase.GetUserData(proxyId); // FixtureProxy return this.callback.ReportFixture(proxy.fixture); } /** * @function World~RayCastCallback * * @param fixture */ /** * Query the world for all fixtures that potentially overlap the provided AABB. * * @param {World~QueryCallback} callback.QueryCallback Called for each fixture * found in the query AABB. It may return `false` to terminate the * query. * * @param aabb The query box. */ World.prototype.QueryAABB = function(callback, aabb) { var wrapper = new WorldQueryWrapper(callback); wrapper.broadPhase = this.m_broadPhase; wrapper.callback = callback; this.m_broadPhase.Query(wrapper, aabb); } function WorldRayCastWrapper() { this.broadPhase; this.callback; }; WorldRayCastWrapper.prototype.RayCastCallback = function(input, proxyId) { var proxy = this.broadPhase.GetUserData(proxyId); // FixtureProxy var fixture = proxy.fixture; var index = proxy.childIndex; var output = new RayCastOutput(); var hit = fixture.RayCast(output, input, index); if (hit) { var fraction = output.fraction; var point = Add(Mul((1.0 - fraction), input.p1), Mul(fraction, input.p2)); return this.callback.ReportFixture(fixture, point, output.normal, fraction); } return input.maxFraction; } /** * @function World~RayCastCallback * * Callback class for ray casts. See World.RayCast * * Called for each fixture found in the query. You control how the ray cast * proceeds by returning a float: return -1: ignore this fixture and continue * return 0: terminate the ray cast return fraction: clip the ray to this point * return 1: don't clip the ray and continue * * @param fixture The fixture hit by the ray * @param point The point of initial intersection * @param normal The normal vector at the point of intersection * @param fraction * * @return {float} -1 to filter, 0 to terminate, fraction to clip the ray for * closest hit, 1 to continue */ /** * * Ray-cast the world for all fixtures in the path of the ray. Your callback * controls whether you get the closest point, any point, or n-points. The * ray-cast ignores shapes that contain the starting point. * * @param {World~RayCastCallback} callback.ReportFixture A user implemented * callback function. * @param point1 The ray starting point * @param point2 The ray ending point */ World.prototype.RayCast = function(callback, point1, point2) { var wrapper = new WorldRayCastWrapper(); wrapper.broadPhase = this.m_broadPhase; wrapper.callback = callback; var input = new RayCastInput(); input.maxFraction = 1.0; input.p1 = point1; input.p2 = point2; this.m_broadPhase.RayCast(wrapper, input); } /** * Get the number of broad-phase proxies. */ World.prototype.GetProxyCount = function() { return this.m_broadPhase.GetProxyCount(); } /** * Get the height of broad-phase dynamic tree. */ World.prototype.GetTreeHeight = function() { return this.m_broadPhase.GetTreeHeight(); } /** * Get the balance of broad-phase dynamic tree. * * @returns {int} */ World.prototype.GetTreeBalance = function() { return this.m_broadPhase.GetTreeBalance(); } /** * Get the quality metric of broad-phase dynamic tree. The smaller the better. * The minimum is 1. * * @returns {float} */ World.prototype.GetTreeQuality = function() { return this.m_broadPhase.GetTreeQuality(); } /** * Shift the world origin. Useful for large worlds. The body shift formula is: * position -= newOrigin * * @param {Vec2} newOrigin The new origin with respect to the old origin */ World.prototype.ShiftOrigin = function(newOrigin) { Assert(this.m_locked == false); if (this.m_locked) { return; } for (var b = this.m_bodyList; b; b = b.m_next) { b.m_xf.p.Sub(newOrigin); b.m_sweep.c0.Sub(newOrigin); b.m_sweep.c.Sub(newOrigin); } for (var j = this.m_jointList; j; j = j.m_next) { j.ShiftOrigin(newOrigin); } this.m_broadPhase.ShiftOrigin(newOrigin); } /** * Joints and fixtures are destroyed when their associated body is destroyed. * Register a destruction listener so that you may nullify references to these * joints and shapes. * * `listener.SayGoodbye(object)` is called when any joint or fixture is about to * be destroyed due to the destruction of one of its attached or parent bodies. */ // World.prototype.SetDestructionListener = function(listener) { // } /** * Register a contact filter to provide specific control over collision. * Otherwise the default filter is used (defaultFilter). The listener is owned * by you and must remain in scope. * * @param {ContactFilter} filter */ // World.prototype.SetContactFilter = function(filter) { // } /** * Register a contact event listener. The listener is owned by you and must * remain in scope. * * @param {ContactFilter} listener */ // World.prototype.SetContactListener = function(listener) { // } /** * Create a rigid body given a definition. No reference to the definition is * retained. * * Warning: This function is locked during callbacks. * * @param {BodyDef|Vec2} def Body definition or position. * @param {float} angle Body angle if def is position. */ World.prototype.CreateBody = function(def, angle) { Assert(this.IsLocked() == false); if (this.IsLocked()) { return null; } if (def && Vec2.IsValid(def)) { def = { position : def, angle : angle }; } var body = new Body(this, def); // Add to world doubly linked list. body.m_prev = null; body.m_next = this.m_bodyList; if (this.m_bodyList) { this.m_bodyList.m_prev = body; } this.m_bodyList = body; ++this.m_bodyCount; return body; } World.prototype.CreateDynamicBody = function(def, angle) { if (!def) { def = { type : 'dynamic' }; } else if (Vec2.IsValid(def)) { def = { type : 'dynamic', position : def, angle : angle }; } else { def.type = 'dynamic'; } return this.CreateBody(def); } /** * Destroy a rigid body given a definition. No reference to the definition is * retained. * * Warning: This automatically deletes all associated shapes and joints. * * Warning: This function is locked during callbacks. * * @param {Body} b */ World.prototype.DestroyBody = function(b) { Assert(this.m_bodyCount > 0); Assert(this.IsLocked() == false); if (this.IsLocked()) { return; } // Delete the attached joints. var je = b.m_jointList; while (je) { var je0 = je; je = je.next; this.SayGoodbye(je0.joint); DestroyJoint(je0.joint); b.m_jointList = je; } b.m_jointList = null; // Delete the attached contacts. var ce = b.m_contactList; while (ce) { var ce0 = ce; ce = ce.next; this.DestroyContact(ce0.contact); } b.m_contactList = null; // Delete the attached fixtures. This destroys broad-phase proxies. var /* Fixture */f = b.m_fixtureList; while (f) { var f0 = f; f = f.m_next; this.SayGoodbye(f0); f0.DestroyProxies(this.m_broadPhase); f0.Destroy(); b.m_fixtureList = f; b.m_fixtureCount -= 1; } b.m_fixtureList = null; b.m_fixtureCount = 0; // Remove world body list. if (b.m_prev) { b.m_prev.m_next = b.m_next; } if (b.m_next) { b.m_next.m_prev = b.m_prev; } if (b == this.m_bodyList) { this.m_bodyList = b.m_next; } --this.m_bodyCount; } /** * Create a joint to constrain bodies together. No reference to the definition * is retained. This may cause the connected bodies to cease colliding. * * Warning: This function is locked during callbacks. * * @param {Joint} join * @param {Body} bodyB * @param {Body} bodyA */ World.prototype.CreateJoint = function(joint) { Assert(!!joint.m_bodyA); Assert(!!joint.m_bodyB); Assert(this.IsLocked() == false); if (this.IsLocked()) { return null; } // Connect to the world list. joint.m_prev = null; joint.m_next = this.m_jointList; if (this.m_jointList) { this.m_jointList.m_prev = joint; } this.m_jointList = joint; ++this.m_jointCount; // Connect to the bodies' doubly linked lists. joint.m_edgeA.joint = joint; joint.m_edgeA.other = joint.m_bodyB; joint.m_edgeA.prev = null; joint.m_edgeA.next = joint.m_bodyA.m_jointList; if (joint.m_bodyA.m_jointList) joint.m_bodyA.m_jointList.prev = joint.m_edgeA; joint.m_bodyA.m_jointList = joint.m_edgeA; joint.m_edgeB.joint = joint; joint.m_edgeB.other = joint.m_bodyA; joint.m_edgeB.prev = null; joint.m_edgeB.next = joint.m_bodyB.m_jointList; if (joint.m_bodyB.m_jointList) joint.m_bodyB.m_jointList.prev = joint.m_edgeB; joint.m_bodyB.m_jointList = joint.m_edgeB; // If the joint prevents collisions, then flag any contacts for filtering. if (joint.m_collideConnected == false) { for (var edge = joint.m_bodyB.GetContactList(); edge; edge = edge.next) { if (edge.other == joint.m_bodyA) { // Flag the contact for filtering at the next time step (where either // body is awake). edge.contact.FlagForFiltering(); } } } // Note: creating a joint doesn't wake the bodies. return joint; } /** * Destroy a joint. This may cause the connected bodies to begin colliding. * Warning: This function is locked during callbacks. * * @param {Joint} join */ World.prototype.DestroyJoint = function(joint) { Assert(this.IsLocked() == false); if (this.IsLocked()) { return; } // Remove from the doubly linked list. if (joint.m_prev) { joint.m_prev.m_next = joint.m_next; } if (joint.m_next) { joint.m_next.m_prev = joint.m_prev; } if (joint == this.m_jointList) { this.m_jointList = joint.m_next; } // Disconnect from bodies. var bodyA = joint.m_bodyA; var bodyB = joint.m_bodyB; // Wake up connected bodies. bodyA.SetAwake(true); bodyB.SetAwake(true); // Remove from body 1. if (joint.m_edgeA.prev) { joint.m_edgeA.prev.next = joint.m_edgeA.next; } if (joint.m_edgeA.next) { joint.m_edgeA.next.prev = joint.m_edgeA.prev; } if (joint.m_edgeA == bodyA.m_jointList) { bodyA.m_jointList = joint.m_edgeA.next; } joint.m_edgeA.prev = null; joint.m_edgeA.next = null; // Remove from body 2 if (joint.m_edgeB.prev) { joint.m_edgeB.prev.next = joint.m_edgeB.next; } if (joint.m_edgeB.next) { joint.m_edgeB.next.prev = joint.m_edgeB.prev; } if (joint.m_edgeB == bodyB.m_jointList) { bodyB.m_jointList = joint.m_edgeB.next; } joint.m_edgeB.prev = null; joint.m_edgeB.next = null; Assert(this.m_jointCount > 0); --this.m_jointCount; // If the joint prevents collisions, then flag any contacts for filtering. if (joint.m_collideConnected == false) { var edge = bodyB.GetContactList(); while (edge) { if (edge.other == bodyA) { // Flag the contact for filtering at the next time step (where either // body is awake). edge.contact.FlagForFiltering(); } edge = edge.next; } } } var s_step = new Solver.TimeStep(); // reuse /** * Take a time step. This performs collision detection, integration, and * constraint solution. * * Broad-phase, narrow-phase, solve and solve time of impacts. * * @param {float} ts Time step, this should not vary. * @param {float} dt Elapsed time, since last call. */ World.prototype.Step = function(ts, dt) { if (typeof dt === 'number') { this.m_t += dt; while (this.m_t > ts) { this.Step(ts); this.m_t -= ts; } } // If new fixtures were added, we need to find the new contacts. if (this.m_newFixture) { this.FindNewContacts(); this.m_newFixture = false; } this.m_locked = true; s_step.Reset(ts); s_step.velocityIterations = this.m_velocityIterations; s_step.positionIterations = this.m_positionIterations; s_step.warmStarting = this.m_warmStarting; s_step.blockSolve = this.m_blockSolve; // Update contacts. This is where some contacts are destroyed. this.UpdateContacts(); // Integrate velocities, solve velocity constraints, and integrate positions. if (this.m_stepComplete && ts > 0.0) { this.m_solver.SolveWorld(s_step); // Synchronize fixtures, check for out of range bodies. for (var b = this.m_bodyList; b; b = b.GetNext()) { // If a body was not in an island then it did not move. if (b.m_islandFlag == false) { continue; } if (b.IsStatic()) { continue; } // Update fixtures (for broad-phase). b.SynchronizeFixtures(); } // Look for new contacts. this.FindNewContacts(); } // Handle TOI events. if (this.m_continuousPhysics && ts > 0.0) { this.m_solver.SolveWorldTOI(s_step); } if (this.m_clearForces) { this.ClearForces(); } this.m_locked = false; } /** * Call this method to find new contacts. */ World.prototype.FindNewContacts = function() { this.m_broadPhase.UpdatePairs(this); } /** * Broad-phase callback. * * @private * * @param {FixtureProxy} proxyA * @param {FixtureProxy} proxyB */ World.prototype.AddPair = function(proxyA, proxyB) { return this.CreateContact(proxyA, proxyB); } /** * @private * * @param {FixtureProxy} proxyA * @param {FixtureProxy} proxyB */ World.prototype.CreateContact = function(proxyA, proxyB) { var fixtureA = proxyA.fixture; var fixtureB = proxyB.fixture; var indexA = proxyA.childIndex; var indexB = proxyB.childIndex; var bodyA = fixtureA.GetBody(); var bodyB = fixtureB.GetBody(); // Are the fixtures on the same body? if (bodyA == bodyB) { return; } // TODO_ERIN use a hash table to remove a potential bottleneck when both // bodies have a lot of contacts. // Does a contact already exist? var edge = bodyB.GetContactList(); // ContactEdge while (edge) { if (edge.other == bodyA) { var fA = edge.contact.GetFixtureA(); var fB = edge.contact.GetFixtureB(); var iA = edge.contact.GetChildIndexA(); var iB = edge.contact.GetChildIndexB(); if (fA == fixtureA && fB == fixtureB && iA == indexA && iB == indexB) { // A contact already exists. return; } if (fA == fixtureB && fB == fixtureA && iA == indexB && iB == indexA) { // A contact already exists. return; } } edge = edge.next; } if (bodyB.ShouldCollide(bodyA) == false) { return; } if (fixtureB.ShouldCollide(fixtureA) == false) { return; } // Call the factory. var contact = Contact.Create(fixtureA, indexA, fixtureB, indexB); if (contact == null) { return; } // Insert into the world. contact.m_prev = null; if (this.m_contactList != null) { contact.m_next = this.m_contactList; this.m_contactList.m_prev = contact; } this.m_contactList = contact; ++this.m_contactCount; } /** * Removes old non-overlapping contacts, applies filters and updates contacts. */ World.prototype.UpdateContacts = function() { // Update awake contacts. var c, next_c = this.m_contactList; while (c = next_c) { next_c = c.GetNext() var fixtureA = c.GetFixtureA(); var fixtureB = c.GetFixtureB(); var indexA = c.GetChildIndexA(); var indexB = c.GetChildIndexB(); var bodyA = fixtureA.GetBody(); var bodyB = fixtureB.GetBody(); // Is this contact flagged for filtering? if (c.m_filterFlag) { if (bodyB.ShouldCollide(bodyA) == false) { this.DestroyContact(c); continue; } if (fixtureB.ShouldCollide(fixtureA) == false) { this.DestroyContact(c); continue; } // Clear the filtering flag. c.m_filterFlag = false; } var activeA = bodyA.IsAwake() && !bodyA.IsStatic(); var activeB = bodyB.IsAwake() && !bodyB.IsStatic(); // At least one body must be awake and it must be dynamic or kinematic. if (activeA == false && activeB == false) { continue; } var proxyIdA = fixtureA.m_proxies[indexA].proxyId; var proxyIdB = fixtureB.m_proxies[indexB].proxyId; var overlap = this.m_broadPhase.TestOverlap(proxyIdA, proxyIdB); // Here we destroy contacts that cease to overlap in the broad-phase. if (overlap == false) { this.DestroyContact(c); continue; } // The contact persists. c.Update(this); } } /** * @param {Contact} contact */ World.prototype.DestroyContact = function(contact) { Contact.Destroy(contact, this); // Remove from the world. if (contact.m_prev) { contact.m_prev.m_next = contact.m_next; } if (contact.m_next) { contact.m_next.m_prev = contact.m_prev; } if (contact == this.m_contactList) { this.m_contactList = contact.m_next; } --this.m_contactCount; } /** * Implement contact callbacks to get contact information. You can use these * results for things like sounds and game logic. You can also get contact * results by traversing the contact lists after the time step. However, you * might miss some contacts because continuous physics leads to sub-stepping. * Additionally you may receive multiple callbacks for the same contact in a * single time step. You should strive to make your callbacks efficient because * there may be many callbacks per time step. */ /** * Called when two fixtures begin to touch. * * Warning: You cannot create/destroy world entities inside these callbacks. * * @param {Contact} contact */ World.prototype.BeginContact = function(contact) { }; /** * Called when two fixtures cease to touch. * * Warning: You cannot create/destroy world entities inside these callbacks. * * @param {Contact} contact */ World.prototype.EndContact = function(contact) { }; /** * This is called after a contact is updated. This allows you to inspect a * contact before it goes to the solver. If you are careful, you can modify the * contact manifold (e.g. disable contact). A copy of the old manifold is * provided so that you can detect changes. Note: this is called only for awake * bodies. Note: this is called even when the number of contact points is zero. * Note: this is not called for sensors. Note: if you set the number of contact * points to zero, you will not get an EndContact callback. However, you may get * a BeginContact callback the next step. * * Warning: You cannot create/destroy world entities inside these callbacks. * * @param {Contact} contact * @param {Manifold} oldManifold */ World.prototype.PreSolve = function(contact, oldManifold) { }; /** * This lets you inspect a contact after the solver is finished. This is useful * for inspecting impulses. Note: the contact manifold does not include time of * impact impulses, which can be arbitrarily large if the sub-step is small. * Hence the impulse is provided explicitly in a separate data structure. Note: * this is only called for contacts that are touching, solid, and awake. * * Warning: You cannot create/destroy world entities inside these callbacks. * * @param {Contact} contact * @param {ContactImpulse} impulse */ World.prototype.PostSolve = function(contact, impulse) { }; /** * Joints and fixtures are destroyed when their associated body is destroyed. * Register a destruction listener so that you may nullify references to these * joints and shapes. * * This method is called when any joint or fixture is about to be destroyed due * to the destruction of one of its attached or parent bodies. */ World.prototype.SayGoodbye = function(object) { };