Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Timestep doesn't take refresh rate into account #702

Closed
hellos3b opened this issue Dec 29, 2018 · 14 comments
Closed

Timestep doesn't take refresh rate into account #702

hellos3b opened this issue Dec 29, 2018 · 14 comments

Comments

@hellos3b
Copy link

hellos3b commented Dec 29, 2018

Setup

  1. Have two monitors, one 160hz and the other 60hz
  2. Open Newton's Cradle demo listed in readme (Link)
  3. Run demo, and move chrome window from one monitor to the other

Observe

The demo runs twice as fast on the 160hz monitor than it does on the 60hz monitor

GIF

I recorded it on my phone to show the difference in simulation speed
https://gfycat.com/HugeAmusingChameleon

I believe this is because of an assumption that requestAnimationFrame target FPS is 60, where as on a higher refresh rate RAF can be called at 140fps

@KingCosmic
Copy link

Same issue occurring for me and others

@marwan38
Copy link

I've been trying to find out how to fix this as well. Running a game, using MatterJS physics, on 2 different monitors (144hz and 60hz) is like putting on fast forward.

@hellos3b hellos3b changed the title Timestep doesn't take FPS into account Timestep doesn't take refresh rate into account Dec 29, 2018
@liabru
Copy link
Owner

liabru commented Jan 13, 2019

I think the issue is that while Matter.Runner should be able to handle this, it actually doesn't because runner.deltaMin and runner.deltaMax clamp the frame delta for stability reasons, but this always assumes a 60hz monitor.

So I'll mark this as a bug, thanks for the report. In the meantime I think the easiest fix is to use Runner.create({ fps: 120 }). I can't test this so please give it a shot and tell me if that does work.

As for detecting the refresh rate automatically, well that might actually be a little tricky, but it would probably take some testing using requestAnimationFrame. Either way I need to revisit Matter.Runner at some point and I'll take this into account.

@liabru liabru added the bug label Jan 13, 2019
@Antriel
Copy link

Antriel commented Jan 13, 2019

A proper solution would be to run in a fixed timestep. Keep in mind that frame rate can be dynamic, i.e. change at any time during single game.

@wmike1987
Copy link

I had this issue with my users a while back and I fixed it by altering the deltaMin and deltaMax so that it didn't assume 60 fps.

I then had one other issue regarding monitor refresh rate and differing performance which may be worth mentioning here: setting velocity via setVelocity() currently does not normalize the velocity parameter. It currently sets the velocity to units-per-delta-time-of-last-frame, which differs with varying refresh rates. See #637 for the issue I opened for that one.

@driescroons
Copy link

driescroons commented Oct 13, 2019

@wmike1987 how did you go about fixing deltaMin and deltaMax so they do not assume 60 fps? I'm also interested in how you fixed the velocity issue.

Edit: Fellow googlers, I limited my fps for monitors with higher refresh rates like so below. I feel like this might be the best solution for now. This also makes it so we do not have to do any weird velocity calculations, because we'll only update the engine on specific intervals while the renderer keeps running.

You can check an example of this from a game I made for Ludum Dare 45:
🎮 https://reutemeteut.dries.io

@liabru is this a correct implementation?

First, I declare the engine, renderer and runner.

    engine = Engine.create({});
    render = Render.create({
      engine,
      element: this.container,
      options: {
        width: this.width,
        height: this.height,
      },
    } as any);

    runner = (Runner as any).create({});

followed by a couple of variables that i'll be using to calculate delta (differentiating time in between frames gets rendered) and only let the engine update (using runner's tick method) when it is above the required interval.

  // fps should be locked at:
  const fps = 60;
  // init of now to base value
  const now = Date.now();
  // setting then to base value
  const then = this.now;
  // what I want time in between frames to be
  const interval = 1000 / this.fps;
  // init of delta
  const delta = 0;

I then run my renderer and call a custom update method.

    Render.run(render);
    this.update();

In my update method I call requestAnimationFrame, passing the function as a parameter. When it gets called by requestAnimationFrame, it gets passed a time variable, a total time since first called requestAnimationFrame, I calculate delta, and only call the tick method on my renderer, when it is above the locked the interval. 1000 ms / 60 fps.

  public update = (time = 0) => {
    requestAnimationFrame(this.update);

    this.now = Date.now();
    this.delta = this.now - this.then;

    if (this.delta > this.interval) {
      Runner.tick(runner, engine, time);
      this.then = this.now - (this.delta % this.interval);
    }
  };

@wassfila
Copy link

wassfila commented Jan 19, 2020

@hellos3b @driescroons ,
there are many ways how to fix this issue. I tried to suggest a solution in the issue I linked above that is independent from any hardware loop time as it identify it during the first two loops, plus for a more realistic simulation it calculates both delta and correction parameters.
Not sure if this helps your case, as it should be well tested, but if it works I don't see what this shouldn't be a sort of default generic solution.

@liabru
Copy link
Owner

liabru commented Nov 12, 2023

PR #1254 includes updates relating to this issue, see details in that branch if you wish to try it out.

@davidreis97
Copy link

davidreis97 commented Jan 31, 2024

Since PR #1254 has been open for a while I decided to write my own loop, it's a fixed time step (so you won't lose determinism) that updates up to 144 times per second and has the same perceived speed on different refresh rates.

