UNPKG

planck-js

Version:

2D JavaScript physics engine for cross-platform HTML5 game development

1,133 lines (963 loc) 29.6 kB
/* * Planck.js * The MIT License * Copyright (c) 2021 Erin Catto, Ali Shakiba * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in all * copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ var _DEBUG = typeof DEBUG === 'undefined' ? false : DEBUG; var _ASSERT = typeof ASSERT === 'undefined' ? false : ASSERT; module.exports = World; var options = require('./util/options'); var common = require('./util/common'); var Vec2 = require('./common/Vec2'); var BroadPhase = require('./collision/BroadPhase'); var Solver = require('./Solver'); var Body = require('./Body'); var Joint = require('./Joint'); var Contact = require('./Contact'); /** * @typedef {Object} WorldDef * * @prop {Vec2} [gravity = { x : 0, y : 0}] * @prop {boolean} [allowSleep = true] * @prop {boolean} [warmStarting = true] * @prop {boolean} [continuousPhysics = true] * @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. */ var WorldDef = { gravity : Vec2.zero(), allowSleep : true, warmStarting : true, continuousPhysics : true, subStepping : false, blockSolve : true, velocityIterations : 8, positionIterations : 3 }; /** * @param {WorldDef|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); 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; // Broad-phase callback. this.addPair = this.createContact.bind(this); } World.prototype._serialize = function() { var bodies = []; var joints = []; for (var b = this.getBodyList(); b; b = b.getNext()) { bodies.push(b); } for (var j = this.getJointList(); j; j = j.getNext()) { if (typeof j._serialize === 'function') { joints.push(j); } } return { gravity: this.m_gravity, bodies: bodies, joints: joints, }; }; World._deserialize = function(data, context, restore) { if (!data) { return new World(); } var world = new World(data.gravity); if (data.bodies) { for(var i = data.bodies.length - 1; i >= 0; i -= 1) { world._addBody(restore(Body, data.bodies[i], world)); } } if (data.joints) { for(var i = data.joints.length - 1; i >= 0; i--) { world.createJoint(restore(Joint, data.joints[i], world)); } } return world; }; /** * 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 World~rayCastCallback * * @param fixture */ /** * Query the world for all fixtures that potentially overlap the provided AABB. * * @param {World~queryCallback} 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(aabb, queryCallback) { _ASSERT && common.assert(typeof queryCallback === 'function'); var broadPhase = this.m_broadPhase; this.m_broadPhase.query(aabb, function(proxyId) { //TODO GC var proxy = broadPhase.getUserData(proxyId); // FixtureProxy return queryCallback(proxy.fixture); }); } /** * @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} reportFixtureCallback A user implemented * callback function. * @param point1 The ray starting point * @param point2 The ray ending point */ World.prototype.rayCast = function(point1, point2, reportFixtureCallback) { _ASSERT && common.assert(typeof reportFixtureCallback === 'function'); var broadPhase = this.m_broadPhase; this.m_broadPhase.rayCast({ maxFraction : 1.0, p1 : point1, p2 : point2 }, function(input, proxyId) { // TODO GC var proxy = broadPhase.getUserData(proxyId); // FixtureProxy var fixture = proxy.fixture; var index = proxy.childIndex; var output = {}; // TODO GC var hit = fixture.rayCast(output, input, index); if (hit) { var fraction = output.fraction; var point = Vec2.add(Vec2.mul((1.0 - fraction), input.p1), Vec2.mul(fraction, input.p2)); return reportFixtureCallback(fixture, point, output.normal, fraction); } return input.maxFraction; }); } /** * 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 && common.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); } /** * @internal Used for deserialize. */ World.prototype._addBody = function(body) { _ASSERT && common.assert(this.isLocked() === false); if (this.isLocked()) { return; } // 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; } /** * 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 && common.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); this._addBody(body); return body; } World.prototype.createDynamicBody = function(def, angle) { if (!def) { def = {}; } else if (Vec2.isValid(def)) { def = { position : def, angle : angle }; } def.type = 'dynamic'; return this.createBody(def); } World.prototype.createKinematicBody = function(def, angle) { if (!def) { def = {}; } else if (Vec2.isValid(def)) { def = { position : def, angle : angle }; } def.type = 'kinematic'; 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 && common.assert(this.m_bodyCount > 0); _ASSERT && common.assert(this.isLocked() == false); if (this.isLocked()) { return; } if (b.m_destroyed) { return false; } // Delete the attached joints. var je = b.m_jointList; while (je) { var je0 = je; je = je.next; this.publish('remove-joint', je0.joint); this.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 = ce; } b.m_contactList = null; // Delete the attached fixtures. This destroys broad-phase proxies. var f = b.m_fixtureList; while (f) { var f0 = f; f = f.m_next; this.publish('remove-fixture', f0); f0.destroyProxies(this.m_broadPhase); b.m_fixtureList = f; } b.m_fixtureList = null; // 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; } b.m_destroyed = true; --this.m_bodyCount; this.publish('remove-body', b); return true; } /** * 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 && common.assert(!!joint.m_bodyA); _ASSERT && common.assert(!!joint.m_bodyB); _ASSERT && common.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 && common.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 && common.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; } } this.publish('remove-joint', joint); } 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} timeStep Time step, this should not vary. * @param {int} velocityIterations * @param {int} positionIterations */ World.prototype.step = function(timeStep, velocityIterations, positionIterations) { this.publish('pre-step', timeStep); if ((velocityIterations | 0) !== velocityIterations) { // TODO: remove this in future velocityIterations = 0; } velocityIterations = velocityIterations || this.m_velocityIterations; positionIterations = positionIterations || this.m_positionIterations; // 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(timeStep); s_step.velocityIterations = velocityIterations; s_step.positionIterations = 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 && timeStep > 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 && timeStep > 0.0) { this.m_solver.solveWorldTOI(s_step); } if (this.m_clearForces) { this.clearForces(); } this.m_locked = false; this.publish('post-step', timeStep); } /** * Call this method to find new contacts. */ World.prototype.findNewContacts = function() { this.m_broadPhase.updatePairs(this.addPair); } /** * @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; } World.prototype._listeners = null; /** * Register an event listener. * * @param {string} name * @param {function} listener */ World.prototype.on = function(name, listener) { if (typeof name !== 'string' || typeof listener !== 'function') { return this; } if (!this._listeners) { this._listeners = {}; } if (!this._listeners[name]) { this._listeners[name] = []; } this._listeners[name].push(listener); return this; }; /** * Remove an event listener. * * @param {string} name * @param {function} listener */ World.prototype.off = function(name, listener) { if (typeof name !== 'string' || typeof listener !== 'function') { return this; } var listeners = this._listeners && this._listeners[name]; if (!listeners || !listeners.length) { return this; } var index = listeners.indexOf(listener); if (index >= 0) { listeners.splice(index, 1); } return this; }; World.prototype.publish = function(name, arg1, arg2, arg3) { var listeners = this._listeners && this._listeners[name]; if (!listeners || !listeners.length) { return 0; } for (var l = 0; l < listeners.length; l++) { listeners[l].call(this, arg1, arg2, arg3); } return listeners.length; }; /** * @event World#remove-body * @event World#remove-joint * @event World#remove-fixture * * 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. * * `function(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. */ /** * @private * @param {Contact} contact */ World.prototype.beginContact = function(contact) { this.publish('begin-contact', contact); }; /** * @event World#begin-contact * * Called when two fixtures begin to touch. * * 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. * * Warning: You cannot create/destroy world entities inside these callbacks. */ /** * @private * @param {Contact} contact */ World.prototype.endContact = function(contact) { this.publish('end-contact', contact); }; /** * @event World#end-contact * * Called when two fixtures cease to touch. * * 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. * * Warning: You cannot create/destroy world entities inside these callbacks. */ /** * @private * @param {Contact} contact * @param {Manifold} oldManifold */ World.prototype.preSolve = function(contact, oldManifold) { this.publish('pre-solve', contact, oldManifold); }; /** * @event World#pre-solve * * 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. */ /** * @private * @param {Contact} contact * @param {ContactImpulse} impulse */ World.prototype.postSolve = function(contact, impulse) { this.publish('post-solve', contact, impulse); }; /** * @event World#post-solve * * 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. */ /** * 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. * * Moved to Fixture. */