主游戏循环中Steady_Clock在更新之间出现了跳过。

11
在尝试在SFML中制定稳定的游戏循环的过程中,我遇到了一个问题,似乎无法解决。我能够剥离所有SFML代码,并仍然通过time.h中的clock()看到这个问题。接着我更深入地去尝试,使用std::chrono::steady_clock仍然能看到问题。
问题是: 有时候,在更新之间可以完成的工作量会出现跳跃。每次更新应该花费1/60秒的时间,其余时间用于Draw()以尽可能多地进行绘制。 有时绘制量会降至0或1,原因不明。除了这些“跳跃”外,完成的绘制数量非常一致。 以下是一张图片(请注意更新时间的跳跃和绘制量的下降): 问题的控制台输出 一些代码:
#include <iostream>
#include <time.h>
#include <chrono>

using namespace std;
using namespace std::chrono;

void Draw()
{
    //for (int i = 0; i < 1000000; i++);
}

int main()
{
    steady_clock::time_point update_time;
    steady_clock::time_point update_next;
    int update_rate = 16666666; // 60 times a second (nanosecs)
    int updates;
    int max_updates = 5;
    int draws = 0;
    update_next = steady_clock::now();

    while (true)
    {
        updates = 0;
        update_time = steady_clock::now();
        while (duration_cast<nanoseconds>(update_time - update_next) > nanoseconds(update_rate) && updates++ < max_updates)
        {
            if (draws <= 1) {
                cout << "!!!!!!!!!!!!!ERROR!!!!!!!!!!!!!" << endl;
            }
            cout << "UPDATE - ";
            cout << "Draws: " << draws 
                 << " - UT - UN: " << duration_cast<nanoseconds>(update_time - update_next).count()
                 << endl;

            draws = 0;
            update_next += nanoseconds(update_rate);
        }
        draws++;
        Draw();
    }

    return 0;
}
  • 也许我对典型应用程序有什么不理解的地方?Windows需要定期占用CPU周期吗?
  • 我在steady_clock、clock和一个完整的SFML应用程序中看到了这个问题,在Update和Draw期间进行了一些工作
  • 我认为SFML时钟可能使用time.h clock
  • 根据我的测试,max_updates检查与此问题无关(我认为它们不会引起问题)

我看到几个不同的计时器都出现了这个问题,这让我相信我的实现或系统存在问题。该示例在VS中运行,但我也在独立的发布exe中看到过。尝试调整更新速率或绘制中完成的工作量可能会帮助您发现它。


在测试完我的后台进程后,我发现了一个奇怪的相关性。只有在Chrome中打开Spotify网络播放器并且大约每秒钟发生一次才会出现这个跳跃问题。

我找到了这篇文章,可能与此相关: https://community.spotify.com/t5/Other-Partners-Web-Player-etc/Web-Player-on-Chrome-causes-lag-stutter/td-p/4587103

4个回答

8
也许我对典型应用程序有什么不理解?Windows需要每隔一段时间夺取CPU周期吗?
是的,绝对需要。Windows同时运行了很多进程。现在你的应用程序出现了,并执行了一个实质上是繁忙自旋循环的操作。在某些时刻,操作系统可能会将其降低优先级,比你预期的时间更长,因为它看起来只是一个长时间计算,而操作系统需要公平地分配CPU时间给其他进程。
一般来说,您不应该依赖于您的绘图例程被精确调用的次数,您的游戏主时钟应该能够处理跳过的帧。我不熟悉SFML,所以无法评论。
但是,我有超过1000次更新每秒的循环中实时音频(以及视频)的经验。您可以通过将线程优先级设置为THREAD_PRIORITY_HIGHEST或THREAD_PRIORITY_TIME_CRITICAL(请参见SetThreadPriority)来提高游戏循环时间共享。
为了使这种方法有效,您还应该成为一款表现良好的应用程序,并定期执行某种等待。等待允许操作系统进行必要的任务切换以服务其他进程(其中几个也将是高优先级的,通常比您作为用户空间应用程序能够强制执行的优先级更高)。
一个明显的等待位置是在下一次绘制周期之前。与使用100%核心利用率旋转计时器相比,只需计算您准备等待多长时间并调用 std::this_thread::sleep_for。请记住,唯一的保证是休眠时间至少为您指定的时间。它绝对可以且将会超过这个时间。但我建议您从那里开始并进行一些实验。

在这种情况下,std::this_thread::yield 有多有效?编辑:也就是说,在调用 std:this_thread::yield 时旋转是否是减少过度睡眠可能性的有效方法。 - James Picone
1
我个人没有使用过这种方式来处理这种情况。我认为它更适用于自旋锁和非常短的时间间隔自旋。如果我提高了线程优先级,我绝对不想使用自旋等待+让出CPU的方式来运行60Hz游戏循环。 - paddy
1
好的,既然你编辑了你的评论,那么这可能是一个性能调整。由于 sleep_for 可能会延迟,因此您可以使用比实际需要更短的睡眠时间,然后使用 yield 进行更紧密的自旋“睡眠”,以填补任何不足之处。这取决于实验和黑魔法。 - paddy
我尝试了这两种方法,在每次更新时添加了10毫秒的线程休眠,但仍然看到了问题 :( - S. Turnage

3
除了 @paddy 的答案之外,我建议你了解一下 固定时间步。如果这不值得实施,那么你还应该注意到 SFML 有 Window.setFramerateLimit()。它不是非常精确,但大多数简单的游戏并不需要很高的精度。

固定步长链接是个不错的参考,我就是用它来得到这段代码示例。即使使用SFML的setFrameLimit,我仍然发现了问题。由于某种原因,时钟会跳过大量时间。 - S. Turnage
好的,FrameLimit 只是防止您在快速 CPU 上频繁运行循环。 它无法阻止帧速率下降。 你在后台运行什么? 您可以尝试在另一台机器上吗? - Summer

1

我曾使用旋转循环加yield实现1 KHz控制循环,效果不错,但可能会出现一些截止期限的错误(每千个周期中也有长时间的睡眠时间)。


0

起初看起来问题是由我的杀毒软件引起的,但我认为我已经将其缩小到 Spotify Web 播放器打开并导致每秒 1 次性能峰值。我不确定这是为什么。


如果您使用Spotify桌面应用程序是否有相同的问题?您可以尝试在Web浏览器上进行性能分析,以查看可能负责的系统调用。同时,请尝试在不同的浏览器中使用Web应用程序,因为这可能与浏览器实现中的细节有关。 - paddy

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接