FPS 锁定不精确

FPS lock not precise

我正在为我的游戏实施 FPS 上限,但不是很精确。

public static volatile int FPS_CAP = 60;

@Override
public void run() {
    long lastTime = System.nanoTime();
    double amountOfTicks = 60.0;
    double ns = 1000000000 / amountOfTicks;
    double delta = 0;
    long timer = System.currentTimeMillis(), lastRender;
    while (running) {
        long now = System.nanoTime();
        delta += (now - lastTime) / ns;
        lastTime = now;
        while (delta >= 1) {
            tick();
            delta--;
        }
        lastRender = System.currentTimeMillis();
        draw.render();
        draw.fps++;
        
        if (FPS_CAP != -1) {
            try {
                int nsToSleep = (int) ((1000 / FPS_CAP) - (System.currentTimeMillis() - lastRender));

                if (nsToSleep > 1 / FPS_CAP) {
                    Thread.sleep(nsToSleep);
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

        if (System.currentTimeMillis() - timer > 1000) {
            timer += 1000;
            draw.lastFPS = draw.fps;
            draw.fps = 0;
            // updates = 0;
        }
    }
}

结果是:

如您所见,它确实不准确,有时比 60 低很多,有时甚至更高!

我希望它尽可能准确。

提前致谢。

Java 不够精确,无法实现这种精确机制。所有渲染库实际上都依赖于 C 或 C++ 层来管理实时精度。

对于您的情况,最好的解决方法是避免使用 Thread.sleep()

您可以依赖定时器事件而不是 运行TimerTask 期间的更新。您的游戏每秒大约有 60 次心跳。

如果 60fps 适合您,另一种解决方案是在渲染屏幕之前等待 VSync。大多数 LCD 屏幕为 60 或 120fps。 VSync 应由您的图形库(swing、JavaFX 或其他)管理。

最后一个解决方案,你可以看看专门的引擎(比如JMonkey)的代码作为参考。

首先,我看到您将 System.currentTimeMilis()System.nanoTime() 混合使用并不是一个好主意,只能使用其中一个。最好只使用 System.nanoTime(),因为你的工作精度很高。

导致您出现问题的原因是 Thread.sleep() 不够精确。所以你需要避免睡觉。将您的睡眠代码更改为此;

lastRender = System.nanoTime(); //change it to nano instead milli
draw.render();
draw.fps++;

if (FPS_CAP > 0) {
    while ( now - lastRender < (1000000000 / FPS_CAP))
    {
        Thread.yield();

        //This stops the app from consuming all your CPU. It makes this slightly less accurate, but is worth it.
        //You can remove this line and it will still work (better), your CPU just climbs on certain OSes.
        //FYI on some OS's this can cause pretty bad stuttering. 
        try {Thread.sleep(1);} catch(Exception e) {}

        now = System.nanoTime();
    }
}

关于如何启用 VSYNC,您的应用程序需要全屏,您应该在每次渲染后调用 Toolkit.sync()

JavaFX 基于 pulse mechanism.

A pulse is an event that indicates to the JavaFX scene graph that it is time to synchronize the state of the elements on the scene graph with Prism. A pulse is throttled at 60 frames per second (fps) maximum and is fired whenever animations are running on the scene graph. Even when animation is not running, a pulse is scheduled when something in the scene graph is changed. For example, if a position of a button is changed, a pulse is scheduled.

When a pulse is fired, the state of the elements on the scene graph is synchronized down to the rendering layer. A pulse enables application developers a way to handle events asynchronously. This important feature allows the system to batch and execute events on the pulse.

. . .

The Glass Windowing Toolkit is responsible for executing the pulse events. It uses the high-resolution native timers to make the execution.

要了解脉冲机制的实现,最好研究一下JavaFX源码。一些关键 classes 是:

Timer.java WinTimer.java win/Timer.h win/Timer.cpp

以上链接适用于通用定时器 class 和 window 特定定时器实现。 JavaFX 源代码包括其他平台的实现,例如 OS X、GTK、iOS、Android 等

一些实现(例如 OS X 看起来)允许 Timer 实现的 vsync 同步,其他实现(例如 Windows 看起来)不允许 vsync 同步。不过,这些系统可能会变得复杂,所以我想在某些情况下,vsync 同步可能是通过硬件图形管道而不是通过计时器来实现的。

windows 本机代码基于 timeSetEvent 调用。

默认情况下,JavaFX 帧速率上限为 60fps,尽管它是 adjustable via undocumented properties

如果您没有使用 JavaFX(您似乎没有),您仍然可以检查 JavaFX source code and learn about its implementation there in case you wanted to port any of the concepts or code for use in your application. You might also be able to shoehorn the JavaFX timer mechanism into a non-JavaFX application by making your application subclass the JavaFX Application class or creating a JFXPanel to initiate the JavaFX toolkit, then implementing your timer callback based upon AnimationTimer.