如何让JavaScript重力模拟器基于时间而非帧率运行并修复暂停异常
重力模拟器基于时间步长的物理更新问题
问题描述
我用JavaScript开发重力模拟器,为实现基于时间而非帧率的运行,将力向量乘以delta time(dt)标量。但效果存在问题:暂停时将dt设为0,物体不再相互吸引,但会保持暂停前的速度继续移动。我尝试对速度向量做dt相关操作,但一乘dt物体就飞散开来。
物理求解器代码
class physicsStepper { /** * Create a physicsStepper class * @param {Object} configs - Configs for the physics stepper. (Optional) * @param {number} configs.G - The Gravitational Constant. * @param {boolean} configs.collisions - Whether or not to detect collisions. * @param {Object} configs.simSize - The size of the simulation (used by the spacial partitioner) * @param {Boolean} configs.useGrid - Whether or not to use a spacial partitioner. */ constructor(configs = {}) { //default configs Object.assign(this, { G: 6.6743 * Math.pow(10, -11), collisions: false, dimensions: 2, useGrid: false, simSize: { height: 1600, width: 1600, }, }); //apply changes Object.assign(this, configs); //setup Grid if (this.useGrid) this.grid = new grid(); } /** * Circle vs circle collider * @param {Object} a - A particle object with radius * @param {Object} b - A particle object with radius * @param {number} r - The distance between the two objects * @returns {boolean} - Whether a collision happened or not */ collider(a, b, r) { if (r < a.radius + b.radius) return true; else return false; } /** * Apply force to an entity * @param {Object} entity - a physics object * @param {Object} force - a force vector2 */ applyForce(entity, force) { force.divideScalar(entity.mass); entity.acceleration.add(force); } /** * Calculate the attraction between two objects * @param {Object} a - A particle object with radius * @param {Object} b - A particle object with radius * @returns {Object} data.r - the distance between the two entities * @returns {Object} data.force - the force of attraction between the two entities */ calcAttraction(a, b) { const { G } = this; const force = new Vector2(); force.subVectors(b.position, a.position); const r = $.distance(a, b); force.normalize(); const strength = ((G * a.mass * b.mass) / r) * r; force.multiplyScalar(strength); return { r, force }; } /** * Translate the entity to its new position by using it's velocity vector2 * @param {Object} entity - A physics object */ translatePositions(entity, dt) { entity.velocity.add(entity.acceleration); entity.position.add(entity.velocity); entity.acceleration.multiplyScalar(0); } /** * Calculates and updates all physics objects * @param {Object[]} scene - The scene containing the physics objects (required) * @param {Number} delta - The delta time (required) */ step(scene, dt) { for (let [i, a] of scene.children.entries()) { if (!a.physics) continue; for (let b of scene.children) { if (!b.physics) continue; if (a.sn === b.sn) continue; const { r, force } = this.calcAttraction(a, b); if (this.collisions && this.collider(a, b, r)) { a.velocity.multiplyScalar(0.999); b.velocity.multiplyScalar(0.999); } force.multiplyScalar(dt); this.applyForce(a, force); } this.translatePositions(a, dt); } } }
Ticker代码
export class ticker { constructor(fps) { //fps control this.frameRate = fps ? fps : 60; this.frameInterval = 1000 / this.frameRate; this.time = 1; //speed of flow of time let timeElapsed, Now, Then = (performance || Date).now(); //for getting delta based on time let last = (performance || Date).now(); let now, delta; this.callback = () => {}; const mainLoop = () => { requestAnimationFrame(mainLoop); Now = (performance || Date).now(); timeElapsed = Now - Then; if (timeElapsed > this.frameInterval) { Then = Now - (timeElapsed % this.frameRate); now = (performance || Date).now(); delta = (now - last) * this.time; last = now; this.callback(delta); } }; mainLoop(); } onTick(callback) { this.callback = callback; } setFrameRate(fps) { this.frameRate = fps; this.frameInterval = 1000 / fps; } }
调用方式
const Ticker = new ticker(120); Ticker.onTick((dt) => { stats.begin(); PhysicsStepper.step(Renderer.scene, dt); Renderer.render(); stats.end(); });
问题分析与解决方案
核心问题
当前物理更新逻辑存在三个关键错误:
- 时间步长应用位置错误:把力乘以dt,导致加速度的物理意义失真——正确逻辑是加速度由力推导,再用加速度乘以dt得到速度变化量,而非直接缩放力。
- 位置更新未乘dt:直接用速度累加位置,导致dt=0时速度仍会推动物体移动。
- 万有引力公式错误:
strength计算中抵消了分母的r,导致引力强度与距离无关,物体运动逻辑异常。 - dt单位不统一:Ticker输出的dt是毫秒级,物理公式需要秒级单位,直接使用会导致数值过大。
修正步骤
修正万有引力计算
把calcAttraction中的强度计算改为正确的平方反比公式:calcAttraction(a, b) { const { G } = this; const force = new Vector2(); force.subVectors(b.position, a.position); const r = $.distance(a, b); // 避免除以0 if (r < 0.001) return { r, force: new Vector2(0,0) }; force.normalize(); const strength = (G * a.mass * b.mass) / (r * r); force.multiplyScalar(strength); return { r, force }; }调整时间步长的应用位置
移除step方法中对力的dt缩放,改为在速度和位置更新时应用:step(scene, dt) { // 暂停时直接返回 if (dt === 0) return; for (let [i, a] of scene.children.entries()) { if (!a.physics) continue; for (let b of scene.children) { if (!b.physics) continue; if (a.sn === b.sn) continue; const { r, force } = this.calcAttraction(a, b); if (this.collisions && this.collider(a, b, r)) { a.velocity.multiplyScalar(0.999); b.velocity.multiplyScalar(0.999); } // 移除force.multiplyScalar(dt) this.applyForce(a, force); } this.translatePositions(a, dt); } }修正位置与速度更新逻辑
转换dt为秒级单位,按照物理公式更新速度和位置:translatePositions(entity, dt) { const dtSec = dt / 1000; // 速度更新:v = v0 + a*dt entity.velocity.add(entity.acceleration.clone().multiplyScalar(dtSec)); // 位置更新:p = p0 + v*dt entity.position.add(entity.velocity.clone().multiplyScalar(dtSec)); // 重置加速度 entity.acceleration.multiplyScalar(0); }完善暂停逻辑
通过修改Ticker的time属性实现暂停,确保dt直接为0:// 暂停 Ticker.time = 0; // 恢复 Ticker.time = 1;
为什么之前乘dt会飞散
之前既缩放了力又尝试缩放速度,导致时间步长被重复应用;加上dt是毫秒级数值,直接用于速度计算会让物体移动速度放大数百倍,最终出现飞散现象。
内容的提问来源于stack exchange,提问作者Kiron Dey
相关产品推荐
相关产品推荐

