0
\$\begingroup\$

I'm developing a browser game and am using the timestep implementation from the famous Fix your timestep! article.

It works perfectly, but there is one problem. If you tab out of the tab and then come back 5 minutes later or so, it freezes, because it has to execute all the missed physics calculations. That is, this code:

accumulator += frameTime;

while (accumulator >= dt)
{
    previousState = currentState;
    integrate( currentState, t, dt );
    t += dt;
    accumulator -= dt;
}

Seems to have to execute for quite a while because the accumulator has missed so many calculations while the tab was .. sleeping I guess?

I'm confused though. Shouldn't Javascript continue to run while the tab is not active? Why is this happening?

Is there any way to fix this? Ideally the Javascript would continue to run so that it wouldn't cause the tab to freeze for 2 minutes when coming back to it after being in other tabs for a while.

\$\endgroup\$
5
  • \$\begingroup\$ "Shouldn't Javascript continue to run while the tab is not active?" - that depends on how you are stepping your game loop, which you have not shown in this question. \$\endgroup\$
    – DMGregory
    Apr 24, 2022 at 0:43
  • \$\begingroup\$ Wait, I think I just had one of those moments of realization that only ever occurs immediately after you hit the submit button. I think the issue might be because I'm using the currentTime minus the prevTime to determine how many physics iterations to do. And because the current time "skips" from, say, t=1000 to t=61000 when you're gone for a minute, it thinks it has to perform 60 seconds worth of physics calculations. But that sounds.. correct..? I guess the main issue is that it seems like the Javascript isn't running when the tab isn't active. \$\endgroup\$ Apr 24, 2022 at 0:45
  • \$\begingroup\$ @DMGregory I'm not sure I understand you. You're saying that it's possible that the Javascript could continue to run while the tab isn't active if I step my game loop differently? I'm confused. \$\endgroup\$ Apr 24, 2022 at 0:49
  • \$\begingroup\$ Oh, I just had another realization. Is it because I am doing my physics in requestAnimationFrame? And that suspends while the tab is inactive? But other Javascript in other sections of the code might continue to run? Hmm, so how should I run the physics instead? I think setInterval also has that behavior. Do I need to use WebWorkers or something? \$\endgroup\$ Apr 24, 2022 at 0:51
  • \$\begingroup\$ I would argue that the current behaviour you have is correct. When the tab is not active, in most cases, the game should not run. I'd make an exception for idle/clicker games, but those should be lightweight enough to simulate at the reduced rate using setInterval as described at the link above (which shows the calls are slowed when out of focus, not stopped like requestAnimationFrame). But we can eliminate the stall when regaining focus as I describe in the answer below. \$\endgroup\$
    – DMGregory
    Apr 24, 2022 at 1:00

3 Answers 3

2
\$\begingroup\$

The way the Unity engine solves the problem of freezing the game while physics catches up is to enforce a maximum value the accumulator can ever hit.

You can implement this by replacing

accumulator += frameTime;

with

accumulator = Math.min(accumulator + frameTime, maxDeltaTime);

...for a maximum time delta that you control. It should be at least as large as dt. A good way to pick this value is to consider your lowest acceptable framerate, then invert it. So for instance, if you don't want your game to drop below 15 frames per second, then maxDeltaTime = 1/15.

This way you'll keep pace with real time as long as the browser/CPU is able to keep up. If tabbing away and back, or heavy processor load causes more real time to pass than what you can keep pace with at your minimum acceptable framerate, then instead of stalling (and dropping to an even lower framerate), this code instead allows the game to fall behind real time. So the game runs slow, but keeps updating and showing progress, rather than hard locking.

