正确的方式是拥有一个独立的更新循环。

huangapple go评论72阅读模式
英文:

What is the proper way to have a separate Update Loop?

问题

I'm using C++ with raylib to create a game. I'm running Windows x64 compiling with MinGW in Eclipse.

For the game loop, the normal tutorials give you something like this:

int main()
{
    InitWindow(800, 600, "Window Title");
    SetWindowState(FLAG_VSYNC_HINT);
    
    while (!WindowShouldClose())
    {
        Update(); // your function to do game update logic
        
        BeginDrawing();
        
        ClearBackground((Color){0, 0, 0, 255});
        
        Draw(); // your function to do game drawing
        
        EndDrawing();
    }
}

However, this is bad practice. So it sets the VSYNC flag to make sure the drawing only happens at the same rate as the monitor refresh rate. That's fine but if you insert your update logic into the same loop, then your update rate is locked to the draw rate.

This is foolish because different users have different refresh rates. Mines uses 60 Hz, in Europe they use 50 Hz, and lots of people use 120, 144, 240, or any custom refresh rate they want.

So that means the game will run at very different speeds. Not what I want.

What I want instead is the update logic to always be 60 FPS (actually 16 ms intervals so it's 62.5 FPS), while the draw logic runs on the VSYNC loop.

So here's what I tried next:

std::thread ULT(scr_run_updateloop); // spawn a new thread that calls scr_run_updateloop() immediately

while (!WindowShouldClose())
{
    // no more Update() call in the draw loop!
    
    BeginDrawing();
        
    ClearBackground((Color){0, 0, 0, 255});
        
    Draw(); // your function to do game drawing
        
    EndDrawing();
}

and the function called by the thread...

void scr_run_updateloop()
{
    auto time_current = chrono::steady_clock::now();
    auto time_last = time_current;
    uint64_t time_d;
    
    while (true)
    {
        time_current = chrono::steady_clock::now();
        time_d = duration_cast<milliseconds>(time_current - time_last).count();

        if (time_d >= 16) // 16 ms interval is 62.5 FPS
        {
            Update();
            time_last = time_current;
        }
    }
}

This code works. However, that thread is hogging CPU like crazy. My task manager says 25% and I'm on a 4-core system.

What can I do? I've read about sleep and thread::sleep_for but the problem is the sleep time is not guaranteed and can be off by many milliseconds because the OS could be busy with whatever.

Note: I don't need the thread to sync with the main thread at all. The Update loop can be completely separate from the Draw Loop, AFAIK they will never need to share a locked resource.

I've also read about a completely alternate approach where your time between Updates is NOT fixed, but uses deltaTime. Then you interpolate positions, speeds and other stuff to make sure things happen at the same overall speed. That seems extraordinarily overcomplicated and unnecessary. If you can VSYNC to a draw refresh rate than I don't see why you can't set up a custom loop to call once every fixed interval. There must be a way to do this, but how?

英文:

I'm using C++ with raylib to create a game. I'm running Windows x64 compiling with MinGW in Eclipse.

For the game loop, the normal tutorials give you something like this:

int main()
{
    InitWindow(800, 600, (&quot;Window Title&quot;).c_str());
    SetWindowState(FLAG_VSYNC_HINT);
    
    while (!WindowShouldClose())
    {
        Update(); // your function to do game update logic
        
        BeginDrawing();
        
        ClearBackground((Color){0, 0, 0, 255});
        
        Draw(); // your function to do game drawing
        
        EndDrawing();
    }
}

However, this is bad practice. So it sets the VSYNC flag to make sure the drawing only happens at the same rate as the monitor refresh rate. That's fine but if you insert your update logic into the same loop, then your update rate is locked to the draw rate.

This is foolish because different users have different refresh rates. Mines uses 60 Hz, in Europe they use 50 Hz, and lots of people use 120, 144, 240, or any custom refresh rate they want.

So that means the game will run at very different speeds. Not what I want.

What I want instead is the update logic to always be 60 FPS (actually 16 ms intervals so it's 62.5 FPS), while the draw logic runs on the VSYNC loop.

So here's what I tried next:

std::thread ULT(scr_run_updateloop); // spawn a new thread that calls scr_run_updateloop() immediately

while (!WindowShouldClose())
{
    // no more Update() call in the draw loop!
    
    BeginDrawing();
        
    ClearBackground((Color){0, 0, 0, 255});
        
    Draw(); // your function to do game drawing
        
    EndDrawing();
}

and the function called by the thread...

void scr_run_updateloop()
{
    auto time_current = chrono::steady_clock::now();
    auto time_last = time_current;
    uint64_t time_d;
    
    while (true)
    {
        time_current = chrono::steady_clock::now();
        time_d = duration_cast&lt;milliseconds&gt;(time_current - time_last).count();

        if (time_d &gt;= 16) // 16 ms interval is 62.5 FPS
        {
	        Update();
	        time_last = time_current;
        }
    }
}

This code works. However, that thread is hogging CPU like crazy. My task manager says 25% and I'm on a 4-core system.

What can I do? I've read about sleep and thread::sleep_for but the problem is the sleep time is not guaranteed and can be off by many milliseconds because the OS could be busy with whatever.

Note: I don't need the thread to sync with the main thread at all. The Update loop can be completely separate from the Draw Loop, AFAIK they will never need to share a locked resource.

I've also read about a completely alternate approach where your time between Updates is NOT fixed, but uses deltaTime. Then you interpolate positions, speeds and other stuff to make sure things happen at the same overall speed. That seems extraordinarily overcomplicated and unnecessary. If you can VSYNC to a draw refresh rate than I don't see why you can't set up a custom loop to call once every fixed interval. There must be a way to do this, but how?

答案1

得分: 2

您的 CPU 负载较高是因为您的循环在等待足够的时间以满足调用 Update 的条件时会非常快速地旋转。为了降低 CPU 使用率,您需要让线程休眠,即在等待时什么也不做。

但是,在您的循环中没有必要插入调用特殊休眠函数的代码,因为您已经有一个允许您休眠的工具:您的 Draw() 函数!启用垂直同步(Vsync)后,此函数会自然地休眠,直到显示器准备好显示下一帧。

现在您需要注意的唯一事项是确保您的更新函数考虑到了这段休眠时间。所以,您不再需要在每个循环中更新一次,而是想要检查自上次更新以来经过了多长时间,然后相应地更新您的状态。

auto time_current = std::chrono::steady_clock::now();
auto time_last = time_current;
auto const update_interval = std::chrono::milliseconds{16};

while (true)
{
    time_current = std::chrono::steady_clock::now();
    auto time_d = time_current - time_last;

    while (time_d >= update_interval) // 每个经过的时间间隔执行一次更新
    {
        Update();
        time_d -= update_interval;
        time_last += update_interval;
    }

    Draw();     // 这就是休眠发生的地方
}
英文:

Your CPU load is high because your loop will be spinning really fast while waiting for enough time to pass to meet the condition for calling Update. To bring the CPU usage down, instead of having your thread spin, you want it to sleep, that is, to do nothing while it waits.

But there's no need to insert a call to special sleep function into your loop, as you already have a facility that allows you to sleep: Your Draw() function! With Vsync enabled, this function will naturally sleep until the monitor is ready to display the next frame.

The only thing you need to take care of now is make sure that your update function takes this sleep time into account. So instead of updating once every loop, you will want to inspect how much time has passed since the last update and then update your state accordingly.

auto time_current = std::chrono::steady_clock::now();
auto time_last = time_current;
auto const update_interval = std::chrono::milliseconds{16};

while (true)
{
    time_current = std::chrono::steady_clock::now();
    auto time_d = time_current - time_last;

    while (time_d &gt;= update_interval) // perform one update for every interval passed
    {
        Update();
        time_d -= update_interval;
        time_last += update_interval;
    }

    Draw();     // this is where the sleep happens
}

huangapple
  • 本文由 发表于 2023年5月22日 09:52:49
  • 转载请务必保留本文链接:https://go.coder-hub.com/76302630.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定