You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何让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();
});

问题分析与解决方案

核心问题

当前物理更新逻辑存在三个关键错误:

  1. 时间步长应用位置错误:把力乘以dt,导致加速度的物理意义失真——正确逻辑是加速度由力推导,再用加速度乘以dt得到速度变化量,而非直接缩放力。
  2. 位置更新未乘dt:直接用速度累加位置,导致dt=0时速度仍会推动物体移动。
  3. 万有引力公式错误:strength计算中抵消了分母的r,导致引力强度与距离无关,物体运动逻辑异常。
  4. dt单位不统一:Ticker输出的dt是毫秒级,物理公式需要秒级单位,直接使用会导致数值过大。

修正步骤

  1. 修正万有引力计算
    把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 };
    }
    
  2. 调整时间步长的应用位置
    移除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);
        }
    }
    
  3. 修正位置与速度更新逻辑
    转换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);
    }
    
  4. 完善暂停逻辑
    通过修改Ticker的time属性实现暂停,确保dt直接为0:

    // 暂停
    Ticker.time = 0;
    // 恢复
    Ticker.time = 1;
    

为什么之前乘dt会飞散

之前既缩放了力又尝试缩放速度,导致时间步长被重复应用;加上dt是毫秒级数值,直接用于速度计算会让物体移动速度放大数百倍,最终出现飞散现象。


内容的提问来源于stack exchange,提问作者Kiron Dey

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.08 04:35:27