游戏循环中最佳睡眠时间计算的研究

Ema*_*uel 19 java animation game-engine

在编写动画和小游戏时,我已经知道了Thread.sleep(n);当我的应用程序不需要任何CPU时,我依靠这种方法来告诉操作系统,并且使用这个让我的程序以可预测的速度进步,这非常重要.

我的问题是JRE在不同的操作系统上使用不同的实现此功能的方法.在基于UNIX(或受影响)的操作系统上,例如Ubuntu和OS X,底层的JRE实现使用功能良好且精确的系统将CPU时间分配给不同的应用程序,从而使我的2D游戏平滑无滞后.但是,在Windows 7和较旧的Microsoft系统上,CPU时间分配似乎有所不同,并且您通常会在给定睡眠量后恢复CPU时间,与目标睡眠时间相差约1-2毫秒.但是,偶尔会有额外10-20毫秒的睡眠时间爆发.这导致我的游戏在发生这种情况时每隔几秒就会滞后一次.我注意到我在Windows上尝试过的大多数Java游戏都存在这个问题,Minecraft就是一个明显的例子.

现在,我一直在互联网上寻找解决这个问题的方法.我已经看到很多人只使用Thread.yield();而不是Thread.sleep(n);,无论你的游戏实际需要多少CPU,它都会以当前使用的CPU核心满负荷为代价完美运行.这对于在笔记本电脑或高能耗工作站上玩游戏并不理想,而且在Mac和Linux系统上进行不必要的权衡.

进一步展望我发现了一种常用的纠正睡眠时间不一致的方法,称为"旋转睡眠",你只能一次命令睡眠1毫秒,并使用该System.nanoTime();方法检查一致性,即使在微软系统上也是如此.这有助于正常1-2毫秒的睡眠不一致,但它无法帮助抵抗偶尔爆发+ 10-20毫秒的睡眠不一致,因为这通常会导致花费更多的时间比我的循环的一个循环应该花费所有一起.

经过大量的观察,我发现了这篇神秘的Andy Malakov文章,这对改善我的循环很有帮助:http://andy-malakov.blogspot.com/2010/06/alternative-to-threadsleep.html

基于他的文章我写了这个睡眠方法:

// Variables for calculating optimal sleep time. In nanoseconds (1s = 10^-9ms).
private long timeBefore = 0L;
private long timeSleepEnd, timeLeft;

// The estimated game update rate.
private double timeUpdateRate;

// The time one game loop cycle should take in order to reach the max FPS.
private long timeLoop;

private void sleep() throws InterruptedException {

    // Skip first game loop cycle.
    if (timeBefore != 0L) {

        // Calculate optimal game loop sleep time.
        timeLeft = timeLoop - (System.nanoTime() - timeBefore);

        // If all necessary calculations took LESS time than given by the sleepTimeBuffer. Max update rate was reached.
        if (timeLeft > 0 && isUpdateRateLimited) {

            // Determine when to stop sleeping.
            timeSleepEnd = System.nanoTime() + timeLeft;

            // Sleep, yield or keep the thread busy until there is not time left to sleep.
            do {
                if (timeLeft > SLEEP_PRECISION) {
                    Thread.sleep(1); // Sleep for approximately 1 millisecond.
                }
                else if (timeLeft > SPIN_YIELD_PRECISION) {
                    Thread.yield(); // Yield the thread.
                }
                if (Thread.interrupted()) {
                    throw new InterruptedException();
            }
                timeLeft = timeSleepEnd - System.nanoTime();
            }
            while (timeLeft > 0);
        }
        // Save the calculated update rate.
        timeUpdateRate =  1000000000D / (double) (System.nanoTime() - timeBefore);
    }
    // Starting point for time measurement.
    timeBefore = System.nanoTime();
}
Run Code Online (Sandbox Code Playgroud)

SLEEP_PRECISION我通常需要大约2毫秒,SPIN_YIELD_PRECISION大约10 000 ns才能在我的Windows 7机器上获得最佳性能.

经过大量的努力,这是我能想到的绝对最好的.所以,既然我仍然关心提高这种睡眠方法的准确性,而且我仍然对性能不满意,我想呼吁你们所有的java游戏黑客和动画师那里有关于更好的解决方案的建议Windows平台.我可以在Windows上使用特定于平台的方式来使其更好吗?我不关心在我的应用程序中使用一些特定于平台的代码,只要大多数代码与操作系统无关.

我还想知道是否有人知道微软和甲骨文正在制定更好的Thread.sleep(n);方法实施,或者甲骨文的未来计划是什么,以改善他们的环境作为需要高时序精度的应用的基础,如音乐软件和游戏?

谢谢大家阅读我冗长的问题/文章.我希望有些人可能会发现我的研究很有帮助!

Léo*_*ond 4

您可以使用 与互斥体关联的循环计时器。这是 IHMO 做您想做的事的最有效方式。但是,您应该考虑跳帧,以防计算机滞后(您可以使用计时器代码中的另一个非阻塞互斥体来做到这一点。)

编辑:一些伪代码来澄清

定时器代码:

While(true):
  if acquireIfPossible(mutexSkipRender):
    release(mutexSkipRender)
    release(mutexRender)
Run Code Online (Sandbox Code Playgroud)

睡眠代码:

acquire(mutexSkipRender)
acquire(mutexRender)
release(mutexSkipRender)
Run Code Online (Sandbox Code Playgroud)

起始值:

mutexSkipRender = 1
mutexRender = 0
Run Code Online (Sandbox Code Playgroud)

编辑:更正初始化值。

以下代码在 Windows 上运行得很好(以 50fps 精确循环,精确到毫秒)

import java.util.Date;
import java.util.Timer;
import java.util.TimerTask;
import java.util.concurrent.Semaphore;


public class Main {
    public static void main(String[] args) throws InterruptedException {
        final Semaphore mutexRefresh = new Semaphore(0);
        final Semaphore mutexRefreshing = new Semaphore(1);
        int refresh = 0;

        Timer timRefresh = new Timer();
        timRefresh.scheduleAtFixedRate(new TimerTask() {
            @Override
            public void run() {
                if(mutexRefreshing.tryAcquire()) {
                    mutexRefreshing.release();
                    mutexRefresh.release();
                }
            }
        }, 0, 1000/50);

        // The timer is started and configured for 50fps
        Date startDate = new Date();
        while(true) { // Refreshing loop
            mutexRefresh.acquire();
            mutexRefreshing.acquire();

            // Refresh 
            refresh += 1;

            if(refresh % 50 == 0) {
                Date endDate = new Date();
                System.out.println(String.valueOf(50.0*1000/(endDate.getTime() - startDate.getTime())) + " fps.");
                startDate = new Date();
            }

            mutexRefreshing.release();
        }
    }
}
Run Code Online (Sandbox Code Playgroud)