Question

I'm trying to copy the logic of the Sonic physics engine, which was written for a fixed-timestep system (60 FPS), in a variable timestep age (Slick2D, to be precise).

In the original upon pressing the jump button, the player's velocity.y is set to -6.5, and each tick 0.21875 is added to velocity.y to model gravity.

Each time my logic update is called, a time delta parameter is passed specifying how many millis have passed. If more millis have passed than I was expecting, then I repeat the update logic, passing an 'inner delta' that is at most 1, or less if we're dealing with the 'remainder' of a target frame.

E.g. if we expect a frame to take 16ms, and it does take 16 ms, the loop will iterate once and pass thisMiniTick as 1. If the delta was not 16ms but 40ms, the loop will execute three times, passing 1, 1, and finally 0.5.

I mistakenly thought that in each of these inner update loops I could do velocity.y += (gravity * thisMiniTickRelative), but this doesn't work. On faster framerates not enough gravity is applied causing a higher jump, and on slower framerates the jump is lower (although not anywhere near as noticeably).

Is there a way of doing this that will work for virtually all framerates, or must I resort to setting an upper and lower bound for delta?

The 'inner update' loop:

    float timeRemaining = delta/1000f; 
    while(timeRemaining > 0)
    {
        float thisMiniTick = Math.min(timeRemaining, 1f / FRAMES_PER_SECOND);
        float thisMiniTickRelative = thisMiniTick / (1f / FRAMES_PER_SECOND);

        updateInput(container, game, thisMiniTickRelative);
        if (playerAirState)
        {
            playerVelocity.y += (GRAVITY * thisMiniTickRelative);
        }
        clampPlayerVelocity();
        playerPosition.add(playerVelocity);
        doCollisions();
        timeRemaining -= thisMiniTick;
    }
Was it helpful?

Solution

Don't think of it as "resorting to setting an upper and lower bound for delta". Your application and threads are subject to the OS scheduling time for your app, among all the other demands on the system, and something you just need to be aware of. This challenge is as old in PC gaming as the day we moved from single-tasking operating systems to multi-tasking operating systems.

With Slick, you can (and should) disconnect your logic updates from your rendering updates, which is why the delta value gets passed around your application. Do this by using the .setMinimumLogicUpdateInterval and .setMaximumUpdateInterval methods.

On projects that I've worked on, including one in Slick, I find that anything in the 30-60 logic updates per second (30.3 milliseconds to 16.6 milliseconds between updates) works great, and gives you the smoothness you need from your movement, physics, and collision calculations.

Literally what that means is, for a 30-60 logic updates per second range, you want to do the following:

container.setMinimumLogicUpdateInterval(16);  // max 60 logic updates per second
container.setMaximumLogicUpdateInterval(31);  // min 30 logic updates per second

Also, it's a common mistake to try to calculate the timeRemaing value, but you don't want to do this. You simply want to multiply how much you move, by how much time has passed. If 30 milliseconds have passed, that's about 1/33rd of a second, so you should move your game object by 1/33rd of the amount it would move in 1 second.

float timeElapsed = delta/1000f;

playerVelocity.y += (GRAVITY * timeElapsed);

With the upper/lower bounds set as specified above, you're sure that timeElapsed will always be a value between 0.03 and 0.06. If your game gets bogged down and your frame rate slows down, your logic updates still won't go outside these bounds. What will happen instead is the whole game will appear to slow down (as it should, just like on the old Sega days when there was too much on the screen), but collisions and physics calculations will still work as expected.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top