From 0784a5b5df7af08c3e38c3c599148c57226a3bdf Mon Sep 17 00:00:00 2001 From: liabru Date: Sun, 1 Sep 2019 12:21:16 +0100 Subject: [PATCH] Added readonly body.deltaTime Added delta property to engine update event Added delta argument to various internal functions Changed timeScale argument to use delta instead on various internal functions Fixed issues when using an engine update delta of 0 Improved time independence for friction, air friction, restitution, sleeping, collisions, constraints Removed optional correction argument from Engine.update Removed correction and timeScale from Body.update and Matter.Runner --- src/body/Body.js | 43 +++++++++++++++++++---------- src/collision/Detector.js | 5 ++-- src/collision/Resolver.js | 25 +++++++++-------- src/collision/SAT.js | 13 +++++++-- src/constraint/Constraint.js | 26 ++++++++++-------- src/core/Common.js | 1 + src/core/Engine.js | 53 ++++++++++++++++-------------------- src/core/Runner.js | 32 ++++++---------------- src/core/Sleeping.js | 23 ++++++++-------- 9 files changed, 114 insertions(+), 107 deletions(-) diff --git a/src/body/Body.js b/src/body/Body.js index 39e0a9a7..8e135f20 100644 --- a/src/body/Body.js +++ b/src/body/Body.js @@ -15,13 +15,13 @@ module.exports = Body; var Vertices = require('../geometry/Vertices'); var Vector = require('../geometry/Vector'); var Sleeping = require('../core/Sleeping'); -var Render = require('../render/Render'); var Common = require('../core/Common'); var Bounds = require('../geometry/Bounds'); var Axes = require('../geometry/Axes'); (function() { + Body._timeCorrection = true; Body._inertiaScale = 4; Body._nextCollidingGroupId = 1; Body._nextNonCollidingGroupId = -1; @@ -95,6 +95,7 @@ var Axes = require('../geometry/Axes'); area: 0, mass: 0, inertia: 0, + deltaTime: null, _original: null }; @@ -462,8 +463,8 @@ var Axes = require('../geometry/Axes'); */ Body.setPosition = function(body, position) { var delta = Vector.sub(position, body.position); - body.positionPrev.x += delta.x; - body.positionPrev.y += delta.y; + body.positionPrev.x += delta.x; + body.positionPrev.y += delta.y; for (var i = 0; i < body.parts.length; i++) { var part = body.parts[i]; @@ -482,7 +483,7 @@ var Axes = require('../geometry/Axes'); */ Body.setAngle = function(body, angle) { var delta = angle - body.angle; - body.anglePrev += delta; + body.anglePrev += delta; for (var i = 0; i < body.parts.length; i++) { var part = body.parts[i]; @@ -625,26 +626,28 @@ var Axes = require('../geometry/Axes'); * Performs a simulation step for the given `body`, including updating position and angle using Verlet integration. * @method update * @param {body} body - * @param {number} deltaTime - * @param {number} timeScale - * @param {number} correction + * @param {number} [deltaTime=16.666] */ - Body.update = function(body, deltaTime, timeScale, correction) { - var deltaTimeSquared = Math.pow(deltaTime * timeScale * body.timeScale, 2); + Body.update = function(body, deltaTime) { + deltaTime = (typeof deltaTime !== 'undefined' ? deltaTime : Common._timeUnit) * body.timeScale; + + var deltaTimeSquared = deltaTime * deltaTime, + correction = Body._timeCorrection ? deltaTime / (body.deltaTime || deltaTime) : 1; // from the previous step - var frictionAir = 1 - body.frictionAir * timeScale * body.timeScale, - velocityPrevX = body.position.x - body.positionPrev.x, - velocityPrevY = body.position.y - body.positionPrev.y; + var frictionAir = 1 - body.frictionAir * (deltaTime / Common._timeUnit), + velocityPrevX = (body.position.x - body.positionPrev.x) * correction, + velocityPrevY = (body.position.y - body.positionPrev.y) * correction; // update velocity with Verlet integration - body.velocity.x = (velocityPrevX * frictionAir * correction) + (body.force.x / body.mass) * deltaTimeSquared; - body.velocity.y = (velocityPrevY * frictionAir * correction) + (body.force.y / body.mass) * deltaTimeSquared; + body.velocity.x = (velocityPrevX * frictionAir) + (body.force.x / body.mass) * deltaTimeSquared; + body.velocity.y = (velocityPrevY * frictionAir) + (body.force.y / body.mass) * deltaTimeSquared; body.positionPrev.x = body.position.x; body.positionPrev.y = body.position.y; body.position.x += body.velocity.x; body.position.y += body.velocity.y; + body.deltaTime = deltaTime; // update angular velocity with Verlet integration body.angularVelocity = ((body.angle - body.anglePrev) * frictionAir * correction) + (body.torque / body.inertia) * deltaTimeSquared; @@ -880,7 +883,7 @@ var Axes = require('../geometry/Axes'); /** * A `Vector` that _measures_ the current velocity of the body after the last `Body.update`. It is read-only. * If you need to modify a body's velocity directly, you should either apply a force or simply change the body's `position` (as the engine uses position-Verlet integration). - * + * * @readOnly * @property velocity * @type vector @@ -1109,6 +1112,16 @@ var Axes = require('../geometry/Axes'); * @default 1 */ + /** + * A `Number` that records the last delta time value used to update this body. + * This is automatically updated by the engine inside of `Body.update`. + * + * @readOnly + * @property deltaTime + * @type number + * @default null + */ + /** * An `Object` that defines the rendering properties to be consumed by the module `Matter.Render`. * diff --git a/src/collision/Detector.js b/src/collision/Detector.js index dfbf5dd2..96408527 100644 --- a/src/collision/Detector.js +++ b/src/collision/Detector.js @@ -21,9 +21,10 @@ var Bounds = require('../geometry/Bounds'); * @method collisions * @param {pair[]} broadphasePairs * @param {engine} engine + * @param {number} delta * @return {array} collisions */ - Detector.collisions = function(broadphasePairs, engine) { + Detector.collisions = function(broadphasePairs, engine, delta) { var collisions = [], pairsTable = engine.pairs.table; @@ -66,7 +67,7 @@ var Bounds = require('../geometry/Bounds'); } // narrow phase - var collision = SAT.collides(partA, partB, previousCollision); + var collision = SAT.collides(partA, partB, previousCollision, delta); // @if DEBUG metrics.narrowphaseTests += 1; diff --git a/src/collision/Resolver.js b/src/collision/Resolver.js index 37c9692a..4ebe999b 100644 --- a/src/collision/Resolver.js +++ b/src/collision/Resolver.js @@ -49,9 +49,9 @@ var Bounds = require('../geometry/Bounds'); * @method solvePosition * @param {pair[]} pairs * @param {body[]} bodies - * @param {number} timeScale + * @param {number} delta */ - Resolver.solvePosition = function(pairs, bodies, timeScale) { + Resolver.solvePosition = function(pairs, bodies, delta) { var i, normalX, normalY, @@ -68,7 +68,8 @@ var Bounds = require('../geometry/Bounds'); bodyBtoAX, bodyBtoAY, positionImpulse, - impulseCoefficient = timeScale * Resolver._positionDampen; + timeScale = delta / Common._timeUnit, + impulseCoefficient = Resolver._positionDampen * timeScale; for (i = 0; i < bodies.length; i++) { var body = bodies[i]; @@ -231,10 +232,12 @@ var Bounds = require('../geometry/Bounds'); * Find a solution for pair velocities. * @method solveVelocity * @param {pair[]} pairs - * @param {number} timeScale + * @param {number} delta */ - Resolver.solveVelocity = function(pairs, timeScale) { - var timeScaleSquared = timeScale * timeScale, + Resolver.solveVelocity = function(pairs, delta) { + var timeScale = delta / Common._timeUnit, + timeScale2 = timeScale * timeScale, + timeScale3 = timeScale2 * timeScale, impulse = Vector._temp[0], tempA = Vector._temp[1], tempB = Vector._temp[2], @@ -287,10 +290,10 @@ var Bounds = require('../geometry/Bounds'); var tangentImpulse = tangentVelocity, maxFriction = Infinity; - if (tangentSpeed > pair.friction * pair.frictionStatic * normalForce * timeScaleSquared) { - maxFriction = tangentSpeed; + if (tangentSpeed > pair.friction * pair.frictionStatic * normalForce * timeScale3) { + maxFriction = tangentSpeed * timeScale; tangentImpulse = Common.clamp( - pair.friction * tangentVelocityDirection * timeScaleSquared, + pair.friction * tangentVelocityDirection * timeScale3, -maxFriction, maxFriction ); } @@ -304,7 +307,7 @@ var Bounds = require('../geometry/Bounds'); tangentImpulse *= share; // handle high velocity and resting collisions separately - if (normalVelocity < 0 && normalVelocity * normalVelocity > Resolver._restingThresh * timeScaleSquared) { + if (normalVelocity < 0 && normalVelocity * normalVelocity > Resolver._restingThresh * timeScale2) { // high normal velocity so clear cached contact normal impulse contact.normalImpulse = 0; } else { @@ -316,7 +319,7 @@ var Bounds = require('../geometry/Bounds'); } // handle high velocity and resting collisions separately - if (tangentVelocity * tangentVelocity > Resolver._restingThreshTangent * timeScaleSquared) { + if (tangentVelocity * tangentVelocity > Resolver._restingThreshTangent * timeScale2) { // high tangent velocity so clear cached contact tangent impulse contact.tangentImpulse = 0; } else { diff --git a/src/collision/SAT.js b/src/collision/SAT.js index 29a58c35..6ef175c6 100644 --- a/src/collision/SAT.js +++ b/src/collision/SAT.js @@ -12,23 +12,30 @@ module.exports = SAT; var Vertices = require('../geometry/Vertices'); var Vector = require('../geometry/Vector'); +var Common = require('../core/Common'); (function() { + SAT._reuseMotionThresh = 0.2; + /** * Detect collision between two bodies using the Separating Axis Theorem. * @method collides * @param {body} bodyA * @param {body} bodyB * @param {collision} previousCollision + * @param {number} [delta=0] * @return {collision} collision */ - SAT.collides = function(bodyA, bodyB, previousCollision) { + SAT.collides = function(bodyA, bodyB, previousCollision, delta) { var overlapAB, overlapBA, minOverlap, collision, - canReusePrevCol = false; + canReusePrevCol = false, + timeScale = delta / Common._timeUnit; + + delta = typeof delta !== 'undefined' ? delta : 0; if (previousCollision) { // estimate total motion @@ -39,7 +46,7 @@ var Vector = require('../geometry/Vector'); // we may be able to (partially) reuse collision result // but only safe if collision was resting - canReusePrevCol = previousCollision && previousCollision.collided && motion < 0.2; + canReusePrevCol = previousCollision && previousCollision.collided && motion < SAT._reuseMotionThresh * timeScale * timeScale; // reuse collision object collision = previousCollision; diff --git a/src/constraint/Constraint.js b/src/constraint/Constraint.js index 9e49f16a..ee1a7acb 100644 --- a/src/constraint/Constraint.js +++ b/src/constraint/Constraint.js @@ -110,9 +110,11 @@ var Common = require('../core/Common'); * @private * @method solveAll * @param {constraint[]} constraints - * @param {number} timeScale + * @param {number} delta */ - Constraint.solveAll = function(constraints, timeScale) { + Constraint.solveAll = function(constraints, delta) { + var timeScale = Common.clamp(delta / Common._timeUnit, 0, 1); + // Solve fixed constraints first. for (var i = 0; i < constraints.length; i += 1) { var constraint = constraints[i], @@ -183,7 +185,9 @@ var Common = require('../core/Common'); // solve distance constraint with Gauss-Siedel method var difference = (currentLength - constraint.length) / currentLength, - stiffness = constraint.stiffness < 1 ? constraint.stiffness * timeScale : constraint.stiffness, + isRigid = constraint.stiffness >= 1 || constraint.length === 0, + stiffness = isRigid ? constraint.stiffness : constraint.stiffness * timeScale * timeScale, + damping = constraint.damping * timeScale, force = Vector.mult(delta, difference * stiffness), massTotal = (bodyA ? bodyA.inverseMass : 0) + (bodyB ? bodyB.inverseMass : 0), inertiaTotal = (bodyA ? bodyA.inverseInertia : 0) + (bodyB ? bodyB.inverseInertia : 0), @@ -193,8 +197,8 @@ var Common = require('../core/Common'); normal, normalVelocity, relativeVelocity; - - if (constraint.damping) { + + if (damping > 0) { var zero = Vector.create(); normal = Vector.div(delta, currentLength); @@ -218,9 +222,9 @@ var Common = require('../core/Common'); bodyA.position.y -= force.y * share; // apply damping - if (constraint.damping) { - bodyA.positionPrev.x -= constraint.damping * normal.x * normalVelocity * share; - bodyA.positionPrev.y -= constraint.damping * normal.y * normalVelocity * share; + if (damping > 0) { + bodyA.positionPrev.x -= damping * normal.x * normalVelocity * share; + bodyA.positionPrev.y -= damping * normal.y * normalVelocity * share; } // apply torque @@ -241,9 +245,9 @@ var Common = require('../core/Common'); bodyB.position.y += force.y * share; // apply damping - if (constraint.damping) { - bodyB.positionPrev.x += constraint.damping * normal.x * normalVelocity * share; - bodyB.positionPrev.y += constraint.damping * normal.y * normalVelocity * share; + if (damping > 0) { + bodyB.positionPrev.x += damping * normal.x * normalVelocity * share; + bodyB.positionPrev.y += damping * normal.y * normalVelocity * share; } // apply torque diff --git a/src/core/Common.js b/src/core/Common.js index 28e29be6..a11dc8ab 100644 --- a/src/core/Common.js +++ b/src/core/Common.js @@ -10,6 +10,7 @@ module.exports = Common; (function() { + Common._timeUnit = 1000 / 60; Common._nextId = 0; Common._seed = 0; Common._nowStartTime = +(new Date()); diff --git a/src/core/Engine.js b/src/core/Engine.js index 4afd6f47..2be4912d 100644 --- a/src/core/Engine.js +++ b/src/core/Engine.js @@ -97,35 +97,29 @@ var Body = require('../body/Body'); /** * Moves the simulation forward in time by `delta` ms. - * The `correction` argument is an optional `Number` that specifies the time correction factor to apply to the update. - * This can help improve the accuracy of the simulation in cases where `delta` is changing between updates. - * The value of `correction` is defined as `delta / lastDelta`, i.e. the percentage change of `delta` over the last step. - * Therefore the value is always `1` (no correction) when `delta` constant (or when no correction is desired, which is the default). - * See the paper on Time Corrected Verlet for more information. - * * Triggers `beforeUpdate` and `afterUpdate` events. * Triggers `collisionStart`, `collisionActive` and `collisionEnd` events. * @method update * @param {engine} engine * @param {number} [delta=16.666] - * @param {number} [correction=1] */ - Engine.update = function(engine, delta, correction) { - delta = delta || 1000 / 60; - correction = correction || 1; - + Engine.update = function(engine, delta) { var world = engine.world, timing = engine.timing, broadphase = engine.broadphase, - broadphasePairs = [], + broadphasePairs, i; + delta = typeof delta !== 'undefined' ? delta : Common._timeUnit; + delta *= timing.timeScale; + // increment timestamp - timing.timestamp += delta * timing.timeScale; + timing.timestamp += delta; // create an event object var event = { - timestamp: timing.timestamp + timestamp: timing.timestamp, + delta: delta }; Events.trigger(engine, 'beforeUpdate', event); @@ -141,18 +135,20 @@ var Body = require('../body/Body'); // if sleeping enabled, call the sleeping controller if (engine.enableSleeping) - Sleeping.update(allBodies, timing.timeScale); + Sleeping.update(allBodies, delta); // applies gravity to all bodies Engine._bodiesApplyGravity(allBodies, world.gravity); // update all body position and rotation by integration - Engine._bodiesUpdate(allBodies, delta, timing.timeScale, correction, world.bounds); + if (delta > 0) { + Engine._bodiesUpdate(allBodies, delta); + } // update all constraints (first pass) Constraint.preSolveAll(allBodies); for (i = 0; i < engine.constraintIterations; i++) { - Constraint.solveAll(allConstraints, timing.timeScale); + Constraint.solveAll(allConstraints, delta); } Constraint.postSolveAll(allBodies); @@ -176,7 +172,7 @@ var Body = require('../body/Body'); } // narrowphase pass: find actual collisions, then create or update collision pairs - var collisions = broadphase.detector(broadphasePairs, engine); + var collisions = broadphase.detector(broadphasePairs, engine, delta); // update collision pairs var pairs = engine.pairs, @@ -186,7 +182,7 @@ var Body = require('../body/Body'); // wake up bodies involved in collisions if (engine.enableSleeping) - Sleeping.afterCollisions(pairs.list, timing.timeScale); + Sleeping.afterCollisions(pairs.list, delta); // trigger collision events if (pairs.collisionStart.length > 0) @@ -195,21 +191,21 @@ var Body = require('../body/Body'); // iteratively resolve position between collisions Resolver.preSolvePosition(pairs.list); for (i = 0; i < engine.positionIterations; i++) { - Resolver.solvePosition(pairs.list, allBodies, timing.timeScale); + Resolver.solvePosition(pairs.list, allBodies, delta); } Resolver.postSolvePosition(allBodies); // update all constraints (second pass) Constraint.preSolveAll(allBodies); for (i = 0; i < engine.constraintIterations; i++) { - Constraint.solveAll(allConstraints, timing.timeScale); + Constraint.solveAll(allConstraints, delta); } Constraint.postSolveAll(allBodies); // iteratively resolve velocity between collisions Resolver.preSolveVelocity(pairs.list); for (i = 0; i < engine.velocityIterations; i++) { - Resolver.solveVelocity(pairs.list, timing.timeScale); + Resolver.solveVelocity(pairs.list, delta); } // trigger collision events @@ -322,21 +318,16 @@ var Body = require('../body/Body'); * @method _bodiesUpdate * @private * @param {body[]} bodies - * @param {number} deltaTime - * The amount of time elapsed between updates - * @param {number} timeScale - * @param {number} correction - * The Verlet correction factor (deltaTime / lastDeltaTime) - * @param {bounds} worldBounds + * @param {number} delta The amount of time elapsed between updates */ - Engine._bodiesUpdate = function(bodies, deltaTime, timeScale, correction, worldBounds) { + Engine._bodiesUpdate = function(bodies, delta) { for (var i = 0; i < bodies.length; i++) { var body = bodies[i]; if (body.isStatic || body.isSleeping) continue; - Body.update(body, deltaTime, timeScale, correction); + Body.update(body, delta); } }; @@ -373,6 +364,7 @@ var Body = require('../body/Body'); * @param {} event An event object * @param {} event.pairs List of affected pairs * @param {number} event.timestamp The engine.timing.timestamp of the event + * @param {number} event.delta The delta time in milliseconds value used in the update * @param {} event.source The source object of the event * @param {} event.name The name of the event */ @@ -384,6 +376,7 @@ var Body = require('../body/Body'); * @param {} event An event object * @param {} event.pairs List of affected pairs * @param {number} event.timestamp The engine.timing.timestamp of the event + * @param {number} event.delta The delta time in milliseconds value used in the update * @param {} event.source The source object of the event * @param {} event.name The name of the event */ diff --git a/src/core/Runner.js b/src/core/Runner.js index 21274beb..f199649b 100644 --- a/src/core/Runner.js +++ b/src/core/Runner.js @@ -53,13 +53,11 @@ var Common = require('./Common'); Runner.create = function(options) { var defaults = { fps: 60, - correction: 1, deltaSampleSize: 60, counterTimestamp: 0, frameCounter: 0, deltaHistory: [], timePrev: null, - timeScalePrev: 1, frameRequestId: null, isFixed: false, enabled: true @@ -100,7 +98,7 @@ var Common = require('./Common'); /** * A game loop utility that updates the engine and renderer by one step (a 'tick'). - * Features delta smoothing, time correction and fixed or dynamic timing. + * Features delta smoothing and fixed or dynamic timing. * Triggers `beforeTick`, `tick` and `afterTick` events on the engine. * Consider just `Engine.update(engine, delta)` if you're using your own loop. * @method tick @@ -110,17 +108,8 @@ var Common = require('./Common'); */ Runner.tick = function(runner, engine, time) { var timing = engine.timing, - correction = 1, delta; - // create an event object - var event = { - timestamp: timing.timestamp - }; - - Events.trigger(runner, 'beforeTick', event); - Events.trigger(engine, 'beforeTick', event); // @deprecated - if (runner.isFixed) { // fixed timestep delta = runner.delta; @@ -133,27 +122,22 @@ var Common = require('./Common'); runner.deltaHistory.push(delta); runner.deltaHistory = runner.deltaHistory.slice(-runner.deltaSampleSize); delta = Math.min.apply(null, runner.deltaHistory); - + // limit delta delta = delta < runner.deltaMin ? runner.deltaMin : delta; delta = delta > runner.deltaMax ? runner.deltaMax : delta; - // correction for delta - correction = delta / runner.delta; - // update engine timing object runner.delta = delta; } - // time correction for time scaling - if (runner.timeScalePrev !== 0) - correction *= timing.timeScale / runner.timeScalePrev; - - if (timing.timeScale === 0) - correction = 0; + // create an event object + var event = { + timestamp: timing.timestamp + }; - runner.timeScalePrev = timing.timeScale; - runner.correction = correction; + Events.trigger(runner, 'beforeTick', event); + Events.trigger(engine, 'beforeTick', event); // @deprecated // fps counter runner.frameCounter += 1; diff --git a/src/core/Sleeping.js b/src/core/Sleeping.js index b284ea93..53be2e87 100644 --- a/src/core/Sleeping.js +++ b/src/core/Sleeping.js @@ -9,6 +9,7 @@ var Sleeping = {}; module.exports = Sleeping; var Events = require('./Events'); +var Common = require('./Common'); (function() { @@ -20,11 +21,11 @@ var Events = require('./Events'); * Puts bodies to sleep or wakes them up depending on their motion. * @method update * @param {body[]} bodies - * @param {number} timeScale + * @param {number} delta */ - Sleeping.update = function(bodies, timeScale) { - var timeFactor = timeScale * timeScale * timeScale; - + Sleeping.update = function(bodies, delta) { + var timeScale = delta / Common._timeUnit; + // update bodies sleeping status for (var i = 0; i < bodies.length; i++) { var body = bodies[i], @@ -41,11 +42,11 @@ var Events = require('./Events'); // biased average motion estimation between frames body.motion = Sleeping._minBias * minMotion + (1 - Sleeping._minBias) * maxMotion; - - if (body.sleepThreshold > 0 && body.motion < Sleeping._motionSleepThreshold * timeFactor) { + + if (body.sleepThreshold > 0 && body.motion < Sleeping._motionSleepThreshold * timeScale * timeScale) { body.sleepCounter += 1; - if (body.sleepCounter >= body.sleepThreshold) + if (body.sleepCounter >= body.sleepThreshold / timeScale) Sleeping.set(body, true); } else if (body.sleepCounter > 0) { body.sleepCounter -= 1; @@ -57,10 +58,10 @@ var Events = require('./Events'); * Given a set of colliding pairs, wakes the sleeping bodies involved. * @method afterCollisions * @param {pair[]} pairs - * @param {number} timeScale + * @param {number} delta */ - Sleeping.afterCollisions = function(pairs, timeScale) { - var timeFactor = timeScale * timeScale * timeScale; + Sleeping.afterCollisions = function(pairs, delta) { + var timeScale = delta / Common._timeUnit; // wake up bodies involved in collisions for (var i = 0; i < pairs.length; i++) { @@ -82,7 +83,7 @@ var Events = require('./Events'); var sleepingBody = (bodyA.isSleeping && !bodyA.isStatic) ? bodyA : bodyB, movingBody = sleepingBody === bodyA ? bodyB : bodyA; - if (!sleepingBody.isStatic && movingBody.motion > Sleeping._motionWakeThreshold * timeFactor) { + if (!sleepingBody.isStatic && movingBody.motion > Sleeping._motionWakeThreshold * timeScale * timeScale) { Sleeping.set(sleepingBody, false); } }