let isRunning = true;
let lastUpdate = performance.now();
const fixedDelta = 1000 / 144;
const runnerFunc = () => {
    const now = performance.now();

    while (lastUpdate < now) {
        Matter.Engine.update(engine, fixedDelta);
        lastUpdate += fixedDelta;
    }

    if (isRunning) {
        requestAnimationFrame(runnerFunc);
    }
}
requestAnimationFrame(runnerFunc);

@epurban
Copy link

epurban commented May 8, 2024

Hey @davidreis97, thanks for sharing this! Would you mind breaking down this code to help us understand how this solves issues associated with different monitor framerates? Thank you!

@davidreis97
Copy link

@epurban I asked GPT and the response was good enough:

The given code snippet addresses the issue of different monitor framerates by decoupling the physics simulation update rate from the frame rendering rate. Here’s how it solves the problem:

  1. Fixed Time Step: The fixedDelta constant is set to 1000 / 144, which means that the physics engine updates at a fixed interval of approximately 6.94 milliseconds. This corresponds to a rate of 144 updates per second. By using a fixed update step, the physics calculations become deterministic and consistent, independent of the monitor's refresh rate. This is crucial for maintaining consistent physics behavior across different systems.
  2. Accumulated Time: The while loop inside the runnerFunc function uses lastUpdate and compares it to the current time now to determine how many physics updates are needed. If lastUpdate is less than now, it means that there is still time "debt" that needs to be compensated with additional physics updates. This ensures that the physics engine processes all necessary updates for the elapsed time since the last frame.
  3. Decoupling from Frame Rate: By updating the physics engine in a loop based on accumulated time rather than directly within the animation frame callback, the physics simulation is kept consistent regardless of the rendering frame rate. This is particularly important in scenarios where the frame rate might drop below the target (e.g., due to complex rendering tasks or lower hardware capabilities), as the physics simulation will continue to update correctly and consistently.
  4. Continuity with requestAnimationFrame: The use of requestAnimationFrame(runnerFunc) ensures that the runner function is called before the next repaint. requestAnimationFrame also provides the benefit of running the updates at an optimal rate for the browser, helping in managing CPU/GPU load efficiently. However, since the physics updates are managed separately within the while loop, the actual rendering frame rate does not disrupt the physics calculations.

In summary, this method allows for a stable and consistent physics simulation that is not affected by varying refresh rates across different monitors. This is particularly useful in gaming or any real-time interactive applications where consistent physics behavior is essential for a fair and uniform user experience.

@ivanjermakov
Copy link

ivanjermakov commented May 30, 2024

@davidreis97 there is a problem with this code if my goal is to run physics steps less frequent than frame update. In your case, physics step will always run at least once for every frame.

The following loop will catch up regardless whether physics delta or frame delta is higher:

const physicsDelta = 1000 / 144;
let lastUpdate = performance.now();
const runnerFunc = () => {
    while (lastUpdate + physicsDelta < performance.now()) {
        lastUpdate += physicsDelta;
        // physics there
        Matter.Engine.update(engine, physicsDelta);
    }
    // drawing there
}
requestAnimationFrame(runnerFunc);

@Notamatall
Copy link

Notamatall commented Jun 21, 2024

A proper solution would be to run in a fixed timestep. Keep in mind that frame rate can be dynamic, i.e. change at any time during single game.

I have implemented the last and "best" solution ( which he has mentioned) in my work project where I am creating Plinko.
I have encountered that physics engine behaves differently on different frame rate screens. I think that I have implemented everything correctly, but balls are insanely slow and collisions are not happening or happening very rarely:
`
.

let t = 0.0;
let dt = 0.01;
let currentTime = Date.now();
let accumulator = 0.0;

let previous: { [x: number]: Vector } = [];

const render = () => {
  this.checkBoundaryCollision();
  ctx.clearRect(0, 0, canvas.width, canvas.height);
  this.showObjects(ctx);

  this.checkPegBallCollision();
};

const interpolate = (alpha: number, current: Vector, prev: Vector) => {
  if (prev) {
    return {
      x: current.x * alpha + prev.x * (1.0 - alpha),
      y: current.y * alpha + prev.y * (1.0 - alpha),
    };
  }
  return current;
};
const update = () => {
  let newTime = Date.now();
  let frameTime = newTime - currentTime;
  if (frameTime > 0.25) frameTime = 0.25;
  currentTime = newTime;

  accumulator += frameTime;

  while (accumulator >= dt) {
    this.ballsArr.forEach((ball) => {
      previous[ball.body.id] = structuredClone(ball.body.position);
    });
    Engine.update(this.engine, dt);
    console.log(this.ballsArr[0]?.body.position === previous[this.ballsArr[0]?.body.id]);
    t += dt;
    accumulator -= dt;
  }

  const alpha = accumulator / dt;
  this.ballsArr.forEach((ball) => {
    const prevBall = previous[ball.body.id];
    ball.body.position = interpolate(alpha, ball.body.position, prevBall);
  });

  render();
  animationFrameId.current = requestAnimationFrame(update);
};

animationFrameId.current = requestAnimationFrame(update);`

@liabru
Copy link
Owner

liabru commented Jun 24, 2024

As of 0.20.0 there is now built in Matter.Runner support for fixed timestep with high refresh displays see #1300.

Closing this one. Thanks all!

@liabru liabru closed this as completed Jun 24, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests