Welcome, Guest. Please login or register. Did you miss your activation email?

Author Topic: I'm losing my mind implementing a fixed frame rate.  (Read 1351 times)

0 Members and 1 Guest are viewing this topic.

tisolo

  • Newbie
  • *
  • Posts: 3
    • View Profile
    • Email
I'm losing my mind implementing a fixed frame rate.
« on: February 03, 2024, 05:17:53 pm »
Hi all,

This problem has existed in my game across two laptops for months now but I've decided to tackle it. Over the past week I've tried to understand how to smooth the movement in my game as currently stutters are very prominent.

I've been browsing videos, tutorials. I've seen both the Fix Your Timestep! and DeWitters pages detailing how to make it independent of frame rate and interpolate for smoothness.

I have an example below with a circle and trying to start as simple as possible.

#include <iostream>
#include <SFML/Graphics.hpp>
#include <fstream>

int main() {
    sf::RenderWindow window(sf::VideoMode(1920, 1080), "SFML Test");
    // window.setFramerateLimit(0);
    // window.setFramerateLimit(60);
    // window.setVerticalSyncEnabled(false);

    float speed = 1.0f;
    sf::Vector2f direction = sf::Vector2f(1.0f, 0);

    sf::CircleShape circle;
    circle.setRadius(20.0f);
    circle.setOrigin(circle.getRadius(), circle.getRadius());
    circle.setPosition(0, window.getSize().y/2);

    sf::Time TIME_PER_FRAME = sf::seconds( 1.0f / 60.0f );

    sf::Clock clock;
    sf::Time accumulator;
    sf::Time frameTime;

    sf::Clock profileClock;
    sf::Time eventsTime;
    std::vector<sf::Time> eventsTimes;
    sf::Time updateTime;
    std::vector<sf::Time> updateTimes;
    sf::Time renderTime;
    std::vector<sf::Time> renderTimes;

    while(window.isOpen()) {
        frameTime = clock.restart();
        accumulator += frameTime;

        // events
        sf::Event event;
        while(window.pollEvent(event)) {
            if (event.key.code == sf::Keyboard::Escape) {
                if (event.type == sf::Event::KeyPressed) {
                    window.close();
                }
            }
        }
        eventsTimes.push_back(profileClock.restart());

        // update
        while (accumulator >= TIME_PER_FRAME) {
            circle.move(direction*speed);
            accumulator -= TIME_PER_FRAME;
        }
        //interpolate
        circle.move(direction*speed*(accumulator/TIME_PER_FRAME));
        updateTimes.push_back(profileClock.restart());

        //render
        window.clear();
        window.draw(circle);
        window.display();
        renderTimes.push_back(profileClock.restart());
    }


    std::ofstream myfile;
    myfile.open ("time_logs.csv");
    for (sf::Time i : eventsTimes) {
        myfile << i.asMicroseconds() << ",";
    }
    myfile << std::endl;
    for (sf::Time i : updateTimes) {
        myfile << i.asMicroseconds() << ",";
    }
    myfile << std::endl;
    for (sf::Time i : renderTimes) {
        myfile << i.asMicroseconds() << ",";
    }
    myfile.close();
    return EXIT_SUCCESS;
}
 

Before this example I had two positions for the circle, one would be the actual position, the other would be interpolated based on this position and an alpha - calculated by the proportion of the timestep (accumulated_time / time_per_frame). In both scenarios, the issue I'm having is that, after profiling the time for events, updates and rendering, rendering time varied considerably. From what I can see, the frameTime varying - making the alpha for interpolation not consistent, could be the issue?

I looked into the differences with setting a framerate limit on the window and vsync, the former made the rendering time consistent, but it didn't affect the result.

I'm really struggling in just getting to a point of smooth movement and any direction is much appreciated, if you want any details in terms of hardware set up, if you'd guide me on how best to do this - I can.

Thank you!



« Last Edit: February 03, 2024, 05:25:57 pm by tisolo »

Hapax

  • Hero Member
  • *****
  • Posts: 3379
  • My number of posts is shown in hexadecimal.
    • View Profile
    • Links
Re: I'm losing my mind implementing a fixed frame rate.
« Reply #1 on: February 03, 2024, 06:12:04 pm »
It's worth noting that using a fixed time step and making it independent of the frame rate means that you care much less about the frame rate as the logic itself it always going to be done in the same step of time and therefore limits can be set based on expected values.

Interpolation is probably the most complicated step.

First thing I notice is that you're moving the actual position of the circle based on the alpha. This movement should only be done as part of the update. The only thing that should be done outside of the update is 'temporary' adjustment for the render.
So, you could move it, draw it and move it back.

Interpolation, by the way should be on actual positions. That is, to do it, you require the current position, its previous position and the length of time that the frame took.
You take the alpha as already you did well.
Then, interpolate between the previous and current position.
Note that it's easier to store the information separate to the circle.
Maybe something like this:
// setup
sf::Vector2f position(0, window.getSize().y / 2); // this is the position of the circle that you are moving

    // within main loop
    sf::Vector2f previousPosition();

        // within fixed step update
        previousPosition = position;
        position += direction * speed; // we now have both a current position and a previous position and they&#39;re different

    // interpolation (still within main loop, before drawing)
    sf::Vector2f interpolatedPosition((position - previousPosition) * alpha); // you already have an "alpha" value ;)
    circle.setPosition(interpolationPosition);

Note that this means your interpolated position is always slightly behind the actual position giving up to a frame of lag. This is explained in Fix You Timestep.

Saving the position and previous position is a good way to understand how the interpolation works because for everything that updates, there needs to be a current and previous version. Side note: the current and previous values are spoken of as "states" in Fix Your Timestep so the current state is all of the current values and previous states is all of their equivalent previous values.

Note that extrapolation is different because, although it removes the lag, it does so by guessing where it's going to go. If it changes direction, the extrapolation was wrong and the movement jerks.

Interpolation is the way to go for sure :)

Side note: the previous values should be updated in the timestep loop as shown so that the alpha should always be within the range of a fixed step (0 - 1).
Selba Ward -SFML drawables
Cheese Map -Drawable Layered Tile Map
Kairos -Timing Library
Grambol
 *Hapaxia Links*

tisolo

  • Newbie
  • *
  • Posts: 3
    • View Profile
    • Email
Re: I'm losing my mind implementing a fixed frame rate.
« Reply #2 on: February 04, 2024, 12:15:45 pm »
This makes way more sense, I had something similar with a Player class involved also originally but I've messed around since to this version - thank you for the explanation!

After implementing what I believe to be similar to how you are mentioning this should be done (I'll add it below without all the profiling this time), I still feel that the underlying issue is the time taken between loops really isn't consistent. There is a check for the first update which isn't the greatest solution to the initial values problem currently but it works, sorry about that.

#include <iostream>
#include <SFML/Graphics.hpp>

int main() {
    sf::RenderWindow window(sf::VideoMode(1920, 1080), "SFML Test");

    float speed = 5.0f;
    sf::Vector2f direction = sf::Vector2f(1.0f, 0);

    sf::CircleShape circle;
    circle.setRadius(20.0f);
    circle.setOrigin(circle.getRadius(), circle.getRadius());
    circle.setPosition(0, window.getSize().y/2);

    sf::Time TIME_PER_FRAME = sf::seconds( 1.0f / 60.0f );

    sf::Clock clock;
    sf::Time accumulator;
    sf::Time frameTime;

    //within main loop    
    sf::Vector2f previousPosition;
    sf::Vector2f position = circle.getPosition();
    bool updated = false;

    while(window.isOpen()) {
        frameTime = clock.restart();
        accumulator += frameTime;

        // events
        sf::Event event;
        while(window.pollEvent(event)) {
            if (event.key.code == sf::Keyboard::Escape) {
                if (event.type == sf::Event::KeyPressed) {
                    window.close();
                }
            }
        }

        // update
        while (accumulator >= TIME_PER_FRAME) {
            previousPosition = position;
            position += (direction*speed);
            accumulator -= TIME_PER_FRAME;
            updated = true;
        }

        if (updated) {
            circle.setPosition(previousPosition + ((position - previousPosition) * (accumulator/TIME_PER_FRAME)));
        }

        //render
        window.clear();
        window.draw(circle);
        window.display();

    }
    return EXIT_SUCCESS;
}
 

I've graphed (there's a url of the graph image below) of the first few cycles of the example to try to explain what I mean as an 'alpha value vs while loop number' bar chart and 'rendering times vs loop number' bar chart.

