超过最大 FPS

Maximum FPS is exceeded

我正在尝试计算程序的 FPS。虽然给定的最大 FPS 正在超限,但我无法解释原因。我在 MAX_FPS 设置为 60 的情况下对其进行了测试,我总是显示大约 63

{...}
int frameCount = 0;
long time = System.currentTimeMillis();
while(running) {
    try {
        {...} // do something
        Thread.sleep(1000 / MAX_FPS);
        frameCount++;
        if (System.currentTimeMillis() - time >= 1000) {
            {...} // display FPS w/ frameCount
            frameCount = 0;
            time = System.currentTimeMillis();
        }
    } catch (InterruptedException ex) {
        System.err.println(ex.toString());
    }
}
{...}

1000 / 60 在整数数学中是 16,而 1000 / 16 大约是 62.5。假设线程确实休眠了将近 16 毫秒(可能少一点),并且如果您的循环不会花费那么长时间来执行迭代,并且如果倒数第二帧在 999 毫秒时通过允许另一个更新潜入, 那么循环可以达到每秒 63 次迭代是有道理的。

您可能想改用 System.nanoTime()。不是为了任何额外的精度,而是因为它是一种更正确的测量经过时间的方法。请参阅 Kevin Bourillion 对 How do I measure time elapsed in Java? 的回答。

根据 NESPowerGlove 的回答,您在达到目标 FPS 时遇到的问题是您必须选择一个整数值来调用 Thread.sleep() - 如果您选择 16,你会得到大约 63fps;如果您选择 17,您将获得大约 58fps。

对此我能想到两种解决方案:

  1. 实施某种负反馈回路来控制 FPS。例如,我发现下面的PI(比例+积分)控制器保持FPS60左右:

    final long sleepTime = (long) (1000.0 / TARGET_FPS);
    
    System.out.println("Sleep time is " + sleepTime);
    long time = System.nanoTime();
    long delta = 0;
    double cumulativeFpsError = 0.0;
    for (int frameCount = 0; true; ++frameCount) {
      Thread.sleep((long) (sleepTime + delta));
      long elapsed = System.nanoTime() - time;
      if (elapsed >= 1_000_000_000) {
        double fps = (double) frameCount / elapsed * 1e9;
        System.out.println(fps);
    
        cumulativeFpsError += (fps - 60);
        delta += (fps - TARGET_FPS) * 0.55 + 0.14 * cumulativeFpsError;
        frameCount = 0;
        time += elapsed;
      }
    }
    

0.550.14的值是通过反复试验找到的。可能有更好的值(但至少它看起来大致稳定)。 fps 输出:

61.08042479827653
61.817816897275485
58.42717726642977
62.0654826347193
58.43759367657694
62.07263954479402
58.444556146850026
62.05489635777375
58.4438970272065
62.05784933619571
58.45590905841833
62.069491426232766
58.44381852435569
62.07438904528996
...

实际上,积分项根本没有太大作用 - 大概是因为睡眠值只能以大小 1 的步长变化。

  1. 使用不同的方法来获得更精确的睡眠时间。例如,How to suspend a java thread for a small period of time, like 100 nanoseconds? 提出了一些替代方案,例如轮询 System.nanoTime() 直到超过某个截止日期。例如

    long time = System.nanoTime();
    for (int frameCount = 0; true; ++frameCount) {
      long end = System.nanoTime() + (long) (1_000_000_000.0 / TARGET_FPS);
      while (System.nanoTime() < end);
      long elapsed = System.nanoTime() - time;
      if (elapsed >= 1_000_000_000) {
        double fps = (double) frameCount / elapsed * 1e9;
        System.out.println(fps);
    
        frameCount = 0;
        time = System.nanoTime();
      }
    }
    

这似乎效果更好,FPS 徘徊在 60 以下:

58.99961555850502
59.99966304189236
59.99942898543434
59.99968068169941
59.99968770162551
59.99919595077507
59.99945862488483
59.999679241714766
59.99753134157542
59.99963898217224
59.999265728986
...

这样做的缺点可能是 while 循环会很热。

当然,您可以采用某种混合方法,对大部分等待使用 Thread.sleep,然后使用热 while 循环来获得更精确的延迟。