\$\endgroup\$
8
  • \$\begingroup\$ This is an interesting answer, but wouldn't this cause the physics to just get messed up if the user was gone for too long? That is, if the user tabbed out for 3 hours and came back, wouldn't the resulting physics just be completely out of whack? I provided additional context in my comments above with my realizations as they came to me. I think the actual solution might be some sort of figuring out a way to keep the Javascript running when the user is tabbed out? \$\endgroup\$ Apr 24, 2022 at 0:54
  • \$\begingroup\$ No, it would not be out of whack, it would just be "back in time". It would treat the game as paused during the time when it was out of focus. Or, if using setInterval, it would treat the game as though it were running at a slower rate while inactive. The physics determinism would be unaffected - you'd still get a correct simulation history, just one corresponding to less time having passed. \$\endgroup\$
    – DMGregory
    Apr 24, 2022 at 0:58
  • \$\begingroup\$ Sorry had to take a brief AFK. I don't really understand still. How does this solve the issue where the tab is frozen while it re-runs physics? Say this is a real-time multiplayer game (it will be shortly). In that case, the physics carries on while the user is tabbed out. Then the user tabs in and now has to somehow re-simulate 30 minutes of missed physics, because they have to catch up to the state of the game. If using a max delta time wouldn't this just catch them up to the wrong point in time? \$\endgroup\$ Apr 24, 2022 at 1:31
  • 1
    \$\begingroup\$ This solves the issue of the tab being frozen by not freezing the tab. The maximum work you ever need to do is bounded to a small amount. "Say this is a real-time multiplayer game" then you should not be simulating physics locally, and this should have been stated in your original question. If you have a long period of inactivity in a multiplayer game, then what you likely want to do is request a complete state snapshot from the server, to time warp ahead to the latest game state. That's a completely different problem, so if you want help with that, it should probably be a new question. \$\endgroup\$
    – DMGregory
    Apr 24, 2022 at 1:39
  • \$\begingroup\$ Yeahhhhh but doing a time warp and requesting a new state is a lot more work than just having the game continue to run.. if I tab out of any other game (Overwatch, DotA, whatever), it keeps running in the background. All I need is a way for the Javascript not to stop running. The actual physics calculations are pretty trivial. And I obviously need to simulate physics locally so it shouldn't be that large of an ask from the browser. Most games do that in addition to simulating physics on the server for client-side prediction, etc. You run the game locally but have an authoritative server. \$\endgroup\$ Apr 24, 2022 at 1:41
0
\$\begingroup\$

Looks like I found a decent solution. So while requestAnimationFrame doesn't really seem to run at all when the tab is tabbed out (makes sense), setTimeout and setInterval seem to get throttled to run once a second (good enough for me!)

So the solution is to put all the rendering logic in requestAnimationFrame (so that this doesn't run at all because who cares about rendering when you can't see it), and then put all physics logic in a setTimeout(physics, 0); which calls itself with another setTimeout(physics, 0);

Then every second it processes all the physics and sleeps for another second on repeat. This seems to solve the issue somewhat decent enough, such that when I come back to the tab after being gone for 10 minutes, it only seems to freeze for like half a second, where previously it would freeze for like 10 seconds.

I looked into using WebWorkers but those can't access the DOM so they seem pretty useless in this case, because I'm not going to be cloning the entire state of the game every frame to pass back to the main thread.. it'd be too slow.

Anyways, this seems okay enough as a solution so I'll probably accept my own answer in a couple days if I remember, unless someone comes up with something better in the interim.

\$\endgroup\$
1
  • \$\begingroup\$ Actually, reading even more around, it seems like a decent hack might be to use a web worker simply to only send a tick to the main thread at an interval. Then you can run the physics code when you receive that tick message. Hmm.. \$\endgroup\$ Apr 24, 2022 at 3:00
0
\$\begingroup\$

Okay, I think I found the actual answer. This prevents all freezing from occurring (because it keeps your game running in the background even if you tab out). Here's how you do it:

First, in your main file, have something like this for drawing:

function drawGame() {
  // Drawing code goes here

  requestAnimationFrame(drawGame);
}

requestAnimationFrame(drawGame);

The reason you put all your drawing code in requestAnimationFrame is because it's not called while the tab is not visible. This is good, because we only want to do physics when the tab is not visible, not rendering.

Next, create a file called worker.js that only contains these 3 lines of code:

setInterval(() => {
  postMessage(1); // IDE complains if we don't pass a useless parameter.
}, 1);

Then in your main file, have something like this:

let prevTime = 0;
let remainingTime = 0;

function gameLoop() {
  const elapsed = performance.now();

  let delta = (elapsed - prevTime) / 1000;
  prevTime = elapsed;

  remainingTime += deltaTime;
 
  while (remainingTime > PHYSICS_TIMESTEP) {
    doPhysics(PHYSICS_TIMESTEP);

    remainingTime -= PHYSICS_TIMESTEP;
  }
}

let intervalWorker = new Worker('./src/worker.js');
intervalWorker.onmessage = gameLoop;

The logic here is that the web worker still sends a "tick" message every 1 millisecond, even if the browser tab is not active. It's basically a hack that allows you to run fast intervals even if the browser tab isn't open. But the game loop will only be executed if it's not already running (the ready variable ensures this)

However, you still use an accumulator in your physics loop. This prevents the physics from running every frame. You still use a fixed timestep and calculate the delta using performance.now() math.

This is a bit complex I know, but it seems to work perfectly.

\$\endgroup\$

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .