javascripttimesimulationgravity

How to make my gravity Simulator's speed time based instead of framerate based


I'm building a gravity Simulator with javascript, For the simulation to run based on time, I multiplied the force vector by a deltatime dt scalar. This approach works to some extent, however it is not ideal. The delta time parameter comes from a Ticker function, and to pause the ticker, I multiply the deltatime by 0. And when I do this, The objects stop attracting each other as expected but the objects keep their velocity and keep moving in the direction they were moving in before the pause.

Here is the code for the physics solver...

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);

        }

    }

}

And here is the code for the 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;

    }

}


And here is how I called the physics solver inside the ticker

const Ticker = new ticker(120);

Ticker.onTick((dt) => {

    stats.begin();

    PhysicsStepper.step(Renderer.scene, dt);

    Renderer.render();

    stats.end();

});

I guess I'll have to perform some operation involving the velocity vectors and deltatime but when I tried to multiply the velocity vector by deltatime, The objects in the simulation explode away.


Solution

  • Turns out all I had to do was make 3 changes.

    Change 1. Convert dt from milliseconds to seconds.

    const dtScaled = dt / 1000;
    

    Change 2. Pass dt to this.applyForce() method and make multiply the force vector with dt.

    applyForce(entity, force, dt) {
        force.multiplyScalar(dt);
        force.divideScalar(entity.mass);
        entity.acceleration.add(force);
    }
    

    Change 3. Edit the this.translatePositions() to.

    translatePositions(entity, dtScaled) {    
       entity.velocity.add(entity.acceleration);
     
       entity.position.add(entity.velocity.clone().multiplyScalar(dtScaled));
    
       entity.acceleration.multiplyScalar(0);
    }
    

    The Explosion problem was occuring because the deltatime was too big, so converting it to milliseconds fixed that error. And multiplying deltatime with velocity fixed the problem where the objects keep moving even if the simulation is paused.