https://imgur.com/a/vBEDtTj

There are moments in this graph that I expect it to act like all the time (101-111 as the loop number for example) as it's consistent in those cases but the rest seems really short and jumpy. These numbers seem to just come from the inconsistent rendering times - it really varies for just a circle.

Maybe I'm completely overlooking something, I thought about sleeping for the time of the rest of the frame but that still doesn't resolve the inconsistency of render time which after profiling that part, makes me think there is an issue there that I'm unsure how to fix.

Again, thanks so much for the response initially, that really helped a lot. :)
« Last Edit: February 04, 2024, 12:26:44 pm by tisolo »

Hapax

  • Hero Member
  • *****
  • Posts: 3379
  • My number of posts is shown in hexadecimal.
    • View Profile
    • Links
Re: I'm losing my mind implementing a fixed frame rate.
« Reply #3 on: February 04, 2024, 12:55:26 pm »
It's important to realise that "position" is never aware of what the graphics are doing. It's the thing that is updated and should not affected by any of the "graphics effects", which is what interpolation is.
This means that "position" should not be set to the circle's position as the circle has been adjusted by interpolation. The position value should continue as if interpolation never happened. The interpolation is only added as an "visual effect" to adjust what we see; it shouldn't be a part of the logic (the actual position).

To fix this, remove "position = circle.getPosition();". The circle should not be affecting what position does.

Shouldn't the "within main loop" section be within the loop?



After that, you could consider adding vertical sync to reduce the number of actual frames. The good thing about this setup is that the logic will not change at all; it will still make the same identical calculations in the updates because the amount of time processed is always the same, regardless of how long it took since the last frame. The only calculations that change are visual (the interpolation) and these should not be affecting the actual logic. If vertical sync isn't an option, you could always use setFramerateLimit.
Selba Ward -SFML drawables
Cheese Map -Drawable Layered Tile Map
Kairos -Timing Library
Grambol
 *Hapaxia Links*

tisolo

  • Newbie
  • *
  • Posts: 3
    • View Profile
    • Email
Re: I'm losing my mind implementing a fixed frame rate.
« Reply #4 on: February 04, 2024, 05:07:15 pm »
Quote
It's important to realise that "position" is never aware of what the graphics are doing. It's the thing that is updated and should not affected by any of the "graphics effects", which is what interpolation is.

Makes complete sense to get rid of referencing the circles position in that case, thank you :)

Quote
Shouldn't the "within main loop" section be within the loop?

I was unsure how this fit this line within the example, as resetting the previousposition, along with the format, where its a function initialisation or something confused me.. I just followed a similar example I'd previously read, the Kairos Timestep example where:

       
        sf::Vector2f currentCirclePosition{ window.getSize() / 2u };
        sf::Vector2f previousCirclePosition = currentCirclePosition;
 

You still feel a little stutter on my t470s linux laptop but on my windows laptop with a dedicated graphics card, it seems smooth as butter, I can't see whether this is possible to fix further, seems like it should be doable for such a small example.

Hapax

  • Hero Member
  • *****
  • Posts: 3379
  • My number of posts is shown in hexadecimal.
    • View Profile
    • Links
Re: I'm losing my mind implementing a fixed frame rate.
« Reply #5 on: February 07, 2024, 04:28:36 pm »
No, you're right.
The previous position can be created and copied from the current position at the start of each loop as I suggested here as it's a small variable; it can be const actually!
The Kairos example, however, is based on the idea that when it is scaled up, most likely would be better to be created once.
For example, if, instead of a single position, you were to store every position of everything as well as everything's rotation, colour, size and many other things (including things involving statistics or other information) in one place (as you likely would when using the "time state" idea), this should probably be only created once and updated often. (you would have two of these objects: current and previous; maybe three with one having all interpolated values for that frame)

Since you already found Kairos, why are you not using it? ;)
« Last Edit: February 07, 2024, 04:40:17 pm by Hapax »
Selba Ward -SFML drawables
Cheese Map -Drawable Layered Tile Map
Kairos -Timing Library
Grambol
 *Hapaxia Links*